@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.
Files changed (151) hide show
  1. package/README.md +249 -10
  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 +12768 -3965
  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,32 @@
1
+ /**
2
+ * Four-anchor project diagnosis types (E2E-B1, contract:
3
+ * `docs/contracts/project-integration-v1.md`).
4
+ *
5
+ * These types are pure data: detectors only READ files and BUILD a plan. They
6
+ * never emit ANSI, never prompt, and never write to disk. Writers/commands that
7
+ * display or apply the plan live outside the detectors.
8
+ */
9
+ export type AnchorKind = "manifest" | "config" | "application" | "style";
10
+ export type AnchorStatus = "ok" | "missing" | "invalid" | "ambiguous" | "unsupported" | "not-required";
11
+ /** A single proposed file change (relative POSIX path under the project root). */
12
+ export interface PlannedFileChange {
13
+ readonly kind: "create" | "replace";
14
+ readonly path: string;
15
+ /** Full previous content (for `replace`); undefined only for `create`. */
16
+ readonly before: string | undefined;
17
+ /** Full next content. */
18
+ readonly after: string;
19
+ }
20
+ export interface AnchorResult {
21
+ readonly kind: AnchorKind;
22
+ readonly status: AnchorStatus;
23
+ readonly path?: string;
24
+ readonly candidates: readonly string[];
25
+ readonly evidence: readonly string[];
26
+ readonly fixes: readonly PlannedFileChange[];
27
+ }
28
+ /** The four anchors in a fixed order, for human and `--json` output. */
29
+ export interface DiagnosticReport {
30
+ readonly projectRoot?: string;
31
+ readonly anchors: readonly AnchorResult[];
32
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Four-anchor project diagnosis types (E2E-B1, contract:
3
+ * `docs/contracts/project-integration-v1.md`).
4
+ *
5
+ * These types are pure data: detectors only READ files and BUILD a plan. They
6
+ * never emit ANSI, never prompt, and never write to disk. Writers/commands that
7
+ * display or apply the plan live outside the detectors.
8
+ */
9
+ export {};
@@ -2,6 +2,7 @@ import { type IconCatalog } from "../catalog/catalog.js";
2
2
  import type { Target } from "../commands/parser.js";
3
3
  export interface MoeiconsThemeConfig {
4
4
  readonly styleGroup: string;
5
+ readonly icons?: readonly string[];
5
6
  readonly styles?: readonly string[];
6
7
  readonly format?: "svg" | "webp" | "png";
7
8
  readonly imageSize?: 64 | 128 | 256 | 512;
@@ -9,21 +10,30 @@ export interface MoeiconsThemeConfig {
9
10
  readonly strokeWidth?: number;
10
11
  readonly className?: string;
11
12
  }
13
+ /** User-confirmed project integration anchors (config schema v3). */
14
+ export interface MoeiconsIntegration {
15
+ readonly adapter: "vite-react" | "next-app" | "next-pages" | "vite-vue" | "nuxt" | "vanilla" | "assets-only";
16
+ readonly entry?: string;
17
+ readonly style?: string;
18
+ }
12
19
  /**
13
20
  * Normalized config: schema version 2 with a REQUIRED `target`. Core code must
14
21
  * never see an optional or missing target — v1 files are migrated by
15
22
  * `readMoeiconsConfig` and invalid v2 files fail with a validation error
16
- * instead of silently defaulting to React.
23
+ * instead of silently defaulting to React. Schema v3 files add an optional
24
+ * `integration` block that is preserved in memory.
17
25
  */
18
26
  export interface MoeiconsConfigFile {
19
- readonly schemaVersion: 2;
27
+ readonly schemaVersion: 2 | 3;
20
28
  readonly tier: "free" | "pro";
21
29
  readonly target: Target;
22
30
  readonly outputDir: string;
23
31
  readonly defaultTheme: string;
24
32
  readonly themes: Readonly<Record<string, MoeiconsThemeConfig>>;
25
33
  readonly icons: readonly string[];
34
+ readonly downloadMode?: "auto" | "icons" | "full";
26
35
  readonly missingIconPolicy?: "fallback" | "error";
36
+ readonly integration?: MoeiconsIntegration;
27
37
  }
28
38
  /**
29
39
  * Legacy v1 input shape. Reads normalize it into a `MoeiconsConfigFile` v2 by
@@ -37,6 +47,7 @@ export interface LegacyConfigInput {
37
47
  readonly defaultTheme: string;
38
48
  readonly themes: Readonly<Record<string, MoeiconsThemeConfig>>;
39
49
  readonly icons: readonly string[];
50
+ readonly downloadMode?: "auto" | "icons" | "full";
40
51
  readonly missingIconPolicy?: "fallback" | "error";
41
52
  }
42
53
  export type ConfigLoadResult = {
@@ -52,9 +63,45 @@ export type ConfigLoadResult = {
52
63
  readonly config: MoeiconsConfigFile;
53
64
  readonly warnings: readonly string[];
54
65
  };
66
+ /** Adapter enum for schema v3 `integration`. */
67
+ export declare const INTEGRATION_ADAPTERS: readonly ["vite-react", "next-app", "next-pages", "vite-vue", "nuxt", "vanilla", "assets-only"];
68
+ export type IntegrationAdapter = (typeof INTEGRATION_ADAPTERS)[number];
55
69
  export declare const SUPPORTED_FILENAMES: readonly ["moeicons.config.jsonc", "moeicons.config.json", "moeicons.config.ts", "moeicons.config.js"];
56
70
  export declare function findConfigFile(root: string): string | undefined;
57
- export declare function readMoeiconsConfig(root: string, sourceCatalog?: IconCatalog): ConfigLoadResult;
71
+ /**
72
+ * DEV-G10-R1: two-phase Pro validation. The bootstrap phase parses only the
73
+ * catalog-independent fields (tier/target/syntax) so a Pro bitmap config can be
74
+ * read BEFORE the release catalog is available; the strict phase re-validates
75
+ * the same config against the verified release catalog and fails closed on any
76
+ * style group / icon / variant the release does not actually ship.
77
+ */
78
+ export interface ConfigReadOptions {
79
+ readonly lenientCatalog?: boolean;
80
+ }
81
+ /**
82
+ * A single immutable read of the on-disk config. DEV-G10-R2: Pro install takes
83
+ * one document snapshot and validates it twice (lenient, then strict). The
84
+ * target subtree and the bitmap tuples therefore always come from the SAME
85
+ * config bytes; a rewrite during download can never mix an old target with new
86
+ * tuples.
87
+ */
88
+ export type ConfigDocument = {
89
+ readonly kind: "missing";
90
+ } | {
91
+ readonly kind: "invalid";
92
+ readonly message: string;
93
+ } | {
94
+ readonly kind: "unsupported";
95
+ readonly version: number;
96
+ } | {
97
+ readonly kind: "ok";
98
+ readonly value: unknown;
99
+ readonly version: 1 | 2 | 3;
100
+ };
101
+ export declare function loadConfigDocument(root: string): ConfigDocument;
102
+ /** Validate a previously loaded document (never re-reads the filesystem). */
103
+ export declare function validateConfigDocument(document: ConfigDocument, sourceCatalog?: IconCatalog, options?: ConfigReadOptions): ConfigLoadResult;
104
+ export declare function readMoeiconsConfig(root: string, sourceCatalog?: IconCatalog, options?: ConfigReadOptions): ConfigLoadResult;
58
105
  export declare function mergeMoeiconsConfig(current: MoeiconsConfigFile | LegacyConfigInput, patch: Partial<Omit<MoeiconsConfigFile, "schemaVersion">>): MoeiconsConfigFile;
59
106
  export declare function createMoeiconsConfig(options: {
60
107
  target?: Target;
@@ -65,10 +112,17 @@ export declare function createMoeiconsConfig(options: {
65
112
  icons?: readonly string[];
66
113
  themes?: Readonly<Record<string, MoeiconsThemeConfig>>;
67
114
  }): MoeiconsConfigFile;
68
- /** Render the editable, grouped JSONC skeleton used by `init`. */
115
+ /**
116
+ * Render the editable, grouped JSONC skeleton used by `init`.
117
+ *
118
+ * Delegates to the canonical renderer bundled from moe-icons-code-library
119
+ * `config-package` (see `src/config-package/generated` + SOURCE.json). The CLI
120
+ * no longer keeps a second full template; a schema v1/v2 default file is
121
+ * produced unless an integration result is supplied (v3 + `integration`).
122
+ */
69
123
  export declare function renderMoeiconsConfigJsonc(options: {
70
124
  target?: Target;
71
- /** Legacy v1 alias; still emits a v2 `target`. */
125
+ /** Legacy v1 alias; still emits a `target` field. */
72
126
  framework?: "react" | "vue";
73
127
  tier?: "free" | "pro";
74
128
  }): string;
@@ -2,6 +2,18 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { parse } from "jsonc-parser";
4
4
  import { catalog, findCatalogIcon, findCatalogStyleGroup, } from "../catalog/catalog.js";
5
+ import { loadGeneratedConfigPackage } from "../config-package/generated-config.js";
6
+ import { toProxyName } from "../core/icon-names.js";
7
+ /** Adapter enum for schema v3 `integration`. */
8
+ export const INTEGRATION_ADAPTERS = [
9
+ "vite-react",
10
+ "next-app",
11
+ "next-pages",
12
+ "vite-vue",
13
+ "nuxt",
14
+ "vanilla",
15
+ "assets-only",
16
+ ];
5
17
  export const SUPPORTED_FILENAMES = [
6
18
  "moeicons.config.jsonc",
7
19
  "moeicons.config.json",
@@ -43,14 +55,18 @@ function flattenIcons(value) {
43
55
  const ALLOWED_COMMON_KEYS = new Set([
44
56
  "schemaVersion",
45
57
  "tier",
58
+ "target",
46
59
  "outputDir",
47
60
  "defaultTheme",
48
61
  "themes",
49
62
  "icons",
50
63
  "missingIconPolicy",
64
+ "downloadMode",
65
+ "integration",
51
66
  ]);
52
67
  const ALLOWED_THEME_KEYS = new Set([
53
68
  "styleGroup",
69
+ "icons",
54
70
  "styles",
55
71
  "format",
56
72
  "imageSize",
@@ -63,25 +79,51 @@ function asRecord(raw) {
63
79
  throw new Error("config must be an object");
64
80
  return raw;
65
81
  }
66
- function requireCommonConfigFields(obj, sourceCatalog) {
82
+ function requireCommonConfigFields(obj, sourceCatalog, lenientCatalog) {
67
83
  const warnings = [];
68
84
  if (obj.tier !== "free" && obj.tier !== "pro")
69
85
  throw new Error("tier must be free or pro");
70
86
  const tier = obj.tier;
71
87
  if (typeof obj.outputDir !== "string" || obj.outputDir.length === 0)
72
88
  throw new Error("outputDir is required");
89
+ const outputParts = obj.outputDir.split("/");
90
+ if (obj.outputDir.includes("\\") ||
91
+ obj.outputDir.includes("\0") ||
92
+ obj.outputDir.startsWith("/") ||
93
+ /^[A-Za-z]:/.test(obj.outputDir) ||
94
+ outputParts.some((part) => !part || part === "." || part === "..")) {
95
+ throw new Error("outputDir must be a POSIX relative path without '.' or '..' segments");
96
+ }
97
+ if ([".git", ".moeicons", "node_modules"].includes(outputParts[0] ?? "")) {
98
+ throw new Error("outputDir cannot use a project control directory");
99
+ }
100
+ if (!/^[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_.-]+)*$/.test(obj.outputDir)) {
101
+ throw new Error("outputDir must use ASCII letters, digits, _, -, and safe dotted child segments");
102
+ }
73
103
  if (typeof obj.defaultTheme !== "string" || obj.defaultTheme.length === 0)
74
104
  throw new Error("defaultTheme is required");
75
105
  if (typeof obj.themes !== "object" || obj.themes === null || Array.isArray(obj.themes)) {
76
106
  throw new Error("themes must be an object");
77
107
  }
108
+ if (obj.downloadMode !== undefined &&
109
+ !["auto", "icons", "full"].includes(String(obj.downloadMode)))
110
+ throw new Error('downloadMode must be "auto", "icons" or "full"');
78
111
  if (obj.missingIconPolicy !== undefined &&
79
112
  obj.missingIconPolicy !== "fallback" &&
80
113
  obj.missingIconPolicy !== "error") {
81
114
  throw new Error(`missingIconPolicy must be "fallback" or "error"`);
82
115
  }
83
116
  const themes = {};
117
+ const themeNames = new Map();
84
118
  for (const [name, value] of Object.entries(obj.themes)) {
119
+ if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(name)) {
120
+ throw new Error(`theme key "${name}" must be lowercase kebab-case`);
121
+ }
122
+ const internalName = toProxyName(name).toLowerCase();
123
+ const previous = themeNames.get(internalName);
124
+ if (previous)
125
+ throw new Error(`theme keys "${previous}" and "${name}" generate the same identifier`);
126
+ themeNames.set(internalName, name);
85
127
  if (typeof value !== "object" || value === null || Array.isArray(value))
86
128
  throw new Error(`theme ${name} is invalid`);
87
129
  const theme = value;
@@ -93,15 +135,39 @@ function requireCommonConfigFields(obj, sourceCatalog) {
93
135
  if (typeof theme.styleGroup !== "string")
94
136
  throw new Error(`theme ${name}.styleGroup is required`);
95
137
  const group = findCatalogStyleGroup(theme.styleGroup, sourceCatalog);
96
- if (!group)
97
- throw new Error(`unknown style group "${theme.styleGroup}"`);
98
- if (!group.tiers.includes(tier)) {
99
- throw new Error(`style group "${group.id}" is not available in ${tier} tier`);
138
+ if (!lenientCatalog) {
139
+ if (!group)
140
+ throw new Error(`unknown style group "${theme.styleGroup}"`);
141
+ if (!group.tiers.includes(tier)) {
142
+ throw new Error(`style group "${group.id}" is not available in ${tier} tier`);
143
+ }
100
144
  }
101
145
  // styles[] is deprecated — still accepted for migration but warns.
102
146
  if (Array.isArray(theme.styles) && theme.styles.length > 0) {
103
147
  warnings.push(`theme "${name}": "styles" is deprecated and has no effect; remove it from your config`);
104
148
  }
149
+ if (theme.format !== undefined && typeof theme.format !== "string")
150
+ throw new Error(`theme ${name}.format is invalid`);
151
+ if (theme.imageSize !== undefined && typeof theme.imageSize !== "number")
152
+ throw new Error(`theme ${name}.imageSize is invalid`);
153
+ if (theme.defaultSize !== undefined &&
154
+ (typeof theme.defaultSize !== "number" ||
155
+ !Number.isFinite(theme.defaultSize) ||
156
+ theme.defaultSize <= 0))
157
+ throw new Error(`theme ${name}.defaultSize must be a positive finite number`);
158
+ if (theme.strokeWidth !== undefined &&
159
+ (typeof theme.strokeWidth !== "number" ||
160
+ !Number.isFinite(theme.strokeWidth) ||
161
+ theme.strokeWidth < 0))
162
+ throw new Error(`theme ${name}.strokeWidth must be a nonnegative finite number`);
163
+ if (theme.className !== undefined && typeof theme.className !== "string")
164
+ throw new Error(`theme ${name}.className must be a string`);
165
+ if (theme.icons !== undefined &&
166
+ (!Array.isArray(theme.icons) ||
167
+ theme.icons.some((id) => typeof id !== "string") ||
168
+ new Set(theme.icons).size !== theme.icons.length)) {
169
+ throw new Error(`theme ${name}.icons must be an array of unique icon IDs`);
170
+ }
105
171
  const format = typeof theme.format === "string" ? theme.format : undefined;
106
172
  if (format !== undefined && format !== "svg" && format !== "webp" && format !== "png") {
107
173
  throw new Error(`theme ${name}.format is invalid`);
@@ -110,20 +176,31 @@ function requireCommonConfigFields(obj, sourceCatalog) {
110
176
  if (imageSize !== undefined && ![64, 128, 256, 512].includes(imageSize)) {
111
177
  throw new Error(`theme ${name}.imageSize is invalid`);
112
178
  }
113
- if (group.type !== "bitmap" &&
114
- ((format !== undefined && format !== "svg") || imageSize !== undefined)) {
115
- throw new Error(`SVG theme ${name} cannot define bitmap options`);
116
- }
117
- if (group.type === "bitmap" && format !== undefined && !group.formats.includes(format)) {
118
- throw new Error(`format ${format} is unavailable for ${group.id}`);
119
- }
120
- if (group.type === "bitmap" &&
121
- imageSize !== undefined &&
122
- !group.imageSizes.includes(imageSize)) {
123
- throw new Error(`imageSize ${imageSize} is unavailable for ${group.id}`);
179
+ if (!lenientCatalog && group) {
180
+ if (group.type !== "bitmap" &&
181
+ ((format !== undefined && format !== "svg") || imageSize !== undefined)) {
182
+ throw new Error(`SVG theme ${name} cannot define bitmap options`);
183
+ }
184
+ if (group.type === "bitmap" && format !== undefined && !group.formats.includes(format)) {
185
+ throw new Error(`format ${format} is unavailable for ${group.id}`);
186
+ }
187
+ if (group.type === "bitmap" &&
188
+ imageSize !== undefined &&
189
+ !group.imageSizes.includes(imageSize)) {
190
+ throw new Error(`imageSize ${imageSize} is unavailable for ${group.id}`);
191
+ }
192
+ if (group.type === "bitmap" && group.variants && group.variants.length > 0) {
193
+ const effectiveFormat = format ?? "webp";
194
+ const effectiveSize = imageSize ?? 256;
195
+ const variantId = `${group.id}-${effectiveSize}-${effectiveFormat}`;
196
+ if (!group.variants.includes(variantId)) {
197
+ throw new Error(`variant ${variantId} is unavailable for ${group.id}`);
198
+ }
199
+ }
124
200
  }
125
201
  themes[name] = {
126
202
  styleGroup: theme.styleGroup,
203
+ ...(theme.icons !== undefined ? { icons: theme.icons } : {}),
127
204
  ...(format !== undefined ? { format } : {}),
128
205
  ...(imageSize !== undefined ? { imageSize: imageSize } : {}),
129
206
  ...(typeof theme.defaultSize === "number" ? { defaultSize: theme.defaultSize } : {}),
@@ -134,9 +211,17 @@ function requireCommonConfigFields(obj, sourceCatalog) {
134
211
  if (!(obj.defaultTheme in themes))
135
212
  throw new Error(`defaultTheme "${obj.defaultTheme}" is not defined`);
136
213
  const icons = flattenIcons(obj.icons);
137
- for (const iconId of icons)
138
- if (!findCatalogIcon(iconId, sourceCatalog))
139
- throw new Error(`unknown icon "${iconId}"`);
214
+ for (const [name, theme] of Object.entries(themes)) {
215
+ for (const iconId of theme.icons ?? []) {
216
+ if (!icons.includes(iconId))
217
+ throw new Error(`theme ${name}.icons contains unregistered icon "${iconId}"; add it to project icons first`);
218
+ }
219
+ }
220
+ if (!lenientCatalog) {
221
+ for (const iconId of icons)
222
+ if (!findCatalogIcon(iconId, sourceCatalog))
223
+ throw new Error(`unknown icon "${iconId}"`);
224
+ }
140
225
  return {
141
226
  warnings,
142
227
  tier,
@@ -144,9 +229,10 @@ function requireCommonConfigFields(obj, sourceCatalog) {
144
229
  defaultTheme: obj.defaultTheme,
145
230
  themes,
146
231
  icons,
147
- ...(obj.missingIconPolicy !== undefined
148
- ? { missingIconPolicy: obj.missingIconPolicy }
232
+ ...(obj.downloadMode !== undefined
233
+ ? { downloadMode: obj.downloadMode }
149
234
  : {}),
235
+ ...(obj.missingIconPolicy !== undefined ? { missingIconPolicy: obj.missingIconPolicy } : {}),
150
236
  };
151
237
  }
152
238
  function normalizeTarget(value) {
@@ -155,49 +241,89 @@ function normalizeTarget(value) {
155
241
  }
156
242
  throw new Error("target must be react, vue, vanilla, or assets");
157
243
  }
158
- /** Validate a schema version 2 config: target is required, framework is rejected. */
159
- function validateV2Config(raw, sourceCatalog) {
244
+ /** Validate a `target`-based config (schema v2 or v3). Framework is rejected. */
245
+ function validateV2Config(raw, sourceCatalog, schemaVersion = 2, lenientCatalog = false) {
160
246
  const obj = asRecord(raw);
161
247
  if (obj.framework !== undefined)
162
- throw new Error('framework is only supported in schemaVersion 1; use target');
248
+ throw new Error("framework is only supported in schemaVersion 1; use target");
163
249
  for (const key of Object.keys(obj)) {
164
- if (!ALLOWED_COMMON_KEYS.has(key) && key !== "target") {
250
+ if (!ALLOWED_COMMON_KEYS.has(key)) {
165
251
  throw new Error(`unknown config field "${key}"`);
166
252
  }
167
253
  }
168
254
  if (obj.target === undefined)
169
255
  throw new Error("target is required");
170
256
  const target = normalizeTarget(obj.target);
171
- const common = requireCommonConfigFields(obj, sourceCatalog);
257
+ const common = requireCommonConfigFields(obj, sourceCatalog, lenientCatalog);
172
258
  return {
173
259
  config: {
174
- schemaVersion: 2,
260
+ schemaVersion,
175
261
  tier: common.tier,
176
262
  target,
177
263
  outputDir: common.outputDir,
178
264
  defaultTheme: common.defaultTheme,
179
265
  themes: common.themes,
180
266
  icons: common.icons,
267
+ ...(common.downloadMode !== undefined ? { downloadMode: common.downloadMode } : {}),
181
268
  ...(common.missingIconPolicy !== undefined
182
269
  ? { missingIconPolicy: common.missingIconPolicy }
183
270
  : {}),
271
+ ...(obj.integration !== undefined
272
+ ? { integration: validateIntegration(obj.integration) }
273
+ : {}),
184
274
  },
185
275
  warnings: common.warnings,
186
276
  };
187
277
  }
278
+ function validateIntegration(value) {
279
+ const record = asRecord(value);
280
+ for (const key of Object.keys(record)) {
281
+ if (!["adapter", "entry", "style"].includes(key)) {
282
+ throw new Error(`unknown integration field "${key}"`);
283
+ }
284
+ }
285
+ if (!INTEGRATION_ADAPTERS.includes(record.adapter)) {
286
+ throw new Error(`integration.adapter is not a supported adapter`);
287
+ }
288
+ const adapter = record.adapter;
289
+ const checkRel = (field, name) => {
290
+ if (field === undefined)
291
+ return undefined;
292
+ if (typeof field !== "string" ||
293
+ !field ||
294
+ field.startsWith("/") ||
295
+ /^[A-Za-z]:/.test(field) ||
296
+ field.includes("\\") ||
297
+ field.includes("\0") ||
298
+ field.split("/").some((part) => !part || part === "." || part === "..")) {
299
+ throw new Error(`${name} must be a POSIX-relative path without .. or escapes`);
300
+ }
301
+ return field;
302
+ };
303
+ const entry = checkRel(record.entry, "integration.entry");
304
+ const style = checkRel(record.style, "integration.style");
305
+ if (adapter !== "assets-only" && entry === undefined) {
306
+ throw new Error(`integration.entry is required for adapter ${adapter}`);
307
+ }
308
+ return {
309
+ adapter,
310
+ ...(entry !== undefined ? { entry } : {}),
311
+ ...(style !== undefined ? { style } : {}),
312
+ };
313
+ }
188
314
  /** Validate a legacy schema version 1 config: framework is required, target is rejected. */
189
- function validateV1Config(raw, sourceCatalog) {
315
+ function validateV1Config(raw, sourceCatalog, lenientCatalog = false) {
190
316
  const obj = asRecord(raw);
191
317
  for (const key of Object.keys(obj)) {
192
318
  if (!ALLOWED_COMMON_KEYS.has(key) && key !== "framework") {
193
319
  throw new Error(`unknown config field "${key}"`);
194
320
  }
195
321
  }
196
- if (obj.target !== undefined)
197
- throw new Error("v1 config cannot set target; migrate to schemaVersion 2");
322
+ if (obj.target !== undefined || obj.integration !== undefined)
323
+ throw new Error("v1 config cannot set target or integration; migrate to schema v2/v3");
198
324
  if (obj.framework !== "react" && obj.framework !== "vue")
199
325
  throw new Error("framework must be react or vue");
200
- const common = requireCommonConfigFields(obj, sourceCatalog);
326
+ const common = requireCommonConfigFields(obj, sourceCatalog, lenientCatalog);
201
327
  return {
202
328
  config: {
203
329
  schemaVersion: 2,
@@ -207,6 +333,7 @@ function validateV1Config(raw, sourceCatalog) {
207
333
  defaultTheme: common.defaultTheme,
208
334
  themes: common.themes,
209
335
  icons: common.icons,
336
+ ...(common.downloadMode !== undefined ? { downloadMode: common.downloadMode } : {}),
210
337
  ...(common.missingIconPolicy !== undefined
211
338
  ? { missingIconPolicy: common.missingIconPolicy }
212
339
  : {}),
@@ -214,7 +341,7 @@ function validateV1Config(raw, sourceCatalog) {
214
341
  warnings: common.warnings,
215
342
  };
216
343
  }
217
- export function readMoeiconsConfig(root, sourceCatalog = catalog) {
344
+ export function loadConfigDocument(root) {
218
345
  const configPath = findConfigFile(root);
219
346
  if (!configPath)
220
347
  return { kind: "missing" };
@@ -231,23 +358,75 @@ export function readMoeiconsConfig(root, sourceCatalog = catalog) {
231
358
  ? parsed.value.schemaVersion
232
359
  : undefined;
233
360
  if (typeof version !== "number") {
234
- return { kind: "invalid", message: "schemaVersion must be 1 or 2" };
361
+ return { kind: "invalid", message: "schemaVersion must be 1, 2, or 3" };
235
362
  }
236
- if (version !== 1 && version !== 2)
363
+ if (version !== 1 && version !== 2 && version !== 3)
237
364
  return { kind: "unsupported", version };
365
+ return { kind: "ok", value: parsed.value, version };
366
+ }
367
+ /** Validate a previously loaded document (never re-reads the filesystem). */
368
+ export function validateConfigDocument(document, sourceCatalog = catalog, options = {}) {
369
+ if (document.kind !== "ok")
370
+ return document;
238
371
  try {
239
- if (version === 1) {
240
- const validated = validateV1Config(parsed.value, sourceCatalog);
372
+ if (options.lenientCatalog !== true) {
373
+ const raw = asRecord(document.value);
374
+ if (document.version === 1 && (raw.target !== undefined || raw.integration !== undefined)) {
375
+ return {
376
+ kind: "invalid",
377
+ message: "v1 config cannot set target or integration; migrate to schema v2/v3",
378
+ };
379
+ }
380
+ const canonical = loadGeneratedConfigPackage().validateConfig(document.value, sourceCatalog);
381
+ if (!canonical.ok) {
382
+ if (canonical.kind === "unsupported" && typeof canonical.version === "number") {
383
+ return { kind: "unsupported", version: canonical.version };
384
+ }
385
+ return { kind: "invalid", message: canonical.message ?? "invalid configuration" };
386
+ }
387
+ // The vendored contract owns normalization. Only the release-specific
388
+ // bitmap variant check and legacy in-memory version remain CLI concerns.
389
+ const normalized = canonical.config;
390
+ for (const theme of Object.values(normalized.themes)) {
391
+ const group = findCatalogStyleGroup(theme.styleGroup, sourceCatalog);
392
+ if (group?.type !== "bitmap" || !group.variants?.length)
393
+ continue;
394
+ const variantId = `${group.id}-${theme.imageSize ?? 256}-${theme.format ?? "webp"}`;
395
+ if (!group.variants.includes(variantId)) {
396
+ return {
397
+ kind: "invalid",
398
+ message: `variant ${variantId} is unavailable for ${group.id}`,
399
+ };
400
+ }
401
+ }
402
+ const warnings = [...(canonical.warnings ?? [])];
403
+ for (const [name, value] of Object.entries(raw.themes)) {
404
+ const styles = value.styles;
405
+ if (Array.isArray(styles) && styles.length > 0) {
406
+ warnings.push(`theme "${name}": "styles" is deprecated and has no effect; remove it from your config`);
407
+ }
408
+ }
409
+ return {
410
+ kind: "ok",
411
+ config: { ...normalized, schemaVersion: document.version === 3 ? 3 : 2 },
412
+ warnings,
413
+ };
414
+ }
415
+ if (document.version === 1) {
416
+ const validated = validateV1Config(document.value, sourceCatalog, options.lenientCatalog === true);
241
417
  validated.warnings.unshift('config schema v1 migrated "framework" to "target"');
242
418
  return { kind: "ok", config: validated.config, warnings: validated.warnings };
243
419
  }
244
- const validated = validateV2Config(parsed.value, sourceCatalog);
420
+ const validated = validateV2Config(document.value, sourceCatalog, document.version, options.lenientCatalog === true);
245
421
  return { kind: "ok", config: validated.config, warnings: validated.warnings };
246
422
  }
247
423
  catch (error) {
248
424
  return { kind: "invalid", message: error instanceof Error ? error.message : String(error) };
249
425
  }
250
426
  }
427
+ export function readMoeiconsConfig(root, sourceCatalog = catalog, options = {}) {
428
+ return validateConfigDocument(loadConfigDocument(root), sourceCatalog, options);
429
+ }
251
430
  /** Resolve the canonical v2 target from either a normalized v2 or legacy v1 input. */
252
431
  function targetFromInput(current) {
253
432
  if ("target" in current)
@@ -255,14 +434,21 @@ function targetFromInput(current) {
255
434
  return current.framework;
256
435
  }
257
436
  export function mergeMoeiconsConfig(current, patch) {
437
+ const integration = patch.integration ?? ("integration" in current ? current.integration : undefined);
258
438
  return {
259
- schemaVersion: 2,
439
+ schemaVersion: integration || current.schemaVersion === 3 ? 3 : 2,
260
440
  tier: patch.tier ?? current.tier,
261
441
  target: patch.target ?? targetFromInput(current),
262
442
  outputDir: patch.outputDir ?? current.outputDir,
263
443
  defaultTheme: patch.defaultTheme ?? current.defaultTheme,
264
444
  themes: patch.themes ?? current.themes,
265
445
  icons: patch.icons ?? current.icons,
446
+ ...(patch.downloadMode !== undefined
447
+ ? { downloadMode: patch.downloadMode }
448
+ : current.downloadMode !== undefined
449
+ ? { downloadMode: current.downloadMode }
450
+ : {}),
451
+ ...(integration ? { integration } : {}),
266
452
  ...(patch.missingIconPolicy !== undefined
267
453
  ? { missingIconPolicy: patch.missingIconPolicy }
268
454
  : current.missingIconPolicy !== undefined
@@ -285,69 +471,15 @@ export function createMoeiconsConfig(options) {
285
471
  missingIconPolicy: "fallback",
286
472
  };
287
473
  }
288
- /** Render the editable, grouped JSONC skeleton used by `init`. */
474
+ /**
475
+ * Render the editable, grouped JSONC skeleton used by `init`.
476
+ *
477
+ * Delegates to the canonical renderer bundled from moe-icons-code-library
478
+ * `config-package` (see `src/config-package/generated` + SOURCE.json). The CLI
479
+ * no longer keeps a second full template; a schema v1/v2 default file is
480
+ * produced unless an integration result is supplied (v3 + `integration`).
481
+ */
289
482
  export function renderMoeiconsConfigJsonc(options) {
290
- const tier = options.tier ?? "free";
291
- const target = options.target ?? options.framework ?? "react";
292
- // Collect style groups available for this tier, ordered by id.
293
- const availableGroups = catalog.styleGroups
294
- .filter((g) => g.tiers.includes(tier))
295
- .sort((a, b) => a.id.localeCompare(b.id));
296
- const defaultThemeName = "outline";
297
- // Build theme entries: one per available SVG group (bitmap groups get their
298
- // own entry with format/imageSize annotations).
299
- const themeLines = [];
300
- for (const group of availableGroups) {
301
- const themeName = group.id.replace(/^moe-/, "");
302
- if (group.type === "bitmap") {
303
- // List available format/imageSize combinations as inline comments.
304
- const formatsComment = `// format options: ${group.formats.join(", ")}`;
305
- const sizesComment = `// imageSize options: ${group.imageSizes.join(", ")}`;
306
- themeLines.push(` // ${themeName} — bitmap style (${group.id})`, ` // ${formatsComment}`, ` // ${sizesComment}`, ` // ${JSON.stringify(themeName)}: {`, ` // "styleGroup": ${JSON.stringify(group.id)},`, ` // "format": "webp",`, ` // "imageSize": 256`, ` // },`);
307
- }
308
- else {
309
- themeLines.push(` ${JSON.stringify(themeName)}: {`, ` "styleGroup": ${JSON.stringify(group.id)}`, ` },`);
310
- }
311
- }
312
- // Collect icons: only those available in at least one style group of this tier,
313
- // grouped by prefix and default-selected (all selected).
314
- const tierGroupIds = new Set(availableGroups.map((g) => g.id));
315
- const groups = new Map();
316
- for (const icon of catalog.icons) {
317
- if (!icon.availableIn.some((sg) => tierGroupIds.has(sg)))
318
- continue;
319
- const ids = groups.get(icon.prefix) ?? [];
320
- ids.push(icon.id);
321
- groups.set(icon.prefix, ids);
322
- }
323
- const iconLines = [...groups.entries()]
324
- .sort(([a], [b]) => a.localeCompare(b))
325
- .flatMap(([prefix, ids]) => [
326
- ` // ${prefix} icons`,
327
- ` ${JSON.stringify(prefix)}: [`,
328
- ...ids.map((id) => ` ${JSON.stringify(id)},`),
329
- " ],",
330
- ]);
331
- return [
332
- "{",
333
- ` "schemaVersion": 2,`,
334
- ` "tier": ${JSON.stringify(tier)},`,
335
- ` "target": ${JSON.stringify(target)},`,
336
- ` "outputDir": "src/moeicons",`,
337
- ` "defaultTheme": ${JSON.stringify(defaultThemeName)},`,
338
- ' "themes": {',
339
- ...themeLines,
340
- ` // Set defaultTheme above to one of: ${availableGroups
341
- .filter((g) => g.type !== "bitmap")
342
- .map((g) => JSON.stringify(g.id.replace(/^moe-/, "")))
343
- .join(", ")}`,
344
- " },",
345
- " // Comment out individual IDs or a complete prefix group to exclude it.",
346
- ' "icons": {',
347
- ...iconLines,
348
- " },",
349
- ' "missingIconPolicy": "fallback"',
350
- "}",
351
- "",
352
- ].join("\n");
483
+ const generated = loadGeneratedConfigPackage();
484
+ return generated.renderMoeiconsConfigJsonc({ ...options, catalog });
353
485
  }