@svgicons-com/cli 0.1.0-alpha.3 → 0.1.0-alpha.5

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 CHANGED
@@ -8,7 +8,7 @@ Public icon search works without changing the default endpoint. Pro workflows re
8
8
 
9
9
  The scanner is read-only by default. It never edits project files unless a future command explicitly adds that behavior.
10
10
 
11
- Current package version: `0.1.0-alpha.3`.
11
+ Current package version: `0.1.0-alpha.5`.
12
12
 
13
13
  ## Requirements
14
14
 
@@ -60,6 +60,7 @@ svgicons icon show 33716
60
60
  svgicons icon url 33716-arrow-circle-up-fill
61
61
  svgicons icon raw 33716-arrow-circle-up-fill
62
62
  svgicons icon download 33716-arrow-circle-up-fill --output ./icons
63
+ svgicons icon png 33716-arrow-circle-up-fill --size 512 --output ./icons
63
64
  svgicons collection list
64
65
  svgicons collection create --name "Dashboard icons" --description "Navigation and status icons"
65
66
  svgicons collection show "Dashboard icons" --icons
@@ -69,7 +70,15 @@ svgicons collection add "Dashboard icons" 33716 240297
69
70
  svgicons collection remove "Dashboard icons" 33716-arrow-circle-up-fill
70
71
  svgicons collection delete "Dashboard icons" --yes
71
72
  svgicons collection export "Dashboard icons" --formats react-ts,vue --color-policy currentColor --output ./exports
73
+ svgicons collection export "Dashboard icons" --formats svelte --output ./exports
74
+ svgicons collection export "Dashboard icons" --formats solid --output ./exports
75
+ svgicons collection export "Dashboard icons" --formats blade --output ./exports
76
+ svgicons collection export "Dashboard icons" --formats storybook --output ./exports
77
+ svgicons collection export "Dashboard icons" --formats npm-package --package-name svgicons-dashboard-icons --output ./exports
78
+ svgicons collection export "Dashboard icons" --formats png-pack --png-sizes 24,48,512 --output ./exports
79
+ svgicons collection export "Dashboard icons" --formats iconify-json --output ./exports
72
80
  svgicons collection export "Dashboard icons" --formats react-ts --no-size-props --no-typescript --output ./exports
81
+ svgicons collection export "Dashboard icons" --formats react-ts,solid --default-size 20 --component-suffix Glyph --no-decorative --output ./exports
73
82
  svgicons export status 55 --collection "Dashboard icons"
74
83
  svgicons export download 55 --collection "Dashboard icons" --output ./exports
75
84
  svgicons init --collection "Dashboard icons" --output ./src/icons
@@ -116,6 +125,16 @@ The icon reference must include both the numeric ID and the icon name, such as `
116
125
 
117
126
  This command uses the MCP `get_icon` tool with raw SVG output, so the token needs `mcp:use` and `icons:read`. Existing files are not overwritten unless you add `--force`.
118
127
 
128
+ Export a Pro PNG asset from a single icon:
129
+
130
+ ```bash
131
+ svgicons icon png 33716-arrow-circle-up-fill --size 512 --output ./icons
132
+ svgicons icon png 33716-arrow-circle-up-fill --sizes 128,256,512 --density 1,2 --zip --output ./icons
133
+ svgicons icon png 33716-arrow-circle-up-fill --size 512 --color black --background solid --background-color "#ffffff"
134
+ ```
135
+
136
+ Single-icon PNG export uses the Pro REST API, not the public anonymous search API. The token needs `icons:read` and `exports:create`. The icon reference must include both ID and name, and the server verifies that the name matches before rendering.
137
+
119
138
  Read-only icon commands accept a numeric ID, an `id-name` reference, or a full svgicons.com icon URL:
120
139
 
121
140
  ```bash
@@ -174,13 +193,28 @@ The command polls every 2 seconds for up to 180 seconds by default. Use `--timeo
174
193
  Supported export flags:
175
194
 
176
195
  ```bash
177
- --formats react-ts,vue
196
+ --formats react-ts,vue,svelte,solid,blade,storybook,npm-package,png-pack,iconify-json
178
197
  --color-policy currentColor|preserve|strip
179
198
  --naming-policy kebab|pascal|camel
180
199
  --size-props / --no-size-props
181
200
  --typescript / --no-typescript
201
+ --default-size 24
202
+ --title-prop / --no-title-prop
203
+ --decorative / --no-decorative
204
+ --component-suffix Icon
205
+ --package-name svgicons-dashboard-icons
206
+ --package-version 0.1.0
207
+ --png-sizes 24,48,512
208
+ --png-densities 1,2
209
+ --png-background transparent|solid
210
+ --png-background-color "#ffffff"
211
+ --png-color preserve|black|white|custom
212
+ --png-icon-color "#2563eb"
213
+ --png-padding 48
182
214
  ```
183
215
 
216
+ Use `--formats png` as a shorthand for `--formats png-pack`. PNG pack exports are generated by the server queue worker and downloaded as part of the collection ZIP. Use `--formats svelte` to include Svelte components, `--formats solid` to include Solid components, `--formats blade` to include Laravel Blade components, `--formats storybook` to include a React Storybook gallery, `--formats npm-package` to include a React TypeScript package scaffold, or `--formats iconify-json` to include an Iconify-compatible `iconify.json` file in the collection ZIP.
217
+
184
218
  Collection commands accept a numeric ID, exact slug, or exact case-insensitive collection name. The legacy `kit` alias remains available for old scripts.
185
219
 
186
220
  Lifecycle commands are available for collection maintenance:
package/RELEASE_NOTES.md CHANGED
@@ -1,4 +1,27 @@
1
- # Svg/icons CLI 0.1.0-alpha.3
1
+ # Svg/icons CLI 0.1.0-alpha.5
2
+
3
+ This alpha refresh keeps the npm alpha channel aligned with the current Svg/icons Pro export workflow.
4
+
5
+ ## New in 0.1.0-alpha.5
6
+
7
+ - Version bump for npm alpha publication after `0.1.0-alpha.4`.
8
+ - No runtime command changes from `0.1.0-alpha.4`.
9
+ - Keeps the package README and release notes aligned with the current CLI export surface.
10
+
11
+ ## 0.1.0-alpha.4
12
+
13
+ This alpha brought the CLI up to date with the current Svg/icons Pro export workflow.
14
+
15
+ - Added single-icon PNG export with `svgicons icon png`, including PNG and ZIP variant downloads.
16
+ - Added collection PNG pack export with size, density, background, color, padding, and fit options.
17
+ - Added collection export format support for `svelte`, `solid`, `blade`, `storybook`, `npm-package`, and `iconify-json`.
18
+ - Added `png` as a shorthand for the `png-pack` collection export format.
19
+ - Added safe framework export options: `--default-size`, `--component-suffix`, `--title-prop`, `--no-title-prop`, `--decorative`, and `--no-decorative`.
20
+ - Added npm package export metadata options: `--package-name` and `--package-version`.
21
+ - Added `svgicons export download <export-id> --collection <collection>` for downloading queued collection export ZIPs.
22
+ - Updated CLI docs and tests for the newer Pro API export formats.
23
+
24
+ ## 0.1.0-alpha.3
2
25
 
3
26
  This alpha refresh improves token setup safety. `login` now rejects obviously truncated numeric tokens, which commonly happens when a shell command is run without quoting a Sanctum-style token containing `|`.
4
27
 
@@ -44,6 +67,7 @@ Before publishing, run:
44
67
 
45
68
  ```bash
46
69
  npm --prefix cli test
70
+ cd cli
47
71
  npm pack --dry-run
48
72
  ```
49
73
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svgicons-com/cli",
3
- "version": "0.1.0-alpha.3",
3
+ "version": "0.1.0-alpha.5",
4
4
  "description": "Svg/icons CLI alpha for icon search, Pro collections, exports, project scanning, and license workflows.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,7 @@
13
13
  "RELEASE_NOTES.md"
14
14
  ],
15
15
  "scripts": {
16
- "test": "node --test tests"
16
+ "test": "node --test"
17
17
  },
18
18
  "engines": {
19
19
  "node": ">=18.18"
package/src/api.js CHANGED
@@ -79,14 +79,26 @@ export async function proApiDownload(pathOrUrl, options = {}) {
79
79
  let response;
80
80
 
81
81
  try {
82
- response = await fetch(url, {
82
+ const headers = {
83
+ Accept: 'image/png, application/zip, application/octet-stream',
84
+ Authorization: `Bearer ${token}`,
85
+ 'User-Agent': 'svgicons-cli/0.1.0-alpha',
86
+ ...options.headers,
87
+ };
88
+ const init = {
83
89
  method: options.method || 'GET',
84
- headers: {
85
- Accept: 'application/zip, application/octet-stream',
86
- Authorization: `Bearer ${token}`,
87
- 'User-Agent': 'svgicons-cli/0.1.0-alpha',
88
- ...options.headers,
89
- },
90
+ headers,
91
+ };
92
+
93
+ if (options.body !== undefined) {
94
+ headers['Content-Type'] = headers['Content-Type'] || 'application/json';
95
+ init.body = typeof options.body === 'string' || options.body instanceof Buffer
96
+ ? options.body
97
+ : JSON.stringify(options.body);
98
+ }
99
+
100
+ response = await fetch(url, {
101
+ ...init,
90
102
  });
91
103
  } catch (error) {
92
104
  throw networkError(error, url);
package/src/cli.js CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  parseIconReference,
23
23
  resolveArchiveDownloadPath,
24
24
  resolveIconDownloadPath,
25
+ resolvePngDownloadPath,
25
26
  sanitizeFileSegment,
26
27
  writeBinaryFile,
27
28
  writeIconSvg,
@@ -56,10 +57,29 @@ const KNOWN_SCOPES = [
56
57
  'exports:create',
57
58
  'mcp:use',
58
59
  ];
59
- const EXPORT_FORMATS = ['svg-folder', 'svg-sprite', 'json-manifest', 'license-manifest', 'react-ts', 'vue'];
60
+ const EXPORT_FORMATS = [
61
+ 'svg-folder',
62
+ 'svg-sprite',
63
+ 'json-manifest',
64
+ 'license-manifest',
65
+ 'react-ts',
66
+ 'vue',
67
+ 'svelte',
68
+ 'solid',
69
+ 'blade',
70
+ 'storybook',
71
+ 'npm-package',
72
+ 'png-pack',
73
+ 'iconify-json',
74
+ ];
60
75
  const FRAMEWORKS = ['svg', 'react-ts', 'vue', 'sprite'];
61
76
  const COLOR_POLICIES = ['currentColor', 'preserve', 'strip'];
62
77
  const NAMING_POLICIES = ['kebab', 'pascal', 'camel'];
78
+ const PNG_SIZES = [16, 24, 32, 48, 96, 128, 256, 512, 1024];
79
+ const PNG_DENSITIES = [1, 2, 3, 4];
80
+ const PNG_BACKGROUNDS = ['transparent', 'solid'];
81
+ const PNG_COLORS = ['preserve', 'black', 'white', 'custom'];
82
+ const PNG_FITS = ['contain'];
63
83
 
64
84
  export async function run(argv) {
65
85
  const { args, options: globalOptions } = extractGlobalOptions(argv);
@@ -633,7 +653,12 @@ async function icon(args) {
633
653
  return;
634
654
  }
635
655
 
636
- throw new Error('Unknown icon command. Use: show, raw, url, open, download.');
656
+ if (subcommand === 'png') {
657
+ await iconPng(rest);
658
+ return;
659
+ }
660
+
661
+ throw new Error('Unknown icon command. Use: show, raw, url, open, download, png.');
637
662
  }
638
663
 
639
664
  async function iconShow(args) {
@@ -715,6 +740,40 @@ async function iconDownload(args) {
715
740
  console.log(` ${outputPath}`);
716
741
  }
717
742
 
743
+ async function iconPng(args) {
744
+ const options = parseOptions(args);
745
+ const [iconReference] = options._;
746
+ const reference = parseIconReference(iconReference);
747
+ const payload = buildIconPngPayload(reference, options);
748
+ const download = await proApiDownload(`/api/pro/icons/${encodeURIComponent(reference.id)}/png-export`, {
749
+ method: 'POST',
750
+ body: payload,
751
+ headers: {
752
+ 'Content-Type': 'application/json',
753
+ },
754
+ });
755
+ const outputPath = resolvePngDownloadPath(options.output || options.o, download.filename, process.cwd());
756
+
757
+ await writeBinaryFile(outputPath, download.bytes, booleanOption(options.force, false));
758
+
759
+ if (options.json) {
760
+ printJson({
761
+ icon: {
762
+ id: reference.id,
763
+ name: reference.slug,
764
+ },
765
+ outputPath,
766
+ contentType: download.contentType,
767
+ filename: download.filename,
768
+ options: payload,
769
+ });
770
+ return;
771
+ }
772
+
773
+ console.log(`Downloaded PNG export`);
774
+ console.log(` ${outputPath}`);
775
+ }
776
+
718
777
  async function collection(args) {
719
778
  const [subcommand, ...rest] = args;
720
779
 
@@ -1173,7 +1232,7 @@ async function buildProject(args) {
1173
1232
  const format = options.format || manifest.format || 'svg';
1174
1233
 
1175
1234
  if (format !== 'svg') {
1176
- throw usageError('Local build currently supports format svg. Use collection export for React TypeScript or Vue component ZIPs.');
1235
+ throw usageError('Local build currently supports format svg. Use collection export for React TypeScript, Vue, Svelte, or Solid component ZIPs.');
1177
1236
  }
1178
1237
 
1179
1238
  const outputDir = resolve(String(options.output || options.o || manifest.output));
@@ -1830,17 +1889,163 @@ function extensionSet(value) {
1830
1889
  }
1831
1890
 
1832
1891
  function buildExportOptions(options) {
1892
+ const formats = exportFormatsOption(options.formats);
1893
+ const png = buildCollectionPngOptions(options);
1894
+ const includesPng = formats?.includes('png-pack') || Object.keys(png).length > 0;
1895
+ const normalizedFormats = includesPng && formats === undefined
1896
+ ? ['png-pack']
1897
+ : formats;
1898
+
1833
1899
  return {
1834
- formats: enumCsvOption(options.formats, 'export format', EXPORT_FORMATS),
1900
+ formats: normalizedFormats,
1835
1901
  options: {
1836
1902
  colorPolicy: enumOption(options['color-policy'], 'color policy', COLOR_POLICIES),
1837
1903
  namingPolicy: enumOption(options['naming-policy'], 'naming policy', NAMING_POLICIES),
1838
1904
  sizeProps: booleanFlagPair(options, 'size-props', 'no-size-props'),
1839
1905
  typescript: booleanFlagPair(options, 'typescript', 'no-typescript'),
1906
+ defaultSize: boundedIntegerOption(options['default-size'], 'default component size', 1, 1024),
1907
+ titleProp: booleanFlagPair(options, 'title-prop', 'no-title-prop'),
1908
+ decorative: booleanFlagPair(options, 'decorative', 'no-decorative'),
1909
+ componentSuffix: identifierOption(options['component-suffix'], 'component suffix'),
1910
+ packageName: packageNameOption(options['package-name']),
1911
+ packageVersion: packageVersionOption(options['package-version']),
1912
+ png: includesPng ? png : undefined,
1840
1913
  },
1841
1914
  };
1842
1915
  }
1843
1916
 
1917
+ function buildIconPngPayload(reference, options) {
1918
+ return {
1919
+ iconName: reference.slug,
1920
+ sizes: integerCsvOption(options.sizes ?? options.size, 'PNG size', PNG_SIZES, [512]),
1921
+ densities: integerCsvOption(options.densities ?? options.density, 'PNG density', PNG_DENSITIES, [1]),
1922
+ backgroundType: enumOption(options.background, 'PNG background', PNG_BACKGROUNDS),
1923
+ backgroundColor: options['background-color'],
1924
+ iconColorMode: enumOption(options.color, 'PNG icon color mode', PNG_COLORS),
1925
+ iconColor: options['icon-color'],
1926
+ padding: integerOption(options.padding, 'PNG padding'),
1927
+ fit: enumOption(options.fit, 'PNG fit mode', PNG_FITS),
1928
+ filename: options.filename,
1929
+ zip: booleanOption(options.zip, undefined),
1930
+ };
1931
+ }
1932
+
1933
+ function buildCollectionPngOptions(options) {
1934
+ return stripUndefined({
1935
+ sizes: integerCsvOption(options['png-sizes'] ?? options['png-size'], 'PNG size', PNG_SIZES, undefined),
1936
+ densities: integerCsvOption(options['png-densities'] ?? options['png-density'], 'PNG density', [1, 2, 3], undefined),
1937
+ backgroundType: enumOption(options['png-background'], 'PNG background', PNG_BACKGROUNDS),
1938
+ backgroundColor: options['png-background-color'],
1939
+ iconColorMode: enumOption(options['png-color'], 'PNG icon color mode', PNG_COLORS),
1940
+ iconColor: options['png-icon-color'],
1941
+ padding: integerOption(options['png-padding'], 'PNG padding'),
1942
+ fit: enumOption(options['png-fit'], 'PNG fit mode', PNG_FITS),
1943
+ });
1944
+ }
1945
+
1946
+ function exportFormatsOption(value) {
1947
+ const values = csvOption(value);
1948
+
1949
+ if (!values) {
1950
+ return undefined;
1951
+ }
1952
+
1953
+ const normalized = values.map((item) => item === 'png' ? 'png-pack' : item);
1954
+ const invalid = normalized.filter((item) => !EXPORT_FORMATS.includes(item));
1955
+ if (invalid.length > 0) {
1956
+ throw usageError(`Invalid export format: ${invalid.join(', ')}. Allowed values: ${EXPORT_FORMATS.join(', ')}, png`);
1957
+ }
1958
+
1959
+ return [...new Set(normalized)];
1960
+ }
1961
+
1962
+ function integerCsvOption(value, label, allowed, fallback) {
1963
+ const values = csvOption(value);
1964
+
1965
+ if (!values) {
1966
+ return fallback;
1967
+ }
1968
+
1969
+ const parsed = values.map((item) => Number(item));
1970
+ const invalid = parsed.filter((item) => !Number.isInteger(item) || !allowed.includes(item));
1971
+
1972
+ if (invalid.length > 0) {
1973
+ throw usageError(`Invalid ${label}: ${invalid.join(', ')}. Allowed values: ${allowed.join(', ')}`);
1974
+ }
1975
+
1976
+ return [...new Set(parsed)];
1977
+ }
1978
+
1979
+ function integerOption(value, label) {
1980
+ if (value === undefined || value === true || value === '') {
1981
+ return undefined;
1982
+ }
1983
+
1984
+ const parsed = Number(value);
1985
+ if (!Number.isInteger(parsed)) {
1986
+ throw usageError(`Invalid ${label}: ${value}. Expected an integer.`);
1987
+ }
1988
+
1989
+ return parsed;
1990
+ }
1991
+
1992
+ function boundedIntegerOption(value, label, min, max) {
1993
+ const parsed = integerOption(value, label);
1994
+
1995
+ if (parsed === undefined) {
1996
+ return undefined;
1997
+ }
1998
+
1999
+ if (parsed < min || parsed > max) {
2000
+ throw usageError(`Invalid ${label}: ${value}. Allowed range: ${min}-${max}.`);
2001
+ }
2002
+
2003
+ return parsed;
2004
+ }
2005
+
2006
+ function identifierOption(value, label) {
2007
+ if (value === undefined || value === true || value === '') {
2008
+ return undefined;
2009
+ }
2010
+
2011
+ const parsed = String(value);
2012
+ if (!/^[A-Za-z][A-Za-z0-9]{0,31}$/.test(parsed)) {
2013
+ throw usageError(`Invalid ${label}: ${value}. Use 1-32 letters or numbers, starting with a letter.`);
2014
+ }
2015
+
2016
+ return parsed;
2017
+ }
2018
+
2019
+ function packageNameOption(value) {
2020
+ if (value === undefined || value === true || value === '') {
2021
+ return undefined;
2022
+ }
2023
+
2024
+ const parsed = String(value);
2025
+ if (parsed.length > 214 || parsed.toLowerCase() !== parsed || !/^(?:@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*|[a-z0-9][a-z0-9._-]*)$/.test(parsed)) {
2026
+ throw usageError(`Invalid npm package name: ${value}. Use a lowercase npm package name such as svgicons-dashboard or @svgicons-com/dashboard.`);
2027
+ }
2028
+
2029
+ return parsed;
2030
+ }
2031
+
2032
+ function packageVersionOption(value) {
2033
+ if (value === undefined || value === true || value === '') {
2034
+ return undefined;
2035
+ }
2036
+
2037
+ const parsed = String(value);
2038
+ if (parsed.length > 64 || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(parsed)) {
2039
+ throw usageError(`Invalid npm package version: ${value}. Use a semver-like version such as 1.0.0 or 1.0.0-beta.1.`);
2040
+ }
2041
+
2042
+ return parsed;
2043
+ }
2044
+
2045
+ function stripUndefined(value) {
2046
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
2047
+ }
2048
+
1844
2049
  function enumOption(value, label, allowed) {
1845
2050
  if (value === undefined || value === true || value === '') {
1846
2051
  return undefined;
@@ -1894,6 +2099,7 @@ Usage:
1894
2099
  svgicons icon url <icon-ref>
1895
2100
  svgicons icon open <icon-ref>
1896
2101
  svgicons icon download <icon-id-name> [--output ./icons] [--force]
2102
+ svgicons icon png <icon-id-name> [--size 512] [--density 1,2] [--output ./icons] [--force]
1897
2103
  svgicons collection list [--json]
1898
2104
  svgicons collection create --name "Dashboard icons"
1899
2105
  svgicons collection show <collection-id-or-name> [--icons] [--json]
@@ -1902,7 +2108,7 @@ Usage:
1902
2108
  svgicons collection add <collection-id-or-name> <icon-id...>
1903
2109
  svgicons collection remove <collection-id-or-name> <icon-id...>
1904
2110
  svgicons collection delete <collection-id-or-name> [--yes]
1905
- svgicons collection export <collection-id-or-name> [--formats react-ts,vue] [--no-size-props] [--no-typescript] [--output ./exports]
2111
+ svgicons collection export <collection-id-or-name> [--formats react-ts,vue,svelte,solid,blade,storybook,npm-package,png-pack,iconify-json] [--default-size 24] [--component-suffix Icon] [--package-name svgicons-dashboard] [--package-version 0.1.0] [--title-prop|--no-title-prop] [--decorative|--no-decorative] [--no-size-props] [--no-typescript] [--output ./exports]
1906
2112
  svgicons export status <export-id> --collection <collection-id-or-name>
1907
2113
  svgicons export download <export-id> --collection <collection-id-or-name> [--output ./exports]
1908
2114
  svgicons init [--collection <collection-id-or-name>] [--output svgicons-icons]
package/src/downloads.js CHANGED
@@ -95,6 +95,25 @@ export function resolveArchiveDownloadPath(output, filename, cwd = process.cwd()
95
95
  return join(target, safeFilename);
96
96
  }
97
97
 
98
+ export function resolvePngDownloadPath(output, filename, cwd = process.cwd()) {
99
+ const safeFilename = sanitizeFileName(filename || 'svgicons-icon.png', 'png');
100
+
101
+ if (!output || output === true) {
102
+ return resolve(cwd, safeFilename);
103
+ }
104
+
105
+ const target = isAbsolute(String(output))
106
+ ? String(output)
107
+ : resolve(cwd, String(output));
108
+
109
+ const extension = extname(target).toLowerCase();
110
+ if (extension === '.png' || extension === '.zip') {
111
+ return target;
112
+ }
113
+
114
+ return join(target, safeFilename);
115
+ }
116
+
98
117
  export async function writeIconSvg(path, svg, force = false) {
99
118
  if (!svg || typeof svg !== 'string') {
100
119
  throw new Error('The icon response did not include SVG markup.');