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.
- package/CHANGELOG.md +42 -0
- package/LICENSE +21 -0
- package/README.md +56 -2
- package/bin/logomarks.js +4 -0
- package/data/fonts/BricolageGrotesque-SemiBold.ttf +0 -0
- package/data/fonts/Cagliostro-Regular.ttf +0 -0
- package/data/fonts/Fraunces-SemiBold.ttf +0 -0
- package/data/fonts/IBMPlexMono-Regular.ttf +0 -0
- package/data/fonts/IBMPlexMono-SemiBold.ttf +0 -0
- package/data/fonts/InstrumentSans-SemiBold.ttf +0 -0
- package/data/fonts/InstrumentSerif-Regular.ttf +0 -0
- package/data/fonts/Inter-SemiBold.ttf +0 -0
- package/data/fonts/Limelight-Regular.ttf +0 -0
- package/data/fonts/OFL.txt +103 -0
- package/data/fonts/Shrikhand-Regular.ttf +0 -0
- package/data/fonts/SpaceGrotesk-SemiBold.ttf +0 -0
- package/data/index.bin +0 -0
- package/docs/api.md +31 -0
- package/docs/commands.md +412 -0
- package/docs/editing.md +61 -0
- package/docs/mcp.md +45 -0
- package/package.json +94 -4
- package/src/brand/brand.d.ts +23 -0
- package/src/brand/brand.js +29 -0
- package/src/brand/fonts.d.ts +61 -0
- package/src/brand/fonts.js +93 -0
- package/src/brand/mark.d.ts +53 -0
- package/src/brand/mark.js +151 -0
- package/src/checks/finding.d.ts +56 -0
- package/src/checks/finding.js +57 -0
- package/src/checks/fit.d.ts +5 -0
- package/src/checks/fit.js +46 -0
- package/src/checks/index.d.ts +33 -0
- package/src/checks/index.js +57 -0
- package/src/checks/specks.d.ts +5 -0
- package/src/checks/specks.js +38 -0
- package/src/checks/widths.d.ts +7 -0
- package/src/checks/widths.js +48 -0
- package/src/cli/args.d.ts +20 -0
- package/src/cli/args.js +35 -0
- package/src/cli/commands/check.d.ts +10 -0
- package/src/cli/commands/check.js +66 -0
- package/src/cli/commands/combine.d.ts +10 -0
- package/src/cli/commands/combine.js +54 -0
- package/src/cli/commands/cover.d.ts +10 -0
- package/src/cli/commands/cover.js +53 -0
- package/src/cli/commands/edit.d.ts +10 -0
- package/src/cli/commands/edit.js +70 -0
- package/src/cli/commands/find.d.ts +10 -0
- package/src/cli/commands/find.js +61 -0
- package/src/cli/commands/get.d.ts +10 -0
- package/src/cli/commands/get.js +57 -0
- package/src/cli/commands/glitch.d.ts +10 -0
- package/src/cli/commands/glitch.js +57 -0
- package/src/cli/commands/index.d.ts +15 -0
- package/src/cli/commands/index.js +24 -0
- package/src/cli/commands/kit.d.ts +10 -0
- package/src/cli/commands/kit.js +66 -0
- package/src/cli/commands/mcp.d.ts +9 -0
- package/src/cli/commands/mcp.js +36 -0
- package/src/cli/commands/mockup.d.ts +10 -0
- package/src/cli/commands/mockup.js +61 -0
- package/src/cli/commands/parts.d.ts +10 -0
- package/src/cli/commands/parts.js +44 -0
- package/src/cli/commands/sheet.d.ts +10 -0
- package/src/cli/commands/sheet.js +66 -0
- package/src/cli/commands/similar.d.ts +10 -0
- package/src/cli/commands/similar.js +111 -0
- package/src/cli/commands/social.d.ts +10 -0
- package/src/cli/commands/social.js +65 -0
- package/src/cli/commands/specks.d.ts +20 -0
- package/src/cli/commands/specks.js +103 -0
- package/src/cli/edited.d.ts +6 -0
- package/src/cli/edited.js +32 -0
- package/src/cli/files.d.ts +4 -0
- package/src/cli/files.js +27 -0
- package/src/cli/help.d.ts +20 -0
- package/src/cli/help.js +55 -0
- package/src/cli/index.d.ts +10 -0
- package/src/cli/index.js +104 -0
- package/src/cli/options.d.ts +30 -0
- package/src/cli/options.js +155 -0
- package/src/cli/report.d.ts +4 -0
- package/src/cli/report.js +28 -0
- package/src/cli/style.d.ts +4 -0
- package/src/cli/style.js +34 -0
- package/src/cover/index.d.ts +49 -0
- package/src/cover/index.js +63 -0
- package/src/edit/combine.d.ts +31 -0
- package/src/edit/combine.js +103 -0
- package/src/edit/doc.d.ts +34 -0
- package/src/edit/doc.js +96 -0
- package/src/edit/glitch.d.ts +48 -0
- package/src/edit/glitch.js +43 -0
- package/src/edit/index.d.ts +8 -0
- package/src/edit/index.js +11 -0
- package/src/edit/ops.d.ts +51 -0
- package/src/edit/ops.js +90 -0
- package/src/edit/preview.d.ts +8 -0
- package/src/edit/preview.js +51 -0
- package/src/find/index.d.ts +54 -0
- package/src/find/index.js +88 -0
- package/src/index.d.ts +30 -0
- package/src/index.js +28 -0
- package/src/kit/index.d.ts +26 -0
- package/src/kit/index.js +31 -0
- package/src/mcp/content.d.ts +30 -0
- package/src/mcp/content.js +56 -0
- package/src/mcp/edited.d.ts +5 -0
- package/src/mcp/edited.js +25 -0
- package/src/mcp/index.d.ts +41 -0
- package/src/mcp/index.js +72 -0
- package/src/mcp/inputs.d.ts +46 -0
- package/src/mcp/inputs.js +122 -0
- package/src/mcp/preview.d.ts +4 -0
- package/src/mcp/preview.js +24 -0
- package/src/mcp/tools/check.d.ts +15 -0
- package/src/mcp/tools/check.js +47 -0
- package/src/mcp/tools/combine.d.ts +15 -0
- package/src/mcp/tools/combine.js +42 -0
- package/src/mcp/tools/cover.d.ts +20 -0
- package/src/mcp/tools/cover.js +46 -0
- package/src/mcp/tools/edit.d.ts +21 -0
- package/src/mcp/tools/edit.js +73 -0
- package/src/mcp/tools/find.d.ts +14 -0
- package/src/mcp/tools/find.js +36 -0
- package/src/mcp/tools/get.d.ts +15 -0
- package/src/mcp/tools/get.js +44 -0
- package/src/mcp/tools/glitch.d.ts +12 -0
- package/src/mcp/tools/glitch.js +39 -0
- package/src/mcp/tools/index.d.ts +4 -0
- package/src/mcp/tools/index.js +23 -0
- package/src/mcp/tools/kit.d.ts +28 -0
- package/src/mcp/tools/kit.js +51 -0
- package/src/mcp/tools/mockup.d.ts +22 -0
- package/src/mcp/tools/mockup.js +51 -0
- package/src/mcp/tools/sheet.d.ts +15 -0
- package/src/mcp/tools/sheet.js +43 -0
- package/src/mcp/tools/similar.d.ts +13 -0
- package/src/mcp/tools/similar.js +42 -0
- package/src/mcp/tools/social.d.ts +5 -0
- package/src/mcp/tools/social.js +68 -0
- package/src/mockup/index.d.ts +52 -0
- package/src/mockup/index.js +152 -0
- package/src/raster/render.d.ts +30 -0
- package/src/raster/render.js +100 -0
- package/src/rules.d.ts +73 -0
- package/src/rules.js +139 -0
- package/src/sheet/card.d.ts +20 -0
- package/src/sheet/card.js +190 -0
- package/src/sheet/draw.d.ts +26 -0
- package/src/sheet/draw.js +94 -0
- package/src/sheet/grid.d.ts +2 -0
- package/src/sheet/grid.js +32 -0
- package/src/sheet/index.d.ts +31 -0
- package/src/sheet/index.js +47 -0
- package/src/sheet/theme.d.ts +8 -0
- package/src/sheet/theme.js +45 -0
- package/src/similar/folder.d.ts +20 -0
- package/src/similar/folder.js +66 -0
- package/src/similar/nearest.d.ts +32 -0
- package/src/similar/nearest.js +60 -0
- package/src/similar/signature.d.ts +12 -0
- package/src/similar/signature.js +42 -0
- package/src/similar/store.d.ts +67 -0
- package/src/similar/store.js +88 -0
- package/src/util/color.d.ts +2 -0
- package/src/util/color.js +23 -0
- package/src/util/files.d.ts +8 -0
- package/src/util/files.js +60 -0
- package/src/util/round.d.ts +2 -0
- package/src/util/round.js +14 -0
- package/src/util/version.d.ts +2 -0
- package/src/util/version.js +10 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module logomarks/cli/commands/specks
|
|
3
|
+
*
|
|
4
|
+
* `logomarks specks <folder | file.svg…>`: the set's speck review (what
|
|
5
|
+
* speckcheck.mjs printed). Every mark's pieces under `--piece` (60) and holes
|
|
6
|
+
* under `--hole` (30), smallest first, with their boxes, so you know where to
|
|
7
|
+
* zoom; tiny, hairline, repeated and reviewed parts are left out unless
|
|
8
|
+
* `--all`. `--raster` reads every mark from the scanline raster. Run it on
|
|
9
|
+
* fitted marks, then render each hit large before changing anything: many
|
|
10
|
+
* small parts are meant.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import { shapes } from '@logomarks/core/svg';
|
|
15
|
+
import { markParts, smallParts } from '@logomarks/core/analyse';
|
|
16
|
+
import { SPECKS, REVIEWED } from '../../rules.js';
|
|
17
|
+
import { UsageError } from '../args.js';
|
|
18
|
+
import { svgFiles, slugOf } from '../files.js';
|
|
19
|
+
|
|
20
|
+
/** What `specks` takes. */
|
|
21
|
+
export const spec = { flags: ['json', 'all', 'raster'], options: { piece: '', hole: '', allow: '' } };
|
|
22
|
+
|
|
23
|
+
/** `logomarks specks --help`. */
|
|
24
|
+
/** @type {import('../help.js').CommandHelp} */
|
|
25
|
+
export const help = {
|
|
26
|
+
summary: 'List every speck, sliver and pin-hole, with its box.',
|
|
27
|
+
usage: ['<file.svg | folder>… [options]'],
|
|
28
|
+
about: [
|
|
29
|
+
'The speck review: pieces under --piece and holes under --hole, smallest first. Tiny,',
|
|
30
|
+
'hairline, repeated and reviewed parts are left out unless --all. Render each hit large',
|
|
31
|
+
'before changing anything: many small parts are meant. Exit 1 when anything is listed.',
|
|
32
|
+
],
|
|
33
|
+
options: [
|
|
34
|
+
['--piece <area>', 'Report pieces smaller than this, in square units. Default 60.'],
|
|
35
|
+
['--hole <area>', 'Report holes smaller than this. Default 30.'],
|
|
36
|
+
['--allow <slugs>', 'Marks whose small parts are meant, comma-separated.'],
|
|
37
|
+
['--all', 'Report every small part, reviewed and repeated ones included.'],
|
|
38
|
+
['--raster', 'Read each mark from the scanline raster instead of its outline.'],
|
|
39
|
+
],
|
|
40
|
+
examples: ['logomarks specks out/', 'logomarks specks mark.svg --piece 80 --json'],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A number option.
|
|
45
|
+
*
|
|
46
|
+
* @param {string | undefined} v The option's value.
|
|
47
|
+
* @param {number} d Its default.
|
|
48
|
+
* @param {string} name Its name, for the error.
|
|
49
|
+
* @returns {number} The number.
|
|
50
|
+
*/
|
|
51
|
+
const num = (v, d, name) => {
|
|
52
|
+
if (v === undefined) return d;
|
|
53
|
+
const n = Number(v);
|
|
54
|
+
if (!Number.isFinite(n) || n < 0) throw new UsageError(`--${name} takes a number`);
|
|
55
|
+
return n;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* One mark's small parts, each tagged with why it is left out ('' = reported).
|
|
60
|
+
*
|
|
61
|
+
* @param {string} file The mark.
|
|
62
|
+
* @param {{ piece: number, hole: number, raster: boolean, allow: Set<string> }} o The limits.
|
|
63
|
+
* @returns {import('./specks.js').MarkSpecks} Its small parts.
|
|
64
|
+
*/
|
|
65
|
+
function review(file, { piece, hole, raster, allow }) {
|
|
66
|
+
const slug = slugOf(file);
|
|
67
|
+
let read;
|
|
68
|
+
try {
|
|
69
|
+
read = markParts(shapes(fs.readFileSync(file, 'utf8')), { seed: slug, raster });
|
|
70
|
+
} catch (e) {
|
|
71
|
+
return { slug, note: `unreadable: ${/** @type {Error} */ (e).message.slice(0, 40)}`, raster: false, parts: [{ kind: 'mark', area: 0, width: 0, box: [0, 0, 0, 0], skip: '' }] };
|
|
72
|
+
}
|
|
73
|
+
const note = read.error ? `svgShape failed: ${read.error.slice(0, 40)}` : '';
|
|
74
|
+
const parts = smallParts(read.parts, { piece, hole, raster: read.raster }).map((p) => (allow.has(slug) ? { ...p, skip: 'allowed' } : p));
|
|
75
|
+
return { slug, note, raster: read.raster, parts };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Runs `specks`.
|
|
80
|
+
*
|
|
81
|
+
* @param {import('../args.js').Parsed} args The parsed arguments.
|
|
82
|
+
* @param {import('../index.js').IO} io Where output goes.
|
|
83
|
+
* @returns {Promise<number>} The exit code: 1 when anything is reported.
|
|
84
|
+
*/
|
|
85
|
+
export async function run(args, io) {
|
|
86
|
+
const files = svgFiles(args.positionals).slice().sort((a, b) => (slugOf(a) < slugOf(b) ? -1 : slugOf(a) > slugOf(b) ? 1 : 0));
|
|
87
|
+
const piece = num(args.options.piece, SPECKS.piece, 'piece'), hole = num(args.options.hole, SPECKS.hole, 'hole');
|
|
88
|
+
const all = args.flags.has('all');
|
|
89
|
+
const allow = new Set([...Object.keys(REVIEWED), ...(args.options.allow ?? '').split(',').filter(Boolean)]);
|
|
90
|
+
const results = files.map((f) => review(f, { piece, hole, raster: args.flags.has('raster'), allow }));
|
|
91
|
+
const f1 = (/** @type {number} */ v) => v.toFixed(1);
|
|
92
|
+
const shown = results.map((r) => ({ ...r, parts: r.parts.filter((p) => all || !p.skip) })).filter((r) => r.parts.length);
|
|
93
|
+
if (args.flags.has('json')) {
|
|
94
|
+
io.out(JSON.stringify(shown.map(({ slug, note, parts }) => ({ slug, note, parts: parts.map((p) => ({ ...p, area: +p.area.toFixed(2), width: +p.width.toFixed(3), box: p.box.map((v) => +v.toFixed(1)) })) })), null, 1));
|
|
95
|
+
} else {
|
|
96
|
+
for (const { slug, note, parts } of shown) io.out(slug.padEnd(28) + ' ' + parts.map((p) => (p.kind === 'mark' ? 'could not be read' : `${p.kind} ${f1(p.area)} at ${f1(p.box[0])},${f1(p.box[1])}–${f1(p.box[2])},${f1(p.box[3])}${'raster' in p && p.raster ? ' (raster)' : ''}${p.skip ? ` [${p.skip}]` : ''}`)).join(' · ') + (note ? ` (${note})` : ''));
|
|
97
|
+
const count = (/** @type {string} */ k) => results.reduce((s, r) => s + r.parts.filter((p) => p.skip === k).length, 0);
|
|
98
|
+
io.out(`${results.filter((r) => r.parts.some((p) => !p.skip)).length} of ${files.length} marks with specks (pieces < ${piece}, holes < ${hole}); `
|
|
99
|
+
+ `left out ${count('tiny')} tiny, ${count('hairline')} hairline, ${count('repeated')} repeated, ${count('allowed')} allowed${all ? '' : ' (--all lists them)'}; `
|
|
100
|
+
+ `${results.filter((r) => r.raster).length} read by raster${results.some((r) => r.note) ? ': ' + results.filter((r) => r.note).map((r) => r.slug).join(', ') : ''}`);
|
|
101
|
+
}
|
|
102
|
+
return results.some((r) => r.parts.some((p) => !p.skip)) ? 1 : 0;
|
|
103
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Parsed } from './args.js';
|
|
2
|
+
import type { IO } from './index.js';
|
|
3
|
+
import type { Edited } from '../edit/doc.js';
|
|
4
|
+
|
|
5
|
+
/** Writes (`-o` .svg or .png) or prints an edited mark and its checks; resolves to 0 when it passes, 1 otherwise. */
|
|
6
|
+
export function output(args: Parsed, io: IO, r: Edited & Record<string, unknown>, o: { name: string; summary: string }): Promise<number>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module logomarks/cli/edited
|
|
3
|
+
*
|
|
4
|
+
* How the editing verbs hand back a result: the SVG to `-o` (or a PNG to
|
|
5
|
+
* look at, for `.png`) or to stdout, and its checks; `--json` for both as
|
|
6
|
+
* one object. Exit 1 when the result fails a check.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { markPreview } from '../edit/preview.js';
|
|
10
|
+
import { writeFile } from '../util/files.js';
|
|
11
|
+
import { reportLines } from './report.js';
|
|
12
|
+
import { style } from './style.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Writes or prints an edited mark and its checks.
|
|
16
|
+
*
|
|
17
|
+
* @param {import('./args.js').Parsed} args The parsed arguments (`-o`, `--json`).
|
|
18
|
+
* @param {import('./index.js').IO} io Where output goes.
|
|
19
|
+
* @param {import('../edit/doc.js').Edited & Record<string, unknown>} r The result, and anything more to report.
|
|
20
|
+
* @param {{ name: string, summary: string }} o The mark's name, and a line on what was done.
|
|
21
|
+
* @returns {Promise<number>} The exit code: 0 when the result passes its checks, 1 otherwise.
|
|
22
|
+
*/
|
|
23
|
+
export async function output(args, io, { svg, checks, ...more }, { name, summary }) {
|
|
24
|
+
const file = args.options.out;
|
|
25
|
+
if (file) writeFile(file, file.toLowerCase().endsWith('.png') ? await markPreview(svg) : svg);
|
|
26
|
+
if (args.flags.has('json')) io.out(JSON.stringify({ ok: checks.ok, ...more, ...(file && { file }), svg, checks }, null, 2));
|
|
27
|
+
else if (!file) {
|
|
28
|
+
io.out(svg.trimEnd());
|
|
29
|
+
io.err(`${summary}\n${reportLines(name, checks)}`);
|
|
30
|
+
} else io.out(`${style.green('✓')} wrote ${style.bold(file)}: ${summary}\n${reportLines(file, checks)}`);
|
|
31
|
+
return checks.ok ? 0 : 1;
|
|
32
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Every SVG named, folders expanded (sorted, not recursive); throws a `UsageError` when nothing is named or a path is missing. */
|
|
2
|
+
export function svgFiles(paths: readonly string[]): string[];
|
|
3
|
+
/** A mark's name from its file: the slug. */
|
|
4
|
+
export function slugOf(file: string): string;
|
package/src/cli/files.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module logomarks/cli/files
|
|
3
|
+
*
|
|
4
|
+
* The SVG files a command was given: files as they are, folders expanded to
|
|
5
|
+
* the `.svg` files in them.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { UsageError } from './args.js';
|
|
9
|
+
import { expandSvgs } from '../util/files.js';
|
|
10
|
+
|
|
11
|
+
export { slugOf } from '../util/files.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Every SVG named, folders expanded (sorted, not recursive).
|
|
15
|
+
*
|
|
16
|
+
* @param {readonly string[]} paths Files and folders.
|
|
17
|
+
* @returns {string[]} The files.
|
|
18
|
+
* @throws {UsageError} When nothing is named or a path is missing.
|
|
19
|
+
*/
|
|
20
|
+
export function svgFiles(paths) {
|
|
21
|
+
if (!paths.length) throw new UsageError('name at least one .svg file or folder');
|
|
22
|
+
try {
|
|
23
|
+
return expandSvgs(paths);
|
|
24
|
+
} catch (e) {
|
|
25
|
+
throw new UsageError(/** @type {Error} */ (e).message);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** A verb's help. */
|
|
2
|
+
export interface CommandHelp {
|
|
3
|
+
/** What it does, in one line. */
|
|
4
|
+
summary: string;
|
|
5
|
+
/** How to call it, after `logomarks <verb>`; one line per form. */
|
|
6
|
+
usage: string[];
|
|
7
|
+
/** Lines of explanation between the usage and the options. */
|
|
8
|
+
about?: string[];
|
|
9
|
+
/** Each option and what it does (`\n` starts an indented line). */
|
|
10
|
+
options?: ReadonlyArray<readonly [string, string]>;
|
|
11
|
+
/** Whether it takes `--json` (default true). */
|
|
12
|
+
json?: boolean;
|
|
13
|
+
/** Example command lines. */
|
|
14
|
+
examples?: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Names joined with commas and wrapped to `width`, later lines starting with `indent`. */
|
|
18
|
+
export function wrapList(names: readonly string[], width?: number, indent?: string): string;
|
|
19
|
+
/** A verb's help as text, with the options every verb takes (`--json`, `--help`) listed last. */
|
|
20
|
+
export function formatHelp(verb: string, help: CommandHelp): string;
|
package/src/cli/help.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module logomarks/cli/help
|
|
3
|
+
*
|
|
4
|
+
* Each verb's `--help`: what it does, how to call it, its options and
|
|
5
|
+
* examples, laid out the same way for every verb.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { pad, style } from './style.js';
|
|
9
|
+
|
|
10
|
+
/** The options every verb takes, listed last. */
|
|
11
|
+
const COMMON = [
|
|
12
|
+
['--json', 'Machine-readable output on stdout.'],
|
|
13
|
+
['-h, --help', 'Show this help.'],
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
/** The width of the options' left-hand column. */
|
|
17
|
+
const COLUMN = 24;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Names as one line, joined with commas, wrapped to a width.
|
|
21
|
+
*
|
|
22
|
+
* @param {readonly string[]} names The names.
|
|
23
|
+
* @param {number} [width] The widest line.
|
|
24
|
+
* @param {string} [indent] What each line after the first starts with.
|
|
25
|
+
* @returns {string} The list.
|
|
26
|
+
*/
|
|
27
|
+
export function wrapList(names, width = 72, indent = '') {
|
|
28
|
+
const lines = [''];
|
|
29
|
+
for (const n of names) {
|
|
30
|
+
const last = lines.length - 1;
|
|
31
|
+
if (lines[last] && lines[last].length + n.length + 2 > width) lines.push('');
|
|
32
|
+
lines[lines.length - 1] += `${lines[lines.length - 1] ? ', ' : ''}${n}`;
|
|
33
|
+
}
|
|
34
|
+
return lines.join(`,\n${indent}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A verb's help as text.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} verb The verb.
|
|
41
|
+
* @param {import('./help.js').CommandHelp} help What to say.
|
|
42
|
+
* @returns {string} The help.
|
|
43
|
+
*/
|
|
44
|
+
export function formatHelp(verb, { summary, usage, about = [], options = [], examples = [], json = true }) {
|
|
45
|
+
const indent = ' '.repeat(COLUMN + 2);
|
|
46
|
+
const option = (/** @type {readonly [string, string]} */ [flag, text]) => {
|
|
47
|
+
const body = text.replaceAll('\n', `\n${indent}`);
|
|
48
|
+
return flag.length >= COLUMN ? ` ${flag}\n${indent}${body}` : ` ${pad(flag, COLUMN)}${body}`;
|
|
49
|
+
};
|
|
50
|
+
const parts = [`${style.bold(`logomarks ${verb}`)}: ${summary}`, `${style.bold('Usage')}\n${usage.map((u) => ` logomarks ${verb} ${u}`).join('\n')}`];
|
|
51
|
+
if (about.length) parts.push(about.join('\n'));
|
|
52
|
+
parts.push(`${style.bold('Options')}\n${[...options, ...COMMON.filter(([flag]) => json || flag !== '--json')].map(option).join('\n')}`);
|
|
53
|
+
if (examples.length) parts.push(`${style.bold('Examples')}\n${examples.map((e) => ` ${e}`).join('\n')}`);
|
|
54
|
+
return parts.join('\n\n');
|
|
55
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Where the CLI writes: lines to stdout and stderr. */
|
|
2
|
+
export interface IO {
|
|
3
|
+
out(s: string): void;
|
|
4
|
+
err(s: string): void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** The help text: every verb in a line, the common examples, and where to read more. */
|
|
8
|
+
export const HELP: string;
|
|
9
|
+
/** Runs the CLI with `argv` (no node or script) and resolves to its exit code: 0 ok, 1 a check failed or the command failed, 2 usage. */
|
|
10
|
+
export function main(argv: readonly string[], io?: IO): Promise<number>;
|
package/src/cli/index.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module logomarks/cli
|
|
3
|
+
*
|
|
4
|
+
* The command line: `logomarks <verb> …`, and `--help` for each verb.
|
|
5
|
+
* Exit codes: 0 ok, 1 a check failed or the command failed, 2 usage.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { COMMANDS } from './commands/index.js';
|
|
9
|
+
import { parseArgs, UsageError } from './args.js';
|
|
10
|
+
import { formatHelp } from './help.js';
|
|
11
|
+
import { style, pad } from './style.js';
|
|
12
|
+
import { VERSION as version } from '../util/version.js';
|
|
13
|
+
|
|
14
|
+
/** The verbs as the help groups them. */
|
|
15
|
+
const GROUPS = [
|
|
16
|
+
['Find and get', ['find', 'get']],
|
|
17
|
+
['Check', ['check', 'similar', 'specks', 'sheet']],
|
|
18
|
+
['Edit', ['parts', 'edit', 'glitch', 'combine']],
|
|
19
|
+
['Build', ['kit', 'social', 'mockup', 'cover']],
|
|
20
|
+
['Agents', ['mcp']],
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/** The help text: every verb in a line, the common examples, and where to read more. */
|
|
24
|
+
export const HELP = `logomarks ${version}: find, check, edit and build on free geometric logomarks.
|
|
25
|
+
|
|
26
|
+
${style.bold('Usage')}
|
|
27
|
+
logomarks <command> [arguments] [options]
|
|
28
|
+
logomarks <command> --help
|
|
29
|
+
|
|
30
|
+
${GROUPS.map(([group, verbs]) => `${style.bold(group)}\n${verbs.map((v) => ` ${pad(v, 10)}${COMMANDS[/** @type {keyof typeof COMMANDS} */ (v)].help.summary}`).join('\n')}`).join('\n\n')}
|
|
31
|
+
|
|
32
|
+
A <mark> is a live slug or name (hex-gem, "Hex Gem") or the path to your own .svg.
|
|
33
|
+
Every command takes --json. Exit codes: 0 ok, 1 a check failed or the command failed, 2 usage.
|
|
34
|
+
|
|
35
|
+
${style.bold('Examples')}
|
|
36
|
+
logomarks find coffee
|
|
37
|
+
logomarks get hex-gem --color '#ff5c3a' -o logo.svg
|
|
38
|
+
logomarks check mark.svg
|
|
39
|
+
logomarks kit hex-gem --name "Hexa Labs" -o hexa/
|
|
40
|
+
|
|
41
|
+
${style.bold('Options')}
|
|
42
|
+
-h, --help Show help (for a command: logomarks <command> --help).
|
|
43
|
+
-v, --version Show the version.
|
|
44
|
+
|
|
45
|
+
Docs: https://logomarks.ai/agents · The marks are CC0; the CLI is MIT.`;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Whether the arguments ask for help.
|
|
49
|
+
*
|
|
50
|
+
* @param {readonly string[]} args The arguments after the verb.
|
|
51
|
+
* @returns {boolean} True for `--help` or `-h`.
|
|
52
|
+
*/
|
|
53
|
+
const wantsHelp = (args) => args.includes('--help') || args.includes('-h');
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* What went wrong, in a line: a missing or unwritable file by its path, anything else by its message.
|
|
57
|
+
*
|
|
58
|
+
* @param {unknown} e The error.
|
|
59
|
+
* @returns {string} The message.
|
|
60
|
+
*/
|
|
61
|
+
function explain(e) {
|
|
62
|
+
const err = /** @type {NodeJS.ErrnoException} */ (e);
|
|
63
|
+
if (err?.code === 'ENOENT') return `no such file or folder: ${err.path}`;
|
|
64
|
+
if (err?.code === 'EACCES' || err?.code === 'EPERM') return `permission denied: ${err.path}`;
|
|
65
|
+
if (err?.code === 'EISDIR') return `${err.path} is a folder`;
|
|
66
|
+
return err instanceof Error ? err.message : String(e);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Runs the CLI.
|
|
71
|
+
*
|
|
72
|
+
* @param {readonly string[]} argv The arguments (without node and the script).
|
|
73
|
+
* @param {import('./index.js').IO} [io] Where output goes (stdout and stderr).
|
|
74
|
+
* @returns {Promise<number>} The exit code: 0 ok, 1 a check failed or the command failed, 2 usage.
|
|
75
|
+
*/
|
|
76
|
+
export async function main(argv, io = { out: (s) => console.log(s), err: (s) => console.error(s) }) {
|
|
77
|
+
const [verb, ...rest] = argv;
|
|
78
|
+
if (!verb) { io.err(HELP); return 2; }
|
|
79
|
+
if (verb === '--help' || verb === '-h') { io.out(HELP); return 0; }
|
|
80
|
+
if (verb === '--version' || verb === '-v' || verb === 'version') { io.out(version); return 0; }
|
|
81
|
+
if (verb === 'help') {
|
|
82
|
+
const topic = COMMANDS[/** @type {keyof typeof COMMANDS} */ (rest[0])];
|
|
83
|
+
if (rest[0] && !topic) { io.err(`logomarks: unknown command "${rest[0]}". Run "logomarks --help" for the list.`); return 2; }
|
|
84
|
+
io.out(topic ? formatHelp(rest[0], topic.help) : HELP);
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
const command = COMMANDS[/** @type {keyof typeof COMMANDS} */ (verb)];
|
|
88
|
+
if (!command) {
|
|
89
|
+
io.err(`logomarks: unknown command "${verb}". Run "logomarks --help" for the list.`);
|
|
90
|
+
return 2;
|
|
91
|
+
}
|
|
92
|
+
if (wantsHelp(rest)) { io.out(formatHelp(verb, command.help)); return 0; }
|
|
93
|
+
try {
|
|
94
|
+
return await command.run(parseArgs(rest, command.spec), io);
|
|
95
|
+
} catch (e) {
|
|
96
|
+
if (e instanceof UsageError) {
|
|
97
|
+
io.err(`logomarks ${verb}: ${e.message}\nRun "logomarks ${verb} --help" for usage.`);
|
|
98
|
+
return 2;
|
|
99
|
+
}
|
|
100
|
+
io.err(`logomarks ${verb}: ${explain(e)}`);
|
|
101
|
+
if (process.env.LOGOMARKS_DEBUG) io.err(String(/** @type {Error} */ (e)?.stack ?? e));
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Parsed } from './args.js';
|
|
2
|
+
import type { BrandOptions } from '../brand/brand.js';
|
|
3
|
+
import type { PackOptions } from '../kit/index.js';
|
|
4
|
+
import type { Mark } from '../brand/mark.js';
|
|
5
|
+
import type { FontId } from '../brand/fonts.js';
|
|
6
|
+
|
|
7
|
+
/** The mark the positionals name (a live slug or name, or an .svg file); a `UsageError` otherwise. */
|
|
8
|
+
export function markArg(positionals: readonly string[]): Mark;
|
|
9
|
+
/** One mark by slug, name or file; a `UsageError` when it isn't found. */
|
|
10
|
+
export function markOf(ref: string): Mark;
|
|
11
|
+
/** A number option (undefined when not given); a `UsageError` when it isn't one. */
|
|
12
|
+
export function numArg(name: string, value: string | undefined): number | undefined;
|
|
13
|
+
/** A colour option as `#RRGGBB` (undefined when not given); a `UsageError` when it isn't one. */
|
|
14
|
+
export function colorArg(value: string | undefined): string | undefined;
|
|
15
|
+
/** A typeface option as its id (undefined when not given); a `UsageError` when it isn't bundled. */
|
|
16
|
+
export function fontArg(value: string | undefined): FontId | undefined;
|
|
17
|
+
/** A whole-number option in range (undefined when not given); a `UsageError` otherwise. */
|
|
18
|
+
export function intArg(name: string, value: string | undefined, min: number, max: number): number | undefined;
|
|
19
|
+
/** Writes a file, making its folder. */
|
|
20
|
+
export function writeFile(file: string, data: string | Uint8Array): void;
|
|
21
|
+
/** Writes files by path under a folder; returns how many. */
|
|
22
|
+
export function writeFiles(dir: string, files: Iterable<[string, string | Uint8Array]>): number;
|
|
23
|
+
/** A comma-separated option as a trimmed list (undefined when not given). */
|
|
24
|
+
export function listArg(value: string | undefined): string[] | undefined;
|
|
25
|
+
/** The brand: the mark, and `--name --color --font --tagline --url --handle`; a `UsageError` when one is wrong. */
|
|
26
|
+
export function brandArgs(args: Parsed): BrandOptions & { mark: Mark; name: string };
|
|
27
|
+
/** The social pack's `--platforms`, `--styles` and `--animated`; a `UsageError` for an unknown style. */
|
|
28
|
+
export function packArgs(args: Parsed): PackOptions;
|
|
29
|
+
/** Runs a call, its RangeErrors and TypeErrors (an unknown platform, style or object) as `UsageError`s. */
|
|
30
|
+
export function asUsage<T>(build: () => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module logomarks/cli/options
|
|
3
|
+
*
|
|
4
|
+
* The options the brand verbs share, read and checked: the mark, a colour,
|
|
5
|
+
* a typeface, a whole number, a list; the brand and the social pack's
|
|
6
|
+
* choices; and writing a file, folders made as needed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { UsageError } from './args.js';
|
|
10
|
+
import { loadMark, MarkNotFound } from '../brand/mark.js';
|
|
11
|
+
import { parseColor } from '../util/color.js';
|
|
12
|
+
import { fontId, FONTS } from '../brand/fonts.js';
|
|
13
|
+
import { STYLE_IDS } from '@logomarks/brand-kit';
|
|
14
|
+
|
|
15
|
+
export { writeFile, writeFiles } from '../util/files.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The mark the positionals name: a live slug or name (words may be unquoted), or an SVG file.
|
|
19
|
+
*
|
|
20
|
+
* @param {readonly string[]} positionals The positionals.
|
|
21
|
+
* @returns {import('../brand/mark.js').Mark} The mark.
|
|
22
|
+
* @throws {UsageError} When none is named or it isn't found.
|
|
23
|
+
*/
|
|
24
|
+
export function markArg(positionals) {
|
|
25
|
+
if (!positionals.length) throw new UsageError('name a live mark (slug or name) or an .svg file');
|
|
26
|
+
return markOf(positionals.join(' '));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* One mark by slug, name or file.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} ref The reference.
|
|
33
|
+
* @returns {import('../brand/mark.js').Mark} The mark.
|
|
34
|
+
* @throws {UsageError} When it isn't found or can't be read.
|
|
35
|
+
*/
|
|
36
|
+
export function markOf(ref) {
|
|
37
|
+
try {
|
|
38
|
+
return loadMark(ref);
|
|
39
|
+
} catch (e) {
|
|
40
|
+
if (e instanceof MarkNotFound) throw new UsageError(e.message);
|
|
41
|
+
throw new UsageError(`can't read the mark: ${/** @type {Error} */ (e).message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A colour option.
|
|
47
|
+
*
|
|
48
|
+
* @param {string | undefined} value The option's value.
|
|
49
|
+
* @returns {string | undefined} `#RRGGBB`, or undefined when it wasn't given.
|
|
50
|
+
* @throws {UsageError} When it isn't a colour.
|
|
51
|
+
*/
|
|
52
|
+
export function colorArg(value) {
|
|
53
|
+
if (value === undefined) return undefined;
|
|
54
|
+
const hex = parseColor(value);
|
|
55
|
+
if (!hex) throw new UsageError(`--color takes a hex colour (#ff5c3a), not "${value}"`);
|
|
56
|
+
return hex;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A typeface option.
|
|
61
|
+
*
|
|
62
|
+
* @param {string | undefined} value The option's value.
|
|
63
|
+
* @returns {import('../brand/fonts.js').FontId | undefined} The face's id, or undefined when it wasn't given.
|
|
64
|
+
* @throws {UsageError} When it isn't bundled.
|
|
65
|
+
*/
|
|
66
|
+
export function fontArg(value) {
|
|
67
|
+
if (value === undefined) return undefined;
|
|
68
|
+
const id = fontId(value);
|
|
69
|
+
if (!id) throw new UsageError(`--font is one of ${Object.keys(FONTS).join(', ')}`);
|
|
70
|
+
return id;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A whole-number option.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} name The option (for the message).
|
|
77
|
+
* @param {string | undefined} value Its value.
|
|
78
|
+
* @param {number} min The smallest allowed.
|
|
79
|
+
* @param {number} max The largest allowed.
|
|
80
|
+
* @returns {number | undefined} The number, or undefined when it wasn't given.
|
|
81
|
+
* @throws {UsageError} When it isn't a whole number in range.
|
|
82
|
+
*/
|
|
83
|
+
export function intArg(name, value, min, max) {
|
|
84
|
+
if (value === undefined) return undefined;
|
|
85
|
+
const n = Number(value);
|
|
86
|
+
if (!Number.isInteger(n) || n < min || n > max) throw new UsageError(`--${name} takes a whole number from ${min} to ${max}`);
|
|
87
|
+
return n;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A number option.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} name The option (for the message).
|
|
94
|
+
* @param {string | undefined} value Its value.
|
|
95
|
+
* @returns {number | undefined} The number, or undefined when it wasn't given.
|
|
96
|
+
* @throws {UsageError} When it isn't a number.
|
|
97
|
+
*/
|
|
98
|
+
export function numArg(name, value) {
|
|
99
|
+
if (value === undefined) return undefined;
|
|
100
|
+
const n = Number(value);
|
|
101
|
+
if (value.trim() === '' || !Number.isFinite(n)) throw new UsageError(`--${name} takes a number, not "${value}"`);
|
|
102
|
+
return n;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A comma-separated option as a list.
|
|
107
|
+
*
|
|
108
|
+
* @param {string | undefined} value The option's value.
|
|
109
|
+
* @returns {string[] | undefined} The items, trimmed; undefined when it wasn't given.
|
|
110
|
+
*/
|
|
111
|
+
export const listArg = (value) => value?.split(',').map((v) => v.trim()).filter(Boolean);
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The brand the brand verbs build on: the mark, and `--name --color --font --tagline --url --handle`.
|
|
115
|
+
*
|
|
116
|
+
* @param {import('./args.js').Parsed} args The parsed arguments.
|
|
117
|
+
* @returns {import('../brand/brand.js').BrandOptions & { mark: import('../brand/mark.js').Mark, name: string }} The brand.
|
|
118
|
+
* @throws {UsageError} When the mark, colour or face is wrong.
|
|
119
|
+
*/
|
|
120
|
+
export function brandArgs(args) {
|
|
121
|
+
const mark = markArg(args.positionals);
|
|
122
|
+
const { tagline, url, handle } = args.options;
|
|
123
|
+
return { mark, name: args.options.name?.trim() || mark.name, color: colorArg(args.options.color), font: fontArg(args.options.font), tagline, url, handle };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* What goes in a social pack: `--platforms`, `--styles`, `--animated`.
|
|
128
|
+
*
|
|
129
|
+
* @param {import('./args.js').Parsed} args The parsed arguments.
|
|
130
|
+
* @returns {import('../kit/index.js').PackOptions} The choices.
|
|
131
|
+
* @throws {UsageError} For a style that isn't one.
|
|
132
|
+
*/
|
|
133
|
+
export function packArgs(args) {
|
|
134
|
+
const styles = listArg(args.options.styles);
|
|
135
|
+
const bad = styles?.filter((s) => !(/** @type {readonly string[]} */ (STYLE_IDS)).includes(s));
|
|
136
|
+
if (bad?.length) throw new UsageError(`--styles takes ${STYLE_IDS.join(', ')}, not ${bad.join(', ')}`);
|
|
137
|
+
return { styles: /** @type {import('@logomarks/brand-kit').StyleId[] | undefined} */ (styles), platforms: listArg(args.options.platforms), animated: args.flags.has('animated') };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Runs a brand-kit call, its RangeErrors (an unknown platform or style) as usage errors.
|
|
142
|
+
*
|
|
143
|
+
* @template T
|
|
144
|
+
* @param {() => Promise<T>} build The call.
|
|
145
|
+
* @returns {Promise<T>} Its result.
|
|
146
|
+
* @throws {UsageError} For a RangeError.
|
|
147
|
+
*/
|
|
148
|
+
export async function asUsage(build) {
|
|
149
|
+
try {
|
|
150
|
+
return await build();
|
|
151
|
+
} catch (e) {
|
|
152
|
+
if (e instanceof RangeError || e instanceof TypeError) throw new UsageError(e.message.replace(/^brand-kit: /, ''));
|
|
153
|
+
throw e;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module logomarks/cli/report
|
|
3
|
+
*
|
|
4
|
+
* A check report as readable lines: a mark's verdict, size and counts, then
|
|
5
|
+
* each finding with its id, message and where it is.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { style, pad } from './style.js';
|
|
9
|
+
|
|
10
|
+
/** Severity labels. */
|
|
11
|
+
const LABEL = { error: style.red('error '), warning: style.yellow('warning'), info: style.dim('note ') };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A report as readable lines.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} file The file (or the mark's name).
|
|
17
|
+
* @param {import('../checks/index.js').Report} r Its report.
|
|
18
|
+
* @param {boolean} [fixed] Whether it was refitted.
|
|
19
|
+
* @returns {string} The lines.
|
|
20
|
+
*/
|
|
21
|
+
export function reportLines(file, r, fixed = false) {
|
|
22
|
+
const sym = !r.ok ? style.red('✗') : r.warnings ? style.yellow('!') : style.green('✓');
|
|
23
|
+
const counts = [r.errors && `${r.errors} error${r.errors > 1 ? 's' : ''}`, r.warnings && `${r.warnings} warning${r.warnings > 1 ? 's' : ''}`].filter(Boolean).join(', ');
|
|
24
|
+
const size = r.size ? `${r.size.w} × ${r.size.h}` : 'empty';
|
|
25
|
+
const head = `${sym} ${style.bold(file)} ${style.dim(size)}${counts ? ` ${counts}` : ''}${fixed ? style.green(' refitted') : ''}`;
|
|
26
|
+
const body = r.findings.map((f) => ` ${LABEL[f.severity]} ${pad(f.id, 12)} ${f.message}${f.box ? style.dim(` [${f.box.join(', ')}]`) : f.point ? style.dim(` (${f.point.join(', ')})`) : ''}`);
|
|
27
|
+
return [head, ...body].join('\n');
|
|
28
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** The output's colours (plain text off a terminal or with `NO_COLOR`). */
|
|
2
|
+
export const style: Readonly<Record<'red' | 'yellow' | 'green' | 'dim' | 'bold', (s: string) => string>>;
|
|
3
|
+
/** Text padded to a width, ignoring colour codes. */
|
|
4
|
+
export function pad(s: string, n: number): string;
|
package/src/cli/style.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module logomarks/cli/style
|
|
3
|
+
*
|
|
4
|
+
* Colour for the readable output, only on a terminal and never with
|
|
5
|
+
* `NO_COLOR` set.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const on = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A function that wraps text in an ANSI colour (or leaves it as it is).
|
|
12
|
+
*
|
|
13
|
+
* @param {number} code The colour's code.
|
|
14
|
+
* @returns {(s: string) => string} The wrapper.
|
|
15
|
+
*/
|
|
16
|
+
const paint = (code) => (s) => (on ? `\x1b[${code}m${s}\x1b[0m` : s);
|
|
17
|
+
|
|
18
|
+
/** The output's colours. */
|
|
19
|
+
export const style = {
|
|
20
|
+
red: paint(31),
|
|
21
|
+
yellow: paint(33),
|
|
22
|
+
green: paint(32),
|
|
23
|
+
dim: paint(2),
|
|
24
|
+
bold: paint(1),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Text padded to a width, ignoring colour codes.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} s The text.
|
|
31
|
+
* @param {number} n The width.
|
|
32
|
+
* @returns {string} The padded text.
|
|
33
|
+
*/
|
|
34
|
+
export const pad = (s, n) => s + ' '.repeat(Math.max(0, n - s.replace(/\x1b\[\d+m/g, '').length));
|