@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
@@ -1,176 +1,98 @@
1
- /**
2
- * Minimal MCP (Model Context Protocol) stdio server. Registers only v1 tools:
3
- * list_icon_groups, get_account, install_icon_group. Protocol data goes only
4
- * to stdout; logs go to stderr. Graceful shutdown on SIGINT/SIGTERM.
5
- */
1
+ /** Local, newline-delimited MCP transport. Business failures are tool results. */
6
2
  import { CLI_VERSION } from "../ui/banner.js";
7
- function validateArgs(method, params, required) {
8
- if (!params) {
9
- return undefined;
10
- }
11
- for (const key of required) {
12
- if (typeof params[key] !== "string" || params[key].length === 0) {
13
- return undefined;
14
- }
15
- }
16
- const out = {};
17
- for (const key of required) {
18
- out[key] = String(params[key]);
19
- }
20
- return out;
3
+ export class McpArgumentError extends Error {
21
4
  }
22
- /**
23
- * Create an MCP server bound to injected streams. Returns a dispatcher that
24
- * handles one decoded JSON-RPC message.
25
- */
5
+ export const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
6
+ export function safeMcpValue(value, projectRoot) {
7
+ if (typeof value === "string")
8
+ return (projectRoot ? value.split(projectRoot).join(".") : value).replace(/https?:\/\/[^\s"<>]+\?[^\s"<>]+/g, "[redacted URL]").replace(/(?:\/Users\/|\/home\/)[^/\s]+/g, "[home]");
9
+ if (Array.isArray(value))
10
+ return value.map((entry) => safeMcpValue(entry, projectRoot));
11
+ if (!isRecord(value))
12
+ return value;
13
+ return Object.fromEntries(Object.entries(value).filter(([key]) => !/(token|secret|authorization|signedurl|verifier)/i.test(key)).map(([key, entry]) => [key, safeMcpValue(entry, projectRoot)]));
14
+ }
15
+ const objectSchema = (properties = {}, required = []) => ({ type: "object", properties, required, additionalProperties: false });
16
+ const legacyTools = [
17
+ { name: "list_icon_groups", description: "List available style groups and catalog identity", inputSchema: objectSchema() },
18
+ { name: "get_account", description: "Read the current account and entitlement without credentials", inputSchema: objectSchema() },
19
+ { name: "install_icon_group", description: "Deprecated: use configuration and install instead", inputSchema: objectSchema({ groupId: { type: "string" }, projectPath: { type: "string" } }, ["groupId", "projectPath"]) },
20
+ ];
26
21
  export function createMcpServer(deps) {
27
- const handle = async (message) => {
28
- if (message.jsonrpc !== "2.0") {
29
- return { jsonrpc: "2.0", id: message.id, error: { code: -32600, message: "invalid request" } };
22
+ const tools = deps.services.tools ?? legacyTools;
23
+ const handle = async (value) => {
24
+ const id = isRecord(value) && (typeof value.id === "string" || typeof value.id === "number") ? value.id : null;
25
+ const error = (code, message) => ({ jsonrpc: "2.0", id, error: { code, message } });
26
+ if (!isRecord(value) || value.jsonrpc !== "2.0" || typeof value.method !== "string" || (value.id !== undefined && id === null))
27
+ return error(-32600, "invalid request");
28
+ if (value.id === undefined)
29
+ return undefined; // Never respond to notifications.
30
+ if (value.params !== undefined && !isRecord(value.params))
31
+ return error(-32602, "params must be an object");
32
+ const params = value.params ?? {};
33
+ if (value.method === "initialize") {
34
+ const requested = params.protocolVersion;
35
+ const supported = ["2024-11-05", "2025-03-26", "2025-06-18"];
36
+ const protocolVersion = typeof requested === "string" && supported.includes(requested) ? requested : "2024-11-05";
37
+ return { jsonrpc: "2.0", id, result: { protocolVersion, capabilities: { tools: {} }, serverInfo: { name: "moeicons", version: CLI_VERSION } } };
30
38
  }
31
- switch (message.method) {
32
- case "initialize":
33
- return {
34
- jsonrpc: "2.0",
35
- id: message.id,
36
- result: {
37
- protocolVersion: "2024-11-05",
38
- capabilities: { tools: {} },
39
- serverInfo: { name: "moeicons", version: CLI_VERSION },
40
- },
41
- };
42
- case "tools/list":
43
- return {
44
- jsonrpc: "2.0",
45
- id: message.id,
46
- result: {
47
- tools: [
48
- {
49
- name: "list_icon_groups",
50
- description: "List available icon groups",
51
- inputSchema: { type: "object", properties: {} },
52
- },
53
- {
54
- name: "get_account",
55
- description: "Get the current account/tier",
56
- inputSchema: { type: "object", properties: {} },
57
- },
58
- {
59
- name: "install_icon_group",
60
- description: "Install an icon group into a project",
61
- inputSchema: {
62
- type: "object",
63
- properties: {
64
- groupId: { type: "string" },
65
- projectPath: { type: "string" },
66
- },
67
- required: ["groupId", "projectPath"],
68
- },
69
- },
70
- ],
71
- },
72
- };
73
- case "tools/call": {
74
- const toolName = message.params?.name;
75
- const args = message.params?.arguments ?? {};
76
- try {
77
- if (toolName === "list_icon_groups") {
78
- const groups = await deps.services.listIconGroups();
79
- return {
80
- jsonrpc: "2.0",
81
- id: message.id,
82
- result: { content: [{ type: "text", text: JSON.stringify(groups) }] },
83
- };
84
- }
85
- if (toolName === "get_account") {
86
- const account = await deps.services.getAccount();
87
- return {
88
- jsonrpc: "2.0",
89
- id: message.id,
90
- result: {
91
- content: [
92
- { type: "text", text: account ? JSON.stringify(account) : "not logged in" },
93
- ],
94
- },
95
- };
96
- }
97
- if (toolName === "install_icon_group") {
98
- const validated = validateArgs(toolName, args, ["groupId", "projectPath"]);
99
- if (!validated) {
100
- return {
101
- jsonrpc: "2.0",
102
- id: message.id,
103
- error: { code: -32602, message: "groupId and projectPath are required strings" },
104
- };
105
- }
106
- const projectPath = validated["projectPath"];
107
- const groupId = validated["groupId"];
108
- if (!projectPath || !groupId) {
109
- return {
110
- jsonrpc: "2.0",
111
- id: message.id,
112
- error: { code: -32602, message: "groupId and projectPath are required strings" },
113
- };
114
- }
115
- if (projectPath.split(/[\\/]/).includes("..")) {
116
- return {
117
- jsonrpc: "2.0",
118
- id: message.id,
119
- error: { code: -32602, message: "projectPath must not contain path traversal" },
120
- };
121
- }
122
- const result = await deps.services.installIconGroup({
123
- groupId,
124
- projectPath,
125
- });
126
- return {
127
- jsonrpc: "2.0",
128
- id: message.id,
129
- result: { content: [{ type: "text", text: JSON.stringify(result) }] },
130
- };
131
- }
132
- return {
133
- jsonrpc: "2.0",
134
- id: message.id,
135
- error: { code: -32601, message: `unknown tool: ${String(toolName)}` },
136
- };
137
- }
138
- catch (error) {
139
- deps.stderr(`mcp tool error: ${String(error)}\n`);
140
- return {
141
- jsonrpc: "2.0",
142
- id: message.id,
143
- error: { code: -32000, message: String(error) },
144
- };
145
- }
39
+ if (value.method === "ping")
40
+ return { jsonrpc: "2.0", id, result: {} };
41
+ if (value.method === "tools/list")
42
+ return { jsonrpc: "2.0", id, result: { tools } };
43
+ if (value.method !== "tools/call")
44
+ return error(-32601, `method not found: ${value.method}`);
45
+ const name = params.name;
46
+ if (typeof name !== "string" || !tools.some((tool) => tool.name === name))
47
+ return error(-32601, "unknown tool");
48
+ if (params.arguments !== undefined && !isRecord(params.arguments))
49
+ return error(-32602, "arguments must be an object");
50
+ const args = params.arguments ?? {};
51
+ try {
52
+ let result;
53
+ if (deps.services.callTool)
54
+ result = await deps.services.callTool(name, args);
55
+ else if (name === "list_icon_groups")
56
+ result = await deps.services.listIconGroups();
57
+ else if (name === "get_account")
58
+ result = await deps.services.getAccount() ?? "not logged in";
59
+ else {
60
+ if (typeof args.groupId !== "string" || !args.groupId || typeof args.projectPath !== "string" || !args.projectPath)
61
+ throw new McpArgumentError("groupId and projectPath are required strings");
62
+ if (args.projectPath.split(/[\\/]/).includes(".."))
63
+ throw new McpArgumentError("projectPath must not contain path traversal");
64
+ result = await deps.services.installIconGroup({ groupId: args.groupId, projectPath: args.projectPath });
146
65
  }
147
- default:
148
- return { jsonrpc: "2.0", id: message.id, error: { code: -32601, message: `method not found: ${message.method}` } };
66
+ const safe = safeMcpValue(result, deps.projectRoot);
67
+ return { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: typeof safe === "string" ? safe : JSON.stringify(safe) }], ...(isRecord(result) && result.ok === false ? { isError: true } : {}) } };
68
+ }
69
+ catch (cause) {
70
+ if (cause instanceof McpArgumentError)
71
+ return error(-32602, cause.message);
72
+ const message = String(safeMcpValue(cause instanceof Error ? cause.message : String(cause), deps.projectRoot));
73
+ deps.stderr(`mcp tool error: ${message}\n`);
74
+ return { jsonrpc: "2.0", id, result: { isError: true, content: [{ type: "text", text: JSON.stringify({ ok: false, code: isRecord(cause) && typeof cause.code === "string" ? cause.code : "TOOL_ERROR", message }) }] } };
149
75
  }
150
76
  };
151
77
  return { handle };
152
78
  }
153
- /**
154
- * Read JSON-RPC messages from an async iterable of lines and write responses to
155
- * stdout. Used by the stdio transport.
156
- */
157
79
  export async function runMcpStdio(deps) {
158
80
  const server = createMcpServer(deps);
159
81
  for await (const line of deps.lines) {
160
- const trimmed = line.trim();
161
- if (!trimmed)
82
+ if (deps.signal?.aborted)
83
+ break;
84
+ if (!line.trim())
162
85
  continue;
163
86
  let message;
164
87
  try {
165
- message = JSON.parse(trimmed);
88
+ message = JSON.parse(line);
166
89
  }
167
90
  catch {
168
91
  deps.stdout(JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "parse error" } }) + "\n");
169
92
  continue;
170
93
  }
171
94
  const response = await server.handle(message);
172
- if (response) {
95
+ if (response)
173
96
  deps.stdout(JSON.stringify(response) + "\n");
174
- }
175
97
  }
176
98
  }
@@ -0,0 +1,4 @@
1
+ import type { CliRuntime } from "../cli.js";
2
+ import { type McpTool, type McpServices } from "./server.js";
3
+ export declare const projectTools: readonly [McpTool, McpTool, McpTool, McpTool, McpTool, McpTool, McpTool, McpTool, McpTool, McpTool, McpTool];
4
+ export declare function createProjectMcpServices(runtime: CliRuntime, execute: (argv: readonly string[]) => Promise<unknown>): McpServices;
@@ -0,0 +1,230 @@
1
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
2
+ import { relative, resolve, basename } from "node:path";
3
+ import { applyEdits, modify, parse } from "jsonc-parser";
4
+ import { catalog } from "../catalog/catalog.js";
5
+ import { loadInstalledCatalogState } from "../core/generate.js";
6
+ import { buildInitPlan, collectSafeFixes, doctorJson, runDoctorApply, runDoctorDiagnose } from "../core/doctor.js";
7
+ import { findConfigFile, loadConfigDocument, validateConfigDocument } from "../project/config.js";
8
+ import { safeManagedPath } from "../project/install.js";
9
+ import { sha256Bytes } from "../project/install-metadata.js";
10
+ import { allowLocalTestFromEnv } from "../core/local-test-env.js";
11
+ import { toProxyName } from "../core/icon-names.js";
12
+ import { McpArgumentError, isRecord } from "./server.js";
13
+ const string = { type: "string" };
14
+ const strings = { type: "array", items: string, uniqueItems: true };
15
+ const boolean = { type: "boolean" };
16
+ const targetSchema = { type: "string", enum: ["vue", "react", "vanilla", "assets"] };
17
+ const tool = (name, description, properties = {}, required = []) => ({ name, description, inputSchema: { type: "object", properties: { projectPath: string, ...properties }, required, additionalProperties: false } });
18
+ export const projectTools = [
19
+ tool("list_icon_groups", "Read style groups from verified installed catalog, or identify bundled fallback"),
20
+ tool("get_account", "Read account/entitlement. Login outside MCP with moeicons login"),
21
+ tool("get_project_config", "Read normalized configuration, hash for update_config, and integration diagnosis"),
22
+ tool("find_icons", "Search icon IDs available in every requested style group", { query: string, styleGroups: strings, limit: { type: "integer", minimum: 1, maximum: 100 }, cursor: string }),
23
+ tool("get_usage_guide", "Read generated-component integration rules", { target: targetSchema }),
24
+ tool("init_config", "Initialize configuration and safe integration fixes. Explicit target resolves mixed frameworks", { target: targetSchema, integration: { type: "object", properties: { adapter: string, entry: string, style: string }, additionalProperties: false }, dryRun: boolean }),
25
+ tool("update_config", "Patch JSONC without overwriting unrelated settings. Reinstall then generate after changing icons", { expectedHash: { type: "string", pattern: "^[a-f0-9]{64}$" }, patch: { type: "object", properties: { tier: { enum: ["free", "pro"] }, target: targetSchema, outputDir: string, defaultTheme: string, downloadMode: { enum: ["auto", "icons", "full"] }, missingIconPolicy: { enum: ["error", "fallback"] }, integration: { type: "object" }, themes: { type: "object", properties: { upsert: { type: "object" }, remove: strings }, additionalProperties: false }, icons: { type: "object", properties: { add: strings, remove: strings }, additionalProperties: false }, themeIcons: { type: "object" } }, additionalProperties: false }, dryRun: boolean }, ["expectedHash", "patch"]),
26
+ tool("install", "Download and verify configured resources from Free/Pro release", { tier: { type: "string", enum: ["free", "pro"] } }, ["tier"]),
27
+ tool("update", "Update resource release and reconcile config (not CLI self-update)"),
28
+ tool("generate", "Generate code using installed verified resources; does not implicitly download"),
29
+ tool("install_icon_group", "Deprecated. Configure style groups then call install", { groupId: string }, ["groupId", "projectPath"]),
30
+ ];
31
+ function keys(value, allowed) {
32
+ if (Object.keys(value).some((key) => !allowed.includes(key)))
33
+ throw new McpArgumentError("unknown argument field");
34
+ }
35
+ function list(value) {
36
+ if (value === undefined)
37
+ return [];
38
+ if (!Array.isArray(value) || value.some((x) => typeof x !== "string" || !x) || new Set(value).size !== value.length)
39
+ throw new McpArgumentError("expected unique string array");
40
+ return value;
41
+ }
42
+ function changes(value) {
43
+ if (!isRecord(value))
44
+ throw new McpArgumentError("icon changes must be an object");
45
+ keys(value, ["add", "remove"]);
46
+ const add = list(value.add), remove = list(value.remove);
47
+ if ([...add, ...remove].some((id) => !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(id)))
48
+ throw new McpArgumentError("icon IDs must be lowercase kebab-case");
49
+ if (add.some((x) => remove.includes(x)))
50
+ throw new McpArgumentError("cannot add and remove the same icon");
51
+ return { add, remove };
52
+ }
53
+ const fail = (code, message) => ({ ok: false, code, message });
54
+ export function createProjectMcpServices(runtime, execute) {
55
+ const root = () => realpathSync(runtime.cwd());
56
+ function source() {
57
+ const state = loadInstalledCatalogState(root(), { readFileSync, existsSync }, { allowLocalTest: allowLocalTestFromEnv(runtime.env) });
58
+ if (state.status === "invalid")
59
+ throw new Error(state.message);
60
+ return { catalog: state.status === "ok" ? state.catalog : catalog, source: state.status === "ok" ? "installed" : "bundled" };
61
+ }
62
+ function loaded() {
63
+ const path = findConfigFile(root());
64
+ if (!path)
65
+ throw new Error("configuration missing; call init_config first");
66
+ safeManagedPath(root(), relative(root(), path));
67
+ const state = source();
68
+ const config = validateConfigDocument(loadConfigDocument(root()), state.catalog, { lenientCatalog: state.source === "bundled" });
69
+ if (config.kind !== "ok")
70
+ throw new Error(config.kind === "invalid" ? config.message : `configuration ${config.kind}`);
71
+ safeManagedPath(root(), config.config.outputDir);
72
+ return { ...state, path, config: config.config, raw: readFileSync(path, "utf8") };
73
+ }
74
+ const installIconGroup = () => Promise.resolve({ ok: false, message: 'single style-group install is not supported — run "moeicons install free" or "moeicons install pro"' });
75
+ const callTool = async (name, args) => {
76
+ if (["init_config", "update_config", "install", "update", "generate"].includes(name) && !existsSync(resolve(root(), "package.json")))
77
+ throw new Error("bind MCP cwd to the project root containing package.json");
78
+ const def = projectTools.find((entry) => entry.name === name);
79
+ if (!def)
80
+ throw new McpArgumentError("unknown tool");
81
+ keys(args, Object.keys(def.inputSchema.properties));
82
+ if (args.projectPath !== undefined && (typeof args.projectPath !== "string" || realpathSync(resolve(root(), args.projectPath)) !== root()))
83
+ throw new McpArgumentError("projectPath must resolve to the bound project root");
84
+ for (const key of ["dryRun"])
85
+ if (args[key] !== undefined && typeof args[key] !== "boolean")
86
+ throw new McpArgumentError(`${key} must be boolean`);
87
+ if (args.target !== undefined && !["react", "vue", "vanilla", "assets"].includes(String(args.target)))
88
+ throw new McpArgumentError("invalid target");
89
+ if (name === "install_icon_group") {
90
+ if (typeof args.groupId !== "string" || typeof args.projectPath !== "string")
91
+ throw new McpArgumentError("groupId and projectPath are required strings");
92
+ return installIconGroup();
93
+ }
94
+ if (name === "list_icon_groups") {
95
+ const s = source();
96
+ return { ok: true, source: s.source, catalogVersion: s.catalog.catalogVersion, sourceVersion: s.catalog.sourceVersion, groups: s.catalog.styleGroups };
97
+ }
98
+ if (name === "get_account") {
99
+ const result = await execute(["account", "--json"]);
100
+ if (!isRecord(result) || result.ok === false)
101
+ return result;
102
+ const account = isRecord(result.account) ? result.account : {};
103
+ return { ok: true, environment: result.environment, account: Object.fromEntries(["accountId", "tier", "entitlementStatus", "expiresAt"].filter((key) => account[key] !== undefined).map((key) => [key, account[key]])) };
104
+ }
105
+ if (name === "get_project_config") {
106
+ const s = loaded();
107
+ return { ok: true, path: basename(s.path), config: s.config, hash: sha256Bytes(s.raw), availability: s.source === "installed" ? "verified" : "unverified", doctor: doctorJson(runDoctorDiagnose(root(), s.catalog).report) };
108
+ }
109
+ if (name === "find_icons") {
110
+ if (args.query !== undefined && typeof args.query !== "string")
111
+ throw new McpArgumentError("query must be a string");
112
+ const groups = list(args.styleGroups), s = source();
113
+ if (groups.some((id) => !s.catalog.styleGroups.some((g) => g.id === id)))
114
+ return fail("UNAVAILABLE_GROUP", "requested style group is absent from this catalog");
115
+ const limit = args.limit ?? 30;
116
+ const cursor = args.cursor ?? "0";
117
+ if (typeof limit !== "number" || !Number.isInteger(limit) || limit < 1 || limit > 100 || typeof cursor !== "string" || !/^\d+$/.test(cursor) || !Number.isSafeInteger(Number(cursor)))
118
+ throw new McpArgumentError("invalid limit/cursor");
119
+ const query = String(args.query ?? "").toLowerCase();
120
+ const items = s.catalog.icons.filter((icon) => groups.every((id) => icon.availableIn.includes(id)) && `${icon.id} ${icon.label} ${(icon.keywords ?? []).join(" ")}`.toLowerCase().includes(query));
121
+ const offset = Number(cursor);
122
+ return { ok: true, source: s.source, catalogVersion: s.catalog.catalogVersion, items: items.slice(offset, offset + limit).map((icon) => ({ id: icon.id, exportName: toProxyName(icon.id), availableIn: icon.availableIn })), nextCursor: offset + limit < items.length ? String(offset + limit) : null };
123
+ }
124
+ if (name === "get_usage_guide")
125
+ return { ok: true, target: args.target ?? "vue", guide: "Run init_config, read config/hash, find icons, update_config, install, then generate. Changing selections requires reinstall/update before generation. Install the generated runtime dependencies clsx and tailwind-merge (use a Tailwind-compatible version), as documented in the manual. Never edit generated files. Import named icon proxies from the generated index. Vue: wrap the application including navigation/footer in MoeiconsProvider and bind theme to an exact configured key; import components explicitly. React: wrap with MoeiconsProvider. Vanilla: use createMoeiconsRuntime and generated create functions. Assets: consume the generated assets. Inspect index.ts and installed MANUAL for exact exports. Decorative icons: aria-hidden and empty alt; icon-only controls need an accessible label. Never embed CLI credentials in browser code.", manual: existsSync(resolve(root(), ".moeicons/MANUAL.md")) ? ".moeicons/MANUAL.md" : null };
126
+ if (name === "init_config") {
127
+ let confirmed;
128
+ if (args.integration !== undefined) {
129
+ if (!isRecord(args.integration))
130
+ throw new McpArgumentError("integration must be an object");
131
+ keys(args.integration, ["adapter", "entry", "style"]);
132
+ for (const [key, value] of Object.entries(args.integration)) {
133
+ if (typeof value !== "string" || !value)
134
+ throw new McpArgumentError("integration fields must be strings");
135
+ if (key !== "adapter")
136
+ safeManagedPath(root(), value);
137
+ else if (!["vite-vue", "vite-react", "next-app", "next-pages", "nuxt", "vanilla", "assets-only"].includes(value))
138
+ throw new McpArgumentError("invalid adapter");
139
+ }
140
+ confirmed = args.integration;
141
+ }
142
+ if (args.target && confirmed?.adapter) {
143
+ const adapterTarget = confirmed.adapter === "vite-vue" || confirmed.adapter === "nuxt" ? "vue" : confirmed.adapter === "vanilla" ? "vanilla" : confirmed.adapter === "assets-only" ? "assets" : "react";
144
+ if (args.target !== adapterTarget)
145
+ throw new McpArgumentError("target and integration.adapter disagree");
146
+ }
147
+ const { outcome } = buildInitPlan(root(), args.target, confirmed, source().catalog);
148
+ if (outcome.report.anchors.some((anchor) => anchor.status === "ambiguous" || anchor.kind === "manifest" && anchor.status !== "ok"))
149
+ return fail("AMBIGUOUS_PROJECT", "select target/integration before initialization");
150
+ const fixes = collectSafeFixes(outcome);
151
+ if (args.dryRun)
152
+ return { ok: true, diffs: fixes, doctor: doctorJson(outcome.report) };
153
+ return runDoctorApply(root(), fixes);
154
+ }
155
+ if (name === "update_config") {
156
+ if (typeof args.expectedHash !== "string" || !/^[a-f0-9]{64}$/.test(args.expectedHash) || !isRecord(args.patch))
157
+ throw new McpArgumentError("expectedHash and patch are required");
158
+ const s = loaded();
159
+ if (sha256Bytes(s.raw) !== args.expectedHash)
160
+ return fail("CONFIG_CHANGED", "configuration changed; read it again before retrying");
161
+ const patch = args.patch;
162
+ keys(patch, ["tier", "target", "outputDir", "defaultTheme", "downloadMode", "missingIconPolicy", "integration", "themes", "icons", "themeIcons"]);
163
+ let text = s.raw;
164
+ const set = (path, value) => { text = applyEdits(text, modify(text, path, value, { formattingOptions: { insertSpaces: true, tabSize: 2 } })); };
165
+ for (const [key, value] of Object.entries(patch))
166
+ if (!["themes", "icons", "themeIcons"].includes(key))
167
+ set([key], value);
168
+ if (patch.integration !== undefined)
169
+ set(["schemaVersion"], 3);
170
+ if (patch.themes !== undefined) {
171
+ if (!isRecord(patch.themes))
172
+ throw new McpArgumentError("themes must be an object");
173
+ keys(patch.themes, ["upsert", "remove"]);
174
+ const remove = list(patch.themes.remove), upsert = patch.themes.upsert ?? {};
175
+ if (!isRecord(upsert) || remove.some((id) => id in upsert))
176
+ throw new McpArgumentError("invalid theme changes");
177
+ for (const key of remove)
178
+ set(["themes", key], undefined);
179
+ for (const [key, value] of Object.entries(upsert))
180
+ set(["themes", key], value);
181
+ }
182
+ if (patch.icons !== undefined) {
183
+ const delta = changes(patch.icons), doc = parse(text);
184
+ if (Array.isArray(doc.icons))
185
+ set(["icons"], [...new Set([...list(doc.icons).filter((id) => !delta.remove.includes(id)), ...delta.add])]);
186
+ else if (isRecord(doc.icons)) {
187
+ for (const [prefix, ids] of Object.entries(doc.icons))
188
+ set(["icons", prefix], list(ids).filter((id) => !delta.remove.includes(id)));
189
+ for (const id of delta.add) {
190
+ const prefix = s.catalog.icons.find((icon) => icon.id === id)?.prefix ?? id.split("-")[0];
191
+ const current = parse(text);
192
+ set(["icons", prefix], [...new Set([...list(current.icons[prefix]), id])]);
193
+ }
194
+ }
195
+ else
196
+ throw new McpArgumentError("unsupported icons structure");
197
+ }
198
+ if (patch.themeIcons !== undefined) {
199
+ if (!isRecord(patch.themeIcons))
200
+ throw new McpArgumentError("themeIcons must be an object");
201
+ for (const [key, value] of Object.entries(patch.themeIcons)) {
202
+ const doc = parse(text);
203
+ if (!doc.themes[key])
204
+ throw new McpArgumentError("unknown theme");
205
+ const delta = changes(value);
206
+ set(["themes", key, "icons"], [...new Set([...list(doc.themes[key].icons).filter((id) => !delta.remove.includes(id)), ...delta.add])]);
207
+ }
208
+ }
209
+ const validated = validateConfigDocument({ kind: "ok", value: parse(text), version: s.config.schemaVersion === 3 || patch.integration ? 3 : 2 }, s.catalog, { lenientCatalog: s.source === "bundled" });
210
+ if (validated.kind !== "ok")
211
+ return fail("INVALID_CONFIG", validated.kind === "invalid" ? validated.message : validated.kind);
212
+ safeManagedPath(root(), validated.config.outputDir);
213
+ const diffs = [{ kind: "replace", path: basename(s.path), before: s.raw, after: text }];
214
+ if (args.dryRun)
215
+ return { ok: true, diffs, requiresInstall: true, requiresGenerate: true };
216
+ const applied = runDoctorApply(root(), diffs);
217
+ return { ...applied, hash: sha256Bytes(text), availability: s.source === "installed" ? "verified" : "unverified", requiresInstall: true, requiresGenerate: true };
218
+ }
219
+ const s = loaded();
220
+ if (name === "install") {
221
+ if (args.tier !== "free" && args.tier !== "pro")
222
+ throw new McpArgumentError("tier must be free or pro");
223
+ if (args.tier !== s.config.tier)
224
+ return fail("TIER_MISMATCH", "install tier must match configuration");
225
+ return execute(["install", args.tier, "--yes", "--json"]);
226
+ }
227
+ return execute([name, "--yes", "--json"]);
228
+ };
229
+ return { tools: projectTools, callTool, listIconGroups: () => callTool("list_icon_groups", {}), getAccount: () => callTool("get_account", {}), installIconGroup };
230
+ }
@@ -0,0 +1,38 @@
1
+ import { type MetadataFileDigest } from "./manifest.js";
2
+ export declare const METADATA_ARCHIVE_ENTRIES: readonly ["MANUAL.md", "catalog.json", "manifest.json"];
3
+ export interface MetadataArchiveFiles {
4
+ readonly manifestJson: string;
5
+ readonly manualMd: string;
6
+ readonly catalogJson: string;
7
+ }
8
+ export type MetadataArchiveVerification = {
9
+ readonly expectedCatalogSha: string;
10
+ readonly expectedTier: "free" | "pro";
11
+ readonly expectedVersion: string;
12
+ /** Free path: the release descriptor provides per-file size + sha256. */
13
+ readonly perFileDigests: Readonly<Record<(typeof METADATA_ARCHIVE_ENTRIES)[number], MetadataFileDigest>>;
14
+ /** ICON-E2E-0907 / E2E-E2: local-test manifests only. */
15
+ readonly allowLocalTest?: boolean;
16
+ } | {
17
+ readonly expectedCatalogSha: string;
18
+ readonly expectedTier: "free" | "pro";
19
+ readonly expectedVersion: string;
20
+ /** Pro path: the signed whole-archive sha256 was already verified. */
21
+ readonly perFileDigests?: undefined;
22
+ readonly allowLocalTest?: boolean;
23
+ };
24
+ export type MetadataArchiveResult = {
25
+ readonly kind: "ok";
26
+ readonly value: MetadataArchiveFiles;
27
+ } | {
28
+ readonly kind: "error";
29
+ readonly reason: "validation" | "checksum-mismatch";
30
+ readonly message: string;
31
+ };
32
+ /**
33
+ * Single implementation for unpacking + verifying a tier metadata archive.
34
+ * Free/Pro share the whole flow; they only differ in whether per-file digests
35
+ * are available (free descriptor) or the whole archive was already verified by
36
+ * its signed sha256 (pro).
37
+ */
38
+ export declare function extractAndVerifyMetadataArchive(bytes: Uint8Array, verification: MetadataArchiveVerification, maxExpandedBytes?: number): MetadataArchiveResult;
@@ -0,0 +1,94 @@
1
+ import { createHash } from "node:crypto";
2
+ import { extractTarGz, decodeUtf8 } from "../project/tar-gz.js";
3
+ import { parseMetadataManifest } from "./manifest.js";
4
+ export const METADATA_ARCHIVE_ENTRIES = ["MANUAL.md", "catalog.json", "manifest.json"];
5
+ /** Exact archive layout the contract requires: nothing more, nothing less. */
6
+ const ALLOWED_ENTRIES = new Set(METADATA_ARCHIVE_ENTRIES.map((name) => `metadata/${name}`));
7
+ function sha256Hex(bytes) {
8
+ return createHash("sha256").update(bytes).digest("hex");
9
+ }
10
+ function sha256HexOfString(value) {
11
+ return createHash("sha256").update(value).digest("hex");
12
+ }
13
+ /**
14
+ * Single implementation for unpacking + verifying a tier metadata archive.
15
+ * Free/Pro share the whole flow; they only differ in whether per-file digests
16
+ * are available (free descriptor) or the whole archive was already verified by
17
+ * its signed sha256 (pro).
18
+ */
19
+ export function extractAndVerifyMetadataArchive(bytes, verification, maxExpandedBytes = 8 * 1024 * 1024) {
20
+ const unpacked = extractTarGz(bytes, { maxEntries: 20_000, maxExpandedBytes });
21
+ if (unpacked.errors.length > 0) {
22
+ return { kind: "error", reason: "validation", message: unpacked.errors[0] ?? "metadata archive extract failed" };
23
+ }
24
+ // Strict whitelist: the archive must contain exactly the three frozen entries.
25
+ const actual = new Set(Object.keys(unpacked.files));
26
+ if (actual.size !== ALLOWED_ENTRIES.size || [...actual].some((name) => !ALLOWED_ENTRIES.has(name))) {
27
+ const extra = [...actual].filter((name) => !ALLOWED_ENTRIES.has(name));
28
+ const missing = [...ALLOWED_ENTRIES].filter((name) => !actual.has(name));
29
+ return {
30
+ kind: "error",
31
+ reason: "validation",
32
+ message: `metadata archive must contain exactly the frozen files${extra.length ? `; unexpected: ${extra.join(", ")}` : ""}${missing.length ? `; missing: ${missing.join(", ")}` : ""}`,
33
+ };
34
+ }
35
+ let manifestJson = "";
36
+ let manualMd = "";
37
+ let catalogJson = "";
38
+ for (const name of METADATA_ARCHIVE_ENTRIES) {
39
+ const raw = unpacked.files[`metadata/${name}`];
40
+ const digest = verification.perFileDigests?.[name];
41
+ if (digest && (sha256Hex(raw) !== digest.sha256 || raw.byteLength !== digest.size)) {
42
+ return {
43
+ kind: "error",
44
+ reason: "checksum-mismatch",
45
+ message: `metadata/${name} digest mismatch: expected ${digest.sha256}, got ${sha256Hex(raw)}`,
46
+ };
47
+ }
48
+ const text = decodeUtf8(raw);
49
+ if (name === "MANUAL.md")
50
+ manualMd = text;
51
+ else if (name === "catalog.json")
52
+ catalogJson = text;
53
+ else
54
+ manifestJson = text;
55
+ }
56
+ if (sha256HexOfString(catalogJson) !== verification.expectedCatalogSha) {
57
+ return {
58
+ kind: "error",
59
+ reason: "checksum-mismatch",
60
+ message: "metadata catalog.json does not match the code archive catalog",
61
+ };
62
+ }
63
+ let manifest;
64
+ try {
65
+ manifest = parseMetadataManifest(manifestJson, {
66
+ allowLocalTest: verification.allowLocalTest === true,
67
+ });
68
+ }
69
+ catch (error) {
70
+ return { kind: "error", reason: "validation", message: error instanceof Error ? error.message : String(error) };
71
+ }
72
+ if (manifest.tier !== verification.expectedTier) {
73
+ return {
74
+ kind: "error",
75
+ reason: "validation",
76
+ message: `manifest tier ${manifest.tier} does not match ${verification.expectedTier}`,
77
+ };
78
+ }
79
+ if (manifest.libraryVersion !== verification.expectedVersion) {
80
+ return {
81
+ kind: "error",
82
+ reason: "validation",
83
+ message: `manifest libraryVersion ${manifest.libraryVersion} does not match release ${verification.expectedVersion}`,
84
+ };
85
+ }
86
+ if (manifest.files["catalog.json"].sha256 !== verification.expectedCatalogSha) {
87
+ return {
88
+ kind: "error",
89
+ reason: "checksum-mismatch",
90
+ message: "manifest catalog digest does not match the code archive catalog",
91
+ };
92
+ }
93
+ return { kind: "ok", value: { manifestJson, manualMd, catalogJson } };
94
+ }