@svgicons-com/cli 0.1.0-alpha.1
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 +281 -0
- package/RELEASE_NOTES.md +42 -0
- package/bin/svgicons.js +13 -0
- package/package.json +26 -0
- package/src/api.js +194 -0
- package/src/cli.js +1900 -0
- package/src/config.js +134 -0
- package/src/downloads.js +173 -0
- package/src/errors.js +127 -0
- package/src/format.js +121 -0
- package/src/project.js +270 -0
- package/src/redact.js +43 -0
- package/src/scanner.js +247 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,1900 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { stdin, stdout } from 'node:process';
|
|
4
|
+
import { createInterface } from 'node:readline/promises';
|
|
5
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
6
|
+
import { join, resolve } from 'node:path';
|
|
7
|
+
import {
|
|
8
|
+
clearConfig,
|
|
9
|
+
clearRuntimeOverrides,
|
|
10
|
+
credentialsStatus,
|
|
11
|
+
defaultBaseUrl,
|
|
12
|
+
normalizeBaseUrl,
|
|
13
|
+
readConfig,
|
|
14
|
+
setConfigPathOverride,
|
|
15
|
+
setCredentialOverrides,
|
|
16
|
+
writeConfig,
|
|
17
|
+
} from './config.js';
|
|
18
|
+
import { callMcpTool, mcpRequest, proApi, proApiDownload } from './api.js';
|
|
19
|
+
import {
|
|
20
|
+
assertIconReferenceMatches,
|
|
21
|
+
parseIconLookupReference,
|
|
22
|
+
parseIconReference,
|
|
23
|
+
resolveArchiveDownloadPath,
|
|
24
|
+
resolveIconDownloadPath,
|
|
25
|
+
sanitizeFileSegment,
|
|
26
|
+
writeBinaryFile,
|
|
27
|
+
writeIconSvg,
|
|
28
|
+
} from './downloads.js';
|
|
29
|
+
import { printCollection, printCollectionDetail, printExport, printJson, printScanProposal, printSearchResults } from './format.js';
|
|
30
|
+
import {
|
|
31
|
+
buildLicenseReport,
|
|
32
|
+
createLockfile,
|
|
33
|
+
createManifest,
|
|
34
|
+
licenseReportToCsv,
|
|
35
|
+
licenseReportToMarkdown,
|
|
36
|
+
lockIconFromIcon,
|
|
37
|
+
lockfilePath,
|
|
38
|
+
manifestPath,
|
|
39
|
+
normalizeCollectionManifestEntry,
|
|
40
|
+
normalizeIconManifestEntry,
|
|
41
|
+
normalizeManifest,
|
|
42
|
+
readJsonFile,
|
|
43
|
+
svgFilenameForIcon,
|
|
44
|
+
writeJsonFile,
|
|
45
|
+
writeTextFile,
|
|
46
|
+
} from './project.js';
|
|
47
|
+
import { redact } from './redact.js';
|
|
48
|
+
import { exportFailedError, exportNotReadyError, exportTimeoutError, licenseViolationError, usageError } from './errors.js';
|
|
49
|
+
import { scanProject } from './scanner.js';
|
|
50
|
+
|
|
51
|
+
const KNOWN_SCOPES = [
|
|
52
|
+
'search:read',
|
|
53
|
+
'icons:read',
|
|
54
|
+
'collections:read',
|
|
55
|
+
'collections:write',
|
|
56
|
+
'exports:create',
|
|
57
|
+
'mcp:use',
|
|
58
|
+
];
|
|
59
|
+
const EXPORT_FORMATS = ['svg-folder', 'svg-sprite', 'json-manifest', 'license-manifest', 'react-ts', 'vue'];
|
|
60
|
+
const FRAMEWORKS = ['svg', 'react-ts', 'vue', 'sprite'];
|
|
61
|
+
const COLOR_POLICIES = ['currentColor', 'preserve', 'strip'];
|
|
62
|
+
const NAMING_POLICIES = ['kebab', 'pascal', 'camel'];
|
|
63
|
+
|
|
64
|
+
export async function run(argv) {
|
|
65
|
+
const { args, options: globalOptions } = extractGlobalOptions(argv);
|
|
66
|
+
clearRuntimeOverrides();
|
|
67
|
+
|
|
68
|
+
if (globalOptions.config) {
|
|
69
|
+
setConfigPathOverride(resolve(String(globalOptions.config)));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
setCredentialOverrides({
|
|
73
|
+
baseUrl: globalOptions['base-url'],
|
|
74
|
+
token: globalOptions.token,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const [command, ...rest] = args;
|
|
78
|
+
|
|
79
|
+
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
80
|
+
printHelp();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (command === 'version' || command === '--version' || command === '-v') {
|
|
85
|
+
await version();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (command === 'login') {
|
|
90
|
+
await login(rest, globalOptions);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (command === 'logout') {
|
|
95
|
+
await logout();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (command === 'auth') {
|
|
100
|
+
await auth(rest, globalOptions);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (command === 'config') {
|
|
105
|
+
await configCommand(rest);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (command === 'doctor') {
|
|
110
|
+
await doctor(rest);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (command === 'search') {
|
|
115
|
+
await search(rest);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (command === 'recommend') {
|
|
120
|
+
await recommend(rest);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (command === 'pick') {
|
|
125
|
+
await pick(rest);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (command === 'icon') {
|
|
130
|
+
await icon(rest);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (command === 'download') {
|
|
135
|
+
await iconDownload(rest);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (command === 'collection' || command === 'kit') {
|
|
140
|
+
await collection(rest);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (command === 'export') {
|
|
145
|
+
await exportCommand(rest);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (command === 'init') {
|
|
150
|
+
await initProject(rest);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (command === 'sync') {
|
|
155
|
+
await syncProject(rest);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (command === 'build') {
|
|
160
|
+
await buildProject(rest);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (command === 'update') {
|
|
165
|
+
await updateProject(rest);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (command === 'license') {
|
|
170
|
+
await licenseCommand(rest);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (command === 'scan') {
|
|
175
|
+
await scan(rest);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
throw new Error(`Unknown command: ${command}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function login(args, globalOptions = {}) {
|
|
183
|
+
const options = parseOptions(args);
|
|
184
|
+
const token = options.token || globalOptions.token || process.env.SVGICONS_TOKEN || process.env.SVGICONS_API_TOKEN;
|
|
185
|
+
|
|
186
|
+
if (!token) {
|
|
187
|
+
throw new Error('Missing token. Use: svgicons login --token <token>');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const baseUrl = normalizeBaseUrl(options['base-url'] || globalOptions['base-url'] || defaultBaseUrl());
|
|
191
|
+
await writeConfig({
|
|
192
|
+
baseUrl,
|
|
193
|
+
token,
|
|
194
|
+
createdAt: new Date().toISOString(),
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
console.log(`Svg/icons CLI token saved for ${baseUrl}.`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function logout() {
|
|
201
|
+
await clearConfig();
|
|
202
|
+
console.log('Svg/icons CLI credentials removed.');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function auth(args, globalOptions = {}) {
|
|
206
|
+
const [subcommand, ...rest] = args;
|
|
207
|
+
|
|
208
|
+
if (subcommand === 'login') {
|
|
209
|
+
await login(rest, globalOptions);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (subcommand === 'logout') {
|
|
214
|
+
await logout();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (subcommand === 'status') {
|
|
219
|
+
await authStatus(rest);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (subcommand === 'scopes') {
|
|
224
|
+
const options = parseOptions(rest);
|
|
225
|
+
const payload = { data: KNOWN_SCOPES };
|
|
226
|
+
|
|
227
|
+
if (options.json) {
|
|
228
|
+
printJson(payload);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
console.log('Known Pro API scopes:');
|
|
233
|
+
for (const scope of KNOWN_SCOPES) {
|
|
234
|
+
console.log(` ${scope}`);
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
throw new Error('Unknown auth command. Use: login, logout, status, scopes.');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function authStatus(args) {
|
|
243
|
+
const options = parseOptions(args);
|
|
244
|
+
const status = await credentialsStatus();
|
|
245
|
+
let remote = null;
|
|
246
|
+
|
|
247
|
+
if (status.hasToken) {
|
|
248
|
+
try {
|
|
249
|
+
remote = {
|
|
250
|
+
ok: true,
|
|
251
|
+
payload: await proApi('/api/pro/me'),
|
|
252
|
+
};
|
|
253
|
+
} catch (error) {
|
|
254
|
+
remote = {
|
|
255
|
+
ok: false,
|
|
256
|
+
code: error.code || 'error',
|
|
257
|
+
status: error.status,
|
|
258
|
+
message: error.message,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const payload = {
|
|
264
|
+
...status,
|
|
265
|
+
remote,
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
if (options.json) {
|
|
269
|
+
printJson(redact(payload));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
console.log(`Config: ${status.configPath}`);
|
|
274
|
+
console.log(`Base URL: ${status.baseUrl} (${status.baseUrlSource})`);
|
|
275
|
+
console.log(`Token: ${status.hasToken ? `configured (${status.tokenSource})` : 'not configured'}`);
|
|
276
|
+
if (remote) {
|
|
277
|
+
console.log(remote.ok
|
|
278
|
+
? 'Remote account endpoint: available'
|
|
279
|
+
: `Remote account endpoint: unavailable (${remote.code}${remote.status ? `, HTTP ${remote.status}` : ''})`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function configCommand(args) {
|
|
284
|
+
const [subcommand, ...rest] = args;
|
|
285
|
+
|
|
286
|
+
if (subcommand === 'list') {
|
|
287
|
+
const options = parseOptions(rest);
|
|
288
|
+
const config = await readConfig();
|
|
289
|
+
const status = await credentialsStatus();
|
|
290
|
+
const payload = redact({
|
|
291
|
+
configPath: status.configPath,
|
|
292
|
+
baseUrl: config.baseUrl || null,
|
|
293
|
+
token: config.token || null,
|
|
294
|
+
createdAt: config.createdAt || null,
|
|
295
|
+
effective: {
|
|
296
|
+
baseUrl: status.baseUrl,
|
|
297
|
+
baseUrlSource: status.baseUrlSource,
|
|
298
|
+
hasToken: status.hasToken,
|
|
299
|
+
tokenSource: status.tokenSource,
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
if (options.json) {
|
|
304
|
+
printJson(payload);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
console.log(`Config: ${payload.configPath}`);
|
|
309
|
+
console.log(`baseUrl: ${payload.baseUrl || '(not set)'}`);
|
|
310
|
+
console.log(`token: ${payload.token || '(not set)'}`);
|
|
311
|
+
console.log(`createdAt: ${payload.createdAt || '(not set)'}`);
|
|
312
|
+
console.log(`effective baseUrl: ${payload.effective.baseUrl} (${payload.effective.baseUrlSource})`);
|
|
313
|
+
console.log(`effective token: ${payload.effective.hasToken ? `configured (${payload.effective.tokenSource})` : 'not configured'}`);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (subcommand === 'get') {
|
|
318
|
+
const [key] = rest;
|
|
319
|
+
const config = await readConfig();
|
|
320
|
+
const normalizedKey = normalizeConfigKey(key);
|
|
321
|
+
|
|
322
|
+
if (!normalizedKey) {
|
|
323
|
+
throw new Error('Config key is required. Use: token, baseUrl, createdAt.');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const value = config[normalizedKey];
|
|
327
|
+
console.log(normalizedKey === 'token' && value ? '[redacted]' : value ?? '');
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (subcommand === 'set') {
|
|
332
|
+
const [key, ...valueParts] = rest;
|
|
333
|
+
const normalizedKey = normalizeConfigKey(key);
|
|
334
|
+
const value = valueParts.join(' ').trim();
|
|
335
|
+
|
|
336
|
+
if (!normalizedKey || !value) {
|
|
337
|
+
throw new Error('Config key and value are required. Example: svgicons config set baseUrl https://svgicons.com');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const config = await readConfig();
|
|
341
|
+
config[normalizedKey] = normalizedKey === 'baseUrl' ? normalizeBaseUrl(value) : value;
|
|
342
|
+
config.updatedAt = new Date().toISOString();
|
|
343
|
+
await writeConfig(config);
|
|
344
|
+
console.log(`Set ${normalizedKey}.`);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
throw new Error('Unknown config command. Use: list, get, set.');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function doctor(args) {
|
|
352
|
+
const options = parseOptions(args);
|
|
353
|
+
const checks = [];
|
|
354
|
+
let status;
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
status = await credentialsStatus();
|
|
358
|
+
addCheck(checks, 'config', 'pass', `Using ${status.configPath}`);
|
|
359
|
+
addCheck(checks, 'base-url', 'pass', status.baseUrl);
|
|
360
|
+
} catch (error) {
|
|
361
|
+
addCheck(checks, 'base-url', 'fail', error.message);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (status?.hasToken) {
|
|
365
|
+
addCheck(checks, 'token', 'pass', `Configured from ${status.tokenSource}`);
|
|
366
|
+
} else {
|
|
367
|
+
addCheck(checks, 'token', 'warn', 'No token configured. Anonymous search can work; Pro collection/export commands require login.');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
await checkDoctorStep(checks, 'mcp', async () => {
|
|
371
|
+
await mcpRequest('ping', {}, { token: '' });
|
|
372
|
+
return 'MCP endpoint responded to anonymous ping.';
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
if (status?.hasToken) {
|
|
376
|
+
await checkDoctorStep(checks, 'pro-api', async () => {
|
|
377
|
+
await proApi('/api/pro/project-kits');
|
|
378
|
+
return 'Pro REST API accepted the current token.';
|
|
379
|
+
});
|
|
380
|
+
} else {
|
|
381
|
+
addCheck(checks, 'pro-api', 'skip', 'Skipped because no token is configured.');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (options.json) {
|
|
385
|
+
printJson({ checks });
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
for (const check of checks) {
|
|
390
|
+
console.log(`${check.status.toUpperCase()}\t${check.name}\t${check.message}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function search(args) {
|
|
395
|
+
const options = parseOptions(args);
|
|
396
|
+
const query = options._.join(' ').trim();
|
|
397
|
+
|
|
398
|
+
if (!query) {
|
|
399
|
+
throw new Error('Search query is required. Example: svgicons search "arrow left"');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const requestOptions = {};
|
|
403
|
+
if (booleanOption(options.anonymous, false)) {
|
|
404
|
+
requestOptions.token = '';
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const payload = await callMcpTool('search_icons', {
|
|
408
|
+
query,
|
|
409
|
+
limit: numberOption(options.limit, 10),
|
|
410
|
+
offset: numberOption(options.offset, 0),
|
|
411
|
+
category: options.category,
|
|
412
|
+
iconSetPrefix: options['icon-set'],
|
|
413
|
+
includeSvg: booleanOption(options['include-svg'], false),
|
|
414
|
+
}, requestOptions);
|
|
415
|
+
|
|
416
|
+
if (options.json) {
|
|
417
|
+
printJson(payload);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
printSearchResults(payload);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function recommend(args) {
|
|
425
|
+
const options = parseOptions(args);
|
|
426
|
+
const prompt = options._.join(' ').trim();
|
|
427
|
+
|
|
428
|
+
if (!prompt) {
|
|
429
|
+
throw usageError('Recommendation prompt is required. Example: svgicons recommend "billing settings dashboard"');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const concepts = listOption(options.concept || options.concepts);
|
|
433
|
+
const limit = numberOption(options.limit, booleanOption(options['create-collection'], false) ? 16 : 10);
|
|
434
|
+
|
|
435
|
+
if (options['create-collection']) {
|
|
436
|
+
const payload = await callMcpTool('generate_icon_kit_for_project', {
|
|
437
|
+
projectName: options['collection-name'] || options.name,
|
|
438
|
+
projectDescription: prompt,
|
|
439
|
+
requiredConcepts: concepts,
|
|
440
|
+
category: options.category,
|
|
441
|
+
maxIcons: limit,
|
|
442
|
+
framework: enumOption(options.framework, 'framework', FRAMEWORKS),
|
|
443
|
+
colorPolicy: enumOption(options['color-policy'], 'color policy', COLOR_POLICIES),
|
|
444
|
+
namingPolicy: enumOption(options['naming-policy'], 'naming policy', NAMING_POLICIES),
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
if (options.json) {
|
|
448
|
+
printJson(payload);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
printRecommendations(payload);
|
|
453
|
+
if (payload.kit) {
|
|
454
|
+
console.log('');
|
|
455
|
+
console.log('Created Icon Collection:');
|
|
456
|
+
printCollection(payload.kit);
|
|
457
|
+
}
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const payload = await callMcpTool('recommend_icons_for_ui', {
|
|
462
|
+
uiDescription: prompt,
|
|
463
|
+
concepts,
|
|
464
|
+
category: options.category,
|
|
465
|
+
limit,
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
if (options.json) {
|
|
469
|
+
printJson(payload);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
printRecommendations(payload);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function pick(args) {
|
|
477
|
+
const options = parseOptions(args);
|
|
478
|
+
const query = options._.join(' ').trim();
|
|
479
|
+
|
|
480
|
+
if (!query) {
|
|
481
|
+
throw usageError('Pick query is required. Use a non-interactive query, for example: svgicons pick "settings gear"');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (booleanOption(options.interactive, false)) {
|
|
485
|
+
assertInteractivePickAllowed(options);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const payload = await callMcpTool('search_icons', {
|
|
489
|
+
query,
|
|
490
|
+
limit: numberOption(options.limit, 10),
|
|
491
|
+
offset: 0,
|
|
492
|
+
category: options.category,
|
|
493
|
+
iconSetPrefix: options['icon-set'],
|
|
494
|
+
includeSvg: false,
|
|
495
|
+
});
|
|
496
|
+
const icons = payload.data || [];
|
|
497
|
+
|
|
498
|
+
if (icons.length === 0) {
|
|
499
|
+
throw usageError(`No icons found for "${query}".`);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const selected = await selectPickedIcon(icons, options);
|
|
503
|
+
const result = {
|
|
504
|
+
query,
|
|
505
|
+
selected,
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
if (options.add) {
|
|
509
|
+
const collectionId = await resolveCollectionId(options.add);
|
|
510
|
+
const added = await proApi(`/api/pro/project-kits/${encodeURIComponent(collectionId)}/icons/bulk`, {
|
|
511
|
+
method: 'POST',
|
|
512
|
+
body: {
|
|
513
|
+
icon_ids: [Number(selected.id)],
|
|
514
|
+
},
|
|
515
|
+
});
|
|
516
|
+
result.added = added.data;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (booleanOption(options.download, false)) {
|
|
520
|
+
const strictReference = `${selected.id}-${sanitizeFileSegment(selected.name || 'icon')}`;
|
|
521
|
+
const icon = await fetchIcon(strictReference, true);
|
|
522
|
+
const outputPath = resolveIconDownloadPath(options.output || options.o, icon);
|
|
523
|
+
|
|
524
|
+
await writeIconSvg(outputPath, icon.svg, booleanOption(options.force, false));
|
|
525
|
+
result.outputPath = outputPath;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (options.json) {
|
|
529
|
+
printJson(result);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
console.log(`Picked ${selected.id}\t${selected.name}`);
|
|
534
|
+
if (selected.pageUrl) {
|
|
535
|
+
console.log(` ${selected.pageUrl}`);
|
|
536
|
+
}
|
|
537
|
+
if (result.added) {
|
|
538
|
+
console.log(`Added to collection: ${options.add}`);
|
|
539
|
+
}
|
|
540
|
+
if (result.outputPath) {
|
|
541
|
+
console.log(`Downloaded SVG: ${result.outputPath}`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function assertInteractivePickAllowed(options) {
|
|
546
|
+
if (booleanOption(options.ci, false) || !stdin.isTTY || !stdout.isTTY) {
|
|
547
|
+
throw usageError('Interactive pick requires an interactive terminal. Omit --interactive to select the first result deterministically.');
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async function selectPickedIcon(icons, options) {
|
|
552
|
+
if (!booleanOption(options.interactive, false)) {
|
|
553
|
+
return icons[0];
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
console.log('Select an icon:');
|
|
557
|
+
icons.forEach((icon, index) => {
|
|
558
|
+
const set = icon.iconSet?.name ? ` - ${icon.iconSet.name}` : '';
|
|
559
|
+
console.log(` ${index + 1}. ${icon.id}\t${icon.name}${set}`);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
563
|
+
try {
|
|
564
|
+
while (true) {
|
|
565
|
+
const answer = await rl.question('Pick icon number [1], or q to cancel: ');
|
|
566
|
+
const value = answer.trim().toLowerCase();
|
|
567
|
+
|
|
568
|
+
if (value === '') {
|
|
569
|
+
return icons[0];
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (value === 'q' || value === 'quit' || value === 'cancel') {
|
|
573
|
+
throw usageError('Pick cancelled.');
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const index = Number(value);
|
|
577
|
+
if (Number.isInteger(index) && index >= 1 && index <= icons.length) {
|
|
578
|
+
return icons[index - 1];
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
console.log(`Enter a number from 1 to ${icons.length}, or q to cancel.`);
|
|
582
|
+
}
|
|
583
|
+
} finally {
|
|
584
|
+
rl.close();
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
async function icon(args) {
|
|
589
|
+
const [subcommand, ...rest] = args;
|
|
590
|
+
|
|
591
|
+
if (subcommand === 'show') {
|
|
592
|
+
await iconShow(rest);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (subcommand === 'raw') {
|
|
597
|
+
await iconRaw(rest);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (subcommand === 'url') {
|
|
602
|
+
await iconUrl(rest);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (subcommand === 'open') {
|
|
607
|
+
await iconOpen(rest);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (subcommand === 'download') {
|
|
612
|
+
await iconDownload(rest);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
throw new Error('Unknown icon command. Use: show, raw, url, open, download.');
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async function iconShow(args) {
|
|
620
|
+
const options = parseOptions(args);
|
|
621
|
+
const [iconReference] = options._;
|
|
622
|
+
const icon = await fetchIcon(iconReference, booleanOption(options['include-svg'], false));
|
|
623
|
+
|
|
624
|
+
if (options.json) {
|
|
625
|
+
printJson({ icon });
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
printIcon(icon);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async function iconRaw(args) {
|
|
633
|
+
const options = parseOptions(args);
|
|
634
|
+
const [iconReference] = options._;
|
|
635
|
+
const icon = await fetchIcon(iconReference, true);
|
|
636
|
+
|
|
637
|
+
if (!icon.svg) {
|
|
638
|
+
throw new Error('The icon response did not include SVG markup.');
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
console.log(icon.svg);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async function iconUrl(args) {
|
|
645
|
+
const options = parseOptions(args);
|
|
646
|
+
const [iconReference] = options._;
|
|
647
|
+
const icon = await fetchIcon(iconReference, false);
|
|
648
|
+
|
|
649
|
+
console.log(icon.pageUrl || '');
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
async function iconOpen(args) {
|
|
653
|
+
const options = parseOptions(args);
|
|
654
|
+
const [iconReference] = options._;
|
|
655
|
+
const icon = await fetchIcon(iconReference, false);
|
|
656
|
+
|
|
657
|
+
if (!icon.pageUrl) {
|
|
658
|
+
throw new Error('The icon response did not include a page URL.');
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
openUrl(icon.pageUrl);
|
|
662
|
+
console.log(`Opened ${icon.pageUrl}`);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async function iconDownload(args) {
|
|
666
|
+
const options = parseOptions(args);
|
|
667
|
+
const [iconReference] = options._;
|
|
668
|
+
|
|
669
|
+
const reference = parseIconReference(iconReference);
|
|
670
|
+
|
|
671
|
+
const payload = await callMcpTool('get_icon', {
|
|
672
|
+
id: reference.id,
|
|
673
|
+
includeSvg: true,
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
const icon = payload.icon;
|
|
677
|
+
assertIconReferenceMatches(iconReference, icon);
|
|
678
|
+
|
|
679
|
+
const outputPath = resolveIconDownloadPath(options.output || options.o, icon);
|
|
680
|
+
await writeIconSvg(outputPath, icon?.svg, booleanOption(options.force, false));
|
|
681
|
+
|
|
682
|
+
if (options.json) {
|
|
683
|
+
printJson({
|
|
684
|
+
icon: {
|
|
685
|
+
id: icon.id,
|
|
686
|
+
name: icon.name,
|
|
687
|
+
pageUrl: icon.pageUrl,
|
|
688
|
+
},
|
|
689
|
+
outputPath,
|
|
690
|
+
});
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
console.log(`Downloaded ${icon.id} ${icon.name}`);
|
|
695
|
+
console.log(` ${outputPath}`);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
async function collection(args) {
|
|
699
|
+
const [subcommand, ...rest] = args;
|
|
700
|
+
|
|
701
|
+
if (subcommand === 'list') {
|
|
702
|
+
const options = parseOptions(rest);
|
|
703
|
+
const payload = await proApi('/api/pro/project-kits');
|
|
704
|
+
|
|
705
|
+
if (options.json) {
|
|
706
|
+
printJson(payload);
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
for (const collection of payload.data || []) {
|
|
711
|
+
printCollection(collection);
|
|
712
|
+
}
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (subcommand === 'create') {
|
|
717
|
+
await collectionCreate(rest);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (subcommand === 'show') {
|
|
722
|
+
await collectionShow(rest);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (subcommand === 'rename') {
|
|
727
|
+
await collectionRename(rest);
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (subcommand === 'update') {
|
|
732
|
+
await collectionUpdate(rest);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (subcommand === 'add') {
|
|
737
|
+
await collectionAdd(rest);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (subcommand === 'remove') {
|
|
742
|
+
await collectionRemove(rest);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (subcommand === 'delete') {
|
|
747
|
+
await collectionDelete(rest);
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (subcommand === 'export') {
|
|
752
|
+
await collectionExport(rest);
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
throw new Error('Unknown collection command. Use: list, create, show, rename, update, add, remove, delete, export.');
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
async function collectionCreate(args) {
|
|
760
|
+
const options = parseOptions(args);
|
|
761
|
+
const name = options.name || options._.join(' ').trim();
|
|
762
|
+
|
|
763
|
+
if (!name) {
|
|
764
|
+
throw new Error('Collection name is required. Example: svgicons collection create --name "Dashboard icons"');
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const payload = await createKitApi({
|
|
768
|
+
name,
|
|
769
|
+
description: options.description || '',
|
|
770
|
+
framework: enumOption(options.framework, 'framework', FRAMEWORKS),
|
|
771
|
+
colorPolicy: enumOption(options['color-policy'], 'color policy', COLOR_POLICIES),
|
|
772
|
+
namingPolicy: enumOption(options['naming-policy'], 'naming policy', NAMING_POLICIES),
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
if (options.json) {
|
|
776
|
+
printJson(payload);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
printCollection(payload.data);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
async function collectionShow(args) {
|
|
784
|
+
const options = parseOptions(args);
|
|
785
|
+
const [collectionReference] = options._;
|
|
786
|
+
|
|
787
|
+
if (!collectionReference) {
|
|
788
|
+
throw new Error('Collection reference is required. Example: svgicons collection show "Dashboard icons"');
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const collectionId = await resolveCollectionId(collectionReference);
|
|
792
|
+
const query = booleanOption(options.icons, false) ? '?per_page=100' : '';
|
|
793
|
+
const payload = await proApi(`/api/pro/project-kits/${encodeURIComponent(collectionId)}${query}`);
|
|
794
|
+
|
|
795
|
+
if (options.json) {
|
|
796
|
+
printJson(payload);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
printCollectionDetail(payload, {
|
|
801
|
+
icons: booleanOption(options.icons, false),
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
async function collectionRename(args) {
|
|
806
|
+
const options = parseOptions(args);
|
|
807
|
+
const [collectionReference] = options._;
|
|
808
|
+
const name = options.name || options._.slice(1).join(' ').trim();
|
|
809
|
+
|
|
810
|
+
if (!collectionReference || !name) {
|
|
811
|
+
throw new Error('Collection reference and --name are required. Example: svgicons collection rename "Dashboard icons" --name "Billing icons"');
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
await collectionPatch(collectionReference, { name }, options);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
async function collectionUpdate(args) {
|
|
818
|
+
const options = parseOptions(args);
|
|
819
|
+
const [collectionReference] = options._;
|
|
820
|
+
|
|
821
|
+
if (!collectionReference) {
|
|
822
|
+
throw new Error('Collection reference is required. Example: svgicons collection update "Dashboard icons" --description "Product UI icons"');
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const body = {
|
|
826
|
+
...(options.name ? { name: options.name } : {}),
|
|
827
|
+
...(options.description !== undefined ? { description: options.description === true ? '' : String(options.description) } : {}),
|
|
828
|
+
...(options.framework !== undefined ? { framework: enumOption(options.framework, 'framework', FRAMEWORKS) } : {}),
|
|
829
|
+
...(options['color-policy'] !== undefined ? { colorPolicy: enumOption(options['color-policy'], 'color policy', COLOR_POLICIES) } : {}),
|
|
830
|
+
...(options['naming-policy'] !== undefined ? { namingPolicy: enumOption(options['naming-policy'], 'naming policy', NAMING_POLICIES) } : {}),
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
if (Object.keys(body).length === 0) {
|
|
834
|
+
throw usageError('At least one update option is required: --name, --description, --framework, --color-policy, or --naming-policy.');
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
await collectionPatch(collectionReference, body, options);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
async function collectionPatch(collectionReference, body, options) {
|
|
841
|
+
const collectionId = await resolveCollectionId(collectionReference);
|
|
842
|
+
const payload = await proApi(`/api/pro/project-kits/${encodeURIComponent(collectionId)}`, {
|
|
843
|
+
method: 'PATCH',
|
|
844
|
+
body,
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
if (options.json) {
|
|
848
|
+
printJson(payload);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
printCollection(payload.data);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
async function collectionAdd(args) {
|
|
856
|
+
const options = parseOptions(args);
|
|
857
|
+
const [collectionReference, ...iconIds] = options._;
|
|
858
|
+
|
|
859
|
+
if (!collectionReference || iconIds.length === 0) {
|
|
860
|
+
throw new Error('Collection reference and at least one icon id are required. Example: svgicons collection add "Dashboard icons" 33716');
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const collectionId = await resolveCollectionId(collectionReference);
|
|
864
|
+
const iconIdValues = iconIds.map((iconReference) => parseIconLookupReference(iconReference).id);
|
|
865
|
+
const payload = await proApi(`/api/pro/project-kits/${encodeURIComponent(collectionId)}/icons/bulk`, {
|
|
866
|
+
method: 'POST',
|
|
867
|
+
body: {
|
|
868
|
+
icon_ids: iconIdValues,
|
|
869
|
+
},
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
if (options.json) {
|
|
873
|
+
printJson(payload);
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
console.log(`Added ${payload.data.createdCount || 0} icon(s); ${payload.data.existingCount || 0} already present.`);
|
|
878
|
+
if (payload.data.kit) {
|
|
879
|
+
printCollection(payload.data.kit);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
async function collectionRemove(args) {
|
|
884
|
+
const options = parseOptions(args);
|
|
885
|
+
const [collectionReference, ...iconReferences] = options._;
|
|
886
|
+
|
|
887
|
+
if (!collectionReference || iconReferences.length === 0) {
|
|
888
|
+
throw new Error('Collection reference and at least one icon id are required. Example: svgicons collection remove "Dashboard icons" 33716');
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const collectionId = await resolveCollectionId(collectionReference);
|
|
892
|
+
const results = [];
|
|
893
|
+
|
|
894
|
+
for (const iconReference of iconReferences) {
|
|
895
|
+
const iconId = parseIconLookupReference(iconReference).id;
|
|
896
|
+
const payload = await proApi(`/api/pro/project-kits/${encodeURIComponent(collectionId)}/icons/${encodeURIComponent(iconId)}`, {
|
|
897
|
+
method: 'DELETE',
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
results.push(payload.data);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (options.json) {
|
|
904
|
+
printJson({ data: results });
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
for (const result of results) {
|
|
909
|
+
console.log(`${result.removed ? 'removed' : 'not present'}: ${result.iconId}`);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
async function collectionDelete(args) {
|
|
914
|
+
const options = parseOptions(args);
|
|
915
|
+
const [collectionReference] = options._;
|
|
916
|
+
|
|
917
|
+
if (!collectionReference) {
|
|
918
|
+
throw new Error('Collection reference is required. Example: svgicons collection delete "Dashboard icons" --yes');
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const collectionId = await resolveCollectionId(collectionReference);
|
|
922
|
+
let collection = { id: collectionId, name: collectionReference };
|
|
923
|
+
|
|
924
|
+
try {
|
|
925
|
+
const payload = await proApi(`/api/pro/project-kits/${encodeURIComponent(collectionId)}`);
|
|
926
|
+
collection = payload.data || collection;
|
|
927
|
+
} catch {
|
|
928
|
+
// Keep delete usable even if the detail endpoint is unavailable after ID resolution.
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
await confirmDeleteCollection(collection, options);
|
|
932
|
+
await proApi(`/api/pro/project-kits/${encodeURIComponent(collectionId)}`, {
|
|
933
|
+
method: 'DELETE',
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
if (options.json) {
|
|
937
|
+
printJson({ deleted: true, collection: { id: collection.id, name: collection.name } });
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
console.log(`Deleted collection ${collection.id} ${collection.name}.`);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
async function collectionExport(args) {
|
|
945
|
+
const options = parseOptions(args);
|
|
946
|
+
const [collectionReference] = options._;
|
|
947
|
+
|
|
948
|
+
if (!collectionReference) {
|
|
949
|
+
throw new Error('Collection reference is required. Example: svgicons collection export "Dashboard icons"');
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const collectionId = await resolveCollectionId(collectionReference);
|
|
953
|
+
const exportOptions = buildExportOptions(options);
|
|
954
|
+
const payload = await proApi(`/api/pro/project-kits/${encodeURIComponent(collectionId)}/exports`, {
|
|
955
|
+
method: 'POST',
|
|
956
|
+
body: {
|
|
957
|
+
formats: exportOptions.formats,
|
|
958
|
+
options: exportOptions.options,
|
|
959
|
+
},
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
if (options.json) {
|
|
963
|
+
if (booleanOption(options['no-wait'], false)) {
|
|
964
|
+
printJson(payload);
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const completed = await waitForExport(collectionId, payload.data, options);
|
|
969
|
+
const outputPath = await downloadExport(collectionId, completed, options);
|
|
970
|
+
printJson({
|
|
971
|
+
data: completed,
|
|
972
|
+
outputPath,
|
|
973
|
+
});
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
if (booleanOption(options['no-wait'], false)) {
|
|
978
|
+
printExport(payload.data);
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
printExport(payload.data);
|
|
983
|
+
const completed = await waitForExport(collectionId, payload.data, options, true);
|
|
984
|
+
const outputPath = await downloadExport(collectionId, completed, options);
|
|
985
|
+
|
|
986
|
+
console.log(`Downloaded export ZIP`);
|
|
987
|
+
console.log(` ${outputPath}`);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
async function exportCommand(args) {
|
|
991
|
+
const [subcommand, ...rest] = args;
|
|
992
|
+
|
|
993
|
+
if (subcommand === 'status') {
|
|
994
|
+
await exportStatus(rest);
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
if (subcommand === 'download') {
|
|
999
|
+
await exportDownload(rest);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
throw new Error('Unknown export command. Use: status, download.');
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
async function exportStatus(args) {
|
|
1007
|
+
const options = parseOptions(args);
|
|
1008
|
+
const [exportId] = options._;
|
|
1009
|
+
const collectionReference = options.collection;
|
|
1010
|
+
|
|
1011
|
+
if (!exportId || !collectionReference) {
|
|
1012
|
+
throw new Error('Export id and --collection are required. Example: svgicons export status 55 --collection "Dashboard icons"');
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
const collectionId = await resolveCollectionId(collectionReference);
|
|
1016
|
+
const payload = await proApi(`/api/pro/project-kits/${encodeURIComponent(collectionId)}/exports/${encodeURIComponent(exportId)}`);
|
|
1017
|
+
|
|
1018
|
+
if (options.json) {
|
|
1019
|
+
printJson(payload);
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
printExport(payload.data);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
async function exportDownload(args) {
|
|
1027
|
+
const options = parseOptions(args);
|
|
1028
|
+
const [exportId] = options._;
|
|
1029
|
+
const collectionReference = options.collection;
|
|
1030
|
+
|
|
1031
|
+
if (!exportId || !collectionReference) {
|
|
1032
|
+
throw new Error('Export id and --collection are required. Example: svgicons export download 55 --collection "Dashboard icons"');
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const collectionId = await resolveCollectionId(collectionReference);
|
|
1036
|
+
const payload = await proApi(`/api/pro/project-kits/${encodeURIComponent(collectionId)}/exports/${encodeURIComponent(exportId)}`);
|
|
1037
|
+
const outputPath = await downloadExport(collectionId, payload.data, options);
|
|
1038
|
+
|
|
1039
|
+
if (options.json) {
|
|
1040
|
+
printJson({
|
|
1041
|
+
data: payload.data,
|
|
1042
|
+
outputPath,
|
|
1043
|
+
});
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
console.log(`Downloaded export ZIP`);
|
|
1048
|
+
console.log(` ${outputPath}`);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
async function waitForExport(collectionId, exportJob, options, verbose = false) {
|
|
1052
|
+
const timeoutSeconds = numberOption(options.timeout, 180);
|
|
1053
|
+
const pollIntervalSeconds = numberOption(options['poll-interval'], 2);
|
|
1054
|
+
const startedAt = Date.now();
|
|
1055
|
+
let current = exportJob;
|
|
1056
|
+
|
|
1057
|
+
while (Date.now() - startedAt <= timeoutSeconds * 1000) {
|
|
1058
|
+
if (current.status === 'completed') {
|
|
1059
|
+
return current;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
if (current.status === 'failed') {
|
|
1063
|
+
throw exportFailedError(current);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
if (verbose) {
|
|
1067
|
+
console.log(`Waiting for export ${current.id}: ${current.status}`);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
await sleep(Math.max(1, pollIntervalSeconds) * 1000);
|
|
1071
|
+
const payload = await proApi(`/api/pro/project-kits/${encodeURIComponent(collectionId)}/exports/${encodeURIComponent(current.id)}`);
|
|
1072
|
+
current = payload.data;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
throw exportTimeoutError(exportJob, timeoutSeconds);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
async function downloadExport(collectionId, exportJob, options) {
|
|
1079
|
+
if (!exportJob.downloadUrl) {
|
|
1080
|
+
throw exportNotReadyError(exportJob);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
const downloadPath = `/api/pro/project-kits/${encodeURIComponent(collectionId)}/exports/${encodeURIComponent(exportJob.id)}/download`;
|
|
1084
|
+
const download = await proApiDownload(downloadPath);
|
|
1085
|
+
const outputPath = resolveArchiveDownloadPath(options.output || options.o, download.filename || exportJob.artifactFilename);
|
|
1086
|
+
|
|
1087
|
+
await writeBinaryFile(outputPath, download.bytes, booleanOption(options.force, false));
|
|
1088
|
+
|
|
1089
|
+
return outputPath;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
async function initProject(args) {
|
|
1093
|
+
const options = parseOptions(args);
|
|
1094
|
+
const path = manifestPath(options);
|
|
1095
|
+
const manifest = createManifest({
|
|
1096
|
+
collection: options.collection,
|
|
1097
|
+
framework: enumOption(options.framework, 'framework', FRAMEWORKS),
|
|
1098
|
+
format: enumOption(options.format, 'format', ['svg', 'react-ts', 'vue']),
|
|
1099
|
+
output: options.output || options.o,
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
try {
|
|
1103
|
+
await writeJsonFile(path, manifest, {
|
|
1104
|
+
force: booleanOption(options.force, false),
|
|
1105
|
+
});
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
if (error?.code === 'EEXIST') {
|
|
1108
|
+
throw usageError(`Manifest already exists: ${path}. Use --force to overwrite it.`);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
throw error;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
if (options.json) {
|
|
1115
|
+
printJson({ manifestPath: path, manifest });
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
console.log(`Created ${path}`);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
async function syncProject(args) {
|
|
1123
|
+
const options = parseOptions(args);
|
|
1124
|
+
const paths = projectPaths(options);
|
|
1125
|
+
const manifest = normalizeManifest(await readProjectJson(paths.manifest, 'manifest'));
|
|
1126
|
+
|
|
1127
|
+
if (options.collection && options.collection !== true) {
|
|
1128
|
+
const nextRef = String(options.collection);
|
|
1129
|
+
const exists = manifest.collections.some((entry) => normalizeCollectionManifestEntry(entry).ref === nextRef);
|
|
1130
|
+
if (!exists) {
|
|
1131
|
+
manifest.collections.push({ ref: nextRef });
|
|
1132
|
+
await writeJsonFile(paths.manifest, manifest);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
const icons = await collectManifestIcons(manifest);
|
|
1137
|
+
const lockfile = createLockfile(icons, manifest);
|
|
1138
|
+
await writeJsonFile(paths.lockfile, lockfile);
|
|
1139
|
+
|
|
1140
|
+
if (options.json) {
|
|
1141
|
+
printJson({ lockfilePath: paths.lockfile, iconsCount: lockfile.icons.length });
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
console.log(`Synced ${lockfile.icons.length} icon(s) into ${paths.lockfile}`);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
async function buildProject(args) {
|
|
1149
|
+
const options = parseOptions(args);
|
|
1150
|
+
const paths = projectPaths(options);
|
|
1151
|
+
const manifest = normalizeManifest(await readProjectJson(paths.manifest, 'manifest'));
|
|
1152
|
+
const lockfile = await readProjectJson(paths.lockfile, 'lockfile');
|
|
1153
|
+
const format = options.format || manifest.format || 'svg';
|
|
1154
|
+
|
|
1155
|
+
if (format !== 'svg') {
|
|
1156
|
+
throw usageError('Local build currently supports format svg. Use collection export for React TypeScript or Vue component ZIPs.');
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
const outputDir = resolve(String(options.output || options.o || manifest.output));
|
|
1160
|
+
let written = 0;
|
|
1161
|
+
|
|
1162
|
+
for (const icon of lockfile.icons || []) {
|
|
1163
|
+
if (!icon.svg) {
|
|
1164
|
+
throw usageError(`Locked icon ${icon.ref || icon.id} does not include SVG markup. Run: svgicons sync`);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
const outputPath = join(outputDir, svgFilenameForIcon(icon));
|
|
1168
|
+
await mkdir(resolve(outputDir), { recursive: true });
|
|
1169
|
+
await writeFile(outputPath, icon.svg.endsWith('\n') ? icon.svg : `${icon.svg}\n`, 'utf8');
|
|
1170
|
+
written++;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
if (options.json) {
|
|
1174
|
+
printJson({ outputDir, written });
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
console.log(`Built ${written} SVG file(s) into ${outputDir}`);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
async function updateProject(args) {
|
|
1182
|
+
const options = parseOptions(args);
|
|
1183
|
+
const [subcommand] = options._;
|
|
1184
|
+
|
|
1185
|
+
if (subcommand !== '--check' && subcommand !== 'check' && options.check === undefined) {
|
|
1186
|
+
throw new Error('Unknown update command. Use: svgicons update --check');
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
const paths = projectPaths(options);
|
|
1190
|
+
const lockfile = await readProjectJson(paths.lockfile, 'lockfile');
|
|
1191
|
+
const changes = [];
|
|
1192
|
+
|
|
1193
|
+
for (const locked of lockfile.icons || []) {
|
|
1194
|
+
const remote = await fetchIcon(locked.ref || String(locked.id), true);
|
|
1195
|
+
const current = lockIconFromIcon(remote);
|
|
1196
|
+
if (current.svgHash !== locked.svgHash) {
|
|
1197
|
+
changes.push({
|
|
1198
|
+
id: locked.id,
|
|
1199
|
+
ref: locked.ref,
|
|
1200
|
+
currentHash: locked.svgHash,
|
|
1201
|
+
remoteHash: current.svgHash,
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
const payload = {
|
|
1207
|
+
ok: changes.length === 0,
|
|
1208
|
+
checkedIcons: (lockfile.icons || []).length,
|
|
1209
|
+
changes,
|
|
1210
|
+
};
|
|
1211
|
+
|
|
1212
|
+
if (options.json) {
|
|
1213
|
+
printJson(payload);
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
console.log(changes.length === 0
|
|
1218
|
+
? `All ${payload.checkedIcons} locked icon(s) are up to date.`
|
|
1219
|
+
: `${changes.length} locked icon(s) changed upstream.`);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
async function licenseCommand(args) {
|
|
1223
|
+
const [subcommand, ...rest] = args;
|
|
1224
|
+
|
|
1225
|
+
if (subcommand === 'check') {
|
|
1226
|
+
await licenseCheck(rest);
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
if (subcommand === 'export') {
|
|
1231
|
+
await licenseExport(rest);
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
throw new Error('Unknown license command. Use: check, export.');
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
async function licenseCheck(args) {
|
|
1239
|
+
const options = parseOptions(args);
|
|
1240
|
+
const paths = projectPaths(options);
|
|
1241
|
+
const lockfile = await readProjectJson(paths.lockfile, 'lockfile');
|
|
1242
|
+
const manifest = await readOptionalManifest(paths.manifest);
|
|
1243
|
+
const report = buildLicenseReport(lockfile, {
|
|
1244
|
+
allow: csvOption(options.allow) || manifest.licensePolicy?.allow || [],
|
|
1245
|
+
deny: csvOption(options.deny) || manifest.licensePolicy?.deny || [],
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
if (options.json) {
|
|
1249
|
+
printJson(report);
|
|
1250
|
+
} else {
|
|
1251
|
+
printLicenseReport(report);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
if (booleanOption(options.fail, false) && !report.ok) {
|
|
1255
|
+
throw licenseViolationError(report);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
async function licenseExport(args) {
|
|
1260
|
+
const options = parseOptions(args);
|
|
1261
|
+
const paths = projectPaths(options);
|
|
1262
|
+
const lockfile = await readProjectJson(paths.lockfile, 'lockfile');
|
|
1263
|
+
const manifest = await readOptionalManifest(paths.manifest);
|
|
1264
|
+
const report = buildLicenseReport(lockfile, manifest.licensePolicy || {});
|
|
1265
|
+
const format = options.format || 'markdown';
|
|
1266
|
+
let content;
|
|
1267
|
+
|
|
1268
|
+
if (format === 'markdown' || format === 'md') {
|
|
1269
|
+
content = licenseReportToMarkdown(report);
|
|
1270
|
+
} else if (format === 'json') {
|
|
1271
|
+
content = `${JSON.stringify(report, null, 2)}\n`;
|
|
1272
|
+
} else if (format === 'csv') {
|
|
1273
|
+
content = licenseReportToCsv(report);
|
|
1274
|
+
} else {
|
|
1275
|
+
throw usageError('Invalid license export format. Allowed values: markdown, json, csv');
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
if (options.output || options.o) {
|
|
1279
|
+
const outputPath = resolve(String(options.output || options.o));
|
|
1280
|
+
await writeTextFile(outputPath, content);
|
|
1281
|
+
if (options.json) {
|
|
1282
|
+
printJson({ outputPath, format });
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
console.log(`Wrote ${outputPath}`);
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
console.log(content.trimEnd());
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
async function scan(args) {
|
|
1294
|
+
const options = parseOptions(args);
|
|
1295
|
+
const root = options._[0] || '.';
|
|
1296
|
+
const proposal = await scanProject(root, {
|
|
1297
|
+
maxFiles: numberOption(options['max-files'], 500),
|
|
1298
|
+
collectionName: options['collection-name'] || options['kit-name'],
|
|
1299
|
+
extensions: extensionSet(options.extensions),
|
|
1300
|
+
});
|
|
1301
|
+
let manifestResult = null;
|
|
1302
|
+
|
|
1303
|
+
if (options['write-manifest']) {
|
|
1304
|
+
manifestResult = await writeScanManifest(proposal, options);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
if (options['create-collection'] || options['create-kit']) {
|
|
1308
|
+
const payload = await createKitApi({
|
|
1309
|
+
name: proposal.collection.name,
|
|
1310
|
+
description: proposal.collection.description,
|
|
1311
|
+
});
|
|
1312
|
+
const iconIds = iconIdsFromScanProposal(proposal);
|
|
1313
|
+
const added = iconIds.length > 0
|
|
1314
|
+
? await proApi(`/api/pro/project-kits/${encodeURIComponent(payload.data.id)}/icons/bulk`, {
|
|
1315
|
+
method: 'POST',
|
|
1316
|
+
body: {
|
|
1317
|
+
icon_ids: iconIds,
|
|
1318
|
+
},
|
|
1319
|
+
})
|
|
1320
|
+
: { data: { createdCount: 0, existingCount: 0 } };
|
|
1321
|
+
|
|
1322
|
+
if (options.json) {
|
|
1323
|
+
printJson({
|
|
1324
|
+
proposal,
|
|
1325
|
+
manifest: manifestResult,
|
|
1326
|
+
created: payload.data,
|
|
1327
|
+
added: added.data,
|
|
1328
|
+
});
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
printScanProposal(proposal, { modified: Boolean(manifestResult) });
|
|
1333
|
+
if (manifestResult) {
|
|
1334
|
+
console.log('');
|
|
1335
|
+
console.log(`Updated manifest: ${manifestResult.manifestPath}`);
|
|
1336
|
+
console.log(`Added icon refs: ${manifestResult.addedIcons}`);
|
|
1337
|
+
}
|
|
1338
|
+
console.log('');
|
|
1339
|
+
console.log('Created Icon Collection:');
|
|
1340
|
+
printCollection(payload.data);
|
|
1341
|
+
console.log(`Added ${added.data.createdCount || 0} icon(s); ${added.data.existingCount || 0} already present.`);
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
if (options.json) {
|
|
1346
|
+
printJson(manifestResult ? { proposal, manifest: manifestResult } : proposal);
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
printScanProposal(proposal, { modified: Boolean(manifestResult) });
|
|
1351
|
+
if (manifestResult) {
|
|
1352
|
+
console.log('');
|
|
1353
|
+
console.log(`Updated manifest: ${manifestResult.manifestPath}`);
|
|
1354
|
+
console.log(`Added icon refs: ${manifestResult.addedIcons}`);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
async function writeScanManifest(proposal, options) {
|
|
1359
|
+
const path = manifestPath(options);
|
|
1360
|
+
let manifest;
|
|
1361
|
+
|
|
1362
|
+
try {
|
|
1363
|
+
manifest = normalizeManifest(await readJsonFile(path));
|
|
1364
|
+
} catch (error) {
|
|
1365
|
+
if (error?.code !== 'ENOENT') {
|
|
1366
|
+
throw error;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
manifest = createManifest({
|
|
1370
|
+
collection: options.collection,
|
|
1371
|
+
framework: enumOption(options.framework, 'framework', FRAMEWORKS),
|
|
1372
|
+
format: enumOption(options.format, 'format', ['svg', 'react-ts', 'vue']),
|
|
1373
|
+
output: options.output || options.o,
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
const existingRefs = new Set(
|
|
1378
|
+
(manifest.icons || [])
|
|
1379
|
+
.map((entry) => normalizeIconManifestEntry(entry).ref)
|
|
1380
|
+
.filter(Boolean),
|
|
1381
|
+
);
|
|
1382
|
+
const detectedRefs = scanIconRefs(proposal);
|
|
1383
|
+
let addedIcons = 0;
|
|
1384
|
+
|
|
1385
|
+
for (const ref of detectedRefs) {
|
|
1386
|
+
if (existingRefs.has(ref)) {
|
|
1387
|
+
continue;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
manifest.icons.push({ ref });
|
|
1391
|
+
existingRefs.add(ref);
|
|
1392
|
+
addedIcons++;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
await writeJsonFile(path, manifest);
|
|
1396
|
+
|
|
1397
|
+
return {
|
|
1398
|
+
manifestPath: path,
|
|
1399
|
+
addedIcons,
|
|
1400
|
+
totalIcons: manifest.icons.length,
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
function scanIconRefs(proposal) {
|
|
1405
|
+
return [...new Set([
|
|
1406
|
+
...(proposal.existingIconRefs || []),
|
|
1407
|
+
...(proposal.generatedIconRefs || []),
|
|
1408
|
+
])].sort((a, b) => Number(String(a).split('-')[0]) - Number(String(b).split('-')[0]) || String(a).localeCompare(String(b)));
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
function iconIdsFromScanProposal(proposal) {
|
|
1412
|
+
return [...new Set([
|
|
1413
|
+
...(proposal.existingIconIds || []),
|
|
1414
|
+
...scanIconRefs(proposal).map((ref) => String(ref).split('-')[0]),
|
|
1415
|
+
])]
|
|
1416
|
+
.map((id) => Number(id))
|
|
1417
|
+
.filter((id) => Number.isFinite(id) && id > 0)
|
|
1418
|
+
.sort((a, b) => a - b);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
function projectPaths(options) {
|
|
1422
|
+
return {
|
|
1423
|
+
manifest: manifestPath(options),
|
|
1424
|
+
lockfile: lockfilePath(options),
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
async function readProjectJson(path, label) {
|
|
1429
|
+
try {
|
|
1430
|
+
return await readJsonFile(path);
|
|
1431
|
+
} catch (error) {
|
|
1432
|
+
if (error?.code === 'ENOENT') {
|
|
1433
|
+
throw usageError(`Missing ${label}: ${path}. ${label === 'manifest' ? 'Run: svgicons init' : 'Run: svgicons sync'}`);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
throw error;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
async function readOptionalManifest(path) {
|
|
1441
|
+
try {
|
|
1442
|
+
return normalizeManifest(await readJsonFile(path));
|
|
1443
|
+
} catch (error) {
|
|
1444
|
+
if (error?.code === 'ENOENT') {
|
|
1445
|
+
return normalizeManifest({});
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
throw error;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
async function collectManifestIcons(manifest) {
|
|
1453
|
+
const icons = [];
|
|
1454
|
+
|
|
1455
|
+
for (const entry of manifest.icons || []) {
|
|
1456
|
+
const { ref } = normalizeIconManifestEntry(entry);
|
|
1457
|
+
if (!ref) {
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
icons.push(await fetchIcon(ref, true));
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
for (const entry of manifest.collections || []) {
|
|
1465
|
+
const { ref } = normalizeCollectionManifestEntry(entry);
|
|
1466
|
+
if (!ref) {
|
|
1467
|
+
continue;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
icons.push(...await collectCollectionIcons(ref));
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
return icons;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
async function collectCollectionIcons(reference) {
|
|
1477
|
+
const collectionId = await resolveCollectionId(reference);
|
|
1478
|
+
const icons = [];
|
|
1479
|
+
let page = 1;
|
|
1480
|
+
let lastPage = 1;
|
|
1481
|
+
|
|
1482
|
+
do {
|
|
1483
|
+
const payload = await proApi(`/api/pro/project-kits/${encodeURIComponent(collectionId)}?per_page=100&page=${page}`);
|
|
1484
|
+
icons.push(...(payload.icons?.data || []));
|
|
1485
|
+
lastPage = Number(payload.icons?.last_page || page);
|
|
1486
|
+
page++;
|
|
1487
|
+
} while (page <= lastPage);
|
|
1488
|
+
|
|
1489
|
+
return icons;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
function printLicenseReport(report) {
|
|
1493
|
+
console.log(`Checked icons: ${report.checkedIcons}`);
|
|
1494
|
+
console.log(`Status: ${report.ok ? 'ok' : 'violations found'}`);
|
|
1495
|
+
|
|
1496
|
+
if (report.licenses.length > 0) {
|
|
1497
|
+
console.log('');
|
|
1498
|
+
console.log('Licenses:');
|
|
1499
|
+
for (const item of report.licenses) {
|
|
1500
|
+
console.log(` ${item.license}: ${item.count}`);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
if (report.violations.length > 0) {
|
|
1505
|
+
console.log('');
|
|
1506
|
+
console.log('Violations:');
|
|
1507
|
+
for (const violation of report.violations) {
|
|
1508
|
+
console.log(` ${violation.icon.ref}: ${violation.license} (${violation.reason})`);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
async function createKitApi(body) {
|
|
1514
|
+
return proApi('/api/pro/project-kits', {
|
|
1515
|
+
method: 'POST',
|
|
1516
|
+
body,
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
async function fetchIcon(iconReference, includeSvg = false) {
|
|
1521
|
+
const reference = parseIconLookupReference(iconReference);
|
|
1522
|
+
const payload = await callMcpTool('get_icon', {
|
|
1523
|
+
id: reference.id,
|
|
1524
|
+
includeSvg,
|
|
1525
|
+
});
|
|
1526
|
+
const icon = payload.icon || payload.data || payload;
|
|
1527
|
+
|
|
1528
|
+
if (reference.slug && sanitizeFileSegment(icon?.name || '') !== reference.slug) {
|
|
1529
|
+
throw new Error(`Icon reference mismatch. Expected ${reference.id}-${reference.slug}, got ${icon?.id || 'unknown'}-${sanitizeFileSegment(icon?.name || '')}.`);
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
return icon;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
async function resolveCollectionId(reference) {
|
|
1536
|
+
const value = String(reference || '').trim();
|
|
1537
|
+
|
|
1538
|
+
if (/^\d+$/.test(value)) {
|
|
1539
|
+
return value;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
const payload = await proApi('/api/pro/project-kits');
|
|
1543
|
+
const collections = payload.data || [];
|
|
1544
|
+
const normalized = value.toLowerCase();
|
|
1545
|
+
const slugMatches = collections.filter((collection) => String(collection.slug || '').toLowerCase() === normalized);
|
|
1546
|
+
const nameMatches = collections.filter((collection) => String(collection.name || '').toLowerCase() === normalized);
|
|
1547
|
+
const matches = slugMatches.length > 0 ? slugMatches : nameMatches;
|
|
1548
|
+
|
|
1549
|
+
if (matches.length === 1) {
|
|
1550
|
+
return String(matches[0].id);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
if (matches.length > 1) {
|
|
1554
|
+
throw new Error(`Collection reference is ambiguous: ${reference}`);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
throw new Error(`Collection not found: ${reference}`);
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
async function confirmDeleteCollection(collection, options) {
|
|
1561
|
+
if (booleanOption(options.yes, false)) {
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
if (booleanOption(options.ci, false) || !stdin.isTTY || !stdout.isTTY) {
|
|
1566
|
+
throw usageError('Deleting a collection requires --yes in CI or non-interactive shells.');
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
1570
|
+
try {
|
|
1571
|
+
const answer = await rl.question(`Delete collection "${collection.name}" (${collection.id})? Type DELETE to confirm: `);
|
|
1572
|
+
if (answer.trim() !== 'DELETE') {
|
|
1573
|
+
throw usageError('Collection deletion cancelled.');
|
|
1574
|
+
}
|
|
1575
|
+
} finally {
|
|
1576
|
+
rl.close();
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
async function version() {
|
|
1581
|
+
const raw = await readFile(new URL('../package.json', import.meta.url), 'utf8');
|
|
1582
|
+
const pkg = JSON.parse(raw);
|
|
1583
|
+
|
|
1584
|
+
console.log(pkg.version);
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
function parseOptions(args) {
|
|
1588
|
+
const options = { _: [] };
|
|
1589
|
+
|
|
1590
|
+
for (let index = 0; index < args.length; index++) {
|
|
1591
|
+
const value = args[index];
|
|
1592
|
+
|
|
1593
|
+
if (!value.startsWith('--')) {
|
|
1594
|
+
options._.push(value);
|
|
1595
|
+
continue;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
const [rawKey, inlineValue] = value.slice(2).split('=', 2);
|
|
1599
|
+
const key = rawKey.trim();
|
|
1600
|
+
|
|
1601
|
+
if (inlineValue !== undefined) {
|
|
1602
|
+
options[key] = inlineValue;
|
|
1603
|
+
continue;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
const next = args[index + 1];
|
|
1607
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
1608
|
+
options[key] = next;
|
|
1609
|
+
index++;
|
|
1610
|
+
continue;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
options[key] = true;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
return options;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
function extractGlobalOptions(argv) {
|
|
1620
|
+
const options = {};
|
|
1621
|
+
const args = [];
|
|
1622
|
+
const globalKeys = new Set(['config', 'profile', 'base-url', 'token']);
|
|
1623
|
+
|
|
1624
|
+
for (let index = 0; index < argv.length; index++) {
|
|
1625
|
+
const value = argv[index];
|
|
1626
|
+
|
|
1627
|
+
if (!value.startsWith('--')) {
|
|
1628
|
+
args.push(value);
|
|
1629
|
+
continue;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
const [rawKey, inlineValue] = value.slice(2).split('=', 2);
|
|
1633
|
+
if (!globalKeys.has(rawKey)) {
|
|
1634
|
+
args.push(value);
|
|
1635
|
+
continue;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
if (inlineValue !== undefined) {
|
|
1639
|
+
options[rawKey] = inlineValue;
|
|
1640
|
+
continue;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
const next = argv[index + 1];
|
|
1644
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
1645
|
+
options[rawKey] = next;
|
|
1646
|
+
index++;
|
|
1647
|
+
continue;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
options[rawKey] = true;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
return { args, options };
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
function normalizeConfigKey(key) {
|
|
1657
|
+
const normalized = String(key || '').trim();
|
|
1658
|
+
|
|
1659
|
+
if (normalized === 'base-url' || normalized === 'baseUrl') {
|
|
1660
|
+
return 'baseUrl';
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
if (['token', 'createdAt'].includes(normalized)) {
|
|
1664
|
+
return normalized;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
return '';
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
function addCheck(checks, name, status, message, details = undefined) {
|
|
1671
|
+
checks.push({
|
|
1672
|
+
name,
|
|
1673
|
+
status,
|
|
1674
|
+
message,
|
|
1675
|
+
...(details ? { details } : {}),
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
async function checkDoctorStep(checks, name, callback) {
|
|
1680
|
+
try {
|
|
1681
|
+
addCheck(checks, name, 'pass', await callback());
|
|
1682
|
+
} catch (error) {
|
|
1683
|
+
addCheck(checks, name, 'fail', error.message, {
|
|
1684
|
+
code: error.code,
|
|
1685
|
+
status: error.status,
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
function printIcon(icon) {
|
|
1691
|
+
console.log(`${icon.id}\t${icon.name}`);
|
|
1692
|
+
if (icon.iconSet?.name) {
|
|
1693
|
+
console.log(` set: ${icon.iconSet.name}`);
|
|
1694
|
+
}
|
|
1695
|
+
if (icon.width || icon.height) {
|
|
1696
|
+
console.log(` size: ${icon.width || '?'}x${icon.height || '?'}`);
|
|
1697
|
+
}
|
|
1698
|
+
if (icon.category) {
|
|
1699
|
+
console.log(` category: ${icon.category}`);
|
|
1700
|
+
}
|
|
1701
|
+
if (icon.pageUrl) {
|
|
1702
|
+
console.log(` ${icon.pageUrl}`);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
function printRecommendations(payload) {
|
|
1707
|
+
const recommendations = payload.data || payload.icons || [];
|
|
1708
|
+
|
|
1709
|
+
if (recommendations.length === 0) {
|
|
1710
|
+
console.log('No icon recommendations found.');
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
console.log('Recommended icons:');
|
|
1715
|
+
for (const recommendation of recommendations) {
|
|
1716
|
+
const icon = recommendation.icon || recommendation;
|
|
1717
|
+
const concept = recommendation.concept ? ` (${recommendation.concept})` : '';
|
|
1718
|
+
const set = icon.iconSet?.name ? ` - ${icon.iconSet.name}` : '';
|
|
1719
|
+
|
|
1720
|
+
console.log(`${icon.id}\t${icon.name}${concept}${set}`);
|
|
1721
|
+
if (recommendation.rationale) {
|
|
1722
|
+
console.log(` ${recommendation.rationale}`);
|
|
1723
|
+
}
|
|
1724
|
+
if (icon.pageUrl) {
|
|
1725
|
+
console.log(` ${icon.pageUrl}`);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
if (payload.meta?.note) {
|
|
1730
|
+
console.log('');
|
|
1731
|
+
console.log(payload.meta.note);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
function openUrl(url) {
|
|
1736
|
+
const platform = process.platform;
|
|
1737
|
+
const command = platform === 'win32'
|
|
1738
|
+
? 'cmd'
|
|
1739
|
+
: platform === 'darwin'
|
|
1740
|
+
? 'open'
|
|
1741
|
+
: 'xdg-open';
|
|
1742
|
+
const args = platform === 'win32'
|
|
1743
|
+
? ['/c', 'start', '', url]
|
|
1744
|
+
: [url];
|
|
1745
|
+
|
|
1746
|
+
const child = spawn(command, args, {
|
|
1747
|
+
detached: true,
|
|
1748
|
+
stdio: 'ignore',
|
|
1749
|
+
});
|
|
1750
|
+
child.unref();
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
function numberOption(value, fallback) {
|
|
1754
|
+
if (value === undefined || value === true || value === '') {
|
|
1755
|
+
return fallback;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
const parsed = Number(value);
|
|
1759
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
function booleanOption(value, fallback) {
|
|
1763
|
+
if (value === undefined) {
|
|
1764
|
+
return fallback;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
if (value === true) {
|
|
1768
|
+
return true;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
if (typeof value === 'string') {
|
|
1772
|
+
return ['1', 'true', 'yes', 'on'].includes(value.toLowerCase());
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
return Boolean(value);
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
function csvOption(value) {
|
|
1779
|
+
if (!value || value === true) {
|
|
1780
|
+
return undefined;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
return String(value)
|
|
1784
|
+
.split(',')
|
|
1785
|
+
.map((item) => item.trim())
|
|
1786
|
+
.filter(Boolean);
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
function listOption(value) {
|
|
1790
|
+
if (!value || value === true) {
|
|
1791
|
+
return [];
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
const values = Array.isArray(value) ? value : [value];
|
|
1795
|
+
|
|
1796
|
+
return values
|
|
1797
|
+
.flatMap((item) => String(item).split(','))
|
|
1798
|
+
.map((item) => item.trim())
|
|
1799
|
+
.filter(Boolean);
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
function extensionSet(value) {
|
|
1803
|
+
const values = listOption(value);
|
|
1804
|
+
|
|
1805
|
+
if (values.length === 0) {
|
|
1806
|
+
return undefined;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
return new Set(values.map((item) => item.startsWith('.') ? item.toLowerCase() : `.${item.toLowerCase()}`));
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
function buildExportOptions(options) {
|
|
1813
|
+
return {
|
|
1814
|
+
formats: enumCsvOption(options.formats, 'export format', EXPORT_FORMATS),
|
|
1815
|
+
options: {
|
|
1816
|
+
colorPolicy: enumOption(options['color-policy'], 'color policy', COLOR_POLICIES),
|
|
1817
|
+
namingPolicy: enumOption(options['naming-policy'], 'naming policy', NAMING_POLICIES),
|
|
1818
|
+
sizeProps: booleanFlagPair(options, 'size-props', 'no-size-props'),
|
|
1819
|
+
typescript: booleanFlagPair(options, 'typescript', 'no-typescript'),
|
|
1820
|
+
},
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
function enumOption(value, label, allowed) {
|
|
1825
|
+
if (value === undefined || value === true || value === '') {
|
|
1826
|
+
return undefined;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
if (!allowed.includes(String(value))) {
|
|
1830
|
+
throw usageError(`Invalid ${label}: ${value}. Allowed values: ${allowed.join(', ')}`);
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
return String(value);
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
function enumCsvOption(value, label, allowed) {
|
|
1837
|
+
const values = csvOption(value);
|
|
1838
|
+
|
|
1839
|
+
if (!values) {
|
|
1840
|
+
return undefined;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
const invalid = values.filter((item) => !allowed.includes(item));
|
|
1844
|
+
if (invalid.length > 0) {
|
|
1845
|
+
throw usageError(`Invalid ${label}: ${invalid.join(', ')}. Allowed values: ${allowed.join(', ')}`);
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
return [...new Set(values)];
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
function booleanFlagPair(options, positiveKey, negativeKey) {
|
|
1852
|
+
if (options[negativeKey] !== undefined) {
|
|
1853
|
+
return false;
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
return booleanOption(options[positiveKey], undefined);
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
function printHelp() {
|
|
1860
|
+
console.log(`Svg/icons CLI alpha
|
|
1861
|
+
|
|
1862
|
+
Usage:
|
|
1863
|
+
svgicons auth login --token <token> [--base-url https://svgicons.com]
|
|
1864
|
+
svgicons auth status [--json]
|
|
1865
|
+
svgicons login --token <token> [--base-url https://svgicons.com]
|
|
1866
|
+
svgicons config list|get|set
|
|
1867
|
+
svgicons doctor [--json]
|
|
1868
|
+
svgicons version
|
|
1869
|
+
svgicons search "arrow left" [--limit 10] [--json]
|
|
1870
|
+
svgicons recommend "billing settings dashboard" [--create-collection] [--json]
|
|
1871
|
+
svgicons pick "settings gear" [--interactive] [--add "Dashboard icons"] [--download]
|
|
1872
|
+
svgicons icon show <icon-ref> [--json]
|
|
1873
|
+
svgicons icon raw <icon-ref>
|
|
1874
|
+
svgicons icon url <icon-ref>
|
|
1875
|
+
svgicons icon open <icon-ref>
|
|
1876
|
+
svgicons icon download <icon-id-name> [--output ./icons] [--force]
|
|
1877
|
+
svgicons collection list [--json]
|
|
1878
|
+
svgicons collection create --name "Dashboard icons"
|
|
1879
|
+
svgicons collection show <collection-id-or-name> [--icons] [--json]
|
|
1880
|
+
svgicons collection rename <collection-id-or-name> --name "Billing icons"
|
|
1881
|
+
svgicons collection update <collection-id-or-name> [--description "..."]
|
|
1882
|
+
svgicons collection add <collection-id-or-name> <icon-id...>
|
|
1883
|
+
svgicons collection remove <collection-id-or-name> <icon-id...>
|
|
1884
|
+
svgicons collection delete <collection-id-or-name> [--yes]
|
|
1885
|
+
svgicons collection export <collection-id-or-name> [--formats react-ts,vue] [--no-size-props] [--no-typescript] [--output ./exports]
|
|
1886
|
+
svgicons export status <export-id> --collection <collection-id-or-name>
|
|
1887
|
+
svgicons export download <export-id> --collection <collection-id-or-name> [--output ./exports]
|
|
1888
|
+
svgicons init [--collection <collection-id-or-name>] [--output svgicons-icons]
|
|
1889
|
+
svgicons sync [--collection <collection-id-or-name>]
|
|
1890
|
+
svgicons build [--ci]
|
|
1891
|
+
svgicons update --check
|
|
1892
|
+
svgicons license check [--allow MIT,Apache-2.0] [--deny GPL] [--fail]
|
|
1893
|
+
svgicons license export --format markdown|json|csv --output THIRD_PARTY_ICONS.md
|
|
1894
|
+
svgicons scan [path] [--json] [--write-manifest] [--create-collection]
|
|
1895
|
+
|
|
1896
|
+
Environment:
|
|
1897
|
+
SVGICONS_TOKEN or SVGICONS_API_TOKEN can provide the token without writing config.
|
|
1898
|
+
SVGICONS_BASE_URL can target local or staging instances.
|
|
1899
|
+
`);
|
|
1900
|
+
}
|