@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
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, and exposes
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 groups list available icon groups
16
- moeicons generate generate target-specific project files
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
- npm install -g @moewolf/moe-icons-cli
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 or written in plaintext.
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
- Scaffold in progress (Phase 5 of the Moeicons development plan).
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 { main } from "../dist/cli.js";
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(
@@ -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/idempotent
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 {
@@ -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/idempotent
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
- `req-${Math.random().toString(36).slice(2, 10)}`;
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
- const response = await fetch(`${config.baseUrl.replace(/\/$/, "")}${path}`, {
27
- method,
28
- headers,
29
- ...(options.body !== undefined ? { body: JSON.stringify(options.body) } : {}),
30
- signal: controller.signal,
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 new CliError("VALIDATION_ERROR", "response is not valid JSON");
132
+ throw transportError(options.stage, response.status, contentType, text, "response is not valid JSON", debugBodyPreview);
43
133
  }
44
134
  }
45
- if (response.status === 401)
46
- throw new CliError("AUTH_ERROR", "unauthorized");
47
- if (response.status === 403)
48
- throw new CliError("FORBIDDEN", "forbidden");
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
- throw new CliError("NETWORK_ERROR", "rate limited");
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 && retries > 0) {
53
- throw new RetryableStatus(response.status);
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 new CliError("NETWORK_ERROR", `request failed with ${response.status}`);
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
- if (error instanceof RetryableStatus && i < retries) {
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 './token-store.js';
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
  }>;