@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.
- package/README.md +27 -19
- package/dist/cli.js +124 -146
- package/dist/config-package/generated/SOURCE.json +3 -3
- package/dist/config-package/generated/moeicons-config.schema.json +1 -1
- package/dist/core/bitmap-shard-budget.d.ts +2 -2
- package/dist/core/bitmap-shard-budget.js +1 -1
- package/dist/core/bootstrap.js +5 -3
- package/dist/core/candidate-version-pointer.d.ts +13 -0
- package/dist/core/candidate-version-pointer.js +37 -0
- package/dist/core/config-package-plan.d.ts +25 -0
- package/dist/core/config-package-plan.js +156 -0
- package/dist/core/doctor.js +4 -0
- package/dist/core/free-download.js +34 -4
- package/dist/core/free-small-package-download.d.ts +67 -0
- package/dist/core/free-small-package-download.js +344 -0
- package/dist/core/generate.js +49 -33
- package/dist/core/install.d.ts +8 -2
- package/dist/core/install.js +70 -0
- package/dist/core/library-update.d.ts +6 -1
- package/dist/core/library-update.js +54 -0
- package/dist/core/manage.d.ts +4 -0
- package/dist/core/manage.js +4 -0
- package/dist/core/metadata-sync.js +119 -1
- package/dist/core/pro-download.js +19 -2
- package/dist/core/pro-install.d.ts +10 -4
- package/dist/core/pro-install.js +47 -0
- package/dist/core/pro-resources.d.ts +23 -0
- package/dist/core/pro-resources.js +51 -0
- package/dist/core/pro-selected-download.js +12 -2
- package/dist/core/pro-small-package-download.d.ts +61 -0
- package/dist/core/pro-small-package-download.js +301 -0
- package/dist/core/release-descriptor.js +6 -2
- package/dist/core/selected-resources.js +2 -2
- package/dist/core/small-pack/PIN.json +11 -0
- package/dist/core/small-pack/candidate-set-v1.schema.json +187 -0
- package/dist/core/small-pack/candidate-set.cjs +286 -0
- package/dist/core/small-pack/candidate-set.d.cts +12 -0
- package/dist/core/small-pack/tar-read.cjs +220 -0
- package/dist/core/small-pack/tar-read.d.cts +2 -0
- package/dist/core/small-package-install.d.ts +40 -0
- package/dist/core/small-package-install.js +252 -0
- package/dist/core/small-package-selection.d.ts +27 -0
- package/dist/core/small-package-selection.js +114 -0
- package/dist/core/update-policy.d.ts +1 -0
- package/dist/core/update-policy.js +10 -3
- package/dist/mcp/services.js +1 -1
- package/dist/project/config.js +1 -1
- package/dist/project/install-metadata.d.ts +2 -1
- package/dist/project/install-metadata.js +6 -3
- package/dist/project/install.d.ts +11 -2
- package/dist/project/install.js +104 -12
- package/dist/project/package-install-metadata.d.ts +31 -0
- package/dist/project/package-install-metadata.js +99 -0
- package/dist/project/tar-gz.d.ts +4 -8
- package/dist/project/tar-gz.js +6 -10
- package/package.json +11 -11
package/dist/project/install.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
+
}
|
package/dist/project/tar-gz.d.ts
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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 =
|
|
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;
|
package/dist/project/tar-gz.js
CHANGED
|
@@ -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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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 =
|
|
14
|
-
export const ICON_ARCHIVE_MAX_EXPANDED_BYTES =
|
|
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
|
|
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.
|
|
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": "
|
|
56
|
-
"@typescript-eslint/parser": "
|
|
57
|
-
"@vue/server-renderer": "
|
|
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": "
|
|
64
|
+
"sharp": "0.35.5",
|
|
65
65
|
"typescript": "^5.5.0",
|
|
66
66
|
"vitest": "^4.1.11",
|
|
67
|
-
"vue": "
|
|
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": "
|
|
77
|
+
"payloadHash": "c453cb7f4725db607e0f5ae585dbe2927cf30060ce485c5695809228c5d187f1"
|
|
78
78
|
}
|