@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/config.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { homedir, platform } from 'node:os';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_BASE_URL = 'https://svgicons.com';
|
|
6
|
+
let configPathOverride = '';
|
|
7
|
+
let credentialOverrides = {};
|
|
8
|
+
|
|
9
|
+
export function defaultBaseUrl() {
|
|
10
|
+
return process.env.SVGICONS_BASE_URL || DEFAULT_BASE_URL;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function configPath() {
|
|
14
|
+
if (configPathOverride) {
|
|
15
|
+
return configPathOverride;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (process.env.SVGICONS_CLI_CONFIG_DIR) {
|
|
19
|
+
return join(process.env.SVGICONS_CLI_CONFIG_DIR, 'config.json');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const home = homedir();
|
|
23
|
+
const os = platform();
|
|
24
|
+
|
|
25
|
+
if (os === 'win32') {
|
|
26
|
+
const base = process.env.APPDATA || join(home, 'AppData', 'Roaming');
|
|
27
|
+
return join(base, 'svgicons', 'cli', 'config.json');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (os === 'darwin') {
|
|
31
|
+
return join(home, 'Library', 'Application Support', 'svgicons', 'cli', 'config.json');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const base = process.env.XDG_CONFIG_HOME || join(home, '.config');
|
|
35
|
+
return join(base, 'svgicons', 'cli', 'config.json');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function readConfig() {
|
|
39
|
+
try {
|
|
40
|
+
const raw = await readFile(configPath(), 'utf8');
|
|
41
|
+
return JSON.parse(raw);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error?.code === 'ENOENT') {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
throw new Error(`Unable to read Svg/icons CLI config: ${error.message}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function writeConfig(config) {
|
|
52
|
+
const path = configPath();
|
|
53
|
+
await mkdir(dirname(path), { recursive: true });
|
|
54
|
+
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function clearConfig() {
|
|
58
|
+
await rm(configPath(), { force: true });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function credentials() {
|
|
62
|
+
const config = await readConfig();
|
|
63
|
+
const envToken = process.env.SVGICONS_TOKEN || process.env.SVGICONS_API_TOKEN;
|
|
64
|
+
const baseUrl = credentialOverrides.baseUrl
|
|
65
|
+
|| process.env.SVGICONS_BASE_URL
|
|
66
|
+
|| config.baseUrl
|
|
67
|
+
|| DEFAULT_BASE_URL;
|
|
68
|
+
const token = credentialOverrides.token !== undefined
|
|
69
|
+
? credentialOverrides.token
|
|
70
|
+
: envToken || config.token || '';
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
baseUrl: normalizeBaseUrl(baseUrl),
|
|
74
|
+
token,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function credentialsStatus() {
|
|
79
|
+
const config = await readConfig();
|
|
80
|
+
const envToken = process.env.SVGICONS_TOKEN || process.env.SVGICONS_API_TOKEN;
|
|
81
|
+
const envBaseUrl = process.env.SVGICONS_BASE_URL;
|
|
82
|
+
const hasTokenOverride = credentialOverrides.token !== undefined;
|
|
83
|
+
const baseUrl = credentialOverrides.baseUrl || envBaseUrl || config.baseUrl || DEFAULT_BASE_URL;
|
|
84
|
+
const token = hasTokenOverride ? credentialOverrides.token : envToken || config.token || '';
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
configPath: configPath(),
|
|
88
|
+
baseUrl: normalizeBaseUrl(baseUrl),
|
|
89
|
+
baseUrlSource: credentialOverrides.baseUrl
|
|
90
|
+
? 'command'
|
|
91
|
+
: envBaseUrl
|
|
92
|
+
? 'environment'
|
|
93
|
+
: config.baseUrl
|
|
94
|
+
? 'config'
|
|
95
|
+
: 'default',
|
|
96
|
+
hasToken: Boolean(token),
|
|
97
|
+
tokenSource: hasTokenOverride
|
|
98
|
+
? 'command'
|
|
99
|
+
: envToken
|
|
100
|
+
? 'environment'
|
|
101
|
+
: config.token
|
|
102
|
+
? 'config'
|
|
103
|
+
: 'none',
|
|
104
|
+
createdAt: config.createdAt || null,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function setConfigPathOverride(path) {
|
|
109
|
+
configPathOverride = path ? String(path) : '';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function setCredentialOverrides(overrides = {}) {
|
|
113
|
+
credentialOverrides = {
|
|
114
|
+
...credentialOverrides,
|
|
115
|
+
...Object.fromEntries(
|
|
116
|
+
Object.entries(overrides).filter(([, value]) => value !== undefined),
|
|
117
|
+
),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function clearRuntimeOverrides() {
|
|
122
|
+
configPathOverride = '';
|
|
123
|
+
credentialOverrides = {};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function normalizeBaseUrl(value) {
|
|
127
|
+
const baseUrl = String(value || DEFAULT_BASE_URL).trim().replace(/\/+$/, '');
|
|
128
|
+
|
|
129
|
+
if (!/^https?:\/\//i.test(baseUrl)) {
|
|
130
|
+
throw new Error('Base URL must start with http:// or https://');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return baseUrl;
|
|
134
|
+
}
|
package/src/downloads.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, extname, isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export function parseIconReference(reference) {
|
|
5
|
+
const value = String(reference || '').trim();
|
|
6
|
+
const match = value.match(/^(\d+)-([a-z0-9][a-z0-9._-]*)$/i);
|
|
7
|
+
|
|
8
|
+
if (!match) {
|
|
9
|
+
throw new Error('Icon reference must include id and name, for example: 33716-arrow-circle-up-fill');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
id: Number(match[1]),
|
|
14
|
+
slug: sanitizeFileSegment(match[2]),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parseIconLookupReference(reference) {
|
|
19
|
+
const value = String(reference || '').trim();
|
|
20
|
+
|
|
21
|
+
if (!value) {
|
|
22
|
+
throw new Error('Icon reference is required.');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const urlMatch = value.match(/\/icon\/(\d+)(?:\/([^/?#]+))?/i);
|
|
26
|
+
if (urlMatch) {
|
|
27
|
+
return {
|
|
28
|
+
id: Number(urlMatch[1]),
|
|
29
|
+
slug: urlMatch[2] ? sanitizeFileSegment(decodeURIComponent(urlMatch[2])) : '',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const idSlugMatch = value.match(/^(\d+)(?:-([a-z0-9][a-z0-9._-]*))?$/i);
|
|
34
|
+
if (idSlugMatch) {
|
|
35
|
+
return {
|
|
36
|
+
id: Number(idSlugMatch[1]),
|
|
37
|
+
slug: idSlugMatch[2] ? sanitizeFileSegment(idSlugMatch[2]) : '',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
throw new Error('Icon reference must be an id, id-name, or svgicons.com icon URL.');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function assertIconReferenceMatches(reference, icon) {
|
|
45
|
+
const expected = parseIconReference(reference);
|
|
46
|
+
const actualSlug = sanitizeFileSegment(icon?.name || '');
|
|
47
|
+
|
|
48
|
+
if (Number(icon?.id) !== expected.id || actualSlug !== expected.slug) {
|
|
49
|
+
throw new Error(`Icon reference mismatch. Expected ${expected.id}-${expected.slug}, got ${icon?.id || 'unknown'}-${actualSlug || 'unknown'}.`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return expected;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function iconDownloadFilename(icon) {
|
|
56
|
+
const id = Number(icon?.id);
|
|
57
|
+
const name = sanitizeFileSegment(icon?.name || 'icon');
|
|
58
|
+
|
|
59
|
+
return `${Number.isFinite(id) && id > 0 ? id : 'icon'}-${name}.svg`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function resolveIconDownloadPath(output, icon, cwd = process.cwd()) {
|
|
63
|
+
const filename = iconDownloadFilename(icon);
|
|
64
|
+
|
|
65
|
+
if (!output || output === true) {
|
|
66
|
+
return resolve(cwd, filename);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const target = isAbsolute(String(output))
|
|
70
|
+
? String(output)
|
|
71
|
+
: resolve(cwd, String(output));
|
|
72
|
+
|
|
73
|
+
if (extname(target).toLowerCase() === '.svg') {
|
|
74
|
+
return target;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return join(target, filename);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function resolveArchiveDownloadPath(output, filename, cwd = process.cwd()) {
|
|
81
|
+
const safeFilename = sanitizeFileName(filename || 'svgicons-collection-export.zip', 'zip');
|
|
82
|
+
|
|
83
|
+
if (!output || output === true) {
|
|
84
|
+
return resolve(cwd, safeFilename);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const target = isAbsolute(String(output))
|
|
88
|
+
? String(output)
|
|
89
|
+
: resolve(cwd, String(output));
|
|
90
|
+
|
|
91
|
+
if (extname(target).toLowerCase() === '.zip') {
|
|
92
|
+
return target;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return join(target, safeFilename);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function writeIconSvg(path, svg, force = false) {
|
|
99
|
+
if (!svg || typeof svg !== 'string') {
|
|
100
|
+
throw new Error('The icon response did not include SVG markup.');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await mkdir(dirname(path), { recursive: true });
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
await writeFile(path, svg.endsWith('\n') ? svg : `${svg}\n`, {
|
|
107
|
+
flag: force ? 'w' : 'wx',
|
|
108
|
+
});
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (error?.code === 'EEXIST') {
|
|
111
|
+
throw new Error(`File already exists: ${path}. Use --force to overwrite it.`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return path;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function writeBinaryFile(path, bytes, force = false) {
|
|
121
|
+
if (!bytes || bytes.length === 0) {
|
|
122
|
+
throw new Error('The download response did not include file data.');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
await mkdir(dirname(path), { recursive: true });
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
await writeFile(path, bytes, {
|
|
129
|
+
flag: force ? 'w' : 'wx',
|
|
130
|
+
});
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (error?.code === 'EEXIST') {
|
|
133
|
+
throw new Error(`File already exists: ${path}. Use --force to overwrite it.`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return path;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function sanitizeFileSegment(value) {
|
|
143
|
+
const cleaned = String(value)
|
|
144
|
+
.trim()
|
|
145
|
+
.toLowerCase()
|
|
146
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
147
|
+
.replace(/-+/g, '-')
|
|
148
|
+
.replace(/^[ ._-]+|[ ._-]+$/g, '')
|
|
149
|
+
.slice(0, 120);
|
|
150
|
+
|
|
151
|
+
return cleaned || 'icon';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function sanitizeFileName(value, fallbackExtension) {
|
|
155
|
+
const extension = fallbackExtension ? `.${fallbackExtension.replace(/^\./, '')}` : '';
|
|
156
|
+
const cleaned = String(value || '')
|
|
157
|
+
.trim()
|
|
158
|
+
.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-')
|
|
159
|
+
.replace(/\s+/g, '-')
|
|
160
|
+
.replace(/-+/g, '-')
|
|
161
|
+
.replace(/^[ ._-]+|[ ._-]+$/g, '')
|
|
162
|
+
.slice(0, 160);
|
|
163
|
+
|
|
164
|
+
if (!cleaned) {
|
|
165
|
+
return `download${extension}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (extension && extname(cleaned).toLowerCase() !== extension.toLowerCase()) {
|
|
169
|
+
return `${cleaned}${extension}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return cleaned;
|
|
173
|
+
}
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { redactString } from './redact.js';
|
|
2
|
+
|
|
3
|
+
export class CliError extends Error {
|
|
4
|
+
constructor(code, message, details = {}) {
|
|
5
|
+
super(redactString(message));
|
|
6
|
+
this.name = 'CliError';
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.status = details.status;
|
|
9
|
+
this.details = details;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function missingTokenError() {
|
|
14
|
+
return new CliError(
|
|
15
|
+
'missing_token',
|
|
16
|
+
'A Pro API token is required. Run: svgicons login --token <token>',
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function networkError(error, url) {
|
|
21
|
+
return new CliError('network_error', `Unable to reach ${url}: ${error?.message || error}`, { url });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function invalidJsonError(url) {
|
|
25
|
+
return new CliError('invalid_json', `Invalid JSON response from ${url}`, { url });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function httpError(status, url, data = {}) {
|
|
29
|
+
const message = data?.message || data?.error?.message || `HTTP ${status} from ${url}`;
|
|
30
|
+
const code = classifyHttpError(status, message, data);
|
|
31
|
+
|
|
32
|
+
return new CliError(code, message, {
|
|
33
|
+
status,
|
|
34
|
+
url,
|
|
35
|
+
requiredScopes: data?.requiredScopes,
|
|
36
|
+
missingScope: data?.missingScope,
|
|
37
|
+
pricingUrl: data?.pricingUrl,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function mcpError(message, details = {}) {
|
|
42
|
+
return new CliError('mcp_error', message || 'MCP request failed', details);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function usageError(message, details = {}) {
|
|
46
|
+
return new CliError('usage_error', message, details);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function exportFailedError(exportJob, message = '') {
|
|
50
|
+
return new CliError(
|
|
51
|
+
'export_failed',
|
|
52
|
+
message || exportJob?.failureMessage || `Collection export ${exportJob?.id || ''} failed.`.trim(),
|
|
53
|
+
{ exportId: exportJob?.id, status: exportJob?.status },
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function exportTimeoutError(exportJob, timeoutSeconds) {
|
|
58
|
+
return new CliError(
|
|
59
|
+
'export_timeout',
|
|
60
|
+
`Timed out waiting for export ${exportJob?.id}. Use --timeout <seconds> or --no-wait.`,
|
|
61
|
+
{ exportId: exportJob?.id, timeoutSeconds },
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function exportNotReadyError(exportJob) {
|
|
66
|
+
return new CliError(
|
|
67
|
+
'export_not_ready',
|
|
68
|
+
'The completed export did not provide a download URL.',
|
|
69
|
+
{ exportId: exportJob?.id, status: exportJob?.status },
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function licenseViolationError(report) {
|
|
74
|
+
return new CliError(
|
|
75
|
+
'license_violation',
|
|
76
|
+
`${report.violations.length} icon license violation(s) found.`,
|
|
77
|
+
{
|
|
78
|
+
checkedIcons: report.checkedIcons,
|
|
79
|
+
violations: report.violations,
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function serializeError(error) {
|
|
85
|
+
return {
|
|
86
|
+
error: {
|
|
87
|
+
code: error?.code || 'error',
|
|
88
|
+
message: redactString(error?.message || String(error)),
|
|
89
|
+
status: error?.status,
|
|
90
|
+
details: error?.details,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function classifyHttpError(status, message, data) {
|
|
96
|
+
const text = String(message || '').toLowerCase();
|
|
97
|
+
|
|
98
|
+
if (status === 401) {
|
|
99
|
+
return 'auth_required';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (status === 403) {
|
|
103
|
+
if (data?.missingScope || text.includes('missing') && text.includes('scope')) {
|
|
104
|
+
return 'missing_scope';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (text.includes('email verification') || text.includes('unverified')) {
|
|
108
|
+
return 'email_unverified';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (text.includes('pro plan') || text.includes('pro subscription') || text.includes('requires pro')) {
|
|
112
|
+
return 'pro_required';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return 'forbidden';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (status === 404) {
|
|
119
|
+
return 'not_found';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (status === 429) {
|
|
123
|
+
return 'rate_limited';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return 'http_error';
|
|
127
|
+
}
|
package/src/format.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export function printJson(value) {
|
|
2
|
+
console.log(JSON.stringify(value, null, 2));
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function printSearchResults(payload) {
|
|
6
|
+
const icons = payload.data || [];
|
|
7
|
+
|
|
8
|
+
if (icons.length === 0) {
|
|
9
|
+
console.log('No icons found.');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
for (const icon of icons) {
|
|
14
|
+
const set = icon.iconSet?.name ? ` - ${icon.iconSet.name}` : '';
|
|
15
|
+
console.log(`${icon.id}\t${icon.name}${set}`);
|
|
16
|
+
if (icon.pageUrl) {
|
|
17
|
+
console.log(` ${icon.pageUrl}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (payload.meta?.hasMore) {
|
|
22
|
+
console.log(`More results available. Next offset: ${payload.meta.nextOffset}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function printCollection(collection) {
|
|
27
|
+
console.log(`${collection.id}\t${collection.name}`);
|
|
28
|
+
console.log(` icons: ${collection.iconsCount ?? 0}`);
|
|
29
|
+
console.log(` framework: ${collection.framework || 'svg'}`);
|
|
30
|
+
console.log(` color: ${collection.colorPolicy || 'currentColor'}`);
|
|
31
|
+
console.log(` naming: ${collection.namingPolicy || 'kebab'}`);
|
|
32
|
+
if (collection.showUrl) {
|
|
33
|
+
console.log(` ${collection.showUrl}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function printCollectionDetail(payload, options = {}) {
|
|
38
|
+
const collection = payload.data || payload;
|
|
39
|
+
printCollection(collection);
|
|
40
|
+
|
|
41
|
+
if (collection.description) {
|
|
42
|
+
console.log(` description: ${collection.description}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const licenses = collection.licenseSummary || [];
|
|
46
|
+
if (licenses.length > 0) {
|
|
47
|
+
console.log('');
|
|
48
|
+
console.log('Licenses:');
|
|
49
|
+
for (const item of licenses) {
|
|
50
|
+
console.log(` ${item.license || 'Unknown'}: ${item.iconsCount || 0} icons`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const iconSets = collection.iconSetSummary || [];
|
|
55
|
+
if (iconSets.length > 0) {
|
|
56
|
+
console.log('');
|
|
57
|
+
console.log('Icon sets:');
|
|
58
|
+
for (const item of iconSets) {
|
|
59
|
+
console.log(` ${item.name || item.prefix}: ${item.iconsCount || 0} icons`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (options.icons) {
|
|
64
|
+
const icons = payload.icons?.data || [];
|
|
65
|
+
if (icons.length > 0) {
|
|
66
|
+
console.log('');
|
|
67
|
+
console.log('Icons:');
|
|
68
|
+
for (const icon of icons) {
|
|
69
|
+
const set = icon.iconSet?.name ? ` - ${icon.iconSet.name}` : '';
|
|
70
|
+
console.log(` ${icon.id}\t${icon.name}${set}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function printExport(exportJob) {
|
|
77
|
+
console.log(`Export ${exportJob.id}: ${exportJob.status}`);
|
|
78
|
+
console.log(` formats: ${(exportJob.formats || []).join(', ')}`);
|
|
79
|
+
if (exportJob.statusUrl) {
|
|
80
|
+
console.log(` status: ${exportJob.statusUrl}`);
|
|
81
|
+
}
|
|
82
|
+
if (exportJob.downloadUrl) {
|
|
83
|
+
console.log(` download: ${exportJob.downloadUrl}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function printScanProposal(proposal, options = {}) {
|
|
88
|
+
console.log(`Scanned ${proposal.stats.filesScanned} files in ${proposal.root}`);
|
|
89
|
+
console.log(`Suggested collection: ${proposal.collection.name}`);
|
|
90
|
+
console.log('');
|
|
91
|
+
|
|
92
|
+
if (proposal.concepts.length > 0) {
|
|
93
|
+
console.log('Detected UI concepts:');
|
|
94
|
+
for (const concept of proposal.concepts.slice(0, 20)) {
|
|
95
|
+
console.log(` ${concept.name} (${concept.count})`);
|
|
96
|
+
}
|
|
97
|
+
console.log('');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (proposal.existingIconIds.length > 0) {
|
|
101
|
+
console.log(`Existing svgicons.com icon URLs found: ${proposal.existingIconIds.join(', ')}`);
|
|
102
|
+
console.log('');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if ((proposal.generatedIconRefs || []).length > 0) {
|
|
106
|
+
console.log(`Generated SVG imports found: ${proposal.generatedIconRefs.join(', ')}`);
|
|
107
|
+
console.log('');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (proposal.stats.inlineSvgCount > 0) {
|
|
111
|
+
console.log(`Inline SVG blocks found: ${proposal.stats.inlineSvgCount}`);
|
|
112
|
+
console.log('');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!options.modified) {
|
|
116
|
+
console.log('No files were modified.');
|
|
117
|
+
}
|
|
118
|
+
console.log(`Next search: svgicons search "${proposal.concepts[0]?.name || 'dashboard'}"`);
|
|
119
|
+
console.log(`Write manifest: svgicons scan "${proposal.root}" --write-manifest`);
|
|
120
|
+
console.log(`Create collection: svgicons collection create --name "${proposal.collection.name}" --description "${proposal.collection.description}"`);
|
|
121
|
+
}
|