logomarks 0.0.0-stage → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/LICENSE +21 -0
  3. package/README.md +56 -2
  4. package/bin/logomarks.js +4 -0
  5. package/data/fonts/BricolageGrotesque-SemiBold.ttf +0 -0
  6. package/data/fonts/Cagliostro-Regular.ttf +0 -0
  7. package/data/fonts/Fraunces-SemiBold.ttf +0 -0
  8. package/data/fonts/IBMPlexMono-Regular.ttf +0 -0
  9. package/data/fonts/IBMPlexMono-SemiBold.ttf +0 -0
  10. package/data/fonts/InstrumentSans-SemiBold.ttf +0 -0
  11. package/data/fonts/InstrumentSerif-Regular.ttf +0 -0
  12. package/data/fonts/Inter-SemiBold.ttf +0 -0
  13. package/data/fonts/Limelight-Regular.ttf +0 -0
  14. package/data/fonts/OFL.txt +103 -0
  15. package/data/fonts/Shrikhand-Regular.ttf +0 -0
  16. package/data/fonts/SpaceGrotesk-SemiBold.ttf +0 -0
  17. package/data/index.bin +0 -0
  18. package/docs/api.md +31 -0
  19. package/docs/commands.md +412 -0
  20. package/docs/editing.md +61 -0
  21. package/docs/mcp.md +45 -0
  22. package/package.json +94 -4
  23. package/src/brand/brand.d.ts +23 -0
  24. package/src/brand/brand.js +29 -0
  25. package/src/brand/fonts.d.ts +61 -0
  26. package/src/brand/fonts.js +93 -0
  27. package/src/brand/mark.d.ts +53 -0
  28. package/src/brand/mark.js +151 -0
  29. package/src/checks/finding.d.ts +56 -0
  30. package/src/checks/finding.js +57 -0
  31. package/src/checks/fit.d.ts +5 -0
  32. package/src/checks/fit.js +46 -0
  33. package/src/checks/index.d.ts +33 -0
  34. package/src/checks/index.js +57 -0
  35. package/src/checks/specks.d.ts +5 -0
  36. package/src/checks/specks.js +38 -0
  37. package/src/checks/widths.d.ts +7 -0
  38. package/src/checks/widths.js +48 -0
  39. package/src/cli/args.d.ts +20 -0
  40. package/src/cli/args.js +35 -0
  41. package/src/cli/commands/check.d.ts +10 -0
  42. package/src/cli/commands/check.js +66 -0
  43. package/src/cli/commands/combine.d.ts +10 -0
  44. package/src/cli/commands/combine.js +54 -0
  45. package/src/cli/commands/cover.d.ts +10 -0
  46. package/src/cli/commands/cover.js +53 -0
  47. package/src/cli/commands/edit.d.ts +10 -0
  48. package/src/cli/commands/edit.js +70 -0
  49. package/src/cli/commands/find.d.ts +10 -0
  50. package/src/cli/commands/find.js +61 -0
  51. package/src/cli/commands/get.d.ts +10 -0
  52. package/src/cli/commands/get.js +57 -0
  53. package/src/cli/commands/glitch.d.ts +10 -0
  54. package/src/cli/commands/glitch.js +57 -0
  55. package/src/cli/commands/index.d.ts +15 -0
  56. package/src/cli/commands/index.js +24 -0
  57. package/src/cli/commands/kit.d.ts +10 -0
  58. package/src/cli/commands/kit.js +66 -0
  59. package/src/cli/commands/mcp.d.ts +9 -0
  60. package/src/cli/commands/mcp.js +36 -0
  61. package/src/cli/commands/mockup.d.ts +10 -0
  62. package/src/cli/commands/mockup.js +61 -0
  63. package/src/cli/commands/parts.d.ts +10 -0
  64. package/src/cli/commands/parts.js +44 -0
  65. package/src/cli/commands/sheet.d.ts +10 -0
  66. package/src/cli/commands/sheet.js +66 -0
  67. package/src/cli/commands/similar.d.ts +10 -0
  68. package/src/cli/commands/similar.js +111 -0
  69. package/src/cli/commands/social.d.ts +10 -0
  70. package/src/cli/commands/social.js +65 -0
  71. package/src/cli/commands/specks.d.ts +20 -0
  72. package/src/cli/commands/specks.js +103 -0
  73. package/src/cli/edited.d.ts +6 -0
  74. package/src/cli/edited.js +32 -0
  75. package/src/cli/files.d.ts +4 -0
  76. package/src/cli/files.js +27 -0
  77. package/src/cli/help.d.ts +20 -0
  78. package/src/cli/help.js +55 -0
  79. package/src/cli/index.d.ts +10 -0
  80. package/src/cli/index.js +104 -0
  81. package/src/cli/options.d.ts +30 -0
  82. package/src/cli/options.js +155 -0
  83. package/src/cli/report.d.ts +4 -0
  84. package/src/cli/report.js +28 -0
  85. package/src/cli/style.d.ts +4 -0
  86. package/src/cli/style.js +34 -0
  87. package/src/cover/index.d.ts +49 -0
  88. package/src/cover/index.js +63 -0
  89. package/src/edit/combine.d.ts +31 -0
  90. package/src/edit/combine.js +103 -0
  91. package/src/edit/doc.d.ts +34 -0
  92. package/src/edit/doc.js +96 -0
  93. package/src/edit/glitch.d.ts +48 -0
  94. package/src/edit/glitch.js +43 -0
  95. package/src/edit/index.d.ts +8 -0
  96. package/src/edit/index.js +11 -0
  97. package/src/edit/ops.d.ts +51 -0
  98. package/src/edit/ops.js +90 -0
  99. package/src/edit/preview.d.ts +8 -0
  100. package/src/edit/preview.js +51 -0
  101. package/src/find/index.d.ts +54 -0
  102. package/src/find/index.js +88 -0
  103. package/src/index.d.ts +30 -0
  104. package/src/index.js +28 -0
  105. package/src/kit/index.d.ts +26 -0
  106. package/src/kit/index.js +31 -0
  107. package/src/mcp/content.d.ts +30 -0
  108. package/src/mcp/content.js +56 -0
  109. package/src/mcp/edited.d.ts +5 -0
  110. package/src/mcp/edited.js +25 -0
  111. package/src/mcp/index.d.ts +41 -0
  112. package/src/mcp/index.js +72 -0
  113. package/src/mcp/inputs.d.ts +46 -0
  114. package/src/mcp/inputs.js +122 -0
  115. package/src/mcp/preview.d.ts +4 -0
  116. package/src/mcp/preview.js +24 -0
  117. package/src/mcp/tools/check.d.ts +15 -0
  118. package/src/mcp/tools/check.js +47 -0
  119. package/src/mcp/tools/combine.d.ts +15 -0
  120. package/src/mcp/tools/combine.js +42 -0
  121. package/src/mcp/tools/cover.d.ts +20 -0
  122. package/src/mcp/tools/cover.js +46 -0
  123. package/src/mcp/tools/edit.d.ts +21 -0
  124. package/src/mcp/tools/edit.js +73 -0
  125. package/src/mcp/tools/find.d.ts +14 -0
  126. package/src/mcp/tools/find.js +36 -0
  127. package/src/mcp/tools/get.d.ts +15 -0
  128. package/src/mcp/tools/get.js +44 -0
  129. package/src/mcp/tools/glitch.d.ts +12 -0
  130. package/src/mcp/tools/glitch.js +39 -0
  131. package/src/mcp/tools/index.d.ts +4 -0
  132. package/src/mcp/tools/index.js +23 -0
  133. package/src/mcp/tools/kit.d.ts +28 -0
  134. package/src/mcp/tools/kit.js +51 -0
  135. package/src/mcp/tools/mockup.d.ts +22 -0
  136. package/src/mcp/tools/mockup.js +51 -0
  137. package/src/mcp/tools/sheet.d.ts +15 -0
  138. package/src/mcp/tools/sheet.js +43 -0
  139. package/src/mcp/tools/similar.d.ts +13 -0
  140. package/src/mcp/tools/similar.js +42 -0
  141. package/src/mcp/tools/social.d.ts +5 -0
  142. package/src/mcp/tools/social.js +68 -0
  143. package/src/mockup/index.d.ts +52 -0
  144. package/src/mockup/index.js +152 -0
  145. package/src/raster/render.d.ts +30 -0
  146. package/src/raster/render.js +100 -0
  147. package/src/rules.d.ts +73 -0
  148. package/src/rules.js +139 -0
  149. package/src/sheet/card.d.ts +20 -0
  150. package/src/sheet/card.js +190 -0
  151. package/src/sheet/draw.d.ts +26 -0
  152. package/src/sheet/draw.js +94 -0
  153. package/src/sheet/grid.d.ts +2 -0
  154. package/src/sheet/grid.js +32 -0
  155. package/src/sheet/index.d.ts +31 -0
  156. package/src/sheet/index.js +47 -0
  157. package/src/sheet/theme.d.ts +8 -0
  158. package/src/sheet/theme.js +45 -0
  159. package/src/similar/folder.d.ts +20 -0
  160. package/src/similar/folder.js +66 -0
  161. package/src/similar/nearest.d.ts +32 -0
  162. package/src/similar/nearest.js +60 -0
  163. package/src/similar/signature.d.ts +12 -0
  164. package/src/similar/signature.js +42 -0
  165. package/src/similar/store.d.ts +67 -0
  166. package/src/similar/store.js +88 -0
  167. package/src/util/color.d.ts +2 -0
  168. package/src/util/color.js +23 -0
  169. package/src/util/files.d.ts +8 -0
  170. package/src/util/files.js +60 -0
  171. package/src/util/round.d.ts +2 -0
  172. package/src/util/round.js +14 -0
  173. package/src/util/version.d.ts +2 -0
  174. package/src/util/version.js +10 -0
@@ -0,0 +1,7 @@
1
+ import type { FilledPath } from '@logomarks/core/svg';
2
+ import type { Finding } from './finding.js';
3
+
4
+ export type { RunGroup } from '@logomarks/core/analyse';
5
+
6
+ /** Thin strokes, narrow gaps and uneven gaps (all warnings). */
7
+ export function widthFindings(paths: readonly FilledPath[]): Finding[];
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @module logomarks/checks/widths
3
+ *
4
+ * Gaps and strokes, measured on a 2 px-a-unit raster as runs of even width
5
+ * (`raster/channels.js`): anything under 10 units closes up or fades at
6
+ * 32 px, and a mark keeps one gap width (22), or a clear 1:2 ratio. These are
7
+ * judgements the live set sometimes makes on purpose (cube letters' 10 gaps,
8
+ * cute animals' 16 features), so they are warnings.
9
+ */
10
+
11
+ import { GAPS, MIN_FEATURE, WIDTH_SCALE, WIDTH_PRECISION, CHIPS } from '../rules.js';
12
+ import { widths } from '@logomarks/core/analyse';
13
+ import { finding } from './finding.js';
14
+
15
+ /** Pixels a unit at the smallest chip that must still read (32 px). */
16
+ const AT_32 = CHIPS[0] / 512;
17
+
18
+ /**
19
+ * How a group reads in a message: "3 gaps 9.8 wide".
20
+ *
21
+ * @param {import('@logomarks/core/analyse').RunGroup} g The group.
22
+ * @param {string} noun What it is.
23
+ * @returns {string} The words.
24
+ */
25
+ const describe = (g, noun) => `${g.count > 1 ? `${g.count} ${noun}s` : `A ${noun}`} ${g.width.toFixed(1)} wide`;
26
+
27
+ /**
28
+ * Width findings for one mark: thin strokes, narrow gaps, uneven gaps.
29
+ *
30
+ * @param {readonly import('@logomarks/core/svg').FilledPath[]} paths The mark's shapes.
31
+ * @returns {import('./finding.js').Finding[]} What was found.
32
+ */
33
+ export function widthFindings(paths) {
34
+ const { gaps, strokes } = widths(paths, { ...GAPS, scale: WIDTH_SCALE, pad: 4 });
35
+ const out = [];
36
+ const under = (/** @type {import('@logomarks/core/analyse').RunGroup} */ g) => g.width < MIN_FEATURE - WIDTH_PRECISION;
37
+ for (const g of strokes.filter(under)) {
38
+ out.push(finding('thin', `${describe(g, 'stroke')}: under ${MIN_FEATURE}, it fades at 32 px (${(g.width * AT_32).toFixed(2)} px).`, { box: g.box, value: g.width }));
39
+ }
40
+ for (const g of gaps.filter(under)) {
41
+ out.push(finding('narrow-gap', `${describe(g, 'gap')}: under ${MIN_FEATURE}, it closes up at 32 px (${(g.width * AT_32).toFixed(2)} px).`, { box: g.box, value: g.width }));
42
+ }
43
+ const main = gaps.slice().sort((a, b) => b.length - a.length)[0];
44
+ for (const g of gaps.filter((s) => s !== main)) {
45
+ out.push(finding('uneven-gaps', `${describe(g, 'gap')}, against the mark's main gap of ${main.width.toFixed(1)}: keep one gap width (${GAPS.house}), or a clear 1:2 ratio.`, { box: g.box, value: g.width }));
46
+ }
47
+ return out;
48
+ }
@@ -0,0 +1,20 @@
1
+ /** A usage error: the CLI prints it with the help and exits 2. */
2
+ export class UsageError extends Error {}
3
+
4
+ /** What a command takes. */
5
+ export interface ArgSpec {
6
+ /** Boolean flags, without dashes (`json`). */
7
+ flags?: string[];
8
+ /** Options with a value, without dashes, each with a one-letter alias or '' (`{ out: 'o' }`). */
9
+ options?: Record<string, string>;
10
+ }
11
+
12
+ /** Parsed arguments. */
13
+ export interface Parsed {
14
+ positionals: string[];
15
+ flags: Set<string>;
16
+ options: Record<string, string>;
17
+ }
18
+
19
+ /** Parses a command's arguments; throws a `UsageError` on anything unknown. */
20
+ export function parseArgs(argv: readonly string[], spec: ArgSpec): Parsed;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @module logomarks/cli/args
3
+ *
4
+ * A small argument parser: positionals, boolean flags, and options with a
5
+ * value (`--top 3`, `--top=3`, `-o out.png`). Anything unknown is a usage
6
+ * error.
7
+ */
8
+
9
+ /** A usage error: the CLI prints it with the help and exits 2. */
10
+ export class UsageError extends Error {}
11
+
12
+ /**
13
+ * Parses a command's arguments.
14
+ *
15
+ * @param {readonly string[]} argv The arguments after the verb.
16
+ * @param {import('./args.js').ArgSpec} spec The flags and options it takes.
17
+ * @returns {import('./args.js').Parsed} The positionals, flags and options.
18
+ * @throws {UsageError} On an unknown flag or a missing value.
19
+ */
20
+ export function parseArgs(argv, { flags = [], options = {} }) {
21
+ /** @type {import('./args.js').Parsed} */
22
+ const out = { positionals: [], flags: new Set(), options: {} };
23
+ const alias = Object.fromEntries(Object.entries(options).filter(([, a]) => a).map(([name, a]) => [`-${a}`, name]));
24
+ for (let i = 0; i < argv.length; i++) {
25
+ const a = argv[i];
26
+ if (!a.startsWith('-') || a === '-') { out.positionals.push(a); continue; }
27
+ const [key, inline] = a.startsWith('--') ? [a.slice(2).split('=')[0], a.includes('=') ? a.slice(a.indexOf('=') + 1) : undefined] : [alias[a], undefined];
28
+ if (key && flags.includes(key) && inline === undefined) { out.flags.add(key); continue; }
29
+ if (!key || !(key in options)) throw new UsageError(`unknown option ${a}`);
30
+ const value = inline ?? argv[++i];
31
+ if (value === undefined) throw new UsageError(`${a} needs a value`);
32
+ out.options[key] = value;
33
+ }
34
+ return out;
35
+ }
@@ -0,0 +1,10 @@
1
+ import type { ArgSpec, Parsed } from '../args.js';
2
+ import type { IO } from '../index.js';
3
+ import type { CommandHelp } from '../help.js';
4
+
5
+ /** What `check` takes. */
6
+ export const spec: ArgSpec;
7
+ /** What `logomarks check --help` prints. */
8
+ export const help: CommandHelp;
9
+ /** Runs `check`; resolves to its exit code (0 pass, 1 fail). */
10
+ export function run(args: Parsed, io: IO): Promise<number>;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * @module logomarks/cli/commands/check
3
+ *
4
+ * `logomarks check <file.svg…>`: every house rule, per mark. `--fix` refits
5
+ * marks whose only fixable problems are the fit, in place.
6
+ */
7
+
8
+ import fs from 'node:fs';
9
+ import { check, fix } from '../../checks/index.js';
10
+ import { svgFiles, slugOf } from '../files.js';
11
+ import { style } from '../style.js';
12
+ import { reportLines as lines } from '../report.js';
13
+
14
+ /** What `check` takes. */
15
+ export const spec = { flags: ['json', 'fix', 'strict'], options: { ignore: '' } };
16
+
17
+ /** `logomarks check --help`. */
18
+ /** @type {import('../help.js').CommandHelp} */
19
+ export const help = {
20
+ summary: 'Check SVG marks against the house rules.',
21
+ usage: ['<file.svg | folder>… [options]'],
22
+ about: [
23
+ 'Measures the fit (512 canvas, longest side 448, centred), the markup, specks, slivers,',
24
+ 'pin-holes, clipping, gap widths and features too small for 32 px. Each finding has an id,',
25
+ 'a severity and a box. Exit 0 when every mark passes, 1 when any fails.',
26
+ ],
27
+ options: [
28
+ ['--fix', 'Refit to the grid in place (the only automatic fix).'],
29
+ ['--strict', 'Count warnings as failures too.'],
30
+ ['--ignore <ids>', 'Leave these finding ids out, e.g. speck,uneven-gaps.'],
31
+ ],
32
+ examples: ['logomarks check mark.svg', 'logomarks check marks/ --fix', 'logomarks check out/*.svg --strict --json'],
33
+ };
34
+
35
+ /**
36
+ * Runs `check`.
37
+ *
38
+ * @param {import('../args.js').Parsed} args The parsed arguments.
39
+ * @param {import('../index.js').IO} io Where output goes.
40
+ * @returns {Promise<number>} The exit code: 0 when every mark passes, 1 otherwise.
41
+ */
42
+ export async function run(args, io) {
43
+ const files = svgFiles(args.positionals);
44
+ const ignore = (args.options.ignore ?? '').split(',').filter(Boolean);
45
+ const strict = args.flags.has('strict');
46
+ const results = files.map((file) => {
47
+ const svg = fs.readFileSync(file, 'utf8');
48
+ const name = slugOf(file);
49
+ let report = check(svg, { name, ignore, strict });
50
+ const fitted = args.flags.has('fix') ? fix(svg, report) : null;
51
+ if (fitted) {
52
+ fs.writeFileSync(file, fitted);
53
+ report = check(fitted, { name, ignore, strict });
54
+ }
55
+ return { file, fixed: Boolean(fitted), report };
56
+ });
57
+ const failed = results.filter((r) => !r.report.ok).length;
58
+ if (args.flags.has('json')) {
59
+ io.out(JSON.stringify({ ok: !failed, passed: results.length - failed, failed, marks: results.map(({ file, fixed, report }) => ({ file, fixed, ...report })) }, null, 2));
60
+ } else {
61
+ for (const { file, fixed, report } of results) io.out(lines(file, report, fixed));
62
+ const warned = results.filter((r) => r.report.ok && r.report.warnings).length;
63
+ io.out(`\n${results.length - failed} of ${results.length} passed${warned ? ` (${warned} with warnings)` : ''}${failed ? style.red(`, ${failed} failed`) : ''}`);
64
+ }
65
+ return failed ? 1 : 0;
66
+ }
@@ -0,0 +1,10 @@
1
+ import type { ArgSpec, Parsed } from '../args.js';
2
+ import type { IO } from '../index.js';
3
+ import type { CommandHelp } from '../help.js';
4
+
5
+ /** What `combine` takes. */
6
+ export const spec: ArgSpec;
7
+ /** What `logomarks combine --help` prints. */
8
+ export const help: CommandHelp;
9
+ /** Runs `combine`; resolves to its exit code. */
10
+ export function run(args: Parsed, io: IO): Promise<number>;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @module logomarks/cli/commands/combine
3
+ *
4
+ * `logomarks combine <a> <b> --op subtract --scale 0.5 -o out.svg`: two
5
+ * marks made one, fitted and checked. Each is a live slug or an .svg file.
6
+ */
7
+
8
+ import { combineMarks, COMBINE_OPS } from '../../edit/combine.js';
9
+ import { UsageError } from '../args.js';
10
+ import { markOf, numArg, asUsage } from '../options.js';
11
+ import { output } from '../edited.js';
12
+
13
+ /** What `combine` takes. */
14
+ export const spec = { flags: ['json'], options: { out: 'o', op: '', scale: '', rotate: '', flip: '', x: '', y: '' } };
15
+
16
+ /** `logomarks combine --help`. */
17
+ /** @type {import('../help.js').CommandHelp} */
18
+ export const help = {
19
+ summary: 'Combine two marks into one.',
20
+ usage: ['<a> <b> [--op union] [-o out.svg] [options]'],
21
+ about: [
22
+ "b is placed over a on the 512 canvas (both fitted first), then combined. The result is",
23
+ 'fitted and checked; exit 1 when it fails a check.',
24
+ ],
25
+ options: [
26
+ ['--op <op>', `${COMBINE_OPS.join(', ')}. Default union.`],
27
+ ['--scale <factor>', 'Scale b about its own centre. Default 1.'],
28
+ ['--rotate <deg>', 'Turn b clockwise about its own centre. Default 0.'],
29
+ ['--flip <axis>', 'Flip b: horizontal or vertical.'],
30
+ ['--x <units>, --y <units>', 'Move b across and down. Default 0.'],
31
+ ['-o, --out <file>', 'Write .svg, or .png drawn at 512. Default: the SVG to stdout.'],
32
+ ],
33
+ examples: ['logomarks combine quad-cove shear-a --op subtract --scale 0.5 -o combined.svg', 'logomarks combine a.svg b.svg --op place --x 120 -o both.png'],
34
+ };
35
+
36
+ /**
37
+ * Runs `combine`.
38
+ *
39
+ * @param {import('../args.js').Parsed} args The parsed arguments.
40
+ * @param {import('../index.js').IO} io Where output goes.
41
+ * @returns {Promise<number>} The exit code: 0 when the result passes its checks, 1 otherwise.
42
+ */
43
+ export async function run(args, io) {
44
+ if (args.positionals.length !== 2) throw new UsageError('combine takes two marks: slugs or .svg files');
45
+ const [a, b] = args.positionals.map(markOf);
46
+ const op = /** @type {import('../../edit/combine.js').CombineOp} */ (args.options.op ?? 'union');
47
+ if (!COMBINE_OPS.includes(op)) throw new UsageError(`--op is one of ${COMBINE_OPS.join(', ')}`);
48
+ const o = args.options;
49
+ if (o.flip && o.flip !== 'horizontal' && o.flip !== 'vertical') throw new UsageError('--flip is horizontal or vertical');
50
+ const transform = { scale: numArg('scale', o.scale), rotate: numArg('rotate', o.rotate), flip: /** @type {'horizontal' | 'vertical' | undefined} */ (o.flip), x: numArg('x', o.x), y: numArg('y', o.y) };
51
+ const name = `${a.slug}-${op}-${b.slug}`;
52
+ const r = await asUsage(async () => combineMarks(a.svg, b.svg, { op, transform, name }));
53
+ return output(args, io, r, { name, summary: `${a.name} ${op} ${b.name}` });
54
+ }
@@ -0,0 +1,10 @@
1
+ import type { ArgSpec, Parsed } from '../args.js';
2
+ import type { IO } from '../index.js';
3
+ import type { CommandHelp } from '../help.js';
4
+
5
+ /** What `cover` takes. */
6
+ export const spec: ArgSpec;
7
+ /** What `logomarks cover --help` prints. */
8
+ export const help: CommandHelp;
9
+ /** Runs `cover`; resolves to its exit code (0). */
10
+ export function run(args: Parsed, io: IO): Promise<number>;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @module logomarks/cli/commands/cover
3
+ *
4
+ * `logomarks cover <slug|file> --recipe mark --title … -o out.png`: a
5
+ * cover with the mark on it. `-o` ending in `.svg` writes the SVG instead.
6
+ */
7
+
8
+ import { cover, RECIPES } from '../../cover/index.js';
9
+ import { UsageError } from '../args.js';
10
+ import { markArg, colorArg, intArg, writeFile } from '../options.js';
11
+ import { style } from '../style.js';
12
+
13
+ /** What `cover` takes. */
14
+ export const spec = { flags: ['json'], options: { out: 'o', recipe: '', title: '', label: '', color: '', aspect: '', width: '', seed: '' } };
15
+
16
+ /** `logomarks cover --help`. */
17
+ /** @type {import('../help.js').CommandHelp} */
18
+ export const help = {
19
+ summary: 'Draw a cover (an OG image, a header or a post) with the mark on it.',
20
+ usage: ['<mark> [--recipe mark] [--title "…"] [-o cover.png] [options]'],
21
+ about: [`Recipes: ${Object.keys(RECIPES).join(', ')}.`],
22
+ options: [
23
+ ['--recipe <id>', 'The look. Default mark.'],
24
+ ['--title <text>', 'A headline; the mark moves out of its way.'],
25
+ ['--label <text>', 'A small line over the title.'],
26
+ ['--color <hex>', 'The brand colour, e.g. #ff5c3a. Default #d4ff3a.'],
27
+ ['--aspect <ratio>', 'og (1200×630), 1:1, 16:9, 4:5, 9:16, any w:h, or a number.\nDefault og.'],
28
+ ['--width <px>', 'The width, 200–6000. Default 1200.'],
29
+ ['--seed <text>', "Drives every random choice. Default: the mark's slug."],
30
+ ['-o, --out <file>', 'Write .png, or .svg (text as outlines). Default cover.png.'],
31
+ ],
32
+ examples: ['logomarks cover hex-gem --recipe aurora --title "Introducing Hexa" -o cover.png', 'logomarks cover mark.svg --recipe stripes --aspect 1:1 -o post.png'],
33
+ };
34
+
35
+ /**
36
+ * Runs `cover`.
37
+ *
38
+ * @param {import('../args.js').Parsed} args The parsed arguments.
39
+ * @param {import('../index.js').IO} io Where output goes.
40
+ * @returns {Promise<number>} The exit code: 0.
41
+ */
42
+ export async function run(args, io) {
43
+ const mark = markArg(args.positionals);
44
+ const { recipe = 'mark', title, label, aspect, seed } = args.options;
45
+ if (!(recipe in RECIPES)) throw new UsageError(`--recipe is one of ${Object.keys(RECIPES).join(', ')}`);
46
+ if (aspect && !/^(og|\d+(\.\d+)?(:\d+(\.\d+)?)?)$/.test(aspect)) throw new UsageError('--aspect is og, w:h (16:9) or a number');
47
+ const out = args.options.out ?? 'cover.png';
48
+ const c = await cover({ mark, recipe, title, label, seed, color: colorArg(args.options.color), aspect: aspect && /^[\d.]+$/.test(aspect) ? Number(aspect) : aspect, width: intArg('width', args.options.width, 200, 6000) });
49
+ writeFile(out, out.toLowerCase().endsWith('.svg') ? c.svg : c.png);
50
+ if (args.flags.has('json')) io.out(JSON.stringify({ ok: true, file: out, recipe, width: c.width, height: c.height }, null, 2));
51
+ else io.out(`${style.green('✓')} wrote ${style.bold(out)}: ${recipe}, ${c.width} × ${c.height}.`);
52
+ return 0;
53
+ }
@@ -0,0 +1,10 @@
1
+ import type { ArgSpec, Parsed } from '../args.js';
2
+ import type { IO } from '../index.js';
3
+ import type { CommandHelp } from '../help.js';
4
+
5
+ /** What `edit` takes. */
6
+ export const spec: ArgSpec;
7
+ /** What `logomarks edit --help` prints. */
8
+ export const help: CommandHelp;
9
+ /** Runs `edit`; resolves to its exit code. */
10
+ export function run(args: Parsed, io: IO): Promise<number>;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @module logomarks/cli/commands/edit
3
+ *
4
+ * `logomarks edit <slug|file.svg> --ops '[…]' -o out.svg`: edits by part,
5
+ * as JSON (`--ops @ops.json` reads a file), fitted and checked.
6
+ */
7
+
8
+ import fs from 'node:fs';
9
+ import { editMark } from '../../edit/ops.js';
10
+ import { UsageError } from '../args.js';
11
+ import { markArg, asUsage } from '../options.js';
12
+ import { output } from '../edited.js';
13
+ import { OPS } from '../../edit/ops.js';
14
+ import { wrapList } from '../help.js';
15
+
16
+ /** What `edit` takes. */
17
+ export const spec = { flags: ['json'], options: { out: 'o', ops: '' } };
18
+
19
+ /** `logomarks edit --help`. */
20
+ /** @type {import('../help.js').CommandHelp} */
21
+ export const help = {
22
+ summary: 'Edit a mark by part, with a list of ops.',
23
+ usage: ["<mark> --ops '<json>' [-o out.svg]", '<mark> --ops @ops.json [-o out.svg]'],
24
+ about: [
25
+ 'Ops run in order; "parts" are indices from logomarks parts. The result is fitted and',
26
+ 'checked, and exit 1 means it fails a check. Each op\'s settings: docs/editing.md.',
27
+ `Ops: ${wrapList(Object.keys(OPS), 66)}.`,
28
+ ],
29
+ options: [
30
+ ['--ops <json | @file>', 'The ops as a JSON array, or @ and a file holding one.'],
31
+ ['-o, --out <file>', 'Write .svg, or .png drawn at 512. Default: the SVG to stdout.'],
32
+ ],
33
+ examples: [
34
+ `logomarks edit quad-cove --ops '[{"op":"delete","parts":[3]},{"op":"round","radius":18}]' -o mine.svg`,
35
+ 'logomarks edit mark.svg --ops @ops.json -o mine.png',
36
+ ],
37
+ };
38
+
39
+ /**
40
+ * The ops option: JSON, or `@file` holding it.
41
+ *
42
+ * @param {string | undefined} value The option's value.
43
+ * @returns {import('../../edit/ops.js').EditOp[]} The ops.
44
+ * @throws {UsageError} When it's missing or isn't a JSON list.
45
+ */
46
+ function opsArg(value) {
47
+ if (!value) throw new UsageError('--ops takes a JSON list of edits, e.g. \'[{"op":"delete","parts":[0]}]\', or @ops.json');
48
+ let ops;
49
+ try {
50
+ ops = JSON.parse(value.startsWith('@') ? fs.readFileSync(value.slice(1), 'utf8') : value);
51
+ } catch (e) {
52
+ throw new UsageError(`--ops: ${/** @type {Error} */ (e).message}`);
53
+ }
54
+ if (!Array.isArray(ops) || !ops.every((o) => o && typeof o.op === 'string')) throw new UsageError('--ops is a JSON list of objects, each with an "op"');
55
+ return ops;
56
+ }
57
+
58
+ /**
59
+ * Runs `edit`.
60
+ *
61
+ * @param {import('../args.js').Parsed} args The parsed arguments.
62
+ * @param {import('../index.js').IO} io Where output goes.
63
+ * @returns {Promise<number>} The exit code: 0 when the result passes its checks, 1 otherwise.
64
+ */
65
+ export async function run(args, io) {
66
+ const mark = markArg(args.positionals);
67
+ const ops = opsArg(args.options.ops);
68
+ const r = await asUsage(async () => editMark(mark.svg, ops, { name: mark.slug }));
69
+ return output(args, io, r, { name: mark.slug, summary: `${mark.name}, ${ops.map((o) => o.op).join(', ')} (${r.parts.length} parts)` });
70
+ }
@@ -0,0 +1,10 @@
1
+ import type { ArgSpec, Parsed } from '../args.js';
2
+ import type { IO } from '../index.js';
3
+ import type { CommandHelp } from '../help.js';
4
+
5
+ /** What `find` takes. */
6
+ export const spec: ArgSpec;
7
+ /** What `logomarks find --help` prints. */
8
+ export const help: CommandHelp;
9
+ /** Runs `find`; resolves to its exit code (0 found, 1 nothing matched). */
10
+ export function run(args: Parsed, io: IO): Promise<number>;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @module logomarks/cli/commands/find
3
+ *
4
+ * `logomarks find <words…>`: search the bundled live set by words, style,
5
+ * tags, industry or letter. With nothing to search by, the values the
6
+ * filters take.
7
+ */
8
+
9
+ import { findMarks, facets } from '../../find/index.js';
10
+ import { intArg } from '../options.js';
11
+ import { style, pad } from '../style.js';
12
+
13
+ /** What `find` takes. */
14
+ export const spec = { flags: ['json'], options: { style: '', tag: '', industry: '', letter: '', top: '' } };
15
+
16
+ /** `logomarks find --help`. */
17
+ /** @type {import('../help.js').CommandHelp} */
18
+ export const help = {
19
+ summary: 'Search the live set by words, style, tag, industry or letter.',
20
+ usage: ['<words…> [options]', '[options]'],
21
+ about: [
22
+ 'Searches as logomarks.ai does: every word must match (synonyms, plurals and trade names',
23
+ 'included) and the best fits come first. With no words or filters it lists every style,',
24
+ 'tag, mood and industry. Exit 1 when nothing matches.',
25
+ ],
26
+ options: [
27
+ ['--style <style>', 'One style, e.g. letter, badge, iso, line, grid, glitch, animal.'],
28
+ ['--tag <tags>', 'Tags a mark must all have, e.g. round,symmetric.'],
29
+ ['--industry <id>', 'An industry it suits, e.g. coffee, fintech, law-firm.'],
30
+ ['--letter <char>', 'The letter or number a lettermark shows.'],
31
+ ['--top <n>', 'How many marks to list (1–500). Default 20.'],
32
+ ],
33
+ examples: ['logomarks find coffee', 'logomarks find calm hexagon --top 5', 'logomarks find --style letter --letter K', 'logomarks find'],
34
+ };
35
+
36
+ /**
37
+ * Runs `find`.
38
+ *
39
+ * @param {import('../args.js').Parsed} args The parsed arguments.
40
+ * @param {import('../index.js').IO} io Where output goes.
41
+ * @returns {Promise<number>} The exit code: 0 when anything matched, 1 otherwise.
42
+ */
43
+ export async function run(args, io) {
44
+ const query = args.positionals.join(' ');
45
+ const { style: kind, industry, letter } = args.options;
46
+ const tags = (args.options.tag ?? '').split(',').map((t) => t.trim()).filter(Boolean);
47
+ const json = args.flags.has('json');
48
+ if (!query && !kind && !tags.length && !industry && !letter) {
49
+ const f = facets();
50
+ if (json) io.out(JSON.stringify(f, null, 2));
51
+ else for (const [k, v] of Object.entries(f)) io.out(`${style.bold(k)} ${Object.entries(v).map(([n, c]) => `${n} ${style.dim(String(c))}`).join(', ')}`);
52
+ return 0;
53
+ }
54
+ const r = findMarks(query, { style: kind, tags, industry, letter, limit: intArg('top', args.options.top, 1, 500) });
55
+ if (json) io.out(JSON.stringify(r, null, 2));
56
+ else {
57
+ for (const m of r.marks) io.out(`${pad(m.slug, 28)} ${pad(m.style, 10)} ${style.dim(m.description)}`);
58
+ io.out(`\n${r.marks.length} of ${r.total} matching`);
59
+ }
60
+ return r.total ? 0 : 1;
61
+ }
@@ -0,0 +1,10 @@
1
+ import type { ArgSpec, Parsed } from '../args.js';
2
+ import type { IO } from '../index.js';
3
+ import type { CommandHelp } from '../help.js';
4
+
5
+ /** What `get` takes. */
6
+ export const spec: ArgSpec;
7
+ /** What `logomarks get --help` prints. */
8
+ export const help: CommandHelp;
9
+ /** Runs `get`; resolves to its exit code (0). */
10
+ export function run(args: Parsed, io: IO): Promise<number>;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @module logomarks/cli/commands/get
3
+ *
4
+ * `logomarks get <slug|name>`: a live mark's SVG (from the bundled index) to
5
+ * stdout or a file, in a colour, or a PNG at a size.
6
+ */
7
+
8
+ import { paint, markPng, MARK_URL } from '../../brand/mark.js';
9
+ import { UsageError } from '../args.js';
10
+ import { markArg, colorArg, intArg, writeFile } from '../options.js';
11
+ import { style } from '../style.js';
12
+
13
+ /** What `get` takes. */
14
+ export const spec = { flags: ['json'], options: { out: 'o', color: '', size: '', format: '' } };
15
+
16
+ /** `logomarks get --help`. */
17
+ /** @type {import('../help.js').CommandHelp} */
18
+ export const help = {
19
+ summary: "Get a live mark's SVG, or a PNG of any mark.",
20
+ usage: ['<mark> [-o file] [options]'],
21
+ about: ['Live marks are CC0. Without -o the SVG goes to stdout.'],
22
+ options: [
23
+ ['--color <hex>', 'Paint it, e.g. #ff5c3a.\nDefault: currentColor (SVG) or black (PNG).'],
24
+ ['--size <px>', 'A PNG this many pixels square (16–4096). Default 512.'],
25
+ ['--format <svg | png>', 'Default: from -o, or png when --size is given.'],
26
+ ['-o, --out <file>', 'Write .svg or .png. A PNG needs -o.'],
27
+ ],
28
+ examples: ['logomarks get hex-gem -o logo.svg', "logomarks get \"Hex Gem\" --color '#ff5c3a' --size 512 -o logo.png"],
29
+ };
30
+
31
+ /** A PNG's side when none is given. */
32
+ const PNG_SIZE = 512;
33
+
34
+ /**
35
+ * Runs `get`.
36
+ *
37
+ * @param {import('../args.js').Parsed} args The parsed arguments.
38
+ * @param {import('../index.js').IO} io Where output goes.
39
+ * @returns {Promise<number>} The exit code: 0.
40
+ */
41
+ export async function run(args, io) {
42
+ const mark = markArg(args.positionals);
43
+ const { out, format: asked } = args.options;
44
+ const color = colorArg(args.options.color);
45
+ const size = intArg('size', args.options.size, 16, 4096);
46
+ const format = asked ?? (out?.toLowerCase().endsWith('.png') || size ? 'png' : 'svg');
47
+ if (format !== 'svg' && format !== 'png') throw new UsageError('--format is svg or png');
48
+ if (format === 'png' && !out) throw new UsageError('a PNG needs -o file.png');
49
+ const svg = paint(mark.svg, color);
50
+ const data = format === 'png' ? await markPng(mark.svg, { size: size ?? PNG_SIZE, color: color ?? '#000000' }) : svg;
51
+ if (out) writeFile(out, data);
52
+ const about = { slug: mark.slug, name: mark.name, ...(mark.live && { url: MARK_URL + mark.slug, licence: 'CC0-1.0' }), format, color: color ?? (format === 'png' ? '#000000' : 'currentColor'), ...(format === 'png' && { size: size ?? PNG_SIZE }) };
53
+ if (args.flags.has('json')) io.out(JSON.stringify({ ...about, ...(out ? { file: out } : { svg }) }, null, 2));
54
+ else if (out) io.out(`${style.green('✓')} wrote ${style.bold(out)}: ${mark.name}${mark.live ? style.dim(` (CC0, ${about.url})`) : ''}`);
55
+ else io.out(svg.trimEnd());
56
+ return 0;
57
+ }
@@ -0,0 +1,10 @@
1
+ import type { ArgSpec, Parsed } from '../args.js';
2
+ import type { IO } from '../index.js';
3
+ import type { CommandHelp } from '../help.js';
4
+
5
+ /** What `glitch` takes. */
6
+ export const spec: ArgSpec;
7
+ /** What `logomarks glitch --help` prints. */
8
+ export const help: CommandHelp;
9
+ /** Runs `glitch`; resolves to its exit code. */
10
+ export function run(args: Parsed, io: IO): Promise<number>;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @module logomarks/cli/commands/glitch
3
+ *
4
+ * `logomarks glitch <slug|file.svg> --preset shear -o out.svg`: a glitch
5
+ * edition, fitted and checked. The same seed gives the same bytes.
6
+ */
7
+
8
+ import { glitchMark, PRESETS } from '../../edit/glitch.js';
9
+ import { UsageError } from '../args.js';
10
+ import { markArg, numArg, intArg, asUsage } from '../options.js';
11
+ import { output } from '../edited.js';
12
+
13
+ /** What `glitch` takes. */
14
+ export const spec = { flags: ['json'], options: { out: 'o', preset: '', angle: '', place: '', slide: '', gap: '', cuts: '', seed: '' } };
15
+
16
+ /** `logomarks glitch --help`. */
17
+ /** @type {import('../help.js').CommandHelp} */
18
+ export const help = {
19
+ summary: 'Make a glitch edition of a mark: cut with the house gap and slid.',
20
+ usage: ['<mark> [--preset shear] [-o out.svg] [options]'],
21
+ about: [
22
+ 'The result is fitted and checked, and the same seed gives the same bytes. Without -o the',
23
+ 'SVG goes to stdout and the checks to stderr. Exit 1 when the result fails a check: try',
24
+ 'another seed.',
25
+ ],
26
+ options: [
27
+ ['--preset <name>', `${Object.keys(PRESETS).join(', ')}. Default shear.`],
28
+ ['--seed <n>', 'Places the cuts; another seed is a shuffle. Default 1.'],
29
+ ['--angle <deg>', "Cut direction, clockwise from across. Default: the mark's slope\n(shear), across the lanes (lane), else 0."],
30
+ ['--place <0–1>', 'Where the first cut crosses the mark. Default: seeded.'],
31
+ ['--slide <units>', 'How far the sides slide apart along the cut. Default 44.'],
32
+ ['--gap <units>', 'The gap at each cut. Default 22.'],
33
+ ['--cuts <n>', 'Cuts for slice (1–3), bands for bands (3–5). Default: seeded.'],
34
+ ['-o, --out <file>', 'Write .svg, or .png drawn at 512.'],
35
+ ],
36
+ examples: ['logomarks glitch quad-cove --preset shear --seed 3 -o glitched.svg', 'logomarks glitch mark.svg --preset bands --cuts 4 -o preview.png'],
37
+ };
38
+
39
+ /**
40
+ * Runs `glitch`.
41
+ *
42
+ * @param {import('../args.js').Parsed} args The parsed arguments.
43
+ * @param {import('../index.js').IO} io Where output goes.
44
+ * @returns {Promise<number>} The exit code: 0 when the result passes its checks, 1 otherwise.
45
+ */
46
+ export async function run(args, io) {
47
+ const mark = markArg(args.positionals);
48
+ const preset = /** @type {import('../../edit/glitch.js').GlitchPreset} */ (args.options.preset ?? 'shear');
49
+ if (!(preset in PRESETS)) throw new UsageError(`--preset is one of ${Object.keys(PRESETS).join(', ')}`);
50
+ const o = args.options;
51
+ const r = await asUsage(async () => glitchMark(mark.svg, {
52
+ preset, name: mark.slug, angle: numArg('angle', o.angle), place: numArg('place', o.place), slide: numArg('slide', o.slide),
53
+ gap: numArg('gap', o.gap), cuts: intArg('cuts', o.cuts, 1, 5), seed: intArg('seed', o.seed, 0, 2 ** 31),
54
+ }));
55
+ const g = r.glitch;
56
+ return output(args, io, r, { name: mark.slug, summary: `${mark.name}, ${preset} at seed ${g.seed} (${g.cuts} cut${g.cuts > 1 ? 's' : ''}, ${g.angle}°, ${g.pieces} pieces)` });
57
+ }
@@ -0,0 +1,15 @@
1
+ import type { ArgSpec, Parsed } from '../args.js';
2
+ import type { IO } from '../index.js';
3
+ import type { CommandHelp } from '../help.js';
4
+
5
+ /** A verb: the arguments it takes and how it runs. */
6
+ export interface Command {
7
+ spec: ArgSpec;
8
+ /** What `logomarks <verb> --help` prints. */
9
+ help: CommandHelp;
10
+ /** Runs the verb and resolves to its exit code (0 pass, 1 fail). */
11
+ run(args: Parsed, io: IO): Promise<number>;
12
+ }
13
+
14
+ /** Every verb the CLI runs. */
15
+ export const COMMANDS: Readonly<Record<'check' | 'similar' | 'specks' | 'sheet' | 'find' | 'parts' | 'glitch' | 'edit' | 'combine' | 'get' | 'kit' | 'social' | 'mockup' | 'cover' | 'mcp', Command>>;