@moewolf/moe-icons-cli 0.0.1 → 0.0.2

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 (151) hide show
  1. package/README.md +243 -9
  2. package/bin/check-node.cjs +36 -0
  3. package/bin/moeicons.js +12 -1
  4. package/dist/api/client.d.ts +13 -2
  5. package/dist/api/client.js +145 -21
  6. package/dist/auth/device-login.d.ts +10 -7
  7. package/dist/auth/device-login.js +60 -37
  8. package/dist/auth/login-schemas.d.ts +24 -0
  9. package/dist/auth/login-schemas.js +67 -0
  10. package/dist/catalog/catalog.d.ts +11 -0
  11. package/dist/catalog/catalog.js +43 -0
  12. package/dist/catalog/catalog.json +538 -3395
  13. package/dist/cli.d.ts +11 -2
  14. package/dist/cli.js +773 -147
  15. package/dist/commands/parser.d.ts +13 -2
  16. package/dist/commands/parser.js +138 -50
  17. package/dist/config-package/generated/SOURCE.json +25 -0
  18. package/dist/config-package/generated/moeicons-config.schema.json +191 -0
  19. package/dist/config-package/generated/moeicons.config.jsonc +42 -0
  20. package/dist/config-package/generated/render-config.cjs +145 -0
  21. package/dist/config-package/generated/validate-config.cjs +293 -0
  22. package/dist/config-package/generated-config.d.ts +32 -0
  23. package/dist/config-package/generated-config.js +19 -0
  24. package/dist/core/auth.d.ts +24 -0
  25. package/dist/core/auth.js +127 -8
  26. package/dist/core/bitmap-assets.d.ts +19 -5
  27. package/dist/core/bitmap-assets.js +40 -31
  28. package/dist/core/bitmap-shard-budget.d.ts +43 -0
  29. package/dist/core/bitmap-shard-budget.js +16 -0
  30. package/dist/core/bitmap-shard-cache.d.ts +68 -0
  31. package/dist/core/bitmap-shard-cache.js +115 -0
  32. package/dist/core/bitmap-shard-download.d.ts +130 -0
  33. package/dist/core/bitmap-shard-download.js +359 -0
  34. package/dist/core/bitmap-shard-resolver.d.ts +129 -0
  35. package/dist/core/bitmap-shard-resolver.js +262 -0
  36. package/dist/core/bitmap-shards.d.ts +70 -0
  37. package/dist/core/bitmap-shards.js +249 -0
  38. package/dist/core/bootstrap.d.ts +37 -0
  39. package/dist/core/bootstrap.js +54 -0
  40. package/dist/core/cache.d.ts +34 -0
  41. package/dist/core/cache.js +96 -0
  42. package/dist/core/context.d.ts +26 -0
  43. package/dist/core/doctor.d.ts +48 -0
  44. package/dist/core/doctor.js +190 -0
  45. package/dist/core/free-download.d.ts +64 -6
  46. package/dist/core/free-download.js +402 -30
  47. package/dist/core/generate.d.ts +29 -0
  48. package/dist/core/generate.js +341 -65
  49. package/dist/core/icon-selection.d.ts +8 -0
  50. package/dist/core/icon-selection.js +19 -0
  51. package/dist/core/install.d.ts +14 -0
  52. package/dist/core/install.js +132 -13
  53. package/dist/core/library-update.d.ts +5 -1
  54. package/dist/core/library-update.js +176 -17
  55. package/dist/core/local-test-env.d.ts +24 -0
  56. package/dist/core/local-test-env.js +43 -0
  57. package/dist/core/metadata-sync.d.ts +30 -0
  58. package/dist/core/metadata-sync.js +97 -0
  59. package/dist/core/pro-download.d.ts +63 -0
  60. package/dist/core/pro-download.js +247 -27
  61. package/dist/core/pro-install.d.ts +11 -3
  62. package/dist/core/pro-install.js +181 -19
  63. package/dist/core/pro-resources.d.ts +79 -0
  64. package/dist/core/pro-resources.js +204 -0
  65. package/dist/core/pro-selected-download.d.ts +18 -0
  66. package/dist/core/pro-selected-download.js +269 -0
  67. package/dist/core/release-descriptor.d.ts +42 -1
  68. package/dist/core/release-descriptor.js +117 -9
  69. package/dist/core/resource-variant.d.ts +27 -10
  70. package/dist/core/resource-variant.js +75 -34
  71. package/dist/core/selected-resources.d.ts +59 -0
  72. package/dist/core/selected-resources.js +350 -0
  73. package/dist/core/signed-artifact.d.ts +8 -1
  74. package/dist/core/signed-artifact.js +35 -11
  75. package/dist/core/target-subtree.d.ts +8 -0
  76. package/dist/core/target-subtree.js +54 -3
  77. package/dist/core/version-service.d.ts +11 -0
  78. package/dist/core/version-service.js +129 -24
  79. package/dist/core/wizard.d.ts +20 -17
  80. package/dist/core/wizard.js +66 -28
  81. package/dist/errors/errors.d.ts +2 -1
  82. package/dist/errors/errors.js +1 -0
  83. package/dist/generator/generate.d.ts +1 -1
  84. package/dist/generator/generate.js +420 -213
  85. package/dist/generator/shared/SOURCE.json +7 -0
  86. package/dist/generator/shared/svg-model.cjs +156 -0
  87. package/dist/generator/svg-model.d.ts +16 -0
  88. package/dist/generator/svg-model.js +4 -0
  89. package/dist/generator/theme-resolve.d.ts +2 -0
  90. package/dist/generator/theme-resolve.js +57 -26
  91. package/dist/mcp/server.d.ts +23 -36
  92. package/dist/mcp/server.js +74 -152
  93. package/dist/mcp/services.d.ts +4 -0
  94. package/dist/mcp/services.js +230 -0
  95. package/dist/metadata/archive.d.ts +38 -0
  96. package/dist/metadata/archive.js +94 -0
  97. package/dist/metadata/manifest.d.ts +33 -0
  98. package/dist/metadata/manifest.js +136 -0
  99. package/dist/metadata/version.d.ts +22 -0
  100. package/dist/metadata/version.js +53 -0
  101. package/dist/project/anchors/application.d.ts +13 -0
  102. package/dist/project/anchors/application.js +377 -0
  103. package/dist/project/anchors/apply.d.ts +28 -0
  104. package/dist/project/anchors/apply.js +65 -0
  105. package/dist/project/anchors/diagnose.d.ts +26 -0
  106. package/dist/project/anchors/diagnose.js +90 -0
  107. package/dist/project/anchors/helpers.d.ts +20 -0
  108. package/dist/project/anchors/helpers.js +55 -0
  109. package/dist/project/anchors/moeicons-config.d.ts +21 -0
  110. package/dist/project/anchors/moeicons-config.js +83 -0
  111. package/dist/project/anchors/parse.d.ts +14 -0
  112. package/dist/project/anchors/parse.js +36 -0
  113. package/dist/project/anchors/project-manifest.d.ts +19 -0
  114. package/dist/project/anchors/project-manifest.js +109 -0
  115. package/dist/project/anchors/style.d.ts +12 -0
  116. package/dist/project/anchors/style.js +87 -0
  117. package/dist/project/anchors/types.d.ts +32 -0
  118. package/dist/project/anchors/types.js +9 -0
  119. package/dist/project/config.d.ts +59 -5
  120. package/dist/project/config.js +234 -102
  121. package/dist/project/install-metadata.d.ts +23 -2
  122. package/dist/project/install-metadata.js +58 -7
  123. package/dist/project/install.d.ts +24 -4
  124. package/dist/project/install.js +402 -190
  125. package/dist/project/project-lock.d.ts +2 -2
  126. package/dist/project/project-lock.js +4 -4
  127. package/dist/project/tailwind.d.ts +2 -2
  128. package/dist/project/tailwind.js +3 -4
  129. package/dist/project/tar-gz.d.ts +18 -1
  130. package/dist/project/tar-gz.js +40 -3
  131. package/dist/tui/components.d.ts +62 -0
  132. package/dist/tui/components.js +100 -0
  133. package/dist/tui/primitives.d.ts +1 -0
  134. package/dist/tui/primitives.js +8 -1
  135. package/dist/ui/banner.d.ts +31 -3
  136. package/dist/ui/banner.js +120 -25
  137. package/dist/ui/branded-prompts.d.ts +41 -0
  138. package/dist/ui/branded-prompts.js +80 -0
  139. package/dist/ui/clack.d.ts +6 -3
  140. package/dist/ui/clack.js +56 -5
  141. package/dist/ui/create-ui.d.ts +1 -0
  142. package/dist/ui/create-ui.js +5 -1
  143. package/dist/ui/generated/wordmark-large.d.ts +17 -0
  144. package/dist/ui/generated/wordmark-large.js +28 -0
  145. package/dist/ui/generated/wordmark.d.ts +2 -0
  146. package/dist/ui/generated/wordmark.js +2 -0
  147. package/dist/ui/non-interactive.js +8 -0
  148. package/dist/ui/stream.js +30 -0
  149. package/dist/ui/theme.d.ts +42 -0
  150. package/dist/ui/theme.js +59 -0
  151. 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 = ["06898acc14d0b9633f259fe20145fd49.r2.cloudflarestorage.com"];
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 allowed = ["ok", "tier", "version", "descriptorSha256", "catalogFilename", "catalogSha256", "url", "expiresAt", "size", "sha256", "targetMetadata"];
47
- if (Object.keys(item).some((key) => !allowed.includes(key)) || item.ok !== true || item.tier !== "pro" || item.version !== expected.version || item.descriptorSha256 !== expected.descriptorSha256 ||
48
- typeof item.version !== "string" || !VERSION.test(item.version) || typeof item.descriptorSha256 !== "string" || !SHA.test(item.descriptorSha256) ||
49
- item.catalogFilename !== "catalog.json" || typeof item.catalogSha256 !== "string" || !SHA.test(item.catalogSha256) || typeof item.url !== "string" ||
50
- typeof item.expiresAt !== "string" || typeof item.size !== "number" || !Number.isSafeInteger(item.size) || item.size < 1 || typeof item.sha256 !== "string" || !SHA.test(item.sha256)) {
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(ENDPOINT, {
68
- method: "POST", redirect: "error", signal: controller.signal,
69
- headers: { authorization: `Bearer ${token}`, accept: "application/json", "content-type": "application/json" },
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
- return { status: response.status, value: parse(await response.json(), expected) };
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
- export async function downloadProArtifact(context, auth, expected, deps = {}) {
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
- let response = await requestDescriptor(token, expected, { fetch: fetchFn, signal: context.signal });
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, { fetch: fetchFn, signal: context.signal });
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
- const descriptor = response.value;
104
- const artifactBytes = await downloadSignedArtifact({ url: descriptor.url, expiresAt: descriptor.expiresAt, size: descriptor.size, sha256: descriptor.sha256 }, { allowedHosts: deps.allowedHosts ?? PRO_DOWNLOAD_HOSTS, fetch: fetchFn, signal: context.signal, now: context.now().getTime(), ...(deps.onProgress ? { onProgress: deps.onProgress } : {}) });
105
- const extracted = extractTarGz(artifactBytes, { maxEntries: 20_000, maxExpandedBytes: 64 * 1024 * 1024 });
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] ?? 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 { descriptor, artifactBytes, catalogJson: decodeUtf8(catalog) };
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 { AuthUseCaseDependencies } from "./auth.js";
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: TransactionalFs;
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 {};