@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
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Canonical config object validator / in-memory migrator.
|
|
4
|
+
//
|
|
5
|
+
// Reads a parsed config value (already JSONC-decoded by the caller) in schema
|
|
6
|
+
// v1/v2/v3, validates fields, and normalizes to an in-memory v3 model. Higher
|
|
7
|
+
// versions are `unsupported` with zero writes. Path fields must be POSIX
|
|
8
|
+
// relative and reject absolute paths, `..`, NUL and backslash escapes.
|
|
9
|
+
|
|
10
|
+
const VERSION = 3;
|
|
11
|
+
|
|
12
|
+
const ALLOWED_COMMON_KEYS = new Set([
|
|
13
|
+
'schemaVersion',
|
|
14
|
+
'tier',
|
|
15
|
+
'target',
|
|
16
|
+
'outputDir',
|
|
17
|
+
'defaultTheme',
|
|
18
|
+
'themes',
|
|
19
|
+
'icons',
|
|
20
|
+
'missingIconPolicy',
|
|
21
|
+
'downloadMode',
|
|
22
|
+
'integration',
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
const ALLOWED_THEME_KEYS = new Set([
|
|
26
|
+
'styleGroup',
|
|
27
|
+
'icons',
|
|
28
|
+
'styles',
|
|
29
|
+
'format',
|
|
30
|
+
'imageSize',
|
|
31
|
+
'defaultSize',
|
|
32
|
+
'strokeWidth',
|
|
33
|
+
'className',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
const TARGETS = new Set(['react', 'vue', 'vanilla', 'assets']);
|
|
37
|
+
const ADAPTERS = new Set([
|
|
38
|
+
'vite-react',
|
|
39
|
+
'next-app',
|
|
40
|
+
'next-pages',
|
|
41
|
+
'vite-vue',
|
|
42
|
+
'nuxt',
|
|
43
|
+
'vanilla',
|
|
44
|
+
'assets-only',
|
|
45
|
+
]);
|
|
46
|
+
const FORMATS = new Set(['svg', 'webp', 'png']);
|
|
47
|
+
const IMAGE_SIZES = new Set([64, 128, 256, 512]);
|
|
48
|
+
|
|
49
|
+
function isRecord(value) {
|
|
50
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function fail(message) {
|
|
54
|
+
return { ok: false, kind: 'invalid', message };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function unsupported(version) {
|
|
58
|
+
return { ok: false, kind: 'unsupported', version };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Reject absolute paths, `..`, NUL and backslash escapes (POSIX relative only). */
|
|
62
|
+
function checkPath(value, field) {
|
|
63
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
64
|
+
return fail(`${field} must be a non-empty string`);
|
|
65
|
+
}
|
|
66
|
+
if (value.includes('\0') || value.includes('\\')) {
|
|
67
|
+
return fail(`${field} must be a POSIX relative path (no backslash/NUL)`);
|
|
68
|
+
}
|
|
69
|
+
if (value.startsWith('/') || /^[A-Za-z]:/.test(value)) {
|
|
70
|
+
return fail(`${field} must be relative, not absolute`);
|
|
71
|
+
}
|
|
72
|
+
const segments = value.split('/');
|
|
73
|
+
if (segments.some((seg) => !seg || seg === '..' || seg === '.')) {
|
|
74
|
+
return fail(`${field} must not contain '.' or '..' segments`);
|
|
75
|
+
}
|
|
76
|
+
if (field === 'outputDir' && ['.git', '.moeicons', 'node_modules'].includes(segments[0])) {
|
|
77
|
+
return fail('outputDir cannot use a project control directory');
|
|
78
|
+
}
|
|
79
|
+
if (field === 'outputDir' && !/^[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_.-]+)*$/.test(value)) {
|
|
80
|
+
return fail('outputDir must use ASCII letters, digits, _, -, and safe dotted child segments');
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function flattenIcons(value) {
|
|
86
|
+
if (Array.isArray(value) && value.every((item) => typeof item === 'string')) return value;
|
|
87
|
+
if (!isRecord(value)) {
|
|
88
|
+
throw new Error('icons must be an array or prefix-group object');
|
|
89
|
+
}
|
|
90
|
+
const result = [];
|
|
91
|
+
for (const prefix of Object.keys(value).sort()) {
|
|
92
|
+
const group = value[prefix];
|
|
93
|
+
if (!Array.isArray(group) || !group.every((item) => typeof item === 'string')) {
|
|
94
|
+
throw new Error(`icons.${prefix} must be an array of icon IDs`);
|
|
95
|
+
}
|
|
96
|
+
result.push(...group);
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function findGroup(id, catalog) {
|
|
102
|
+
return (catalog.styleGroups || []).find((g) => g.id === id);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function findIcon(id, catalog) {
|
|
106
|
+
return (catalog.icons || []).find((icon) => icon.id === id);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Validate + normalize into the v3 in-memory model. */
|
|
110
|
+
function validateConfig(raw, catalog) {
|
|
111
|
+
if (!isRecord(raw)) return fail('config must be an object');
|
|
112
|
+
const version = raw.schemaVersion;
|
|
113
|
+
if (typeof version !== 'number') return fail('schemaVersion must be a number');
|
|
114
|
+
if (version !== 1 && version !== 2 && version !== 3) return unsupported(version);
|
|
115
|
+
|
|
116
|
+
// Common unknown-field rejection per version.
|
|
117
|
+
for (const key of Object.keys(raw)) {
|
|
118
|
+
if (!ALLOWED_COMMON_KEYS.has(key)) {
|
|
119
|
+
if (key === 'framework' && version === 1) continue;
|
|
120
|
+
return fail(`unknown config field "${key}"`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (raw.tier !== 'free' && raw.tier !== 'pro') return fail('tier must be free or pro');
|
|
125
|
+
if (typeof raw.outputDir !== 'string' || raw.outputDir.length === 0) {
|
|
126
|
+
return fail('outputDir is required');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const pathCheck = checkPath(raw.outputDir, 'outputDir');
|
|
130
|
+
if (pathCheck) return pathCheck;
|
|
131
|
+
|
|
132
|
+
let target;
|
|
133
|
+
if (version === 1) {
|
|
134
|
+
if (raw.framework !== 'react' && raw.framework !== 'vue') {
|
|
135
|
+
return fail('framework must be react or vue (schema v1)');
|
|
136
|
+
}
|
|
137
|
+
target = raw.framework;
|
|
138
|
+
} else {
|
|
139
|
+
if (!TARGETS.has(raw.target)) return fail('target must be react, vue, vanilla, or assets');
|
|
140
|
+
target = raw.target;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (typeof raw.defaultTheme !== 'string' || raw.defaultTheme.length === 0) {
|
|
144
|
+
return fail('defaultTheme is required');
|
|
145
|
+
}
|
|
146
|
+
if (raw.downloadMode !== undefined && !['auto', 'icons', 'full'].includes(raw.downloadMode)) return fail('downloadMode must be "auto", "icons" or "full"');
|
|
147
|
+
if (
|
|
148
|
+
raw.missingIconPolicy !== undefined &&
|
|
149
|
+
raw.missingIconPolicy !== 'fallback' &&
|
|
150
|
+
raw.missingIconPolicy !== 'error'
|
|
151
|
+
) {
|
|
152
|
+
return fail('missingIconPolicy must be "fallback" or "error"');
|
|
153
|
+
}
|
|
154
|
+
if (!isRecord(raw.themes)) return fail('themes must be an object');
|
|
155
|
+
|
|
156
|
+
const themes = {};
|
|
157
|
+
const themeNames = new Map();
|
|
158
|
+
for (const [name, value] of Object.entries(raw.themes)) {
|
|
159
|
+
if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(name)) {
|
|
160
|
+
return fail(`theme key "${name}" must be lowercase kebab-case`);
|
|
161
|
+
}
|
|
162
|
+
const internalName = name.replace(/-/g, '').toLowerCase();
|
|
163
|
+
if (themeNames.has(internalName)) {
|
|
164
|
+
return fail(`theme keys "${themeNames.get(internalName)}" and "${name}" generate the same identifier`);
|
|
165
|
+
}
|
|
166
|
+
themeNames.set(internalName, name);
|
|
167
|
+
if (!isRecord(value)) return fail(`theme ${name} is invalid`);
|
|
168
|
+
for (const key of Object.keys(value)) {
|
|
169
|
+
if (!ALLOWED_THEME_KEYS.has(key)) return fail(`unknown field "${key}" in theme "${name}"`);
|
|
170
|
+
}
|
|
171
|
+
const group = findGroup(value.styleGroup, catalog);
|
|
172
|
+
if (!group) return fail(`unknown style group "${value.styleGroup}"`);
|
|
173
|
+
if (!group.tiers.includes(raw.tier)) {
|
|
174
|
+
return fail(`style group "${group.id}" is not available in ${raw.tier} tier`);
|
|
175
|
+
}
|
|
176
|
+
if (value.format !== undefined && typeof value.format !== 'string') return fail(`theme ${name}.format is invalid`);
|
|
177
|
+
if (value.imageSize !== undefined && typeof value.imageSize !== 'number') return fail(`theme ${name}.imageSize is invalid`);
|
|
178
|
+
if (value.defaultSize !== undefined && (typeof value.defaultSize !== 'number' || !Number.isFinite(value.defaultSize) || value.defaultSize <= 0)) return fail(`theme ${name}.defaultSize must be a positive finite number`);
|
|
179
|
+
if (value.strokeWidth !== undefined && (typeof value.strokeWidth !== 'number' || !Number.isFinite(value.strokeWidth) || value.strokeWidth < 0)) return fail(`theme ${name}.strokeWidth must be a nonnegative finite number`);
|
|
180
|
+
if (value.className !== undefined && typeof value.className !== 'string') return fail(`theme ${name}.className must be a string`);
|
|
181
|
+
if (value.icons !== undefined && (!Array.isArray(value.icons) || value.icons.some((id) => typeof id !== 'string') || new Set(value.icons).size !== value.icons.length)) {
|
|
182
|
+
return fail(`theme ${name}.icons must be an array of unique icon IDs`);
|
|
183
|
+
}
|
|
184
|
+
const format = typeof value.format === 'string' ? value.format : undefined;
|
|
185
|
+
if (format !== undefined && !FORMATS.has(format)) return fail(`theme ${name}.format is invalid`);
|
|
186
|
+
const imageSize = typeof value.imageSize === 'number' ? value.imageSize : undefined;
|
|
187
|
+
if (imageSize !== undefined && !IMAGE_SIZES.has(imageSize)) {
|
|
188
|
+
return fail(`theme ${name}.imageSize is invalid`);
|
|
189
|
+
}
|
|
190
|
+
if (
|
|
191
|
+
group.type !== 'bitmap' &&
|
|
192
|
+
((format !== undefined && format !== 'svg') || imageSize !== undefined)
|
|
193
|
+
) {
|
|
194
|
+
return fail(`SVG theme ${name} cannot define bitmap options`);
|
|
195
|
+
}
|
|
196
|
+
if (
|
|
197
|
+
group.type === 'bitmap' &&
|
|
198
|
+
format !== undefined &&
|
|
199
|
+
!(group.formats || []).includes(format)
|
|
200
|
+
) {
|
|
201
|
+
return fail(`format ${format} is unavailable for ${group.id}`);
|
|
202
|
+
}
|
|
203
|
+
if (
|
|
204
|
+
group.type === 'bitmap' &&
|
|
205
|
+
imageSize !== undefined &&
|
|
206
|
+
!(group.imageSizes || []).includes(imageSize)
|
|
207
|
+
) {
|
|
208
|
+
return fail(`imageSize ${imageSize} is unavailable for ${group.id}`);
|
|
209
|
+
}
|
|
210
|
+
themes[name] = {
|
|
211
|
+
styleGroup: value.styleGroup,
|
|
212
|
+
...(value.icons !== undefined ? { icons: [...value.icons] } : {}),
|
|
213
|
+
...(format !== undefined ? { format } : {}),
|
|
214
|
+
...(imageSize !== undefined ? { imageSize } : {}),
|
|
215
|
+
...(typeof value.defaultSize === 'number' ? { defaultSize: value.defaultSize } : {}),
|
|
216
|
+
...(typeof value.strokeWidth === 'number' ? { strokeWidth: value.strokeWidth } : {}),
|
|
217
|
+
...(typeof value.className === 'string' ? { className: value.className } : {}),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
if (!(raw.defaultTheme in themes)) {
|
|
221
|
+
return fail(`defaultTheme "${raw.defaultTheme}" is not defined`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let icons;
|
|
225
|
+
try {
|
|
226
|
+
icons = flattenIcons(raw.icons);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
229
|
+
}
|
|
230
|
+
for (const iconId of icons) {
|
|
231
|
+
if (!findIcon(iconId, catalog)) return fail(`unknown icon "${iconId}"`);
|
|
232
|
+
}
|
|
233
|
+
for (const [name, theme] of Object.entries(themes)) {
|
|
234
|
+
for (const iconId of theme.icons || []) {
|
|
235
|
+
if (!icons.includes(iconId)) return fail(`theme ${name}.icons contains unregistered icon "${iconId}"; add it to project icons first`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let integration;
|
|
240
|
+
if (raw.integration !== undefined) {
|
|
241
|
+
if (!isRecord(raw.integration)) return fail('integration must be an object');
|
|
242
|
+
for (const key of Object.keys(raw.integration)) {
|
|
243
|
+
if (!['adapter', 'entry', 'style'].includes(key)) {
|
|
244
|
+
return fail(`unknown integration field "${key}"`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (!ADAPTERS.has(raw.integration.adapter)) {
|
|
248
|
+
return fail(`integration.adapter is not a supported adapter`);
|
|
249
|
+
}
|
|
250
|
+
if (raw.integration.adapter !== 'assets-only') {
|
|
251
|
+
const entry = checkPath(raw.integration.entry, 'integration.entry');
|
|
252
|
+
if (entry) return entry;
|
|
253
|
+
} else if (raw.integration.entry !== undefined) {
|
|
254
|
+
const entry = checkPath(raw.integration.entry, 'integration.entry');
|
|
255
|
+
if (entry) return entry;
|
|
256
|
+
}
|
|
257
|
+
if (raw.integration.style !== undefined) {
|
|
258
|
+
const style = checkPath(raw.integration.style, 'integration.style');
|
|
259
|
+
if (style) return style;
|
|
260
|
+
}
|
|
261
|
+
integration = {
|
|
262
|
+
adapter: raw.integration.adapter,
|
|
263
|
+
...(raw.integration.entry !== undefined ? { entry: raw.integration.entry } : {}),
|
|
264
|
+
...(raw.integration.style !== undefined ? { style: raw.integration.style } : {}),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
ok: true,
|
|
270
|
+
warnings:
|
|
271
|
+
version === 1
|
|
272
|
+
? ['config schema v1 migrated "framework" to "target"']
|
|
273
|
+
: version === 2
|
|
274
|
+
? ['config schema v2 normalized to in-memory v3']
|
|
275
|
+
: [],
|
|
276
|
+
config: {
|
|
277
|
+
schemaVersion: VERSION,
|
|
278
|
+
tier: raw.tier,
|
|
279
|
+
target,
|
|
280
|
+
outputDir: raw.outputDir,
|
|
281
|
+
defaultTheme: raw.defaultTheme,
|
|
282
|
+
themes,
|
|
283
|
+
icons,
|
|
284
|
+
...(raw.downloadMode !== undefined ? { downloadMode: raw.downloadMode } : {}),
|
|
285
|
+
...(raw.missingIconPolicy !== undefined
|
|
286
|
+
? { missingIconPolicy: raw.missingIconPolicy }
|
|
287
|
+
: {}),
|
|
288
|
+
...(integration !== undefined ? { integration } : {}),
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
module.exports = { validateConfig, VERSION, ADAPTERS };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface GeneratedRenderOptions {
|
|
2
|
+
target?: "react" | "vue" | "vanilla" | "assets";
|
|
3
|
+
framework?: "react" | "vue";
|
|
4
|
+
tier?: "free" | "pro";
|
|
5
|
+
fullCatalog?: boolean;
|
|
6
|
+
catalog: unknown;
|
|
7
|
+
integration?: {
|
|
8
|
+
adapter: string;
|
|
9
|
+
entry?: string;
|
|
10
|
+
style?: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export interface GeneratedConfigPackage {
|
|
14
|
+
renderMoeiconsConfigJsonc(options: GeneratedRenderOptions): string;
|
|
15
|
+
validateConfig(raw: unknown, catalog: unknown): {
|
|
16
|
+
ok: boolean;
|
|
17
|
+
kind?: string;
|
|
18
|
+
message?: string;
|
|
19
|
+
version?: number;
|
|
20
|
+
warnings?: string[];
|
|
21
|
+
config?: unknown;
|
|
22
|
+
};
|
|
23
|
+
VERSION: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Load the code-library canonical config-package modules that were synced into
|
|
27
|
+
* `src/config-package/generated/`. The generated copy is committed with
|
|
28
|
+
* SOURCE.json (code-library commit + file digests) and copied to dist at build
|
|
29
|
+
* time so both tests (src) and the packaged CLI (dist) resolve the same
|
|
30
|
+
* relative path.
|
|
31
|
+
*/
|
|
32
|
+
export declare function loadGeneratedConfigPackage(): GeneratedConfigPackage;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
/**
|
|
6
|
+
* Load the code-library canonical config-package modules that were synced into
|
|
7
|
+
* `src/config-package/generated/`. The generated copy is committed with
|
|
8
|
+
* SOURCE.json (code-library commit + file digests) and copied to dist at build
|
|
9
|
+
* time so both tests (src) and the packaged CLI (dist) resolve the same
|
|
10
|
+
* relative path.
|
|
11
|
+
*/
|
|
12
|
+
export function loadGeneratedConfigPackage() {
|
|
13
|
+
const root = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const renderPath = join(root, "..", "config-package", "generated", "render-config.cjs");
|
|
15
|
+
const validatePath = join(root, "..", "config-package", "generated", "validate-config.cjs");
|
|
16
|
+
const renderer = require(renderPath);
|
|
17
|
+
const validator = require(validatePath);
|
|
18
|
+
return { ...renderer, ...validator };
|
|
19
|
+
}
|
package/dist/core/auth.d.ts
CHANGED
|
@@ -23,10 +23,34 @@ export type SessionStatus = {
|
|
|
23
23
|
};
|
|
24
24
|
/** Probe existing credentials without prompting for or creating fallback storage. */
|
|
25
25
|
export declare function runSessionStatusUseCase(context: CommandContext, deps?: AuthUseCaseDependencies): Promise<SessionStatus>;
|
|
26
|
+
export type MoeiconsEnvironment = "production" | "local";
|
|
27
|
+
export interface AuthEnvironment {
|
|
28
|
+
readonly mode: MoeiconsEnvironment;
|
|
29
|
+
readonly label: string;
|
|
30
|
+
readonly apiBaseUrl: string;
|
|
31
|
+
readonly websiteOrigin: string;
|
|
32
|
+
readonly auth0Issuer: string;
|
|
33
|
+
readonly auth0ClientId: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* H5: resolve a single coherent auth environment. `MOEICONS_ENV` may declare
|
|
37
|
+
* `local` or `production`; when set it must agree with the endpoints. Loopback
|
|
38
|
+
* HTTP is only allowed for local, and the API/website must not be mixed across
|
|
39
|
+
* environments so a dev token can never be written to production (or vice versa).
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveAuthEnvironment(env: Record<string, string | undefined>): AuthEnvironment;
|
|
42
|
+
/** H5: human-readable environment label for `login`/`account` output. */
|
|
43
|
+
export declare function describeAuthEnvironment(context: CommandContext): string;
|
|
26
44
|
export declare function runLoginUseCase(context: CommandContext, deps?: AuthUseCaseDependencies): Promise<ReturnType<typeof redactSession>>;
|
|
27
45
|
/** Internal authenticated transport credential; adapters must never render it. */
|
|
28
46
|
export declare function runAccessTokenUseCase(context: CommandContext, deps?: AuthUseCaseDependencies, forceRefresh?: boolean): Promise<string>;
|
|
29
47
|
export declare function runAccountUseCase(context: CommandContext, deps?: AuthUseCaseDependencies): Promise<ReturnType<typeof redactSession>>;
|
|
48
|
+
/** Remote Worker entitlement lookup (H2); never guesses a plan from scope. */
|
|
49
|
+
export declare function runRemoteAccountUseCase(context: CommandContext, deps?: AuthUseCaseDependencies): Promise<{
|
|
50
|
+
tier: string;
|
|
51
|
+
entitlementStatus: string;
|
|
52
|
+
validUntil: string | null;
|
|
53
|
+
} | undefined>;
|
|
30
54
|
export declare function runLogoutUseCase(context: CommandContext, deps?: AuthUseCaseDependencies): Promise<{
|
|
31
55
|
revoked: boolean;
|
|
32
56
|
}>;
|
package/dist/core/auth.js
CHANGED
|
@@ -3,8 +3,9 @@ import { createFileTokenStore, createSystemTokenStore, redactSession } from "../
|
|
|
3
3
|
import { openBrowser } from "../auth/open-browser.js";
|
|
4
4
|
import { requestJson } from "../api/client.js";
|
|
5
5
|
import { CliError } from "../errors/index.js";
|
|
6
|
-
const
|
|
7
|
-
const
|
|
6
|
+
const DEFAULT_API_BASE_URL = "https://api.moeicons.com";
|
|
7
|
+
const DEFAULT_WEBSITE_ORIGIN = "https://moeicons.com";
|
|
8
|
+
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
|
|
8
9
|
function existingStore(context, deps) {
|
|
9
10
|
return deps.tokenStore ?? (context.env.MOEICONS_DISABLE_SYSTEM_KEYCHAIN === "1" ? undefined : (deps.systemTokenStore ?? createSystemTokenStore)()) ??
|
|
10
11
|
(context.env.MOEICONS_TOKEN_STORE_DIR ? (deps.fileTokenStore ?? ((rootDir) => createFileTokenStore(rootDir ? { rootDir } : {})))(context.env.MOEICONS_TOKEN_STORE_DIR) : undefined);
|
|
@@ -53,20 +54,101 @@ async function selectStore(context, deps) {
|
|
|
53
54
|
throw new CliError("AUTH_ERROR", "login requires secure credential storage");
|
|
54
55
|
return (deps.fileTokenStore ?? ((rootDir) => createFileTokenStore(rootDir ? { rootDir } : {})))(context.env.MOEICONS_TOKEN_STORE_DIR);
|
|
55
56
|
}
|
|
57
|
+
function assertTrustedHttpBase(value, envName) {
|
|
58
|
+
let parsed;
|
|
59
|
+
try {
|
|
60
|
+
parsed = new URL(value);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
throw new CliError("VALIDATION_ERROR", `${envName} is not a valid URL`);
|
|
64
|
+
}
|
|
65
|
+
const loopback = parsed.protocol === "http:" && LOOPBACK_HOSTS.has(parsed.hostname);
|
|
66
|
+
if (parsed.protocol !== "https:" && !loopback) {
|
|
67
|
+
throw new CliError("VALIDATION_ERROR", `${envName} must be https or loopback http`);
|
|
68
|
+
}
|
|
69
|
+
return value.replace(/\/$/, "");
|
|
70
|
+
}
|
|
71
|
+
function isLoopbackHttp(value) {
|
|
72
|
+
try {
|
|
73
|
+
const parsed = new URL(value);
|
|
74
|
+
return parsed.protocol === "http:" && LOOPBACK_HOSTS.has(parsed.hostname);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* H5: resolve a single coherent auth environment. `MOEICONS_ENV` may declare
|
|
82
|
+
* `local` or `production`; when set it must agree with the endpoints. Loopback
|
|
83
|
+
* HTTP is only allowed for local, and the API/website must not be mixed across
|
|
84
|
+
* environments so a dev token can never be written to production (or vice versa).
|
|
85
|
+
*/
|
|
86
|
+
export function resolveAuthEnvironment(env) {
|
|
87
|
+
const apiBaseUrl = assertTrustedHttpBase(env.MOEICONS_API_BASE_URL ?? DEFAULT_API_BASE_URL, "MOEICONS_API_BASE_URL");
|
|
88
|
+
const websiteOrigin = assertTrustedHttpBase(env.MOEICONS_WEBSITE_ORIGIN ?? DEFAULT_WEBSITE_ORIGIN, "MOEICONS_WEBSITE_ORIGIN");
|
|
89
|
+
const apiLoopback = isLoopbackHttp(apiBaseUrl);
|
|
90
|
+
const websiteLoopback = isLoopbackHttp(websiteOrigin);
|
|
91
|
+
if (apiLoopback !== websiteLoopback) {
|
|
92
|
+
throw new CliError("VALIDATION_ERROR", "MOEICONS_API_BASE_URL and MOEICONS_WEBSITE_ORIGIN must both be loopback (local) or both be non-loopback (production)");
|
|
93
|
+
}
|
|
94
|
+
const declared = env.MOEICONS_ENV;
|
|
95
|
+
if (declared !== undefined && declared !== "local" && declared !== "production") {
|
|
96
|
+
throw new CliError("VALIDATION_ERROR", "MOEICONS_ENV must be local or production");
|
|
97
|
+
}
|
|
98
|
+
const inferred = apiLoopback ? "local" : "production";
|
|
99
|
+
if (declared && declared !== inferred) {
|
|
100
|
+
throw new CliError("VALIDATION_ERROR", `MOEICONS_ENV=${declared} conflicts with configured endpoints (${inferred})`);
|
|
101
|
+
}
|
|
102
|
+
const mode = declared ?? inferred;
|
|
103
|
+
// H5: the Auth0 issuer receives refresh tokens, so it must match the mode.
|
|
104
|
+
const auth0Issuer = env.MOEICONS_AUTH0_ISSUER ?? "";
|
|
105
|
+
if (auth0Issuer) {
|
|
106
|
+
assertTrustedHttpBase(auth0Issuer, "MOEICONS_AUTH0_ISSUER");
|
|
107
|
+
const issuerLoopback = isLoopbackHttp(auth0Issuer);
|
|
108
|
+
if (mode === "local" && !issuerLoopback) {
|
|
109
|
+
throw new CliError("VALIDATION_ERROR", "local MOEICONS_AUTH0_ISSUER must be loopback http");
|
|
110
|
+
}
|
|
111
|
+
if (mode === "production" && issuerLoopback) {
|
|
112
|
+
throw new CliError("VALIDATION_ERROR", "production MOEICONS_AUTH0_ISSUER must be https");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
mode,
|
|
117
|
+
label: mode === "local" ? "local (dev)" : "production",
|
|
118
|
+
apiBaseUrl,
|
|
119
|
+
websiteOrigin,
|
|
120
|
+
auth0Issuer,
|
|
121
|
+
auth0ClientId: env.MOEICONS_AUTH0_CLIENT_ID ?? "",
|
|
122
|
+
};
|
|
123
|
+
}
|
|
56
124
|
function auth0Config(context) {
|
|
125
|
+
const resolved = resolveAuthEnvironment(context.env);
|
|
57
126
|
return {
|
|
58
|
-
apiBaseUrl:
|
|
59
|
-
websiteOrigin:
|
|
60
|
-
auth0Issuer:
|
|
61
|
-
auth0ClientId:
|
|
127
|
+
apiBaseUrl: resolved.apiBaseUrl,
|
|
128
|
+
websiteOrigin: resolved.websiteOrigin,
|
|
129
|
+
auth0Issuer: resolved.auth0Issuer,
|
|
130
|
+
auth0ClientId: resolved.auth0ClientId,
|
|
62
131
|
};
|
|
63
132
|
}
|
|
133
|
+
/** H5: human-readable environment label for `login`/`account` output. */
|
|
134
|
+
export function describeAuthEnvironment(context) {
|
|
135
|
+
return resolveAuthEnvironment(context.env).label;
|
|
136
|
+
}
|
|
64
137
|
export async function runLoginUseCase(context, deps = {}) {
|
|
65
138
|
const tokenStore = await selectStore(context, deps);
|
|
66
|
-
const
|
|
139
|
+
const apiBaseUrl = auth0Config(context).apiBaseUrl;
|
|
140
|
+
const request = deps.request ??
|
|
141
|
+
((path, options) => requestJson({ baseUrl: apiBaseUrl }, path, {
|
|
142
|
+
...options,
|
|
143
|
+
retries: options.method === "GET" || options.method === "DELETE" ? 3 : 0,
|
|
144
|
+
...(options.stage ? { stage: `login ${options.stage}` } : {}),
|
|
145
|
+
}));
|
|
146
|
+
// H4 test seam: loopback acceptance runs headless. Production never sets this.
|
|
147
|
+
const browserOpener = deps.openBrowser ??
|
|
148
|
+
(context.env.MOEICONS_NO_BROWSER === "1" ? () => Promise.resolve() : openBrowser);
|
|
67
149
|
const session = await loginWithDeviceSession(auth0Config(context), {
|
|
68
150
|
request,
|
|
69
|
-
openBrowser:
|
|
151
|
+
openBrowser: browserOpener,
|
|
70
152
|
sleep: deps.sleep ?? ((milliseconds, signal) => new Promise((resolve, reject) => {
|
|
71
153
|
const timer = setTimeout(resolve, milliseconds);
|
|
72
154
|
signal?.addEventListener("abort", () => { clearTimeout(timer); reject(new CliError("CANCELLED", "login cancelled")); }, { once: true });
|
|
@@ -105,6 +187,43 @@ export async function runAccessTokenUseCase(context, deps = {}, forceRefresh = f
|
|
|
105
187
|
export async function runAccountUseCase(context, deps = {}) {
|
|
106
188
|
return redactSession((await activeSession(context, deps)).session);
|
|
107
189
|
}
|
|
190
|
+
/** Remote Worker entitlement lookup (H2); never guesses a plan from scope. */
|
|
191
|
+
export async function runRemoteAccountUseCase(context, deps = {}) {
|
|
192
|
+
const apiBaseUrl = auth0Config(context).apiBaseUrl;
|
|
193
|
+
const doFetch = deps.fetch ?? fetch;
|
|
194
|
+
let session;
|
|
195
|
+
try {
|
|
196
|
+
session = (await activeSession(context, deps)).session;
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
throw new CliError("AUTH_ERROR", "not logged in");
|
|
200
|
+
}
|
|
201
|
+
const call = async (token) => doFetch(`${apiBaseUrl}/v1/cli/account`, {
|
|
202
|
+
method: "GET",
|
|
203
|
+
headers: { authorization: `Bearer ${token}` },
|
|
204
|
+
signal: context.signal,
|
|
205
|
+
});
|
|
206
|
+
let response = await call(session.accessToken);
|
|
207
|
+
// 401 => refresh exactly once and retry (H2 point 6).
|
|
208
|
+
if (response.status === 401) {
|
|
209
|
+
const refreshed = await runAccessTokenUseCase(context, deps, true);
|
|
210
|
+
response = await call(refreshed);
|
|
211
|
+
}
|
|
212
|
+
if (response.status === 403) {
|
|
213
|
+
// no/expired entitlement: caller still has a valid account; report free.
|
|
214
|
+
return { tier: "free", entitlementStatus: "none", validUntil: null };
|
|
215
|
+
}
|
|
216
|
+
if (!response.ok) {
|
|
217
|
+
throw new CliError("NETWORK_ERROR", `account lookup failed with ${response.status}`);
|
|
218
|
+
}
|
|
219
|
+
const value = (await response.json());
|
|
220
|
+
if (typeof value.tier !== "string" ||
|
|
221
|
+
typeof value.entitlementStatus !== "string" ||
|
|
222
|
+
(value.validUntil !== null && typeof value.validUntil !== "string")) {
|
|
223
|
+
throw new CliError("VALIDATION_ERROR", "invalid account response");
|
|
224
|
+
}
|
|
225
|
+
return { tier: value.tier, entitlementStatus: value.entitlementStatus, validUntil: value.validUntil ?? null };
|
|
226
|
+
}
|
|
108
227
|
export async function runLogoutUseCase(context, deps = {}) {
|
|
109
228
|
const store = existingStore(context, deps);
|
|
110
229
|
if (!store)
|
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
import { type ResourceVariant } from "./resource-variant.js";
|
|
2
2
|
export interface SelectedBitmapAsset {
|
|
3
|
-
/** POSIX path relative to outputDir, e.g. assets/
|
|
3
|
+
/** POSIX path relative to outputDir, e.g. assets/example-bitmap-style-256-webp/ui-search.webp */
|
|
4
4
|
readonly destRel: string;
|
|
5
5
|
readonly bytes: Uint8Array;
|
|
6
6
|
readonly resourceVariantId: string;
|
|
7
7
|
readonly iconId: string;
|
|
8
8
|
}
|
|
9
|
+
export interface MatchBitmapOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Media contract of the archive being read. Defaults to 2 (canonical actual
|
|
12
|
+
* directory names). Set to 1 to read frozen v1 artifacts through the explicit
|
|
13
|
+
* legacy parser; the two parsers are never mixed.
|
|
14
|
+
*/
|
|
15
|
+
readonly mediaContractVersion?: 1 | 2;
|
|
16
|
+
/** Exact variant/icon pairs, used when a missing theme falls back to another group. */
|
|
17
|
+
readonly requestedPairs?: readonly {
|
|
18
|
+
readonly variantId: string;
|
|
19
|
+
readonly iconId: string;
|
|
20
|
+
}[];
|
|
21
|
+
}
|
|
9
22
|
/**
|
|
10
|
-
* Identify a bitmap file
|
|
11
|
-
*
|
|
23
|
+
* Identify a bitmap file at its canonical archive location
|
|
24
|
+
* `assets/<resourceVariantId>/<iconId>.<format>`. v2 uses controlled keyword
|
|
25
|
+
* tokens; v1 uses the explicit legacy `<group>-<format>-<size>` branch.
|
|
12
26
|
*/
|
|
13
|
-
export declare function matchArchiveBitmapFile(archivePath: string): {
|
|
27
|
+
export declare function matchArchiveBitmapFile(archivePath: string, options?: MatchBitmapOptions): {
|
|
14
28
|
readonly variant: ResourceVariant;
|
|
15
29
|
readonly iconId: string;
|
|
16
30
|
} | undefined;
|
|
@@ -26,4 +40,4 @@ export type SelectBitmapAssetsResult = {
|
|
|
26
40
|
* Keep only archive files that belong to the requested variants and selected
|
|
27
41
|
* icons. Other sizes/formats stay in the tarball/cache and are not written.
|
|
28
42
|
*/
|
|
29
|
-
export declare function selectBitmapVariantAssets(archiveFiles: Readonly<Record<string, Uint8Array>>, variants: readonly ResourceVariant[], iconIds: readonly string[]): SelectBitmapAssetsResult;
|
|
43
|
+
export declare function selectBitmapVariantAssets(archiveFiles: Readonly<Record<string, Uint8Array>>, variants: readonly ResourceVariant[], iconIds: readonly string[], options?: MatchBitmapOptions): SelectBitmapAssetsResult;
|