@moewolf/moe-icons-cli 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +249 -10
- package/bin/check-node.cjs +36 -0
- package/bin/moeicons.js +12 -1
- package/dist/api/client.d.ts +13 -2
- package/dist/api/client.js +145 -21
- package/dist/auth/device-login.d.ts +10 -7
- package/dist/auth/device-login.js +60 -37
- package/dist/auth/login-schemas.d.ts +24 -0
- package/dist/auth/login-schemas.js +67 -0
- package/dist/catalog/catalog.d.ts +11 -0
- package/dist/catalog/catalog.js +43 -0
- package/dist/catalog/catalog.json +12768 -3965
- package/dist/cli.d.ts +11 -2
- package/dist/cli.js +773 -147
- package/dist/commands/parser.d.ts +13 -2
- package/dist/commands/parser.js +138 -50
- package/dist/config-package/generated/SOURCE.json +25 -0
- package/dist/config-package/generated/moeicons-config.schema.json +191 -0
- package/dist/config-package/generated/moeicons.config.jsonc +42 -0
- package/dist/config-package/generated/render-config.cjs +145 -0
- package/dist/config-package/generated/validate-config.cjs +293 -0
- package/dist/config-package/generated-config.d.ts +32 -0
- package/dist/config-package/generated-config.js +19 -0
- package/dist/core/auth.d.ts +24 -0
- package/dist/core/auth.js +127 -8
- package/dist/core/bitmap-assets.d.ts +19 -5
- package/dist/core/bitmap-assets.js +40 -31
- package/dist/core/bitmap-shard-budget.d.ts +43 -0
- package/dist/core/bitmap-shard-budget.js +16 -0
- package/dist/core/bitmap-shard-cache.d.ts +68 -0
- package/dist/core/bitmap-shard-cache.js +115 -0
- package/dist/core/bitmap-shard-download.d.ts +130 -0
- package/dist/core/bitmap-shard-download.js +359 -0
- package/dist/core/bitmap-shard-resolver.d.ts +129 -0
- package/dist/core/bitmap-shard-resolver.js +262 -0
- package/dist/core/bitmap-shards.d.ts +70 -0
- package/dist/core/bitmap-shards.js +249 -0
- package/dist/core/bootstrap.d.ts +37 -0
- package/dist/core/bootstrap.js +54 -0
- package/dist/core/cache.d.ts +34 -0
- package/dist/core/cache.js +96 -0
- package/dist/core/context.d.ts +26 -0
- package/dist/core/doctor.d.ts +48 -0
- package/dist/core/doctor.js +190 -0
- package/dist/core/free-download.d.ts +64 -6
- package/dist/core/free-download.js +402 -30
- package/dist/core/generate.d.ts +29 -0
- package/dist/core/generate.js +341 -65
- package/dist/core/icon-selection.d.ts +8 -0
- package/dist/core/icon-selection.js +19 -0
- package/dist/core/install.d.ts +14 -0
- package/dist/core/install.js +132 -13
- package/dist/core/library-update.d.ts +5 -1
- package/dist/core/library-update.js +176 -17
- package/dist/core/local-test-env.d.ts +24 -0
- package/dist/core/local-test-env.js +43 -0
- package/dist/core/metadata-sync.d.ts +30 -0
- package/dist/core/metadata-sync.js +97 -0
- package/dist/core/pro-download.d.ts +63 -0
- package/dist/core/pro-download.js +247 -27
- package/dist/core/pro-install.d.ts +11 -3
- package/dist/core/pro-install.js +181 -19
- package/dist/core/pro-resources.d.ts +79 -0
- package/dist/core/pro-resources.js +204 -0
- package/dist/core/pro-selected-download.d.ts +18 -0
- package/dist/core/pro-selected-download.js +269 -0
- package/dist/core/release-descriptor.d.ts +42 -1
- package/dist/core/release-descriptor.js +117 -9
- package/dist/core/resource-variant.d.ts +27 -10
- package/dist/core/resource-variant.js +75 -34
- package/dist/core/selected-resources.d.ts +59 -0
- package/dist/core/selected-resources.js +350 -0
- package/dist/core/signed-artifact.d.ts +8 -1
- package/dist/core/signed-artifact.js +35 -11
- package/dist/core/target-subtree.d.ts +8 -0
- package/dist/core/target-subtree.js +54 -3
- package/dist/core/version-service.d.ts +11 -0
- package/dist/core/version-service.js +129 -24
- package/dist/core/wizard.d.ts +20 -17
- package/dist/core/wizard.js +66 -28
- package/dist/errors/errors.d.ts +2 -1
- package/dist/errors/errors.js +1 -0
- package/dist/generator/generate.d.ts +1 -1
- package/dist/generator/generate.js +420 -213
- package/dist/generator/shared/SOURCE.json +7 -0
- package/dist/generator/shared/svg-model.cjs +156 -0
- package/dist/generator/svg-model.d.ts +16 -0
- package/dist/generator/svg-model.js +4 -0
- package/dist/generator/theme-resolve.d.ts +2 -0
- package/dist/generator/theme-resolve.js +57 -26
- package/dist/mcp/server.d.ts +23 -36
- package/dist/mcp/server.js +74 -152
- package/dist/mcp/services.d.ts +4 -0
- package/dist/mcp/services.js +230 -0
- package/dist/metadata/archive.d.ts +38 -0
- package/dist/metadata/archive.js +94 -0
- package/dist/metadata/manifest.d.ts +33 -0
- package/dist/metadata/manifest.js +136 -0
- package/dist/metadata/version.d.ts +22 -0
- package/dist/metadata/version.js +53 -0
- package/dist/project/anchors/application.d.ts +13 -0
- package/dist/project/anchors/application.js +377 -0
- package/dist/project/anchors/apply.d.ts +28 -0
- package/dist/project/anchors/apply.js +65 -0
- package/dist/project/anchors/diagnose.d.ts +26 -0
- package/dist/project/anchors/diagnose.js +90 -0
- package/dist/project/anchors/helpers.d.ts +20 -0
- package/dist/project/anchors/helpers.js +55 -0
- package/dist/project/anchors/moeicons-config.d.ts +21 -0
- package/dist/project/anchors/moeicons-config.js +83 -0
- package/dist/project/anchors/parse.d.ts +14 -0
- package/dist/project/anchors/parse.js +36 -0
- package/dist/project/anchors/project-manifest.d.ts +19 -0
- package/dist/project/anchors/project-manifest.js +109 -0
- package/dist/project/anchors/style.d.ts +12 -0
- package/dist/project/anchors/style.js +87 -0
- package/dist/project/anchors/types.d.ts +32 -0
- package/dist/project/anchors/types.js +9 -0
- package/dist/project/config.d.ts +59 -5
- package/dist/project/config.js +234 -102
- package/dist/project/install-metadata.d.ts +23 -2
- package/dist/project/install-metadata.js +58 -7
- package/dist/project/install.d.ts +24 -4
- package/dist/project/install.js +402 -190
- package/dist/project/project-lock.d.ts +2 -2
- package/dist/project/project-lock.js +4 -4
- package/dist/project/tailwind.d.ts +2 -2
- package/dist/project/tailwind.js +3 -4
- package/dist/project/tar-gz.d.ts +18 -1
- package/dist/project/tar-gz.js +40 -3
- package/dist/tui/components.d.ts +62 -0
- package/dist/tui/components.js +100 -0
- package/dist/tui/primitives.d.ts +1 -0
- package/dist/tui/primitives.js +8 -1
- package/dist/ui/banner.d.ts +31 -3
- package/dist/ui/banner.js +120 -25
- package/dist/ui/branded-prompts.d.ts +41 -0
- package/dist/ui/branded-prompts.js +80 -0
- package/dist/ui/clack.d.ts +6 -3
- package/dist/ui/clack.js +56 -5
- package/dist/ui/create-ui.d.ts +1 -0
- package/dist/ui/create-ui.js +5 -1
- package/dist/ui/generated/wordmark-large.d.ts +17 -0
- package/dist/ui/generated/wordmark-large.js +28 -0
- package/dist/ui/generated/wordmark.d.ts +2 -0
- package/dist/ui/generated/wordmark.js +2 -0
- package/dist/ui/non-interactive.js +8 -0
- package/dist/ui/stream.js +30 -0
- package/dist/ui/theme.d.ts +42 -0
- package/dist/ui/theme.js +59 -0
- package/package.json +14 -6
package/README.md
CHANGED
|
@@ -1,32 +1,271 @@
|
|
|
1
1
|
# moeicons
|
|
2
2
|
|
|
3
3
|
Public CLI for the Moeicons icon library. Installs icon style groups into your
|
|
4
|
-
project, manages login/tokens, generates React/Vue proxy components,
|
|
5
|
-
an MCP server for AI clients.
|
|
4
|
+
project, manages login/tokens, generates React/Vue proxy components, syncs the
|
|
5
|
+
AI manual + icon catalog, and exposes an MCP server for AI clients.
|
|
6
6
|
|
|
7
7
|
## Commands
|
|
8
8
|
|
|
9
9
|
```text
|
|
10
10
|
moeicons interactive guided flow (free / pro / login)
|
|
11
|
-
moeicons install [group] install an icon group
|
|
11
|
+
moeicons install [group] install an icon group (free | pro)
|
|
12
12
|
moeicons login browser login (PKCE)
|
|
13
13
|
moeicons logout clear local session
|
|
14
14
|
moeicons account show account/tier info
|
|
15
|
-
moeicons
|
|
16
|
-
|
|
15
|
+
moeicons generate generate target-specific project files
|
|
16
|
+
moeicons doctor --check check configuration and integration
|
|
17
|
+
moeicons recover recover an interrupted transaction
|
|
18
|
+
moeicons init create moeicons.config.jsonc
|
|
19
|
+
moeicons update metadata sync MANUAL.md/catalog.json/manifest.json only
|
|
20
|
+
moeicons update full code + metadata update
|
|
17
21
|
moeicons mcp start the MCP stdio server
|
|
18
|
-
|
|
19
|
-
moeicons --version show version
|
|
22
|
+
moeicons --version show version
|
|
20
23
|
moeicons --help show help
|
|
21
24
|
```
|
|
22
25
|
|
|
26
|
+
## Requirements
|
|
27
|
+
|
|
28
|
+
Node.js **22 or later**. Node 20 reached end-of-life on 2026-03-24 and is not
|
|
29
|
+
on the supported line (no security patches). The CLI fails fast with an install
|
|
30
|
+
link instead of a `SyntaxError` if the runtime is too old.
|
|
31
|
+
|
|
32
|
+
Primary development and CI use Node 24. See https://nodejs.org/en/about/eol
|
|
33
|
+
|
|
34
|
+
**Platforms:** Linux x64 and macOS ARM64 use Node 22/24 in CI. Windows x64
|
|
35
|
+
verification covers Node 22/24, npm/pnpm, Free React/Vue core commands,
|
|
36
|
+
production builds and interrupted-write recovery. Windows Pro authentication
|
|
37
|
+
and interactive terminal/PTY behavior are outside this verified scope.
|
|
38
|
+
|
|
39
|
+
## Framework support
|
|
40
|
+
|
|
41
|
+
| Target | Current integration |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| Vite React / Vite Vue | Generated local proxies; use `init` → `install` → `generate` |
|
|
44
|
+
| Tailwind CSS 3 | Content glob added to a supported static config |
|
|
45
|
+
| Tailwind CSS 4 or no Tailwind | Core icon dimensions work without a Tailwind config rewrite |
|
|
46
|
+
| Next App Router / Nuxt SSR | Generated local components verified with single/multiple themes, production server rendering and browser hydration |
|
|
47
|
+
| Vanilla DOM | SVG factories and project runtime; bitmap style groups are unavailable |
|
|
48
|
+
| Windows x64 | Free React/Vue core flow verified on Node 22/24 with npm/pnpm |
|
|
49
|
+
|
|
50
|
+
Next Pages Router and custom Nuxt modules require their own integration checks.
|
|
51
|
+
Custom bootstrap shapes can require manual Provider placement; `doctor --check`
|
|
52
|
+
reports unsupported integration shapes.
|
|
53
|
+
|
|
54
|
+
## First run
|
|
55
|
+
|
|
56
|
+
On the first interactive start inside a project, the CLI offers to install the
|
|
57
|
+
Free icon library plus its metadata (manual + catalog + manifest). The
|
|
58
|
+
completion marker lives in the cache directory and is only written after a
|
|
59
|
+
successful install; failures print a retry command and never suppress retry.
|
|
60
|
+
|
|
61
|
+
For a React or Vue project, the explicit path is:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
npm install -D @moewolf/moe-icons-cli
|
|
65
|
+
npx moeicons init
|
|
66
|
+
npx moeicons install free
|
|
67
|
+
npx moeicons generate
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Edit `moeicons.config.jsonc` before generating to select the icon IDs and
|
|
71
|
+
themes you need. When changing React/Vue/Vanilla/assets target, edit the config
|
|
72
|
+
first, then run `moeicons install free --target <target>` and `moeicons generate`.
|
|
73
|
+
For React/Vue, installation now keeps only the configured SVG component modules
|
|
74
|
+
and their required files in `.moeicons/artifact`. After adding icon IDs to the
|
|
75
|
+
config, run `moeicons install` again before `moeicons generate`; generate reports
|
|
76
|
+
the missing installed component if this step is skipped. New releases support
|
|
77
|
+
selected-resource downloads as well as full archives; `downloadMode` controls
|
|
78
|
+
this choice. Legacy releases without resource indexes still use a full archive
|
|
79
|
+
in `auto` mode. See Download modes and resource selection below.
|
|
80
|
+
Each theme may select a subset of the registered project icons:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"icons": ["ui-search", "arrow-bold-right"],
|
|
85
|
+
"defaultTheme": "outline",
|
|
86
|
+
"missingIconPolicy": "fallback",
|
|
87
|
+
"themes": {
|
|
88
|
+
"outline": { "styleGroup": "moe-outline", "icons": ["ui-search"] },
|
|
89
|
+
"solid": { "styleGroup": "moe-solid", "icons": ["arrow-bold-right"] }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Omitting a theme's `icons` selects all registered icons; `[]` selects none.
|
|
95
|
+
Fallback searches the requested theme, then the default theme, then configured
|
|
96
|
+
themes in ASCII name order. An unregistered icon or an icon unavailable in every
|
|
97
|
+
selected theme fails validation/generation. `error` requires each theme to have
|
|
98
|
+
every registered icon. Adding a selected variant requires installing again.
|
|
99
|
+
|
|
100
|
+
Transactions preserve a recovery journal with original bytes in
|
|
101
|
+
`.moeicons/.reconcile-backup-<id>/files`. After a process interruption, run `moeicons recover` at the project root
|
|
102
|
+
before rerunning the original command. It reclaims a lock only when its owner
|
|
103
|
+
process is dead, restores the previous state, and requires a fresh plan. If a file changed after the interruption, recovery stops and retains both
|
|
104
|
+
the user's file and the backups; compare them before restoring manually. A
|
|
105
|
+
completed transaction is identified by its commit marker and is never rolled
|
|
106
|
+
back because backup cleanup failed. Do not delete these directories to bypass
|
|
107
|
+
a recovery conflict. Recovery covers process termination, not power loss or a
|
|
108
|
+
hostile filesystem replacing directories during filesystem operations.
|
|
109
|
+
|
|
110
|
+
The installer rejects a target that disagrees with the config. Import generated
|
|
111
|
+
PascalCase components from your configured `outputDir` (default `src/moeicons`):
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
import { ArrowBoldRight, MoeiconsProvider } from './moeicons';
|
|
115
|
+
|
|
116
|
+
export function App() {
|
|
117
|
+
return <MoeiconsProvider><ArrowBoldRight size={37} aria-label="Next" /></MoeiconsProvider>;
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Vue uses the same generated names and output directory. The CLI imports the
|
|
122
|
+
version-pinned code in `.moeicons/artifact`; `generate` requires a completed
|
|
123
|
+
`install`. After generation, install any dependencies reported by the CLI with
|
|
124
|
+
your package manager before type checking or building. Commit the config,
|
|
125
|
+
generated source, `.moeicons/artifact` and install metadata together if CI must
|
|
126
|
+
build without downloading the library. Treat all of them as one versioned set.
|
|
127
|
+
|
|
128
|
+
## Metadata
|
|
129
|
+
|
|
130
|
+
Installing or updating a tier writes `<project>/.moeicons/`:
|
|
131
|
+
|
|
132
|
+
```text
|
|
133
|
+
.moeicons/
|
|
134
|
+
├── MANUAL.md # human/agent manual (generated by the builder)
|
|
135
|
+
├── catalog.json # machine-queryable icon catalog (JSON only)
|
|
136
|
+
├── manifest.json # schema, tier, versions, dependencies, digests
|
|
137
|
+
├── install-metadata.json
|
|
138
|
+
└── artifact/<target>/
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`moeicons update metadata` re-downloads only the small metadata archive for the
|
|
142
|
+
installed code version and reconciles the three files, without touching the
|
|
143
|
+
code artifact.
|
|
144
|
+
|
|
145
|
+
## Pro resources
|
|
146
|
+
|
|
147
|
+
After an authenticated login (TTY) the CLI offers a one-time pre-download of the
|
|
148
|
+
full Pro code + metadata. If skipped, the authenticated home screen shows the
|
|
149
|
+
resource state and offers `Download / Update / Repair Pro resources`. Logging
|
|
150
|
+
out keeps cached archives but blocks new unauthenticated installs/updates.
|
|
151
|
+
|
|
23
152
|
## Security
|
|
24
153
|
|
|
25
154
|
- Tokens are stored in the OS keychain where available.
|
|
26
|
-
- API keys are never logged
|
|
155
|
+
- API keys are never logged, written in plaintext, passed to subprocesses, or
|
|
156
|
+
embedded in `.moeicons/manifest.json`.
|
|
27
157
|
- Installation is transactional: failures never corrupt an existing project.
|
|
28
|
-
- Downloads verify SHA-256 and never shell out to `unzip`/`tar
|
|
158
|
+
- Downloads verify SHA-256 and size, never shell out to `unzip`/`tar`, enforce
|
|
159
|
+
host allowlists and bounded redirects/timeouts, and never forward API
|
|
160
|
+
credentials to signed asset hosts.
|
|
161
|
+
- Code and metadata archives must pass a manifest compatibility check
|
|
162
|
+
(tier/version/digests); mismatches fail loudly instead of mixing tiers.
|
|
163
|
+
|
|
164
|
+
## Environment
|
|
165
|
+
|
|
166
|
+
| variable | purpose |
|
|
167
|
+
| --- | --- |
|
|
168
|
+
| `MOEICONS_CACHE_DIR` | global artifact/metadata cache (default `~/.moeicons/cache`) |
|
|
169
|
+
| `MOEICONS_FREE_RELEASE_DIR` | test-only free release fixture directory |
|
|
170
|
+
| `MOEICONS_BOOTSTRAP_FILE` | override bootstrap completion marker path |
|
|
171
|
+
| `MOEICONS_PRO_DESCRIPTOR_URL` | test-only Pro descriptor endpoint override (https or loopback http) |
|
|
172
|
+
| `MOEICONS_LIBRARY_VERSIONS_URL` | test-only library versions endpoint override (https or loopback http) |
|
|
173
|
+
| `MOEICONS_TOKEN_STORE_DIR` | 0600 file token store directory |
|
|
174
|
+
| `MOEICONS_DISABLE_SYSTEM_KEYCHAIN` | `1` disables OS keychain storage |
|
|
175
|
+
| `MOEICONS_AUTH0_ISSUER` / `MOEICONS_AUTH0_CLIENT_ID` | Auth0 refresh/revoke |
|
|
29
176
|
|
|
30
177
|
## Status
|
|
31
178
|
|
|
32
|
-
|
|
179
|
+
Metadata distribution (`MANUAL-DIST`) implemented: builder-generated manual,
|
|
180
|
+
catalog and manifest, per-tier archives and descriptors, Free bootstrap,
|
|
181
|
+
metadata-only sync, and authenticated Pro pre-download/update.
|
|
182
|
+
|
|
183
|
+
### Download modes and resource selection
|
|
184
|
+
|
|
185
|
+
Set `downloadMode` in `moeicons.config.jsonc` (schemaVersion 3):
|
|
186
|
+
|
|
187
|
+
```json
|
|
188
|
+
{
|
|
189
|
+
"schemaVersion": 3,
|
|
190
|
+
"tier": "free",
|
|
191
|
+
"target": "react",
|
|
192
|
+
"outputDir": "src/icons",
|
|
193
|
+
"downloadMode": "auto",
|
|
194
|
+
"icons": ["ui-search", "arrow-bold-right"],
|
|
195
|
+
"defaultTheme": "outline",
|
|
196
|
+
"themes": {
|
|
197
|
+
"outline": { "styleGroup": "moe-outline", "icons": ["ui-search"] },
|
|
198
|
+
"solid": { "styleGroup": "moe-solid", "icons": ["arrow-bold-right"] }
|
|
199
|
+
},
|
|
200
|
+
"missingIconPolicy": "fallback"
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
- `auto` (default): selected resources when the fixed release advertises them. A legacy release without this capability uses the full archive and explains why.
|
|
205
|
+
- `icons`: require selected resources. Unsupported releases, authorization failures, bad ranges and invalid digests stop the operation; they never trigger a full archive download.
|
|
206
|
+
- `full`: download and verify the complete archive, then install the configured target/resources.
|
|
207
|
+
|
|
208
|
+
`icons` registers the allowed icon IDs. Install/update requires at least one global icon ID; an empty global list stops before version queries or downloads and preserves the existing installation. An individual theme may still use `icons: []`. A theme's `icons` restricts that theme to a subset; omitting it selects every registered ID available in the style group, and `[]` selects none. Fallback chooses the requested theme, then the default theme, then theme names in ASCII order. An icon with no selected variant fails validation. Bitmap themes also select `format` and `imageSize`; changing these fields may require new resources.
|
|
209
|
+
|
|
210
|
+
The CLI reads the release metadata before planning resources, so validation uses that release's catalog. It shows the target, registered icon/theme counts, required file count and compressed payload budget before reading the data object. Verified cache entries reduce traffic. `--json` reports `downloadMode`, `downloadNotes`, `selectedFiles` and `networkBytes` (resource payload only; descriptor/metadata/index traffic is separate).
|
|
211
|
+
|
|
212
|
+
Run `moeicons install free` or `moeicons install pro` after changing the selected icons, themes, bitmap format or size, then `moeicons generate`. `moeicons update` also reconciles the configuration when the release version is unchanged. Missing resources name the affected path and ask for reinstall; stale resources are removed only if still owned and unmodified. A configuration change during download stops before project writes; retry with the final configuration.
|
|
213
|
+
|
|
214
|
+
Generation works offline from the project's verified installed resources; `moeicons recover` also needs no network. Install/update still consult the fixed descriptor and metadata, and Pro checks current entitlement. A resource cache hit does not bypass authorization, and there is no offline install flag. Corrupt cache entries are rejected and fetched again online. Interrupted downloads retain only verified cache members; project files change in one recoverable transaction after all required resources have verified.
|
|
215
|
+
|
|
216
|
+
Version queries have a deadline covering both response headers and body, with bounded JSON sizes. Invalid or unavailable version responses make update fail explicitly. Cache cleanup preserves staging files owned by a live process; disk-full and permission failures report actionable errors.
|
|
217
|
+
|
|
218
|
+
## MCP project integration
|
|
219
|
+
|
|
220
|
+
Run `moeicons mcp` in the project root containing `package.json`. Use a fixed,
|
|
221
|
+
installed CLI executable in your MCP client's command configuration:
|
|
222
|
+
|
|
223
|
+
```json
|
|
224
|
+
{
|
|
225
|
+
"command": "node",
|
|
226
|
+
"args": ["/absolute/path/to/node_modules/@moewolf/moe-icons-cli/bin/moeicons.js", "mcp"],
|
|
227
|
+
"cwd": "/absolute/path/to/your/project"
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The client must support a working directory for subprocesses. The server uses
|
|
232
|
+
stdio and needs no port. Log in with `moeicons login` outside the MCP server;
|
|
233
|
+
never place credentials in client configuration or browser source code.
|
|
234
|
+
|
|
235
|
+
Use `init_config` (explicit `target: "vue"` for mixed-framework projects), then
|
|
236
|
+
`get_project_config` to obtain the current config hash. `list_icon_groups` and
|
|
237
|
+
`find_icons` expose installed, verified catalog data when available; bundled
|
|
238
|
+
results are identified separately. Call `update_config` with `expectedHash` and
|
|
239
|
+
an explicit patch, `install` with the configured tier, then `generate`.
|
|
240
|
+
`update` updates icon resources, not the CLI executable. Changes to selection
|
|
241
|
+
require resource reconciliation before generation. Read `get_usage_guide` and
|
|
242
|
+
the installed manual before editing application code.
|
|
243
|
+
|
|
244
|
+
For example, add or remove selections without replacing the whole config:
|
|
245
|
+
|
|
246
|
+
```json
|
|
247
|
+
{
|
|
248
|
+
"expectedHash": "<hash from get_project_config>",
|
|
249
|
+
"patch": {
|
|
250
|
+
"icons": { "add": ["dashboard"], "remove": ["weather-sun"] }
|
|
251
|
+
},
|
|
252
|
+
"dryRun": true
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Review the dry-run diff, then call with `dryRun: false`. If the config changed,
|
|
257
|
+
read it again and revise the patch. Generated files are owned by the generator:
|
|
258
|
+
import their named exports rather than editing them. Install the runtime
|
|
259
|
+
`clsx` and a `tailwind-merge` version compatible with your Tailwind version.
|
|
260
|
+
For Vue, put `MoeiconsProvider` around all consuming components and bind its
|
|
261
|
+
`theme` to an exact configured theme key. Give decorative icons `aria-hidden`
|
|
262
|
+
and bitmap icons empty `alt`; icon-only controls require accessible labels.
|
|
263
|
+
Custom application bootstrap can require manual Provider placement; doctor
|
|
264
|
+
reports unsupported shapes rather than certifying them automatically.
|
|
265
|
+
|
|
266
|
+
Available tools: `list_icon_groups`, `get_account`, `get_project_config`,
|
|
267
|
+
`find_icons`, `get_usage_guide`, `init_config`, `update_config`, `install`,
|
|
268
|
+
`update`, and `generate`. The legacy `install_icon_group` tool remains an
|
|
269
|
+
explicit unsupported operation with migration guidance. Tool failures use
|
|
270
|
+
`isError`; malformed protocol arguments use JSON-RPC errors. The server binds
|
|
271
|
+
all operations to one project root and does not expose arbitrary shell commands.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var MIN_MAJOR = 22;
|
|
4
|
+
var LTS_URL = "https://nodejs.org/en/download";
|
|
5
|
+
var EOL_URL = "https://nodejs.org/en/about/eol";
|
|
6
|
+
|
|
7
|
+
function parseMajor(version) {
|
|
8
|
+
var raw = String(version || "").replace(/^v/, "");
|
|
9
|
+
var major = parseInt(raw.split(".")[0], 10);
|
|
10
|
+
if (!isFinite(major)) return null;
|
|
11
|
+
return major;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function assertSupportedNode(version) {
|
|
15
|
+
var major = parseMajor(version);
|
|
16
|
+
var display = String(version || "").charAt(0) === "v" ? String(version) : "v" + String(version);
|
|
17
|
+
if (major === null || major < MIN_MAJOR) {
|
|
18
|
+
return {
|
|
19
|
+
ok: false,
|
|
20
|
+
major: major,
|
|
21
|
+
message:
|
|
22
|
+
"moeicons requires Node.js " + MIN_MAJOR + " or later (you have " + display + ").\n" +
|
|
23
|
+
"Node " + (major === null ? display : major) + " is outside the supported line. Node 20 reached end-of-life on 2026-03-24 and no longer receives security patches.\n" +
|
|
24
|
+
"Install the current Node.js LTS: " + LTS_URL + "\n" +
|
|
25
|
+
"Policy: " + EOL_URL + "\n",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return { ok: true, major: major, message: "" };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
MIN_MAJOR: MIN_MAJOR,
|
|
33
|
+
LTS_URL: LTS_URL,
|
|
34
|
+
parseMajor: parseMajor,
|
|
35
|
+
assertSupportedNode: assertSupportedNode,
|
|
36
|
+
};
|
package/bin/moeicons.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const { assertSupportedNode } = require("./check-node.cjs");
|
|
6
|
+
const checked = assertSupportedNode(process.version);
|
|
7
|
+
if (!checked.ok) {
|
|
8
|
+
process.stderr.write(checked.message);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { main } = await import("../dist/cli.js");
|
|
3
13
|
|
|
4
14
|
const runtime = {
|
|
5
15
|
cwd: () => process.cwd(),
|
|
@@ -7,6 +17,7 @@ const runtime = {
|
|
|
7
17
|
stderr: (text) => process.stderr.write(text),
|
|
8
18
|
env: process.env,
|
|
9
19
|
isTTY: () => Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
20
|
+
columns: () => process.stdout.columns,
|
|
10
21
|
};
|
|
11
22
|
|
|
12
23
|
main(process.argv.slice(2), runtime).then(
|
package/dist/api/client.d.ts
CHANGED
|
@@ -16,15 +16,26 @@ export interface RequestJsonOptions {
|
|
|
16
16
|
readonly signal?: AbortSignal;
|
|
17
17
|
readonly retries?: number;
|
|
18
18
|
readonly requestId?: string;
|
|
19
|
+
/** Optional stage label included in transport diagnostics (e.g. login create). */
|
|
20
|
+
readonly stage?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Opt-in debug only. Default user-facing errors never include response body
|
|
23
|
+
* text (kind + byte length only). When true, JSON bodies may include a
|
|
24
|
+
* recursively redacted preview; HTML/opaque bodies are still omitted.
|
|
25
|
+
*/
|
|
26
|
+
readonly debugBodyPreview?: boolean;
|
|
19
27
|
}
|
|
20
28
|
export interface ApiResult<T> {
|
|
21
29
|
readonly status: number;
|
|
22
30
|
readonly data: T;
|
|
23
31
|
readonly requestId: string;
|
|
24
32
|
}
|
|
33
|
+
/** Recursively redact secret keys and secret-shaped leaf values for debug previews only. */
|
|
34
|
+
export declare function redactJsonForPreview(value: unknown): unknown;
|
|
25
35
|
/**
|
|
26
|
-
* Perform a JSON request against the fixed base URL. Retries GET/
|
|
27
|
-
* calls with capped exponential backoff on 5xx and network errors.
|
|
36
|
+
* Perform a JSON request against the fixed base URL. Retries GET/DELETE
|
|
37
|
+
* (idempotent) calls with capped exponential backoff on 5xx and network errors.
|
|
38
|
+
* POST/PUT never retry. External AbortSignal stops immediately without retry.
|
|
28
39
|
*/
|
|
29
40
|
export declare function requestJson<T>(config: ApiConfig, path: string, options?: RequestJsonOptions): Promise<ApiResult<T>>;
|
|
30
41
|
export interface ApiKeyEntitlement {
|
package/dist/api/client.js
CHANGED
|
@@ -1,14 +1,91 @@
|
|
|
1
1
|
import { CliError } from "../errors/index.js";
|
|
2
|
+
const SECRET_KEY = /^(access[_-]?token|refresh[_-]?token|polling[_-]?token|exchange[_-]?code|code[_-]?verifier|authorization|password|secret|cookie|set-cookie|email)$/i;
|
|
3
|
+
const JWT_LIKE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
4
|
+
const BEARER_LIKE = /^Bearer\s+\S+/i;
|
|
5
|
+
const EMAIL_LIKE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
6
|
+
function isIdempotentMethod(method) {
|
|
7
|
+
return method === "GET" || method === "DELETE";
|
|
8
|
+
}
|
|
9
|
+
function bodyKind(text, contentType) {
|
|
10
|
+
const trimmed = text.trim();
|
|
11
|
+
if (trimmed.length === 0)
|
|
12
|
+
return "empty";
|
|
13
|
+
const lower = (contentType ?? "").toLowerCase();
|
|
14
|
+
if (lower.includes("text/html") || /^<!doctype html/i.test(trimmed) || /^<html/i.test(trimmed)) {
|
|
15
|
+
return "html";
|
|
16
|
+
}
|
|
17
|
+
if (lower.includes("application/json") || lower.includes("+json"))
|
|
18
|
+
return "json-declared";
|
|
19
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("["))
|
|
20
|
+
return "json-looking";
|
|
21
|
+
if (lower.includes("text/plain"))
|
|
22
|
+
return "text/plain";
|
|
23
|
+
return "opaque";
|
|
24
|
+
}
|
|
25
|
+
/** Recursively redact secret keys and secret-shaped leaf values for debug previews only. */
|
|
26
|
+
export function redactJsonForPreview(value) {
|
|
27
|
+
if (Array.isArray(value))
|
|
28
|
+
return value.map(redactJsonForPreview);
|
|
29
|
+
if (value && typeof value === "object") {
|
|
30
|
+
const out = {};
|
|
31
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
32
|
+
out[key] = SECRET_KEY.test(key) ? "[redacted]" : redactJsonForPreview(nested);
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
if (typeof value === "string") {
|
|
37
|
+
if (JWT_LIKE.test(value) || BEARER_LIKE.test(value) || EMAIL_LIKE.test(value))
|
|
38
|
+
return "[redacted]";
|
|
39
|
+
if (/[?&](access_token|refresh_token|code|token)=/i.test(value))
|
|
40
|
+
return "[redacted-url]";
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function summarizeBody(text, contentType, debugBodyPreview) {
|
|
46
|
+
const kind = bodyKind(text, contentType);
|
|
47
|
+
const base = `body=${kind}, ${text.length} bytes`;
|
|
48
|
+
if (!debugBodyPreview)
|
|
49
|
+
return base;
|
|
50
|
+
if (kind === "html" || kind === "opaque" || kind === "empty" || kind === "text/plain") {
|
|
51
|
+
return base;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(text);
|
|
55
|
+
const redacted = redactJsonForPreview(parsed);
|
|
56
|
+
const preview = JSON.stringify(redacted).replace(/\s+/g, " ").slice(0, 120);
|
|
57
|
+
return `${base}, preview=${JSON.stringify(preview)}`;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return base;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function transportError(stage, status, contentType, text, reason, debugBodyPreview) {
|
|
64
|
+
const stagePrefix = stage ? `${stage}: ` : "";
|
|
65
|
+
const ct = contentType ?? "unknown";
|
|
66
|
+
const message = `${stagePrefix}${reason} (HTTP ${status}, content-type=${ct}, ${summarizeBody(text, contentType, debugBodyPreview)})`;
|
|
67
|
+
if (status === 401)
|
|
68
|
+
return new CliError("AUTH_ERROR", message);
|
|
69
|
+
if (status === 403)
|
|
70
|
+
return new CliError("FORBIDDEN", message);
|
|
71
|
+
if (status === 404)
|
|
72
|
+
return new CliError("NOT_FOUND", message);
|
|
73
|
+
if (status === 0)
|
|
74
|
+
return new CliError("NETWORK_ERROR", message);
|
|
75
|
+
return new CliError("VALIDATION_ERROR", message);
|
|
76
|
+
}
|
|
2
77
|
/**
|
|
3
|
-
* Perform a JSON request against the fixed base URL. Retries GET/
|
|
4
|
-
* calls with capped exponential backoff on 5xx and network errors.
|
|
78
|
+
* Perform a JSON request against the fixed base URL. Retries GET/DELETE
|
|
79
|
+
* (idempotent) calls with capped exponential backoff on 5xx and network errors.
|
|
80
|
+
* POST/PUT never retry. External AbortSignal stops immediately without retry.
|
|
5
81
|
*/
|
|
6
82
|
export async function requestJson(config, path, options = {}) {
|
|
7
83
|
const method = options.method ?? "GET";
|
|
8
84
|
const retries = options.retries ?? 0;
|
|
9
85
|
const timeoutMs = config.timeoutMs ?? 10_000;
|
|
10
|
-
const requestId = options.requestId ??
|
|
11
|
-
|
|
86
|
+
const requestId = options.requestId ?? `req-${Math.random().toString(36).slice(2, 10)}`;
|
|
87
|
+
const debugBodyPreview = options.debugBodyPreview === true;
|
|
88
|
+
const canRetry = isIdempotentMethod(method) && retries > 0;
|
|
12
89
|
const attempt = async () => {
|
|
13
90
|
const controller = new AbortController();
|
|
14
91
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -23,13 +100,26 @@ export async function requestJson(config, path, options = {}) {
|
|
|
23
100
|
headers[options.authHeader ?? "authorization"] = options.auth;
|
|
24
101
|
}
|
|
25
102
|
try {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
103
|
+
let response;
|
|
104
|
+
try {
|
|
105
|
+
response = await fetch(`${config.baseUrl.replace(/\/$/, "")}${path}`, {
|
|
106
|
+
method,
|
|
107
|
+
headers,
|
|
108
|
+
...(options.body !== undefined ? { body: JSON.stringify(options.body) } : {}),
|
|
109
|
+
signal: controller.signal,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
114
|
+
if (externalSignal?.aborted) {
|
|
115
|
+
throw new CliError("NETWORK_ERROR", `${options.stage ? `${options.stage}: ` : ""}request was aborted`);
|
|
116
|
+
}
|
|
117
|
+
throw new RetryableTransportError(`${options.stage ? `${options.stage}: ` : ""}request timed out`);
|
|
118
|
+
}
|
|
119
|
+
throw new RetryableTransportError(`${options.stage ? `${options.stage}: ` : ""}network error (${error instanceof Error ? error.message : String(error)})`);
|
|
120
|
+
}
|
|
32
121
|
const text = await response.text();
|
|
122
|
+
const contentType = response.headers.get("content-type");
|
|
33
123
|
if (text.length > (config.maxBodyBytes ?? 5 * 1024 * 1024)) {
|
|
34
124
|
throw new CliError("VALIDATION_ERROR", "response body exceeds size limit");
|
|
35
125
|
}
|
|
@@ -39,21 +129,35 @@ export async function requestJson(config, path, options = {}) {
|
|
|
39
129
|
data = JSON.parse(text);
|
|
40
130
|
}
|
|
41
131
|
catch {
|
|
42
|
-
throw
|
|
132
|
+
throw transportError(options.stage, response.status, contentType, text, "response is not valid JSON", debugBodyPreview);
|
|
43
133
|
}
|
|
44
134
|
}
|
|
45
|
-
if (response.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
135
|
+
else if (response.ok) {
|
|
136
|
+
data = null;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
throw transportError(options.stage, response.status, contentType, text, "empty error response", debugBodyPreview);
|
|
140
|
+
}
|
|
141
|
+
if (response.status === 401) {
|
|
142
|
+
throw transportError(options.stage, response.status, contentType, text, "unauthorized", debugBodyPreview);
|
|
143
|
+
}
|
|
144
|
+
if (response.status === 403) {
|
|
145
|
+
throw transportError(options.stage, response.status, contentType, text, "forbidden", debugBodyPreview);
|
|
146
|
+
}
|
|
49
147
|
if (response.status === 429) {
|
|
50
|
-
|
|
148
|
+
if (canRetry) {
|
|
149
|
+
throw new RetryableTransportError(`${options.stage ? `${options.stage}: ` : ""}rate limited`);
|
|
150
|
+
}
|
|
151
|
+
throw new CliError("NETWORK_ERROR", `${options.stage ? `${options.stage}: ` : ""}rate limited`);
|
|
51
152
|
}
|
|
52
|
-
if (response.status >= 500
|
|
53
|
-
|
|
153
|
+
if (response.status >= 500) {
|
|
154
|
+
if (canRetry) {
|
|
155
|
+
throw new RetryableStatus(response.status);
|
|
156
|
+
}
|
|
157
|
+
throw transportError(options.stage, response.status, contentType, text, `request failed with ${response.status}`, debugBodyPreview);
|
|
54
158
|
}
|
|
55
159
|
if (response.status >= 400) {
|
|
56
|
-
throw
|
|
160
|
+
throw transportError(options.stage, response.status, contentType, text, `request failed with ${response.status}`, debugBodyPreview);
|
|
57
161
|
}
|
|
58
162
|
return { status: response.status, data: data, requestId };
|
|
59
163
|
}
|
|
@@ -68,13 +172,27 @@ export async function requestJson(config, path, options = {}) {
|
|
|
68
172
|
return await attempt();
|
|
69
173
|
}
|
|
70
174
|
catch (error) {
|
|
71
|
-
if (error instanceof CliError)
|
|
175
|
+
if (error instanceof CliError) {
|
|
176
|
+
// Auth / schema / validation / not-found never retry.
|
|
72
177
|
throw error;
|
|
73
|
-
|
|
178
|
+
}
|
|
179
|
+
if (options.signal?.aborted) {
|
|
180
|
+
throw new CliError("NETWORK_ERROR", `${options.stage ? `${options.stage}: ` : ""}request was aborted`);
|
|
181
|
+
}
|
|
182
|
+
const retryable = canRetry &&
|
|
183
|
+
i < retries &&
|
|
184
|
+
(error instanceof RetryableStatus || error instanceof RetryableTransportError);
|
|
185
|
+
if (retryable) {
|
|
74
186
|
const backoff = Math.min(500 * 2 ** i, 4000);
|
|
75
187
|
await new Promise((resolve) => setTimeout(resolve, backoff));
|
|
76
188
|
continue;
|
|
77
189
|
}
|
|
190
|
+
if (error instanceof RetryableTransportError) {
|
|
191
|
+
throw new CliError("NETWORK_ERROR", error.message);
|
|
192
|
+
}
|
|
193
|
+
if (error instanceof RetryableStatus) {
|
|
194
|
+
throw new CliError("NETWORK_ERROR", `${options.stage ? `${options.stage}: ` : ""}request failed with ${error.status}`);
|
|
195
|
+
}
|
|
78
196
|
lastError = error;
|
|
79
197
|
}
|
|
80
198
|
}
|
|
@@ -87,6 +205,12 @@ class RetryableStatus extends Error {
|
|
|
87
205
|
this.status = status;
|
|
88
206
|
}
|
|
89
207
|
}
|
|
208
|
+
class RetryableTransportError extends Error {
|
|
209
|
+
constructor(message) {
|
|
210
|
+
super(message);
|
|
211
|
+
this.name = "RetryableTransportError";
|
|
212
|
+
}
|
|
213
|
+
}
|
|
90
214
|
/**
|
|
91
215
|
* Verify an API key. Trim once, never log, send via the configured header,
|
|
92
216
|
* map 401/403/429/5xx distinctly. No persistent storage by default.
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import type { StoredSession, TokenStore } from
|
|
1
|
+
import type { StoredSession, TokenStore } from "./token-store.js";
|
|
2
2
|
export interface DeviceLoginConfig {
|
|
3
3
|
readonly apiBaseUrl: string;
|
|
4
4
|
readonly websiteOrigin: string;
|
|
5
5
|
readonly auth0Issuer: string;
|
|
6
6
|
readonly auth0ClientId: string;
|
|
7
7
|
}
|
|
8
|
+
export type LoginRequestStage = "create" | "poll" | "exchange" | "cleanup";
|
|
9
|
+
export interface DeviceLoginRequestOptions {
|
|
10
|
+
readonly method: "GET" | "POST" | "DELETE";
|
|
11
|
+
readonly auth?: string;
|
|
12
|
+
readonly body?: unknown;
|
|
13
|
+
readonly signal?: AbortSignal;
|
|
14
|
+
readonly stage?: LoginRequestStage;
|
|
15
|
+
}
|
|
8
16
|
export interface DeviceLoginDependencies {
|
|
9
|
-
readonly request: <T>(path: string, options: {
|
|
10
|
-
method: 'GET' | 'POST' | 'DELETE';
|
|
11
|
-
auth?: string;
|
|
12
|
-
body?: unknown;
|
|
13
|
-
signal?: AbortSignal;
|
|
14
|
-
}) => Promise<{
|
|
17
|
+
readonly request: <T>(path: string, options: DeviceLoginRequestOptions) => Promise<{
|
|
15
18
|
status: number;
|
|
16
19
|
data: T;
|
|
17
20
|
}>;
|