@moewolf/moe-icons-cli 0.0.1

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 (103) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +32 -0
  3. package/bin/moeicons.js +20 -0
  4. package/dist/api/client.d.ts +42 -0
  5. package/dist/api/client.js +105 -0
  6. package/dist/auth/callback.d.ts +37 -0
  7. package/dist/auth/callback.js +72 -0
  8. package/dist/auth/device-login.d.ts +29 -0
  9. package/dist/auth/device-login.js +74 -0
  10. package/dist/auth/login.d.ts +41 -0
  11. package/dist/auth/login.js +85 -0
  12. package/dist/auth/open-browser.d.ts +2 -0
  13. package/dist/auth/open-browser.js +15 -0
  14. package/dist/auth/pkce.d.ts +17 -0
  15. package/dist/auth/pkce.js +20 -0
  16. package/dist/auth/token-store.d.ts +38 -0
  17. package/dist/auth/token-store.js +126 -0
  18. package/dist/catalog/catalog.d.ts +32 -0
  19. package/dist/catalog/catalog.js +80 -0
  20. package/dist/catalog/catalog.json +9371 -0
  21. package/dist/cli.d.ts +23 -0
  22. package/dist/cli.js +507 -0
  23. package/dist/commands/parser.d.ts +41 -0
  24. package/dist/commands/parser.js +146 -0
  25. package/dist/core/auth.d.ts +32 -0
  26. package/dist/core/auth.js +131 -0
  27. package/dist/core/bitmap-assets.d.ts +29 -0
  28. package/dist/core/bitmap-assets.js +77 -0
  29. package/dist/core/cli-update.d.ts +43 -0
  30. package/dist/core/cli-update.js +71 -0
  31. package/dist/core/context.d.ts +22 -0
  32. package/dist/core/context.js +1 -0
  33. package/dist/core/free-download.d.ts +66 -0
  34. package/dist/core/free-download.js +193 -0
  35. package/dist/core/generate.d.ts +33 -0
  36. package/dist/core/generate.js +285 -0
  37. package/dist/core/icon-names.d.ts +14 -0
  38. package/dist/core/icon-names.js +54 -0
  39. package/dist/core/init.d.ts +12 -0
  40. package/dist/core/init.js +23 -0
  41. package/dist/core/install.d.ts +67 -0
  42. package/dist/core/install.js +117 -0
  43. package/dist/core/library-update.d.ts +24 -0
  44. package/dist/core/library-update.js +161 -0
  45. package/dist/core/manage.d.ts +24 -0
  46. package/dist/core/manage.js +38 -0
  47. package/dist/core/pro-download.d.ts +29 -0
  48. package/dist/core/pro-download.js +112 -0
  49. package/dist/core/pro-install.d.ts +24 -0
  50. package/dist/core/pro-install.js +72 -0
  51. package/dist/core/release-descriptor.d.ts +40 -0
  52. package/dist/core/release-descriptor.js +114 -0
  53. package/dist/core/resource-variant.d.ts +35 -0
  54. package/dist/core/resource-variant.js +70 -0
  55. package/dist/core/signed-artifact.d.ts +29 -0
  56. package/dist/core/signed-artifact.js +81 -0
  57. package/dist/core/target-subtree.d.ts +49 -0
  58. package/dist/core/target-subtree.js +132 -0
  59. package/dist/core/update-policy.d.ts +31 -0
  60. package/dist/core/update-policy.js +27 -0
  61. package/dist/core/version-service.d.ts +26 -0
  62. package/dist/core/version-service.js +71 -0
  63. package/dist/core/wizard.d.ts +49 -0
  64. package/dist/core/wizard.js +71 -0
  65. package/dist/errors/errors.d.ts +34 -0
  66. package/dist/errors/errors.js +39 -0
  67. package/dist/errors/index.d.ts +1 -0
  68. package/dist/errors/index.js +1 -0
  69. package/dist/generator/cn.d.ts +5 -0
  70. package/dist/generator/cn.js +14 -0
  71. package/dist/generator/generate.d.ts +34 -0
  72. package/dist/generator/generate.js +562 -0
  73. package/dist/generator/theme-resolve.d.ts +26 -0
  74. package/dist/generator/theme-resolve.js +100 -0
  75. package/dist/mcp/server.d.ts +58 -0
  76. package/dist/mcp/server.js +176 -0
  77. package/dist/project/config.d.ts +74 -0
  78. package/dist/project/config.js +353 -0
  79. package/dist/project/detect.d.ts +26 -0
  80. package/dist/project/detect.js +104 -0
  81. package/dist/project/install-metadata.d.ts +29 -0
  82. package/dist/project/install-metadata.js +108 -0
  83. package/dist/project/install.d.ts +115 -0
  84. package/dist/project/install.js +537 -0
  85. package/dist/project/project-lock.d.ts +11 -0
  86. package/dist/project/project-lock.js +81 -0
  87. package/dist/project/tailwind.d.ts +51 -0
  88. package/dist/project/tailwind.js +188 -0
  89. package/dist/project/tar-gz.d.ts +12 -0
  90. package/dist/project/tar-gz.js +100 -0
  91. package/dist/tui/primitives.d.ts +36 -0
  92. package/dist/tui/primitives.js +88 -0
  93. package/dist/ui/banner.d.ts +11 -0
  94. package/dist/ui/banner.js +105 -0
  95. package/dist/ui/clack.d.ts +8 -0
  96. package/dist/ui/clack.js +53 -0
  97. package/dist/ui/create-ui.d.ts +14 -0
  98. package/dist/ui/create-ui.js +16 -0
  99. package/dist/ui/non-interactive.d.ts +5 -0
  100. package/dist/ui/non-interactive.js +30 -0
  101. package/dist/ui/stream.d.ts +11 -0
  102. package/dist/ui/stream.js +50 -0
  103. package/package.json +70 -0
@@ -0,0 +1,72 @@
1
+ import { detectProject } from "../project/detect.js";
2
+ import { readMoeiconsConfig } from "../project/config.js";
3
+ import { createInstallPlan, executeInstallPlan } from "../project/install.js";
4
+ import { serializeInstallMetadata, sha256Bytes } from "../project/install-metadata.js";
5
+ import { withProjectLock } from "../project/project-lock.js";
6
+ import { downloadProArtifact } from "./pro-download.js";
7
+ import { artifactCachePath } from "./free-download.js";
8
+ import { selectTargetSubtree } from "./target-subtree.js";
9
+ import { CliError } from "../errors/index.js";
10
+ import { homedir } from "node:os";
11
+ import { join } from "node:path";
12
+ function resolveCacheDir(env) {
13
+ return env.MOEICONS_CACHE_DIR ?? join(homedir(), ".moeicons", "cache");
14
+ }
15
+ /** Persist the full verified archive so vanilla/bitmap generate can read assets/. */
16
+ function cacheVerifiedArtifact(fs_, cacheDir, version, sha256, bytes) {
17
+ const cached = artifactCachePath(cacheDir, version, sha256);
18
+ if (fs_.existsSync(cached))
19
+ return;
20
+ fs_.mkdirSync(join(cached, ".."), { recursive: true });
21
+ fs_.writeFileSync(cached, bytes);
22
+ }
23
+ export async function runProInstallUseCase(context, deps, expected) {
24
+ const project = detectProject(context.cwd);
25
+ if (!project)
26
+ throw new CliError("VALIDATION_ERROR", "no package.json found in the current directory or parents");
27
+ const config = readMoeiconsConfig(project.root);
28
+ if (config.kind !== "ok" || config.config.tier !== "pro")
29
+ throw new CliError("VALIDATION_ERROR", "pro install requires a valid tier=pro config");
30
+ const target = expected.target ?? config.config.target;
31
+ const downloaded = await downloadProArtifact(context, deps.auth, expected, {
32
+ ...(deps.fetch ? { fetch: deps.fetch } : {}),
33
+ ...(deps.allowedHosts ? { allowedHosts: deps.allowedHosts } : {}),
34
+ ...(deps.onProgress ? { onProgress: deps.onProgress } : {}),
35
+ });
36
+ const subtree = selectTargetSubtree(downloaded.artifactBytes, downloaded.descriptor, target);
37
+ if (!subtree.ok) {
38
+ throw new CliError("VALIDATION_ERROR", subtree.message);
39
+ }
40
+ cacheVerifiedArtifact(deps.fs, resolveCacheDir(context.env), downloaded.descriptor.version, downloaded.descriptor.sha256, downloaded.artifactBytes);
41
+ const files = {
42
+ ".moeicons/catalog.json": downloaded.catalogJson,
43
+ "src/moeicons/types.ts": `export type { ReactIconProps } from "moe-icons";\n`,
44
+ "src/moeicons/.moeicons-pro.marker": "pro\n",
45
+ };
46
+ for (const [rel, bytes] of Object.entries(subtree.files)) {
47
+ files[`.moeicons/artifact/${target}/${rel}`] = bytes;
48
+ }
49
+ const managedFiles = Object.fromEntries(Object.entries(files).map(([path, content]) => [path, sha256Bytes(content)]));
50
+ files[".moeicons/install-metadata.json"] = serializeInstallMetadata({
51
+ schemaVersion: 1,
52
+ artifactVersion: downloaded.descriptor.version,
53
+ tier: "pro",
54
+ target,
55
+ descriptorSha256: downloaded.descriptor.descriptorSha256,
56
+ artifactSha256: downloaded.descriptor.sha256,
57
+ catalogSha256: downloaded.descriptor.catalogSha256,
58
+ installedAt: context.now().toISOString(),
59
+ managedFiles,
60
+ targetSha256: subtree.sha256,
61
+ targetFileCount: subtree.fileCount,
62
+ targetByteCount: subtree.byteCount,
63
+ });
64
+ await withProjectLock(project.root, "install", () => executeInstallPlan(createInstallPlan(project.root, files), deps.fs));
65
+ return {
66
+ projectRoot: project.root,
67
+ artifactVersion: downloaded.descriptor.version,
68
+ descriptorSha256: downloaded.descriptor.descriptorSha256,
69
+ catalogSha256: downloaded.descriptor.catalogSha256,
70
+ artifactBytes: downloaded.artifactBytes.byteLength,
71
+ };
72
+ }
@@ -0,0 +1,40 @@
1
+ export type ReleaseTarget = "react" | "vue" | "vanilla" | "assets";
2
+ export declare const RELEASE_TARGETS: readonly ReleaseTarget[];
3
+ /** Per-target subtree metadata written by the code-library release packer. */
4
+ export interface ReleaseTargetMetadata {
5
+ readonly path: string;
6
+ readonly sha256: string;
7
+ readonly fileCount: number;
8
+ readonly byteCount: number;
9
+ }
10
+ export interface ReleaseTierArtifact {
11
+ readonly filename: string;
12
+ readonly sha256: string;
13
+ readonly styleGroups?: readonly string[];
14
+ readonly styleGroupCount?: number;
15
+ readonly targets?: readonly ReleaseTarget[];
16
+ readonly targetMetadata?: Readonly<Partial<Record<ReleaseTarget, ReleaseTargetMetadata>>>;
17
+ }
18
+ export interface ReleaseCatalogRef {
19
+ readonly filename: string;
20
+ readonly sha256: string;
21
+ readonly schemaVersion: number;
22
+ readonly iconCount?: number;
23
+ readonly styleGroupCount?: number;
24
+ }
25
+ export interface ReleaseDescriptor {
26
+ readonly fullVersion: string;
27
+ readonly free: ReleaseTierArtifact;
28
+ readonly catalog: ReleaseCatalogRef;
29
+ }
30
+ /** Parse release-descriptor.json. Never invent free.filename. */
31
+ export declare function parseReleaseDescriptor(bytes: Uint8Array): ReleaseDescriptor;
32
+ /** Parse `hex filename` sidecar produced next to the descriptor. */
33
+ export declare function parseSha256Sidecar(text: string): string;
34
+ export declare const PUBLIC_FREE_REPO: {
35
+ readonly owner: "moewolf-dev";
36
+ readonly name: "moe-icons";
37
+ };
38
+ export declare const FREE_DOWNLOAD_HOSTS: readonly ["github.com", "objects.githubusercontent.com"];
39
+ export declare function githubReleaseAssetUrl(tag: string, filename: string): string;
40
+ export declare function cacheKey(fullVersion: string, sha256: string): string;
@@ -0,0 +1,114 @@
1
+ const SHA256_HEX = /^[0-9a-f]{64}$/i;
2
+ const SAFE_FILENAME = /^[A-Za-z0-9._-]+$/;
3
+ const SAFE_SUBTREE_PATH = /^[A-Za-z0-9._/-]+$/;
4
+ export const RELEASE_TARGETS = ["react", "vue", "vanilla", "assets"];
5
+ function isRecord(value) {
6
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7
+ }
8
+ function requireSha(value, field) {
9
+ if (typeof value !== "string" || !SHA256_HEX.test(value)) {
10
+ throw new Error(`${field} must be a 64-character SHA-256 hex digest`);
11
+ }
12
+ return value.toLowerCase();
13
+ }
14
+ function requireFilename(value, field) {
15
+ if (typeof value !== "string" || !SAFE_FILENAME.test(value)) {
16
+ throw new Error(`${field} must be a basename without path separators`);
17
+ }
18
+ return value;
19
+ }
20
+ function requireTarget(value, field) {
21
+ if (value !== "react" && value !== "vue" && value !== "vanilla" && value !== "assets") {
22
+ throw new Error(`${field} must be one of react, vue, vanilla, assets`);
23
+ }
24
+ return value;
25
+ }
26
+ function parseTargetMetadata(value, field) {
27
+ if (!isRecord(value))
28
+ throw new Error(`${field} must be an object`);
29
+ const result = {};
30
+ for (const [key, entry] of Object.entries(value)) {
31
+ const target = requireTarget(key, `${field}.${key}`);
32
+ if (!isRecord(entry))
33
+ throw new Error(`${field}.${key} must be an object`);
34
+ if (typeof entry.path !== "string" || !SAFE_SUBTREE_PATH.test(entry.path)) {
35
+ throw new Error(`${field}.${key}.path must be a safe subtree path`);
36
+ }
37
+ if (typeof entry.fileCount !== "number" || !Number.isSafeInteger(entry.fileCount) || entry.fileCount < 0) {
38
+ throw new Error(`${field}.${key}.fileCount must be a non-negative integer`);
39
+ }
40
+ if (typeof entry.byteCount !== "number" || !Number.isSafeInteger(entry.byteCount) || entry.byteCount < 0) {
41
+ throw new Error(`${field}.${key}.byteCount must be a non-negative integer`);
42
+ }
43
+ result[target] = {
44
+ path: entry.path,
45
+ sha256: requireSha(entry.sha256, `${field}.${key}.sha256`),
46
+ fileCount: entry.fileCount,
47
+ byteCount: entry.byteCount,
48
+ };
49
+ }
50
+ return result;
51
+ }
52
+ function parseTier(value, field) {
53
+ if (!isRecord(value))
54
+ throw new Error(`${field} must be an object`);
55
+ return {
56
+ filename: requireFilename(value.filename, `${field}.filename`),
57
+ sha256: requireSha(value.sha256, `${field}.sha256`),
58
+ ...(Array.isArray(value.styleGroups) && value.styleGroups.every((item) => typeof item === "string")
59
+ ? { styleGroups: value.styleGroups }
60
+ : {}),
61
+ ...(typeof value.styleGroupCount === "number" ? { styleGroupCount: value.styleGroupCount } : {}),
62
+ ...(Array.isArray(value.targets)
63
+ ? { targets: value.targets.map((item) => requireTarget(item, `${field}.targets[]`)) }
64
+ : {}),
65
+ ...(value.targetMetadata !== undefined
66
+ ? { targetMetadata: parseTargetMetadata(value.targetMetadata, `${field}.targetMetadata`) }
67
+ : {}),
68
+ };
69
+ }
70
+ /** Parse release-descriptor.json. Never invent free.filename. */
71
+ export function parseReleaseDescriptor(bytes) {
72
+ let parsed;
73
+ try {
74
+ parsed = JSON.parse(Buffer.from(bytes).toString("utf8"));
75
+ }
76
+ catch {
77
+ throw new Error("release-descriptor.json is not valid JSON");
78
+ }
79
+ if (!isRecord(parsed))
80
+ throw new Error("release-descriptor.json must be an object");
81
+ if (typeof parsed.fullVersion !== "string" || parsed.fullVersion.length === 0) {
82
+ throw new Error("release-descriptor.json missing fullVersion");
83
+ }
84
+ if (!isRecord(parsed.catalog))
85
+ throw new Error("release-descriptor.json missing catalog");
86
+ return {
87
+ fullVersion: parsed.fullVersion,
88
+ free: parseTier(parsed.free, "free"),
89
+ catalog: {
90
+ filename: requireFilename(parsed.catalog.filename, "catalog.filename"),
91
+ sha256: requireSha(parsed.catalog.sha256, "catalog.sha256"),
92
+ schemaVersion: typeof parsed.catalog.schemaVersion === "number" ? parsed.catalog.schemaVersion : 1,
93
+ ...(typeof parsed.catalog.iconCount === "number" ? { iconCount: parsed.catalog.iconCount } : {}),
94
+ ...(typeof parsed.catalog.styleGroupCount === "number" ? { styleGroupCount: parsed.catalog.styleGroupCount } : {}),
95
+ },
96
+ };
97
+ }
98
+ /** Parse `hex filename` sidecar produced next to the descriptor. */
99
+ export function parseSha256Sidecar(text) {
100
+ const token = text.trim().split(/\s+/)[0] ?? "";
101
+ if (!SHA256_HEX.test(token))
102
+ throw new Error("descriptor sha256 sidecar is not a 64-character hex digest");
103
+ return token.toLowerCase();
104
+ }
105
+ export const PUBLIC_FREE_REPO = { owner: "moewolf-dev", name: "moe-icons" };
106
+ export const FREE_DOWNLOAD_HOSTS = ["github.com", "objects.githubusercontent.com"];
107
+ export function githubReleaseAssetUrl(tag, filename) {
108
+ if (!tag.startsWith("v"))
109
+ throw new Error("release tag must be v<fullVersion>");
110
+ return `https://github.com/${PUBLIC_FREE_REPO.owner}/${PUBLIC_FREE_REPO.name}/releases/download/${encodeURIComponent(tag)}/${encodeURIComponent(filename)}`;
111
+ }
112
+ export function cacheKey(fullVersion, sha256) {
113
+ return `${PUBLIC_FREE_REPO.owner}/${PUBLIC_FREE_REPO.name}/${fullVersion}/${sha256.toLowerCase()}`;
114
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * G0/G1/G2: styleGroupId vs resourceVariantId.
3
+ * Variant ids are deterministic: `<styleGroupId>-<webp|png>-<64|128|256|512>`.
4
+ * Parse from the right: size, then format, remainder is styleGroupId.
5
+ */
6
+ export declare const BITMAP_STYLE_GROUP_RE: RegExp;
7
+ export declare const BITMAP_FORMATS: readonly ["webp", "png"];
8
+ export declare const BITMAP_SIZES: readonly [64, 128, 256, 512];
9
+ export type BitmapFormat = (typeof BITMAP_FORMATS)[number];
10
+ export type BitmapSize = (typeof BITMAP_SIZES)[number];
11
+ export interface ResourceVariant {
12
+ readonly styleGroupId: string;
13
+ readonly format: BitmapFormat;
14
+ readonly imageSize: BitmapSize;
15
+ readonly resourceVariantId: string;
16
+ }
17
+ export declare const DEFAULT_BITMAP_FORMAT: BitmapFormat;
18
+ export declare const DEFAULT_BITMAP_SIZE: BitmapSize;
19
+ export declare function isBitmapStyleGroupId(id: string): boolean;
20
+ export declare function isBitmapFormat(value: string): value is BitmapFormat;
21
+ export declare function isBitmapSize(value: number): value is BitmapSize;
22
+ /** Build a resourceVariantId; never invent aliases. */
23
+ export declare function buildResourceVariantId(styleGroupId: string, format?: BitmapFormat, imageSize?: BitmapSize): string;
24
+ /**
25
+ * Parse `<styleGroupId>-<format>-<size>` from the right.
26
+ * Rejects guessing by scanning for an arbitrary `-3d` substring.
27
+ */
28
+ export declare function parseResourceVariantId(resourceVariantId: string): ResourceVariant;
29
+ /** Resolve format/size for a bitmap theme, applying catalog defaults when omitted. */
30
+ export declare function resolveResourceVariant(styleGroupId: string, options?: {
31
+ readonly format?: string;
32
+ readonly imageSize?: number;
33
+ }): ResourceVariant;
34
+ /** Relative managed asset path under outputDir (POSIX). */
35
+ export declare function assetRelativePath(resourceVariantId: string, iconId: string, format: BitmapFormat): string;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * G0/G1/G2: styleGroupId vs resourceVariantId.
3
+ * Variant ids are deterministic: `<styleGroupId>-<webp|png>-<64|128|256|512>`.
4
+ * Parse from the right: size, then format, remainder is styleGroupId.
5
+ */
6
+ export const BITMAP_STYLE_GROUP_RE = /^moe-[a-z0-9]+(?:-[a-z0-9]+)*-3d$/;
7
+ export const BITMAP_FORMATS = ["webp", "png"];
8
+ export const BITMAP_SIZES = [64, 128, 256, 512];
9
+ export const DEFAULT_BITMAP_FORMAT = "webp";
10
+ export const DEFAULT_BITMAP_SIZE = 256;
11
+ export function isBitmapStyleGroupId(id) {
12
+ return BITMAP_STYLE_GROUP_RE.test(id);
13
+ }
14
+ export function isBitmapFormat(value) {
15
+ return BITMAP_FORMATS.includes(value);
16
+ }
17
+ export function isBitmapSize(value) {
18
+ return BITMAP_SIZES.includes(value);
19
+ }
20
+ /** Build a resourceVariantId; never invent aliases. */
21
+ export function buildResourceVariantId(styleGroupId, format = DEFAULT_BITMAP_FORMAT, imageSize = DEFAULT_BITMAP_SIZE) {
22
+ if (!isBitmapStyleGroupId(styleGroupId)) {
23
+ throw new Error(`styleGroupId "${styleGroupId}" is not a bitmap style group`);
24
+ }
25
+ return `${styleGroupId}-${format}-${imageSize}`;
26
+ }
27
+ /**
28
+ * Parse `<styleGroupId>-<format>-<size>` from the right.
29
+ * Rejects guessing by scanning for an arbitrary `-3d` substring.
30
+ */
31
+ export function parseResourceVariantId(resourceVariantId) {
32
+ const parts = resourceVariantId.split("-");
33
+ if (parts.length < 4) {
34
+ throw new Error(`invalid resourceVariantId "${resourceVariantId}"`);
35
+ }
36
+ const sizeToken = parts[parts.length - 1] ?? "";
37
+ const formatToken = parts[parts.length - 2] ?? "";
38
+ const imageSize = Number(sizeToken);
39
+ if (!isBitmapSize(imageSize)) {
40
+ throw new Error(`invalid imageSize in resourceVariantId "${resourceVariantId}"`);
41
+ }
42
+ if (!isBitmapFormat(formatToken)) {
43
+ throw new Error(`invalid format in resourceVariantId "${resourceVariantId}"`);
44
+ }
45
+ const styleGroupId = parts.slice(0, -2).join("-");
46
+ if (!isBitmapStyleGroupId(styleGroupId)) {
47
+ throw new Error(`invalid styleGroupId derived from "${resourceVariantId}"`);
48
+ }
49
+ return {
50
+ styleGroupId,
51
+ format: formatToken,
52
+ imageSize,
53
+ resourceVariantId: buildResourceVariantId(styleGroupId, formatToken, imageSize),
54
+ };
55
+ }
56
+ /** Resolve format/size for a bitmap theme, applying catalog defaults when omitted. */
57
+ export function resolveResourceVariant(styleGroupId, options = {}) {
58
+ const format = options.format === undefined ? DEFAULT_BITMAP_FORMAT : options.format;
59
+ const imageSize = options.imageSize === undefined ? DEFAULT_BITMAP_SIZE : options.imageSize;
60
+ if (!isBitmapFormat(format))
61
+ throw new Error(`unsupported bitmap format "${format}"`);
62
+ if (!isBitmapSize(imageSize))
63
+ throw new Error(`unsupported bitmap imageSize ${String(imageSize)}`);
64
+ const resourceVariantId = buildResourceVariantId(styleGroupId, format, imageSize);
65
+ return { styleGroupId, format, imageSize, resourceVariantId };
66
+ }
67
+ /** Relative managed asset path under outputDir (POSIX). */
68
+ export function assetRelativePath(resourceVariantId, iconId, format) {
69
+ return `assets/${resourceVariantId}/${iconId}.${format}`;
70
+ }
@@ -0,0 +1,29 @@
1
+ export interface SignedArtifactDescriptor {
2
+ readonly url: string;
3
+ readonly expiresAt: string;
4
+ readonly size: number;
5
+ readonly sha256: string;
6
+ }
7
+ /**
8
+ * Exchange an access token for a descriptor. The caller supplies only the
9
+ * frozen API path; credentials can never be sent to another origin.
10
+ */
11
+ export declare function fetchSignedArtifactDescriptor(apiPath: string, accessToken: string, deps?: {
12
+ readonly fetch?: typeof fetch;
13
+ readonly signal?: AbortSignal;
14
+ readonly timeoutMs?: number;
15
+ readonly now?: number;
16
+ }): Promise<SignedArtifactDescriptor>;
17
+ /** Download signed bytes without forwarding API credentials or cookies. */
18
+ export declare function downloadSignedArtifact(descriptor: SignedArtifactDescriptor, options: {
19
+ readonly allowedHosts: readonly string[];
20
+ readonly fetch?: typeof fetch;
21
+ readonly signal?: AbortSignal;
22
+ readonly timeoutMs?: number;
23
+ readonly maxRedirects?: number;
24
+ readonly now?: number;
25
+ readonly onProgress?: (event: {
26
+ readonly downloadedBytes: number;
27
+ readonly totalBytes?: number;
28
+ }) => void;
29
+ }): Promise<Uint8Array>;
@@ -0,0 +1,81 @@
1
+ import { CliError } from "../errors/index.js";
2
+ import { downloadArtifact, verifyArtifact } from "../project/install.js";
3
+ const API_ORIGIN = "https://api.moeicons.com";
4
+ const SHA256 = /^[a-f0-9]{64}$/;
5
+ function parseDescriptor(value, now) {
6
+ if (typeof value !== "object" || value === null || Array.isArray(value))
7
+ throw new CliError("VALIDATION_ERROR", "invalid signed artifact descriptor");
8
+ const item = value;
9
+ if (Object.keys(item).some((key) => !["url", "expiresAt", "size", "sha256"].includes(key)) ||
10
+ typeof item.url !== "string" || typeof item.expiresAt !== "string" ||
11
+ typeof item.size !== "number" || !Number.isSafeInteger(item.size) || item.size < 1 ||
12
+ typeof item.sha256 !== "string" || !SHA256.test(item.sha256)) {
13
+ throw new CliError("VALIDATION_ERROR", "invalid signed artifact descriptor");
14
+ }
15
+ let url;
16
+ try {
17
+ url = new URL(item.url);
18
+ }
19
+ catch {
20
+ throw new CliError("VALIDATION_ERROR", "invalid signed artifact URL");
21
+ }
22
+ const expires = Date.parse(item.expiresAt);
23
+ if (url.protocol !== "https:" || !Number.isFinite(expires) || expires <= now)
24
+ throw new CliError("VALIDATION_ERROR", "signed artifact descriptor is expired or insecure");
25
+ return item;
26
+ }
27
+ /**
28
+ * Exchange an access token for a descriptor. The caller supplies only the
29
+ * frozen API path; credentials can never be sent to another origin.
30
+ */
31
+ export async function fetchSignedArtifactDescriptor(apiPath, accessToken, deps = {}) {
32
+ const url = new URL(apiPath, API_ORIGIN);
33
+ if (url.origin !== API_ORIGIN || !apiPath.startsWith("/"))
34
+ throw new CliError("VALIDATION_ERROR", "pro descriptor path must use the trusted API origin");
35
+ const controller = new AbortController();
36
+ const timer = setTimeout(() => controller.abort(), deps.timeoutMs ?? 5_000);
37
+ const abort = () => controller.abort();
38
+ if (deps.signal?.aborted)
39
+ controller.abort();
40
+ deps.signal?.addEventListener("abort", abort, { once: true });
41
+ try {
42
+ const response = await (deps.fetch ?? fetch)(url, {
43
+ method: "GET", redirect: "error", signal: controller.signal,
44
+ headers: { accept: "application/json", authorization: `Bearer ${accessToken}` },
45
+ });
46
+ if (!response.ok)
47
+ throw new CliError(response.status === 401 ? "AUTH_ERROR" : response.status === 403 ? "FORBIDDEN" : "NETWORK_ERROR", `pro descriptor request failed with ${response.status}`);
48
+ return parseDescriptor(await response.json(), deps.now ?? Date.now());
49
+ }
50
+ catch (error) {
51
+ if (error instanceof CliError)
52
+ throw error;
53
+ throw new CliError(deps.signal?.aborted ? "CANCELLED" : "NETWORK_ERROR", deps.signal?.aborted ? "pro descriptor request cancelled" : "pro descriptor request failed");
54
+ }
55
+ finally {
56
+ clearTimeout(timer);
57
+ deps.signal?.removeEventListener("abort", abort);
58
+ }
59
+ }
60
+ /** Download signed bytes without forwarding API credentials or cookies. */
61
+ export async function downloadSignedArtifact(descriptor, options) {
62
+ const checked = parseDescriptor(descriptor, options.now ?? Date.now());
63
+ const result = await downloadArtifact(checked.url, {
64
+ maxBytes: checked.size,
65
+ timeoutMs: options.timeoutMs ?? 30_000,
66
+ maxRedirects: options.maxRedirects ?? 5,
67
+ allowedHosts: options.allowedHosts,
68
+ ...(options.onProgress ? { onProgress: options.onProgress } : {}),
69
+ }, {
70
+ ...(options.fetch ? { fetchFn: options.fetch } : {}),
71
+ ...(options.signal ? { signal: options.signal } : {}),
72
+ });
73
+ if (!result.ok)
74
+ throw new CliError(result.code === "NETWORK_ERROR" ? "NETWORK_ERROR" : "VALIDATION_ERROR", result.message);
75
+ if (result.bytes.byteLength !== checked.size)
76
+ throw new CliError("VALIDATION_ERROR", `artifact size mismatch: expected ${checked.size}, got ${result.bytes.byteLength}`);
77
+ const verified = verifyArtifact(result.bytes, checked.sha256);
78
+ if (!verified.ok)
79
+ throw new CliError("VALIDATION_ERROR", `artifact SHA-256 mismatch: expected ${checked.sha256}, got ${verified.actual}`);
80
+ return result.bytes;
81
+ }
@@ -0,0 +1,49 @@
1
+ import type { ReleaseTarget, ReleaseTargetMetadata } from "./release-descriptor.js";
2
+ import type { Target } from "../commands/parser.js";
3
+ /**
4
+ * Select the target subtree the CLI must install. Mirrors the archive layout
5
+ * frozen in the code-library contract: each tier archive contains
6
+ * `react/`, `vue/`, `vanilla/` and `assets/` subtrees and the CLI installs only
7
+ * the selected one. When the source carries per-target subtree metadata
8
+ * (`targetMetadata[target] = { path, sha256, fileCount, byteCount }`) the
9
+ * subtree hash/size is verified against the downloaded archive bytes.
10
+ */
11
+ export interface TargetSubtreeSource {
12
+ readonly targets?: readonly ReleaseTarget[];
13
+ readonly targetMetadata?: Readonly<Partial<Record<ReleaseTarget, ReleaseTargetMetadata>>>;
14
+ }
15
+ export interface TargetSubtreeSelection {
16
+ readonly ok: true;
17
+ readonly target: Target;
18
+ /** Subtree-relative file map (e.g. `moe-outline/foo.svg` for the assets subtree). */
19
+ readonly files: Readonly<Record<string, Uint8Array>>;
20
+ readonly sha256: string;
21
+ readonly fileCount: number;
22
+ readonly byteCount: number;
23
+ }
24
+ export type TargetSubtreeFailure = {
25
+ readonly ok: false;
26
+ readonly reason: "validation" | "checksum-mismatch";
27
+ readonly message: string;
28
+ };
29
+ export type TargetSubtreeResult = TargetSubtreeSelection | TargetSubtreeFailure;
30
+ /**
31
+ * Deterministic subtree hash over subtree-relative files. Files and directories
32
+ * are visited depth-first sorted by `localeCompare`, and each file contributes
33
+ * `<relative>\0<bytes>\0` — the same algorithm used by the code-library release
34
+ * packer (`subtreeHash` in scripts/pack-release-artifacts.cjs).
35
+ */
36
+ export declare function computeSubtreeHash(files: Readonly<Record<string, Uint8Array>>): {
37
+ readonly sha256: string;
38
+ readonly fileCount: number;
39
+ readonly byteCount: number;
40
+ };
41
+ /**
42
+ * Extract and verify the selected target's subtree from an archive.
43
+ *
44
+ * Modern descriptors (those that declare `targets` and/or `targetMetadata`)
45
+ * must include the selected target, provide matching metadata, and yield a
46
+ * non-empty subtree. Legacy `{ filename, sha256 }` descriptors skip hash
47
+ * verification for backward compatibility but still reject an empty subtree.
48
+ */
49
+ export declare function selectTargetSubtree(archiveBytes: Uint8Array, source: TargetSubtreeSource, target: Target): TargetSubtreeResult;
@@ -0,0 +1,132 @@
1
+ import { createHash } from "node:crypto";
2
+ import { extractTarGz } from "../project/tar-gz.js";
3
+ /**
4
+ * Deterministic subtree hash over subtree-relative files. Files and directories
5
+ * are visited depth-first sorted by `localeCompare`, and each file contributes
6
+ * `<relative>\0<bytes>\0` — the same algorithm used by the code-library release
7
+ * packer (`subtreeHash` in scripts/pack-release-artifacts.cjs).
8
+ */
9
+ export function computeSubtreeHash(files) {
10
+ const hash = createHash("sha256");
11
+ let fileCount = 0;
12
+ let byteCount = 0;
13
+ const namesAt = (prefix) => {
14
+ const names = new Set();
15
+ for (const rel of Object.keys(files)) {
16
+ if (!prefix || rel.startsWith(`${prefix}/`)) {
17
+ const rest = prefix ? rel.slice(prefix.length + 1) : rel;
18
+ const slash = rest.indexOf("/");
19
+ names.add(slash === -1 ? rest : rest.slice(0, slash));
20
+ }
21
+ }
22
+ return [...names].sort((a, b) => a.localeCompare(b));
23
+ };
24
+ const walk = (prefix) => {
25
+ for (const name of namesAt(prefix)) {
26
+ const rel = prefix ? `${prefix}/${name}` : name;
27
+ const bytes = files[rel];
28
+ if (bytes) {
29
+ hash.update(rel).update("\0").update(bytes).update("\0");
30
+ fileCount += 1;
31
+ byteCount += bytes.byteLength;
32
+ }
33
+ else {
34
+ walk(rel);
35
+ }
36
+ }
37
+ };
38
+ walk("");
39
+ return { sha256: hash.digest("hex"), fileCount, byteCount };
40
+ }
41
+ function subtreeFilesOf(extracted, target) {
42
+ const prefix = `${target}/`;
43
+ const files = {};
44
+ for (const [path, bytes] of Object.entries(extracted)) {
45
+ if (path === target || path.startsWith(prefix)) {
46
+ files[path.slice(prefix.length)] = bytes;
47
+ }
48
+ }
49
+ return files;
50
+ }
51
+ function hasModernTargetContract(source) {
52
+ if (source.targets !== undefined && source.targets.length > 0)
53
+ return true;
54
+ if (source.targetMetadata !== undefined && Object.keys(source.targetMetadata).length > 0)
55
+ return true;
56
+ return false;
57
+ }
58
+ /**
59
+ * Extract and verify the selected target's subtree from an archive.
60
+ *
61
+ * Modern descriptors (those that declare `targets` and/or `targetMetadata`)
62
+ * must include the selected target, provide matching metadata, and yield a
63
+ * non-empty subtree. Legacy `{ filename, sha256 }` descriptors skip hash
64
+ * verification for backward compatibility but still reject an empty subtree.
65
+ */
66
+ export function selectTargetSubtree(archiveBytes, source, target) {
67
+ const extracted = extractTarGz(archiveBytes, {
68
+ maxEntries: 20_000,
69
+ maxExpandedBytes: 64 * 1024 * 1024,
70
+ });
71
+ if (extracted.errors.length > 0) {
72
+ return { ok: false, reason: "validation", message: extracted.errors[0] ?? "invalid archive" };
73
+ }
74
+ const files = subtreeFilesOf(extracted.files, target);
75
+ const computed = computeSubtreeHash(files);
76
+ const modern = hasModernTargetContract(source);
77
+ if (modern) {
78
+ if (source.targets !== undefined && !source.targets.includes(target)) {
79
+ return {
80
+ ok: false,
81
+ reason: "validation",
82
+ message: `descriptor does not declare target "${target}"`,
83
+ };
84
+ }
85
+ const expected = source.targetMetadata?.[target];
86
+ if (!expected) {
87
+ return {
88
+ ok: false,
89
+ reason: "validation",
90
+ message: `descriptor is missing targetMetadata for "${target}"`,
91
+ };
92
+ }
93
+ const normalizedPath = expected.path.replace(/\/$/, "");
94
+ if (normalizedPath !== target) {
95
+ return {
96
+ ok: false,
97
+ reason: "validation",
98
+ message: `targetMetadata["${target}"].path must be "${target}" (got "${expected.path}")`,
99
+ };
100
+ }
101
+ }
102
+ if (computed.fileCount === 0) {
103
+ return {
104
+ ok: false,
105
+ reason: "validation",
106
+ message: `target subtree "${target}" is empty or missing from the archive`,
107
+ };
108
+ }
109
+ if (modern) {
110
+ const expected = source.targetMetadata?.[target];
111
+ if (expected &&
112
+ (computed.sha256 !== expected.sha256 ||
113
+ computed.fileCount !== expected.fileCount ||
114
+ computed.byteCount !== expected.byteCount)) {
115
+ return {
116
+ ok: false,
117
+ reason: "checksum-mismatch",
118
+ message: `target subtree "${target}" mismatch: ` +
119
+ `expected sha256=${expected.sha256} files=${expected.fileCount} bytes=${expected.byteCount}, ` +
120
+ `got sha256=${computed.sha256} files=${computed.fileCount} bytes=${computed.byteCount}`,
121
+ };
122
+ }
123
+ }
124
+ return {
125
+ ok: true,
126
+ target,
127
+ files,
128
+ sha256: computed.sha256,
129
+ fileCount: computed.fileCount,
130
+ byteCount: computed.byteCount,
131
+ };
132
+ }
@@ -0,0 +1,31 @@
1
+ export type Tier = "free" | "pro";
2
+ interface ParsedVersion {
3
+ major: number;
4
+ minor: number;
5
+ patch: number;
6
+ channel: "stable" | "alpha" | "beta";
7
+ }
8
+ export declare function parseVersion(value: string): ParsedVersion | undefined;
9
+ export type UpdateDecision = {
10
+ readonly kind: "update";
11
+ } | {
12
+ readonly kind: "current";
13
+ } | {
14
+ readonly kind: "refuse";
15
+ readonly reason: "tier-mismatch" | "channel-mismatch" | "downgrade" | "invalid-version";
16
+ };
17
+ export declare function decideUpdate(input: {
18
+ installedTier: Tier;
19
+ remoteTier: Tier;
20
+ installedVersion: string;
21
+ remoteVersion: string;
22
+ }): UpdateDecision;
23
+ export interface UpdateCandidateIdentity {
24
+ readonly tier: Tier;
25
+ readonly version: string;
26
+ readonly descriptorSha256: string;
27
+ readonly artifactSha256: string;
28
+ }
29
+ /** Recheck the downloaded candidate immediately before commit to prevent TOCTOU. */
30
+ export declare function verifyCandidateIdentity(expected: UpdateCandidateIdentity, actual: UpdateCandidateIdentity): boolean;
31
+ export {};