@moewolf/moe-icons-cli 0.0.1 → 0.0.3
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 +249 -10
- package/bin/check-node.cjs +36 -0
- package/bin/moeicons.js +12 -1
- package/dist/api/client.d.ts +13 -2
- package/dist/api/client.js +145 -21
- package/dist/auth/device-login.d.ts +10 -7
- package/dist/auth/device-login.js +60 -37
- package/dist/auth/login-schemas.d.ts +24 -0
- package/dist/auth/login-schemas.js +67 -0
- package/dist/catalog/catalog.d.ts +11 -0
- package/dist/catalog/catalog.js +43 -0
- package/dist/catalog/catalog.json +12768 -3965
- package/dist/cli.d.ts +11 -2
- package/dist/cli.js +773 -147
- package/dist/commands/parser.d.ts +13 -2
- package/dist/commands/parser.js +138 -50
- package/dist/config-package/generated/SOURCE.json +25 -0
- package/dist/config-package/generated/moeicons-config.schema.json +191 -0
- package/dist/config-package/generated/moeicons.config.jsonc +42 -0
- package/dist/config-package/generated/render-config.cjs +145 -0
- package/dist/config-package/generated/validate-config.cjs +293 -0
- package/dist/config-package/generated-config.d.ts +32 -0
- package/dist/config-package/generated-config.js +19 -0
- package/dist/core/auth.d.ts +24 -0
- package/dist/core/auth.js +127 -8
- package/dist/core/bitmap-assets.d.ts +19 -5
- package/dist/core/bitmap-assets.js +40 -31
- package/dist/core/bitmap-shard-budget.d.ts +43 -0
- package/dist/core/bitmap-shard-budget.js +16 -0
- package/dist/core/bitmap-shard-cache.d.ts +68 -0
- package/dist/core/bitmap-shard-cache.js +115 -0
- package/dist/core/bitmap-shard-download.d.ts +130 -0
- package/dist/core/bitmap-shard-download.js +359 -0
- package/dist/core/bitmap-shard-resolver.d.ts +129 -0
- package/dist/core/bitmap-shard-resolver.js +262 -0
- package/dist/core/bitmap-shards.d.ts +70 -0
- package/dist/core/bitmap-shards.js +249 -0
- package/dist/core/bootstrap.d.ts +37 -0
- package/dist/core/bootstrap.js +54 -0
- package/dist/core/cache.d.ts +34 -0
- package/dist/core/cache.js +96 -0
- package/dist/core/context.d.ts +26 -0
- package/dist/core/doctor.d.ts +48 -0
- package/dist/core/doctor.js +190 -0
- package/dist/core/free-download.d.ts +64 -6
- package/dist/core/free-download.js +402 -30
- package/dist/core/generate.d.ts +29 -0
- package/dist/core/generate.js +341 -65
- package/dist/core/icon-selection.d.ts +8 -0
- package/dist/core/icon-selection.js +19 -0
- package/dist/core/install.d.ts +14 -0
- package/dist/core/install.js +132 -13
- package/dist/core/library-update.d.ts +5 -1
- package/dist/core/library-update.js +176 -17
- package/dist/core/local-test-env.d.ts +24 -0
- package/dist/core/local-test-env.js +43 -0
- package/dist/core/metadata-sync.d.ts +30 -0
- package/dist/core/metadata-sync.js +97 -0
- package/dist/core/pro-download.d.ts +63 -0
- package/dist/core/pro-download.js +247 -27
- package/dist/core/pro-install.d.ts +11 -3
- package/dist/core/pro-install.js +181 -19
- package/dist/core/pro-resources.d.ts +79 -0
- package/dist/core/pro-resources.js +204 -0
- package/dist/core/pro-selected-download.d.ts +18 -0
- package/dist/core/pro-selected-download.js +269 -0
- package/dist/core/release-descriptor.d.ts +42 -1
- package/dist/core/release-descriptor.js +117 -9
- package/dist/core/resource-variant.d.ts +27 -10
- package/dist/core/resource-variant.js +75 -34
- package/dist/core/selected-resources.d.ts +59 -0
- package/dist/core/selected-resources.js +350 -0
- package/dist/core/signed-artifact.d.ts +8 -1
- package/dist/core/signed-artifact.js +35 -11
- package/dist/core/target-subtree.d.ts +8 -0
- package/dist/core/target-subtree.js +54 -3
- package/dist/core/version-service.d.ts +11 -0
- package/dist/core/version-service.js +129 -24
- package/dist/core/wizard.d.ts +20 -17
- package/dist/core/wizard.js +66 -28
- package/dist/errors/errors.d.ts +2 -1
- package/dist/errors/errors.js +1 -0
- package/dist/generator/generate.d.ts +1 -1
- package/dist/generator/generate.js +420 -213
- package/dist/generator/shared/SOURCE.json +7 -0
- package/dist/generator/shared/svg-model.cjs +156 -0
- package/dist/generator/svg-model.d.ts +16 -0
- package/dist/generator/svg-model.js +4 -0
- package/dist/generator/theme-resolve.d.ts +2 -0
- package/dist/generator/theme-resolve.js +57 -26
- package/dist/mcp/server.d.ts +23 -36
- package/dist/mcp/server.js +74 -152
- package/dist/mcp/services.d.ts +4 -0
- package/dist/mcp/services.js +230 -0
- package/dist/metadata/archive.d.ts +38 -0
- package/dist/metadata/archive.js +94 -0
- package/dist/metadata/manifest.d.ts +33 -0
- package/dist/metadata/manifest.js +136 -0
- package/dist/metadata/version.d.ts +22 -0
- package/dist/metadata/version.js +53 -0
- package/dist/project/anchors/application.d.ts +13 -0
- package/dist/project/anchors/application.js +377 -0
- package/dist/project/anchors/apply.d.ts +28 -0
- package/dist/project/anchors/apply.js +65 -0
- package/dist/project/anchors/diagnose.d.ts +26 -0
- package/dist/project/anchors/diagnose.js +90 -0
- package/dist/project/anchors/helpers.d.ts +20 -0
- package/dist/project/anchors/helpers.js +55 -0
- package/dist/project/anchors/moeicons-config.d.ts +21 -0
- package/dist/project/anchors/moeicons-config.js +83 -0
- package/dist/project/anchors/parse.d.ts +14 -0
- package/dist/project/anchors/parse.js +36 -0
- package/dist/project/anchors/project-manifest.d.ts +19 -0
- package/dist/project/anchors/project-manifest.js +109 -0
- package/dist/project/anchors/style.d.ts +12 -0
- package/dist/project/anchors/style.js +87 -0
- package/dist/project/anchors/types.d.ts +32 -0
- package/dist/project/anchors/types.js +9 -0
- package/dist/project/config.d.ts +59 -5
- package/dist/project/config.js +234 -102
- package/dist/project/install-metadata.d.ts +23 -2
- package/dist/project/install-metadata.js +58 -7
- package/dist/project/install.d.ts +24 -4
- package/dist/project/install.js +402 -190
- package/dist/project/project-lock.d.ts +2 -2
- package/dist/project/project-lock.js +4 -4
- package/dist/project/tailwind.d.ts +2 -2
- package/dist/project/tailwind.js +3 -4
- package/dist/project/tar-gz.d.ts +18 -1
- package/dist/project/tar-gz.js +40 -3
- package/dist/tui/components.d.ts +62 -0
- package/dist/tui/components.js +100 -0
- package/dist/tui/primitives.d.ts +1 -0
- package/dist/tui/primitives.js +8 -1
- package/dist/ui/banner.d.ts +31 -3
- package/dist/ui/banner.js +120 -25
- package/dist/ui/branded-prompts.d.ts +41 -0
- package/dist/ui/branded-prompts.js +80 -0
- package/dist/ui/clack.d.ts +6 -3
- package/dist/ui/clack.js +56 -5
- package/dist/ui/create-ui.d.ts +1 -0
- package/dist/ui/create-ui.js +5 -1
- package/dist/ui/generated/wordmark-large.d.ts +17 -0
- package/dist/ui/generated/wordmark-large.js +28 -0
- package/dist/ui/generated/wordmark.d.ts +2 -0
- package/dist/ui/generated/wordmark.js +2 -0
- package/dist/ui/non-interactive.js +8 -0
- package/dist/ui/stream.js +30 -0
- package/dist/ui/theme.d.ts +42 -0
- package/dist/ui/theme.js +59 -0
- package/package.json +14 -6
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { CliError } from "../errors/index.js";
|
|
4
|
+
import { detectProject } from "../project/detect.js";
|
|
5
|
+
import { parseInstallMetadata, serializeInstallMetadata, sha256Bytes, } from "../project/install-metadata.js";
|
|
6
|
+
import { executeManagedReconcile } from "../project/install.js";
|
|
7
|
+
import { withProjectLock } from "../project/project-lock.js";
|
|
8
|
+
import { cacheArtifact } from "./cache.js";
|
|
9
|
+
import { downloadMetadataArchive, fetchFreeDescriptor, metadataCachePath, } from "./free-download.js";
|
|
10
|
+
import { downloadSignedArtifact } from "./signed-artifact.js";
|
|
11
|
+
import { extractProMetadata, fetchProDescriptor, proSignedDownloadOptions } from "./pro-download.js";
|
|
12
|
+
function toCliError(result) {
|
|
13
|
+
return new CliError(result.reason === "cancelled" ? "CANCELLED" : "VALIDATION_ERROR", result.message);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Metadata-only sync: downloads only the small metadata archive for the
|
|
17
|
+
* already-installed code version and reconciles `.moeicons/{MANUAL.md,
|
|
18
|
+
* catalog.json, manifest.json}`. Never downloads the code archive and never
|
|
19
|
+
* changes the installed code version.
|
|
20
|
+
*/
|
|
21
|
+
export async function runMetadataSyncUseCase(context, deps, options = {}) {
|
|
22
|
+
const project = detectProject(context.cwd);
|
|
23
|
+
if (!project)
|
|
24
|
+
throw new CliError("VALIDATION_ERROR", "no package.json found in the current directory or parents");
|
|
25
|
+
const metadataPath = join(project.root, ".moeicons", "install-metadata.json");
|
|
26
|
+
if (!deps.fs.existsSync(metadataPath))
|
|
27
|
+
throw new CliError("VALIDATION_ERROR", "managed install metadata is missing; run install first");
|
|
28
|
+
const old = parseInstallMetadata(deps.fs.readFileSync(metadataPath, "utf8"));
|
|
29
|
+
if (!old)
|
|
30
|
+
throw new CliError("VALIDATION_ERROR", "managed install metadata is invalid; run install first");
|
|
31
|
+
const tier = options.tier ?? old.tier;
|
|
32
|
+
const version = old.artifactVersion;
|
|
33
|
+
let catalogJson;
|
|
34
|
+
let manifestJson;
|
|
35
|
+
let manualMd;
|
|
36
|
+
let metadataSha256;
|
|
37
|
+
if (tier === "free") {
|
|
38
|
+
const fetched = await fetchFreeDescriptor({ ...deps.free, signal: context.signal, ...(deps.onProgress ? { onProgress: deps.onProgress } : {}) }, version);
|
|
39
|
+
if (!fetched.ok)
|
|
40
|
+
throw toCliError(fetched);
|
|
41
|
+
const metaRef = fetched.descriptor.free.metadata;
|
|
42
|
+
if (!metaRef)
|
|
43
|
+
throw new CliError("VALIDATION_ERROR", "release descriptor is missing the metadata archive");
|
|
44
|
+
const meta = await downloadMetadataArchive({ ...deps.free, signal: context.signal, ...(deps.onProgress ? { onProgress: deps.onProgress } : {}) }, metaRef, fetched.descriptor.catalog.sha256, "free", version, fetched.tag);
|
|
45
|
+
if (!meta.ok)
|
|
46
|
+
throw toCliError(meta);
|
|
47
|
+
catalogJson = meta.value.catalogJson;
|
|
48
|
+
manifestJson = meta.value.manifestJson;
|
|
49
|
+
manualMd = meta.value.manualMd;
|
|
50
|
+
metadataSha256 = meta.value.metadataSha256;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const descriptor = await fetchProDescriptor(context, deps.auth, { version, descriptorSha256: old.descriptorSha256 }, { ...(deps.fetch ? { fetch: deps.fetch } : {}) });
|
|
54
|
+
if (!descriptor.metadata)
|
|
55
|
+
throw new CliError("VALIDATION_ERROR", "pro descriptor is missing the metadata archive");
|
|
56
|
+
const metaBytes = await downloadSignedArtifact(descriptor.metadata, proSignedDownloadOptions(context, {
|
|
57
|
+
...(deps.fetch ? { fetch: deps.fetch } : {}),
|
|
58
|
+
...(deps.allowedProHosts ? { allowedHosts: deps.allowedProHosts } : {}),
|
|
59
|
+
...(deps.onProgress ? { onProgress: deps.onProgress } : {}),
|
|
60
|
+
}));
|
|
61
|
+
const meta = extractProMetadata(metaBytes, descriptor.catalogSha256, descriptor.version);
|
|
62
|
+
catalogJson = meta.catalogJson;
|
|
63
|
+
manifestJson = meta.manifestJson;
|
|
64
|
+
manualMd = meta.manualMd;
|
|
65
|
+
metadataSha256 = descriptor.metadata.sha256;
|
|
66
|
+
const cacheDir = context.env.MOEICONS_CACHE_DIR ?? join(homedir(), ".moeicons", "cache");
|
|
67
|
+
const cached = metadataCachePath(cacheDir, descriptor.version, descriptor.metadata.sha256);
|
|
68
|
+
if (!deps.fs.existsSync(cached)) {
|
|
69
|
+
cacheArtifact(deps.fs, cached, metaBytes, descriptor.metadata.sha256);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const writes = {
|
|
73
|
+
".moeicons/MANUAL.md": manualMd,
|
|
74
|
+
".moeicons/catalog.json": catalogJson,
|
|
75
|
+
".moeicons/manifest.json": manifestJson,
|
|
76
|
+
};
|
|
77
|
+
const managedFiles = {
|
|
78
|
+
...old.managedFiles,
|
|
79
|
+
".moeicons/MANUAL.md": sha256Bytes(manualMd),
|
|
80
|
+
".moeicons/catalog.json": sha256Bytes(catalogJson),
|
|
81
|
+
".moeicons/manifest.json": sha256Bytes(manifestJson),
|
|
82
|
+
};
|
|
83
|
+
const nextMetadata = {
|
|
84
|
+
...old,
|
|
85
|
+
installedAt: context.now().toISOString(),
|
|
86
|
+
managedFiles,
|
|
87
|
+
};
|
|
88
|
+
writes[".moeicons/install-metadata.json"] = serializeInstallMetadata(nextMetadata);
|
|
89
|
+
await withProjectLock(project.root, "update", () => executeManagedReconcile(project.root, writes, [], deps.fs));
|
|
90
|
+
return {
|
|
91
|
+
projectRoot: project.root,
|
|
92
|
+
tier,
|
|
93
|
+
artifactVersion: version,
|
|
94
|
+
metadataSha256,
|
|
95
|
+
files: [".moeicons/MANUAL.md", ".moeicons/catalog.json", ".moeicons/manifest.json"],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -1,21 +1,79 @@
|
|
|
1
|
+
import { type ResourceRefs } from "./selected-resources.js";
|
|
2
|
+
import { type ConfigDocument, type MoeiconsConfigFile } from "../project/config.js";
|
|
3
|
+
import type { SelectedResourceDownload } from "./free-download.js";
|
|
1
4
|
import { type SignedArtifactDescriptor } from "./signed-artifact.js";
|
|
2
5
|
import { type AuthUseCaseDependencies } from "./auth.js";
|
|
3
6
|
import type { CommandContext } from "./context.js";
|
|
4
7
|
import type { ReleaseTarget, ReleaseTargetMetadata } from "./release-descriptor.js";
|
|
5
8
|
export declare const PRO_DOWNLOAD_HOSTS: readonly ["06898acc14d0b9633f259fe20145fd49.r2.cloudflarestorage.com"];
|
|
9
|
+
/**
|
|
10
|
+
* Test seam: `MOEICONS_PRO_DESCRIPTOR_URL` overrides the authenticated Pro
|
|
11
|
+
* descriptor endpoint. Only an https URL or a loopback http URL is accepted, so
|
|
12
|
+
* production behavior is unchanged and a local mock can drive the packed CLI
|
|
13
|
+
* during candidate acceptance.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveProDescriptorEndpoint(env: Readonly<Record<string, string | undefined>>): {
|
|
16
|
+
readonly url: string;
|
|
17
|
+
readonly allowLoopback: boolean;
|
|
18
|
+
readonly loopbackHost: string | null;
|
|
19
|
+
};
|
|
6
20
|
export interface ProArtifactDescriptor extends SignedArtifactDescriptor {
|
|
21
|
+
readonly resources?: ResourceRefs;
|
|
7
22
|
readonly tier: "pro";
|
|
8
23
|
readonly version: string;
|
|
9
24
|
readonly descriptorSha256: string;
|
|
10
25
|
readonly catalogFilename: "catalog.json";
|
|
11
26
|
readonly catalogSha256: string;
|
|
27
|
+
/** Signed URL for the small metadata archive (MANUAL.md + manifest.json). */
|
|
28
|
+
readonly metadata?: SignedArtifactDescriptor;
|
|
12
29
|
/** Optional per-target subtree metadata; verified when the API provides it. */
|
|
13
30
|
readonly targetMetadata?: Readonly<Partial<Record<ReleaseTarget, ReleaseTargetMetadata>>>;
|
|
31
|
+
/** A-1b: present only for an accepted local-test candidate. */
|
|
32
|
+
readonly channel?: "local-test";
|
|
33
|
+
readonly publishable?: boolean;
|
|
14
34
|
}
|
|
35
|
+
/** Exchange auth for the Pro descriptor (token dance + error mapping). */
|
|
36
|
+
export declare function fetchProDescriptor(context: CommandContext, auth: AuthUseCaseDependencies, expected: {
|
|
37
|
+
version: string;
|
|
38
|
+
descriptorSha256: string;
|
|
39
|
+
allowLocalTest?: boolean;
|
|
40
|
+
}, deps?: {
|
|
41
|
+
readonly fetch?: typeof fetch;
|
|
42
|
+
}): Promise<ProArtifactDescriptor>;
|
|
43
|
+
/** Extract + verify the pro metadata archive; the manifest must match the pro release. */
|
|
44
|
+
export declare function extractProMetadata(artifactBytes: Uint8Array, catalogSha256: string, version: string, allowLocalTest?: boolean): {
|
|
45
|
+
readonly manifestJson: string;
|
|
46
|
+
readonly manualMd: string;
|
|
47
|
+
readonly catalogJson: string;
|
|
48
|
+
};
|
|
49
|
+
/** Shared signed-download options for the Pro archive (endpoint-aware loopback). */
|
|
50
|
+
export declare function proSignedDownloadOptions(context: CommandContext, deps?: {
|
|
51
|
+
readonly fetch?: typeof fetch;
|
|
52
|
+
readonly allowedHosts?: readonly string[];
|
|
53
|
+
readonly onProgress?: (event: {
|
|
54
|
+
readonly downloadedBytes: number;
|
|
55
|
+
readonly totalBytes?: number;
|
|
56
|
+
}) => void;
|
|
57
|
+
}): {
|
|
58
|
+
onProgress?: (event: {
|
|
59
|
+
readonly downloadedBytes: number;
|
|
60
|
+
readonly totalBytes?: number;
|
|
61
|
+
}) => void;
|
|
62
|
+
allowLoopback?: boolean;
|
|
63
|
+
signal: AbortSignal;
|
|
64
|
+
now: number;
|
|
65
|
+
fetch?: typeof fetch;
|
|
66
|
+
allowedHosts: readonly string[];
|
|
67
|
+
};
|
|
15
68
|
export declare function downloadProArtifact(context: CommandContext, auth: AuthUseCaseDependencies, expected: {
|
|
16
69
|
version: string;
|
|
17
70
|
descriptorSha256: string;
|
|
71
|
+
allowLocalTest?: boolean;
|
|
18
72
|
}, deps?: {
|
|
73
|
+
readonly selection?: {
|
|
74
|
+
config: MoeiconsConfigFile;
|
|
75
|
+
document: ConfigDocument;
|
|
76
|
+
};
|
|
19
77
|
readonly fetch?: typeof fetch;
|
|
20
78
|
readonly allowedHosts?: readonly string[];
|
|
21
79
|
readonly onProgress?: (event: {
|
|
@@ -26,4 +84,9 @@ export declare function downloadProArtifact(context: CommandContext, auth: AuthU
|
|
|
26
84
|
readonly descriptor: ProArtifactDescriptor;
|
|
27
85
|
readonly artifactBytes: Uint8Array;
|
|
28
86
|
readonly catalogJson: string;
|
|
87
|
+
readonly manifestJson: string;
|
|
88
|
+
readonly manualMd: string;
|
|
89
|
+
readonly metadataSha256: string;
|
|
90
|
+
readonly metadataBytes?: Uint8Array;
|
|
91
|
+
readonly selected?: SelectedResourceDownload;
|
|
29
92
|
}>;
|
|
@@ -1,22 +1,60 @@
|
|
|
1
|
+
import { parseResourceRefs } from "./selected-resources.js";
|
|
2
|
+
import { downloadProSelected } from "./pro-selected-download.js";
|
|
3
|
+
import { validateConfigDocument, } from "../project/config.js";
|
|
4
|
+
import { parseCatalog } from "../catalog/catalog.js";
|
|
1
5
|
import { CliError } from "../errors/index.js";
|
|
2
|
-
import { extractTarGz, decodeUtf8 } from "../project/tar-gz.js";
|
|
6
|
+
import { extractTarGz, decodeUtf8, ICON_ARCHIVE_MAX_ENTRIES, ICON_ARCHIVE_MAX_EXPANDED_BYTES, } from "../project/tar-gz.js";
|
|
3
7
|
import { sha256Bytes } from "../project/install-metadata.js";
|
|
4
|
-
import { downloadSignedArtifact } from "./signed-artifact.js";
|
|
8
|
+
import { downloadSignedArtifact, parseSignedDescriptor, } from "./signed-artifact.js";
|
|
5
9
|
import { runAccessTokenUseCase } from "./auth.js";
|
|
10
|
+
import { extractAndVerifyMetadataArchive } from "../metadata/archive.js";
|
|
6
11
|
const ENDPOINT = "https://api.moeicons.com/v1/icon-library/pro/artifact-descriptor";
|
|
7
|
-
export const PRO_DOWNLOAD_HOSTS = [
|
|
12
|
+
export const PRO_DOWNLOAD_HOSTS = [
|
|
13
|
+
"06898acc14d0b9633f259fe20145fd49.r2.cloudflarestorage.com",
|
|
14
|
+
];
|
|
8
15
|
const SHA = /^[a-f0-9]{64}$/;
|
|
9
16
|
const VERSION = /^\d+\.\d+\.\d+(?:-(?:alpha|beta))?$/;
|
|
17
|
+
/** A-1b: local-test candidate version; only accepted with an explicit local context. */
|
|
18
|
+
const LOCAL_TEST_VERSION = /^\d+\.\d+\.\d+-test$/;
|
|
19
|
+
const LOOPBACK = new Set(["127.0.0.1", "localhost", "::1"]);
|
|
20
|
+
/**
|
|
21
|
+
* Test seam: `MOEICONS_PRO_DESCRIPTOR_URL` overrides the authenticated Pro
|
|
22
|
+
* descriptor endpoint. Only an https URL or a loopback http URL is accepted, so
|
|
23
|
+
* production behavior is unchanged and a local mock can drive the packed CLI
|
|
24
|
+
* during candidate acceptance.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveProDescriptorEndpoint(env) {
|
|
27
|
+
const override = env.MOEICONS_PRO_DESCRIPTOR_URL;
|
|
28
|
+
if (!override)
|
|
29
|
+
return { url: ENDPOINT, allowLoopback: false, loopbackHost: null };
|
|
30
|
+
let parsed;
|
|
31
|
+
try {
|
|
32
|
+
parsed = new URL(override);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
throw new CliError("VALIDATION_ERROR", "MOEICONS_PRO_DESCRIPTOR_URL is not a valid URL");
|
|
36
|
+
}
|
|
37
|
+
const loopback = parsed.protocol === "http:" && LOOPBACK.has(parsed.hostname);
|
|
38
|
+
if (parsed.protocol !== "https:" && !loopback) {
|
|
39
|
+
throw new CliError("VALIDATION_ERROR", "MOEICONS_PRO_DESCRIPTOR_URL must be https or loopback http");
|
|
40
|
+
}
|
|
41
|
+
return { url: override, allowLoopback: loopback, loopbackHost: loopback ? parsed.host : null };
|
|
42
|
+
}
|
|
43
|
+
/** A-1b: the descriptor is an accepted local-test candidate. */
|
|
44
|
+
function isLocalTestDescriptor(item, allowLocalTest) {
|
|
45
|
+
return (allowLocalTest &&
|
|
46
|
+
typeof item.version === "string" &&
|
|
47
|
+
LOCAL_TEST_VERSION.test(item.version) &&
|
|
48
|
+
item.channel === "local-test" &&
|
|
49
|
+
item.publishable === false);
|
|
50
|
+
}
|
|
10
51
|
function parseTargetMetadata(value) {
|
|
11
52
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
12
53
|
return undefined;
|
|
13
54
|
const record = value;
|
|
14
55
|
const result = {};
|
|
15
56
|
for (const [key, entry] of Object.entries(record)) {
|
|
16
|
-
if (key !== "react" &&
|
|
17
|
-
key !== "vue" &&
|
|
18
|
-
key !== "vanilla" &&
|
|
19
|
-
key !== "assets") {
|
|
57
|
+
if (key !== "react" && key !== "vue" && key !== "vanilla" && key !== "assets") {
|
|
20
58
|
return undefined;
|
|
21
59
|
}
|
|
22
60
|
if (typeof entry !== "object" || entry === null || Array.isArray(entry))
|
|
@@ -39,24 +77,74 @@ function parseTargetMetadata(value) {
|
|
|
39
77
|
}
|
|
40
78
|
return result;
|
|
41
79
|
}
|
|
42
|
-
function parse(value, expected) {
|
|
80
|
+
function parse(value, expected, now, allowLoopback) {
|
|
43
81
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
44
82
|
throw new CliError("VALIDATION_ERROR", "invalid pro artifact descriptor");
|
|
45
83
|
const item = value;
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
84
|
+
const allowLocalTest = expected.allowLocalTest === true;
|
|
85
|
+
const baseAllowed = [
|
|
86
|
+
"ok",
|
|
87
|
+
"tier",
|
|
88
|
+
"version",
|
|
89
|
+
"descriptorSha256",
|
|
90
|
+
"catalogFilename",
|
|
91
|
+
"catalogSha256",
|
|
92
|
+
"url",
|
|
93
|
+
"expiresAt",
|
|
94
|
+
"size",
|
|
95
|
+
"sha256",
|
|
96
|
+
"targetMetadata",
|
|
97
|
+
"metadata",
|
|
98
|
+
"resources",
|
|
99
|
+
];
|
|
100
|
+
const allowed = allowLocalTest ? [...baseAllowed, "channel", "publishable"] : baseAllowed;
|
|
101
|
+
const strictVersion = typeof item.version === "string" && VERSION.test(item.version);
|
|
102
|
+
// A formal (stable/alpha/beta) version must never carry the local-test marker.
|
|
103
|
+
if (strictVersion && (item.channel !== undefined || item.publishable !== undefined)) {
|
|
104
|
+
throw new CliError("VALIDATION_ERROR", "invalid or changed pro artifact descriptor");
|
|
105
|
+
}
|
|
106
|
+
const versionAllowed = typeof item.version === "string" &&
|
|
107
|
+
(strictVersion || isLocalTestDescriptor(item, allowLocalTest));
|
|
108
|
+
if (Object.keys(item).some((key) => !allowed.includes(key)) ||
|
|
109
|
+
item.ok !== true ||
|
|
110
|
+
item.tier !== "pro" ||
|
|
111
|
+
item.version !== expected.version ||
|
|
112
|
+
item.descriptorSha256 !== expected.descriptorSha256 ||
|
|
113
|
+
!versionAllowed ||
|
|
114
|
+
typeof item.descriptorSha256 !== "string" ||
|
|
115
|
+
!SHA.test(item.descriptorSha256) ||
|
|
116
|
+
item.catalogFilename !== "catalog.json" ||
|
|
117
|
+
typeof item.catalogSha256 !== "string" ||
|
|
118
|
+
!SHA.test(item.catalogSha256) ||
|
|
119
|
+
typeof item.url !== "string" ||
|
|
120
|
+
typeof item.expiresAt !== "string" ||
|
|
121
|
+
typeof item.size !== "number" ||
|
|
122
|
+
!Number.isSafeInteger(item.size) ||
|
|
123
|
+
item.size < 1 ||
|
|
124
|
+
typeof item.sha256 !== "string" ||
|
|
125
|
+
!SHA.test(item.sha256)) {
|
|
51
126
|
throw new CliError("VALIDATION_ERROR", "invalid or changed pro artifact descriptor");
|
|
52
127
|
}
|
|
128
|
+
if (item.resources !== undefined) {
|
|
129
|
+
const refs = parseResourceRefs(item.resources);
|
|
130
|
+
if (refs.index.filename !== `moe-icons-pro-resource-index-${item.version}.json.gz` ||
|
|
131
|
+
refs.bundle.filename !== `moe-icons-pro-resources-${item.version}.bin`)
|
|
132
|
+
throw new CliError("VALIDATION_ERROR", "pro resource filenames do not match tier/version");
|
|
133
|
+
}
|
|
53
134
|
const targetMetadata = item.targetMetadata !== undefined ? parseTargetMetadata(item.targetMetadata) : undefined;
|
|
135
|
+
const metadata = item.metadata !== undefined
|
|
136
|
+
? parseSignedDescriptor(item.metadata, now, { allowLoopback })
|
|
137
|
+
: undefined;
|
|
138
|
+
const localTest = isLocalTestDescriptor(item, allowLocalTest);
|
|
54
139
|
return {
|
|
55
140
|
...item,
|
|
56
141
|
...(targetMetadata ? { targetMetadata } : {}),
|
|
142
|
+
...(metadata ? { metadata } : {}),
|
|
143
|
+
...(item.resources !== undefined ? { resources: parseResourceRefs(item.resources) } : {}),
|
|
144
|
+
...(localTest ? { channel: "local-test", publishable: false } : {}),
|
|
57
145
|
};
|
|
58
146
|
}
|
|
59
|
-
async function requestDescriptor(token, expected, deps) {
|
|
147
|
+
async function requestDescriptor(token, endpoint, expected, deps) {
|
|
60
148
|
const controller = new AbortController();
|
|
61
149
|
const timer = setTimeout(() => controller.abort(), 5_000);
|
|
62
150
|
const abort = () => controller.abort();
|
|
@@ -64,14 +152,47 @@ async function requestDescriptor(token, expected, deps) {
|
|
|
64
152
|
controller.abort();
|
|
65
153
|
deps.signal.addEventListener("abort", abort, { once: true });
|
|
66
154
|
try {
|
|
67
|
-
const response = await deps.fetch(
|
|
68
|
-
method: "POST",
|
|
69
|
-
|
|
155
|
+
const response = await deps.fetch(endpoint, {
|
|
156
|
+
method: "POST",
|
|
157
|
+
redirect: "error",
|
|
158
|
+
signal: controller.signal,
|
|
159
|
+
headers: {
|
|
160
|
+
authorization: `Bearer ${token}`,
|
|
161
|
+
accept: "application/json",
|
|
162
|
+
"content-type": "application/json",
|
|
163
|
+
"X-Moeicons-Resources": "v1",
|
|
164
|
+
},
|
|
70
165
|
body: JSON.stringify(expected),
|
|
71
166
|
});
|
|
72
|
-
if (!response.ok)
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
await response.body?.cancel();
|
|
73
169
|
return { status: response.status };
|
|
74
|
-
|
|
170
|
+
}
|
|
171
|
+
const reader = response.body?.getReader();
|
|
172
|
+
if (!reader)
|
|
173
|
+
throw new CliError("NETWORK_ERROR", "empty pro descriptor response");
|
|
174
|
+
const chunks = [];
|
|
175
|
+
let size = 0;
|
|
176
|
+
try {
|
|
177
|
+
for (;;) {
|
|
178
|
+
const part = await reader.read();
|
|
179
|
+
if (part.done)
|
|
180
|
+
break;
|
|
181
|
+
size += part.value.length;
|
|
182
|
+
if (size > 256 * 1024) {
|
|
183
|
+
await reader.cancel();
|
|
184
|
+
throw new CliError("VALIDATION_ERROR", "pro descriptor exceeds 256 KiB");
|
|
185
|
+
}
|
|
186
|
+
chunks.push(part.value);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
reader.releaseLock();
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
status: response.status,
|
|
194
|
+
value: parse(JSON.parse(Buffer.concat(chunks).toString("utf8")), expected, deps.now, deps.allowLoopback),
|
|
195
|
+
};
|
|
75
196
|
}
|
|
76
197
|
catch (error) {
|
|
77
198
|
if (error instanceof CliError)
|
|
@@ -83,13 +204,26 @@ async function requestDescriptor(token, expected, deps) {
|
|
|
83
204
|
deps.signal.removeEventListener("abort", abort);
|
|
84
205
|
}
|
|
85
206
|
}
|
|
86
|
-
|
|
207
|
+
/** Exchange auth for the Pro descriptor (token dance + error mapping). */
|
|
208
|
+
export async function fetchProDescriptor(context, auth, expected, deps = {}) {
|
|
87
209
|
const fetchFn = deps.fetch ?? fetch;
|
|
210
|
+
const { url: endpoint, allowLoopback } = resolveProDescriptorEndpoint(context.env);
|
|
88
211
|
let token = await runAccessTokenUseCase(context, auth);
|
|
89
|
-
|
|
212
|
+
const now = () => context.now().getTime();
|
|
213
|
+
let response = await requestDescriptor(token, endpoint, expected, {
|
|
214
|
+
fetch: fetchFn,
|
|
215
|
+
signal: context.signal,
|
|
216
|
+
now: now(),
|
|
217
|
+
allowLoopback,
|
|
218
|
+
});
|
|
90
219
|
if (response.status === 401) {
|
|
91
220
|
token = await runAccessTokenUseCase(context, auth, true);
|
|
92
|
-
response = await requestDescriptor(token, expected, {
|
|
221
|
+
response = await requestDescriptor(token, endpoint, expected, {
|
|
222
|
+
fetch: fetchFn,
|
|
223
|
+
signal: context.signal,
|
|
224
|
+
now: now(),
|
|
225
|
+
allowLoopback,
|
|
226
|
+
});
|
|
93
227
|
}
|
|
94
228
|
token = "";
|
|
95
229
|
if (response.status === 401)
|
|
@@ -100,13 +234,99 @@ export async function downloadProArtifact(context, auth, expected, deps = {}) {
|
|
|
100
234
|
throw new CliError("NOT_FOUND", "pro artifact not found");
|
|
101
235
|
if (!response.value)
|
|
102
236
|
throw new CliError("NETWORK_ERROR", `pro descriptor request failed with ${response.status}`);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
237
|
+
return response.value;
|
|
238
|
+
}
|
|
239
|
+
/** Extract + verify the pro metadata archive; the manifest must match the pro release. */
|
|
240
|
+
export function extractProMetadata(artifactBytes, catalogSha256, version, allowLocalTest = false) {
|
|
241
|
+
const result = extractAndVerifyMetadataArchive(artifactBytes, {
|
|
242
|
+
expectedCatalogSha: catalogSha256,
|
|
243
|
+
expectedTier: "pro",
|
|
244
|
+
expectedVersion: version,
|
|
245
|
+
allowLocalTest,
|
|
246
|
+
});
|
|
247
|
+
if (result.kind !== "ok")
|
|
248
|
+
throw new CliError("VALIDATION_ERROR", result.message);
|
|
249
|
+
return result.value;
|
|
250
|
+
}
|
|
251
|
+
/** Shared signed-download options for the Pro archive (endpoint-aware loopback). */
|
|
252
|
+
export function proSignedDownloadOptions(context, deps = {}) {
|
|
253
|
+
const { allowLoopback, loopbackHost } = resolveProDescriptorEndpoint(context.env);
|
|
254
|
+
return {
|
|
255
|
+
allowedHosts: deps.allowedHosts ??
|
|
256
|
+
(loopbackHost ? [...PRO_DOWNLOAD_HOSTS, loopbackHost] : PRO_DOWNLOAD_HOSTS),
|
|
257
|
+
...(deps.fetch ? { fetch: deps.fetch } : {}),
|
|
258
|
+
signal: context.signal,
|
|
259
|
+
now: context.now().getTime(),
|
|
260
|
+
...(allowLoopback ? { allowLoopback: true } : {}),
|
|
261
|
+
...(deps.onProgress ? { onProgress: deps.onProgress } : {}),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
export async function downloadProArtifact(context, auth, expected, deps = {}) {
|
|
265
|
+
const fetchFn = deps.fetch ?? fetch;
|
|
266
|
+
const descriptor = await fetchProDescriptor(context, auth, expected, { fetch: fetchFn });
|
|
267
|
+
if (deps.selection &&
|
|
268
|
+
deps.selection.config.downloadMode !== "full" &&
|
|
269
|
+
descriptor.resources &&
|
|
270
|
+
descriptor.channel !== "local-test") {
|
|
271
|
+
const selected = await downloadProSelected(context, auth, descriptor, deps.selection, deps);
|
|
272
|
+
if (selected)
|
|
273
|
+
return {
|
|
274
|
+
descriptor,
|
|
275
|
+
artifactBytes: new Uint8Array(),
|
|
276
|
+
...selected,
|
|
277
|
+
metadataSha256: descriptor.metadata.sha256,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
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");
|
|
282
|
+
context.ui.note(deps.selection?.config.downloadMode === "full"
|
|
283
|
+
? "Download mode: full (explicitly configured)."
|
|
284
|
+
: "Release does not advertise selected downloads; auto uses the verified full archive. Set downloadMode=icons to require selected downloads.", context.signal);
|
|
285
|
+
const downloadOptions = proSignedDownloadOptions(context, deps);
|
|
286
|
+
let metadata;
|
|
287
|
+
let metadataBytes;
|
|
288
|
+
if (descriptor.metadata) {
|
|
289
|
+
metadataBytes = await downloadSignedArtifact(descriptor.metadata, downloadOptions);
|
|
290
|
+
metadata = extractProMetadata(metadataBytes, descriptor.catalogSha256, descriptor.version, expected.allowLocalTest === true);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
throw new CliError("VALIDATION_ERROR", "pro release descriptor is missing the metadata archive");
|
|
294
|
+
}
|
|
295
|
+
if (deps.selection) {
|
|
296
|
+
try {
|
|
297
|
+
const strict = validateConfigDocument(deps.selection.document, parseCatalog(JSON.parse(metadata.catalogJson)));
|
|
298
|
+
if (strict.kind !== "ok")
|
|
299
|
+
throw new CliError("VALIDATION_ERROR", strict.kind === "invalid" ? strict.message : `config state: ${strict.kind}`);
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
if (error instanceof CliError)
|
|
303
|
+
throw error;
|
|
304
|
+
throw new CliError("VALIDATION_ERROR", `invalid fixed release catalog: ${error instanceof Error ? error.message : String(error)}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const artifactBytes = await downloadSignedArtifact({
|
|
308
|
+
url: descriptor.url,
|
|
309
|
+
expiresAt: descriptor.expiresAt,
|
|
310
|
+
size: descriptor.size,
|
|
311
|
+
sha256: descriptor.sha256,
|
|
312
|
+
}, downloadOptions);
|
|
313
|
+
const extracted = extractTarGz(artifactBytes, {
|
|
314
|
+
maxEntries: ICON_ARCHIVE_MAX_ENTRIES,
|
|
315
|
+
maxExpandedBytes: ICON_ARCHIVE_MAX_EXPANDED_BYTES,
|
|
316
|
+
});
|
|
106
317
|
if (extracted.errors.length > 0)
|
|
107
318
|
throw new CliError("VALIDATION_ERROR", extracted.errors[0] ?? "invalid pro archive");
|
|
108
|
-
const catalog = extracted.files[descriptor.catalogFilename] ??
|
|
319
|
+
const catalog = extracted.files[descriptor.catalogFilename] ??
|
|
320
|
+
extracted.files[`./${descriptor.catalogFilename}`];
|
|
109
321
|
if (!catalog || sha256Bytes(catalog) !== descriptor.catalogSha256)
|
|
110
322
|
throw new CliError("VALIDATION_ERROR", "pro catalog SHA-256 mismatch");
|
|
111
|
-
return {
|
|
323
|
+
return {
|
|
324
|
+
descriptor,
|
|
325
|
+
artifactBytes,
|
|
326
|
+
catalogJson: decodeUtf8(catalog),
|
|
327
|
+
manifestJson: metadata.manifestJson,
|
|
328
|
+
manualMd: metadata.manualMd,
|
|
329
|
+
metadataSha256: descriptor.metadata.sha256,
|
|
330
|
+
metadataBytes,
|
|
331
|
+
};
|
|
112
332
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { type TransactionalFs } from "../project/install.js";
|
|
2
|
-
import type
|
|
1
|
+
import { type TransactionalFs, type TransactionalFsWithCopy } from "../project/install.js";
|
|
2
|
+
import { type AuthUseCaseDependencies } from "./auth.js";
|
|
3
3
|
import type { CommandContext } from "./context.js";
|
|
4
4
|
import type { Target } from "../commands/parser.js";
|
|
5
|
+
type ProInstallFs = TransactionalFs & Partial<Pick<TransactionalFsWithCopy, "readFileSync" | "readdirSync" | "copyFileSync">>;
|
|
5
6
|
export declare function runProInstallUseCase(context: CommandContext, deps: {
|
|
6
|
-
readonly fs:
|
|
7
|
+
readonly fs: ProInstallFs;
|
|
7
8
|
readonly auth: AuthUseCaseDependencies;
|
|
8
9
|
readonly fetch?: typeof fetch;
|
|
9
10
|
readonly allowedHosts?: readonly string[];
|
|
@@ -15,10 +16,17 @@ export declare function runProInstallUseCase(context: CommandContext, deps: {
|
|
|
15
16
|
readonly version: string;
|
|
16
17
|
readonly descriptorSha256: string;
|
|
17
18
|
readonly target?: Target;
|
|
19
|
+
/** A-1b: allow a declared local-test candidate (never true for production). */
|
|
20
|
+
readonly allowLocalTest?: boolean;
|
|
18
21
|
}): Promise<{
|
|
19
22
|
readonly projectRoot: string;
|
|
20
23
|
readonly artifactVersion: string;
|
|
21
24
|
readonly descriptorSha256: string;
|
|
22
25
|
readonly catalogSha256: string;
|
|
23
26
|
readonly artifactBytes: number;
|
|
27
|
+
readonly downloadMode: "icons" | "full";
|
|
28
|
+
readonly downloadNotes: readonly string[];
|
|
29
|
+
readonly networkBytes?: number;
|
|
30
|
+
readonly selectedFiles?: number;
|
|
24
31
|
}>;
|
|
32
|
+
export {};
|