logomarks 1.0.0 → 2.0.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 (73) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +10 -2
  3. package/docs/api.md +9 -2
  4. package/docs/commands.md +19 -3
  5. package/docs/data.md +84 -0
  6. package/docs/mcp.md +3 -3
  7. package/package.json +9 -9
  8. package/src/brand/mark.js +2 -2
  9. package/src/cli/commands/combine.d.ts +2 -0
  10. package/src/cli/commands/combine.js +3 -0
  11. package/src/cli/commands/cover.d.ts +2 -0
  12. package/src/cli/commands/cover.js +3 -0
  13. package/src/cli/commands/edit.d.ts +2 -0
  14. package/src/cli/commands/edit.js +3 -0
  15. package/src/cli/commands/find.d.ts +2 -0
  16. package/src/cli/commands/find.js +4 -1
  17. package/src/cli/commands/get.d.ts +2 -0
  18. package/src/cli/commands/get.js +4 -1
  19. package/src/cli/commands/glitch.d.ts +2 -0
  20. package/src/cli/commands/glitch.js +3 -0
  21. package/src/cli/commands/index.d.ts +5 -3
  22. package/src/cli/commands/index.js +2 -1
  23. package/src/cli/commands/kit.d.ts +2 -0
  24. package/src/cli/commands/kit.js +3 -0
  25. package/src/cli/commands/mcp.d.ts +3 -2
  26. package/src/cli/commands/mcp.js +7 -3
  27. package/src/cli/commands/mockup.d.ts +2 -0
  28. package/src/cli/commands/mockup.js +3 -0
  29. package/src/cli/commands/parts.d.ts +2 -0
  30. package/src/cli/commands/parts.js +3 -0
  31. package/src/cli/commands/sheet.d.ts +2 -0
  32. package/src/cli/commands/sheet.js +3 -0
  33. package/src/cli/commands/similar.d.ts +2 -0
  34. package/src/cli/commands/similar.js +5 -2
  35. package/src/cli/commands/social.d.ts +2 -0
  36. package/src/cli/commands/social.js +3 -0
  37. package/src/cli/commands/update.d.ts +10 -0
  38. package/src/cli/commands/update.js +54 -0
  39. package/src/cli/help.js +1 -0
  40. package/src/cli/index.d.ts +6 -0
  41. package/src/cli/index.js +14 -3
  42. package/src/cli/options.js +3 -0
  43. package/src/find/index.d.ts +1 -1
  44. package/src/find/index.js +3 -3
  45. package/src/index.d.ts +3 -1
  46. package/src/index.js +4 -1
  47. package/src/live/build.d.ts +36 -0
  48. package/src/live/build.js +55 -0
  49. package/src/live/cache.d.ts +25 -0
  50. package/src/live/cache.js +75 -0
  51. package/src/live/current.d.ts +9 -0
  52. package/src/live/current.js +54 -0
  53. package/src/live/ensure.d.ts +43 -0
  54. package/src/live/ensure.js +92 -0
  55. package/src/live/errors.d.ts +16 -0
  56. package/src/live/errors.js +19 -0
  57. package/src/live/format.d.ts +13 -0
  58. package/src/live/format.js +113 -0
  59. package/src/live/index.d.ts +7 -0
  60. package/src/live/index.js +12 -0
  61. package/src/live/paths.d.ts +17 -0
  62. package/src/live/paths.js +64 -0
  63. package/src/live/remote.d.ts +24 -0
  64. package/src/live/remote.js +109 -0
  65. package/src/mcp/index.d.ts +5 -3
  66. package/src/mcp/index.js +26 -13
  67. package/src/mcp/tools/find.js +2 -2
  68. package/src/sheet/index.d.ts +1 -1
  69. package/src/similar/folder.js +2 -2
  70. package/src/similar/nearest.d.ts +1 -1
  71. package/src/similar/store.d.ts +9 -6
  72. package/src/similar/store.js +23 -62
  73. package/data/index.bin +0 -0
@@ -11,6 +11,9 @@ import { gridSheet } from '../../sheet/grid.js';
11
11
  import { svgFiles, slugOf } from '../files.js';
12
12
  import { style } from '../style.js';
13
13
 
14
+ /** It reads the live set: the CLI gets it first. */
15
+ export const live = true;
16
+
14
17
  /** What `sheet` takes. */
15
18
  export const spec = { flags: ['json', 'strict', 'plain'], options: { out: 'o', ignore: '', cols: '' } };
16
19
 
@@ -2,6 +2,8 @@ import type { ArgSpec, Parsed } from '../args.js';
2
2
  import type { IO } from '../index.js';
3
3
  import type { CommandHelp } from '../help.js';
4
4
 
5
+ /** It reads the live set: the CLI gets it first. */
6
+ export const live: true;
5
7
  /** What `similar` takes. */
6
8
  export const spec: ArgSpec;
7
9
  /** What `logomarks similar --help` prints. */
@@ -5,7 +5,7 @@
5
5
  * Over the house threshold a match is too close (look at the two side by
6
6
  * side: solid shapes often share an outline); over the near-copy threshold
7
7
  * the mark fails. `--against <dir>` compares with a folder of SVGs instead of
8
- * the bundled index (the site's svg folder while a batch is drawn), `--each`
8
+ * the live set (the site's svg folder while a batch is drawn), `--each`
9
9
  * with the other candidates too, and `--all <data dir>` writes "More like this"
10
10
  * for a whole set (similar.json).
11
11
  */
@@ -23,6 +23,9 @@ import { UsageError } from '../args.js';
23
23
  import { svgFiles } from '../files.js';
24
24
  import { style, pad } from '../style.js';
25
25
 
26
+ /** It reads the live set: the CLI gets it first. */
27
+ export const live = true;
28
+
26
29
  /** What `similar` takes. */
27
30
  export const spec = { flags: ['json', 'letters', 'each'], options: { top: '', against: '', all: '', out: 'o' } };
28
31
 
@@ -38,7 +41,7 @@ export const help = {
38
41
  options: [
39
42
  ['--top <n>', 'How many matches per mark. Default 5.'],
40
43
  ['--letters', `Compare a badge's knocked-out letter instead of its outline\n(${LETTERS.tooClose} too close, ${LETTERS.nearCopy} a near-copy).`],
41
- ['--against <dir>', 'Compare with a folder of SVGs instead of the bundled live set.'],
44
+ ['--against <dir>', 'Compare with a folder of SVGs instead of the live set.'],
42
45
  ['--each', 'Compare the candidates with each other too (marked *).'],
43
46
  ['--all <data dir>', 'Write every mark\'s 8 nearest for a whole set ("More like this"),\nfrom its marks.json, descriptions.json and svg/.'],
44
47
  ['-o, --out <file>', 'Where --all writes. Default: stdout.'],
@@ -2,6 +2,8 @@ import type { ArgSpec, Parsed } from '../args.js';
2
2
  import type { IO } from '../index.js';
3
3
  import type { CommandHelp } from '../help.js';
4
4
 
5
+ /** It reads the live set: the CLI gets it first. */
6
+ export const live: true;
5
7
  /** What `social` takes. */
6
8
  export const spec: ArgSpec;
7
9
  /** What `logomarks social --help` prints. */
@@ -15,6 +15,9 @@ import { FONTS } from '../../brand/fonts.js';
15
15
  import { STYLE_IDS, DEFAULT_STYLES } from '@logomarks/brand-kit';
16
16
  import { wrapList } from '../help.js';
17
17
 
18
+ /** It reads the live set: the CLI gets it first. */
19
+ export const live = true;
20
+
18
21
  /** What `social` takes. */
19
22
  export const spec = { flags: ['json', 'animated'], options: { out: 'o', name: '', color: '', font: '', tagline: '', url: '', handle: '', styles: '', platforms: '' } };
20
23
 
@@ -0,0 +1,10 @@
1
+ import type { ArgSpec, Parsed } from '../args.js';
2
+ import type { IO, RunContext } from '../index.js';
3
+ import type { CommandHelp } from '../help.js';
4
+
5
+ /** What `update` takes. */
6
+ export const spec: ArgSpec;
7
+ /** What `logomarks update --help` prints. */
8
+ export const help: CommandHelp;
9
+ /** Runs `update`: checks the site now and downloads a newer set; resolves to 0, or throws an `IndexError`. */
10
+ export function run(args: Parsed, io: IO, ctx?: RunContext): Promise<number>;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @module logomarks/cli/commands/update
3
+ *
4
+ * `logomarks update`: ask logomarks.ai for the latest index now, rather than
5
+ * waiting for the daily check, and download it when it's newer.
6
+ */
7
+
8
+ import fs from 'node:fs';
9
+ import { ensureIndex } from '../../live/ensure.js';
10
+ import { indexHeader } from '../../live/format.js';
11
+ import { style } from '../style.js';
12
+
13
+ /** What `update` takes. */
14
+ export const spec = { flags: ['json'], options: {} };
15
+
16
+ /** `logomarks update --help`. */
17
+ /** @type {import('../help.js').CommandHelp} */
18
+ export const help = {
19
+ summary: 'Download the latest Logomarks set now (it is otherwise checked once a day).',
20
+ usage: [''],
21
+ about: [
22
+ 'The set comes from https://logomarks.ai/cli/index.bin (about 3 MB), checked against its sha256,',
23
+ 'and is cached in the user cache folder (~/.cache/logomarks, ~/Library/Caches/logomarks on macOS,',
24
+ '%LOCALAPPDATA%\\logomarks\\Cache on Windows; $XDG_CACHE_HOME/logomarks when set). Every command',
25
+ 'reuses it, checking for a newer one at most once a day. Exit 1 when it can\'t be had.',
26
+ 'Environment: LOGOMARKS_OFFLINE=1 (as --offline), LOGOMARKS_INDEX=<file> (use a local index),',
27
+ 'LOGOMARKS_INDEX_URL=<url> (download from elsewhere).',
28
+ ],
29
+ examples: ['logomarks update', 'logomarks update --json'],
30
+ };
31
+
32
+ /** How each source is described. */
33
+ const SAID = {
34
+ download: 'Downloaded the latest set',
35
+ cache: 'Already up to date',
36
+ file: 'LOGOMARKS_INDEX is set; nothing to update. Using',
37
+ };
38
+
39
+ /**
40
+ * Runs `update`.
41
+ *
42
+ * @param {import('../args.js').Parsed} args The parsed arguments.
43
+ * @param {import('../index.js').IO} io Where output goes.
44
+ * @param {import('../index.js').RunContext} [ctx] Whether `--offline` was given (then it fails: updating needs the network).
45
+ * @returns {Promise<number>} The exit code: 0 when the set is current.
46
+ */
47
+ export async function run(args, io, { offline } = {}) {
48
+ const status = await ensureIndex({ force: true, offline });
49
+ const bytes = fs.readFileSync(status.file);
50
+ const { format, built, marks } = indexHeader(bytes);
51
+ if (args.flags.has('json')) io.out(JSON.stringify({ source: status.source, file: status.file, format, built, marks, bytes: bytes.length, sha256: status.manifest?.sha256 }, null, 2));
52
+ else io.out(`${SAID[status.source]}: ${marks.toLocaleString('en-GB')} marks, built ${built} (${Math.round(bytes.length / 1024)} KB).\n${style.dim(status.file)}`);
53
+ return 0;
54
+ }
package/src/cli/help.js CHANGED
@@ -10,6 +10,7 @@ import { pad, style } from './style.js';
10
10
  /** The options every verb takes, listed last. */
11
11
  const COMMON = [
12
12
  ['--json', 'Machine-readable output on stdout.'],
13
+ ['--offline', 'Use the cached set only, never the network.'],
13
14
  ['-h, --help', 'Show this help.'],
14
15
  ];
15
16
 
@@ -1,3 +1,9 @@
1
+ /** What every verb is told besides its arguments. */
2
+ export interface RunContext {
3
+ /** `--offline`: use the cached set only. */
4
+ offline?: boolean;
5
+ }
6
+
1
7
  /** Where the CLI writes: lines to stdout and stderr. */
2
8
  export interface IO {
3
9
  out(s: string): void;
package/src/cli/index.js CHANGED
@@ -10,6 +10,7 @@ import { parseArgs, UsageError } from './args.js';
10
10
  import { formatHelp } from './help.js';
11
11
  import { style, pad } from './style.js';
12
12
  import { VERSION as version } from '../util/version.js';
13
+ import { prepareIndex } from '../live/ensure.js';
13
14
 
14
15
  /** The verbs as the help groups them. */
15
16
  const GROUPS = [
@@ -18,6 +19,7 @@ const GROUPS = [
18
19
  ['Edit', ['parts', 'edit', 'glitch', 'combine']],
19
20
  ['Build', ['kit', 'social', 'mockup', 'cover']],
20
21
  ['Agents', ['mcp']],
22
+ ['The set', ['update']],
21
23
  ];
22
24
 
23
25
  /** The help text: every verb in a line, the common examples, and where to read more. */
@@ -31,6 +33,7 @@ ${GROUPS.map(([group, verbs]) => `${style.bold(group)}\n${verbs.map((v) => ` ${
31
33
 
32
34
  A <mark> is a live slug or name (hex-gem, "Hex Gem") or the path to your own .svg.
33
35
  Every command takes --json. Exit codes: 0 ok, 1 a check failed or the command failed, 2 usage.
36
+ The set is downloaded from logomarks.ai on first use and cached; it's checked for a newer one once a day.
34
37
 
35
38
  ${style.bold('Examples')}
36
39
  logomarks find coffee
@@ -41,6 +44,7 @@ ${style.bold('Examples')}
41
44
  ${style.bold('Options')}
42
45
  -h, --help Show help (for a command: logomarks <command> --help).
43
46
  -v, --version Show the version.
47
+ --offline Use the cached set only, never the network (or LOGOMARKS_OFFLINE=1).
44
48
 
45
49
  Docs: https://logomarks.ai/agents · The marks are CC0; the CLI is MIT.`;
46
50
 
@@ -66,15 +70,20 @@ function explain(e) {
66
70
  return err instanceof Error ? err.message : String(e);
67
71
  }
68
72
 
73
+ /** The flag every verb takes: use the cached set only. */
74
+ const OFFLINE = '--offline';
75
+
69
76
  /**
70
- * Runs the CLI.
77
+ * Runs the CLI. A verb that reads the live set gets it first (cached, or downloaded from the site); when it can't be
78
+ * had, the verb still runs, and fails only if it needs a live mark.
71
79
  *
72
80
  * @param {readonly string[]} argv The arguments (without node and the script).
73
81
  * @param {import('./index.js').IO} [io] Where output goes (stdout and stderr).
74
82
  * @returns {Promise<number>} The exit code: 0 ok, 1 a check failed or the command failed, 2 usage.
75
83
  */
76
84
  export async function main(argv, io = { out: (s) => console.log(s), err: (s) => console.error(s) }) {
77
- const [verb, ...rest] = argv;
85
+ const offline = argv.includes(OFFLINE) || undefined;
86
+ const [verb, ...rest] = argv.filter((a) => a !== OFFLINE);
78
87
  if (!verb) { io.err(HELP); return 2; }
79
88
  if (verb === '--help' || verb === '-h') { io.out(HELP); return 0; }
80
89
  if (verb === '--version' || verb === '-v' || verb === 'version') { io.out(version); return 0; }
@@ -91,7 +100,9 @@ export async function main(argv, io = { out: (s) => console.log(s), err: (s) =>
91
100
  }
92
101
  if (wantsHelp(rest)) { io.out(formatHelp(verb, command.help)); return 0; }
93
102
  try {
94
- return await command.run(parseArgs(rest, command.spec), io);
103
+ const args = parseArgs(rest, command.spec);
104
+ if ('live' in command && command.live) await prepareIndex({ offline, warn: io.err });
105
+ return await command.run(args, io, { offline });
95
106
  } catch (e) {
96
107
  if (e instanceof UsageError) {
97
108
  io.err(`logomarks ${verb}: ${e.message}\nRun "logomarks ${verb} --help" for usage.`);
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { UsageError } from './args.js';
10
10
  import { loadMark, MarkNotFound } from '../brand/mark.js';
11
+ import { IndexError } from '../live/errors.js';
11
12
  import { parseColor } from '../util/color.js';
12
13
  import { fontId, FONTS } from '../brand/fonts.js';
13
14
  import { STYLE_IDS } from '@logomarks/brand-kit';
@@ -32,12 +33,14 @@ export function markArg(positionals) {
32
33
  * @param {string} ref The reference.
33
34
  * @returns {import('../brand/mark.js').Mark} The mark.
34
35
  * @throws {UsageError} When it isn't found or can't be read.
36
+ * @throws {IndexError} When it names a live mark and the live set can't be had.
35
37
  */
36
38
  export function markOf(ref) {
37
39
  try {
38
40
  return loadMark(ref);
39
41
  } catch (e) {
40
42
  if (e instanceof MarkNotFound) throw new UsageError(e.message);
43
+ if (e instanceof IndexError) throw e;
41
44
  throw new UsageError(`can't read the mark: ${/** @type {Error} */ (e).message}`);
42
45
  }
43
46
  }
@@ -12,7 +12,7 @@ export interface FindOptions {
12
12
  letter?: string;
13
13
  /** How many to return (20). */
14
14
  limit?: number;
15
- /** The live set (the bundled index). */
15
+ /** The live set (the cached index). */
16
16
  index?: MarkIndex;
17
17
  }
18
18
 
package/src/find/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @module logomarks/find
3
3
  *
4
- * Searches the bundled live set the way the board on logomarks.ai does (its
4
+ * Searches the live set the way the board on logomarks.ai does (its
5
5
  * packed @arraypress/search index): every word must be found, typed at the
6
6
  * start of a word or as a synonym (UK and US trades: "solicitor" finds the
7
7
  * lawyers' marks), best first (a word of the name or a search word, then an
@@ -28,12 +28,12 @@ const searches = new WeakMap();
28
28
  *
29
29
  * @param {import('../similar/store.js').MarkIndex} index The live set.
30
30
  * @returns {import('@arraypress/search').Search<Record<string, unknown>>} Its search.
31
- * @throws {Error} When the index has no search (only the bundled one does).
31
+ * @throws {Error} When the index has no search (only the live set's does, not a folder's).
32
32
  */
33
33
  function searchOf(index) {
34
34
  let s = searches.get(index);
35
35
  if (!s) {
36
- if (!index.search) throw new Error('this index has no search: findMarks needs the bundled index (npm run index)');
36
+ if (!index.search) throw new Error("this index has no search: findMarks needs the live set's index, not a folder's");
37
37
  searches.set(index, (s = unpack(index.search, { stopWords: STOP })));
38
38
  }
39
39
  return s;
package/src/index.d.ts CHANGED
@@ -5,7 +5,9 @@ export type { FindingKind } from './checks/finding.js';
5
5
  export { similar } from './similar/nearest.js';
6
6
  export type { Match, Verdict, SimilarOptions } from './similar/nearest.js';
7
7
  export { loadIndex } from './similar/store.js';
8
- export type { MarkIndex, IndexedMark, IndexedLetter, MarkAbout } from './similar/store.js';
8
+ export type { MarkIndex, IndexedMark, IndexedLetter, MarkAbout, IndexInput } from './similar/store.js';
9
+ export { ensureIndex, IndexError, INDEX_FORMAT, DEFAULT_INDEX_URL, cacheDir, buildIndex, encodeIndex, decodeIndex } from './live/index.js';
10
+ export type { IndexErrorCode, EnsureOptions, IndexStatus, BuildInput, BuildMark, BuiltIndex, Manifest, CacheRecord } from './live/index.js';
9
11
  export { findMarks, facets } from './find/index.js';
10
12
  export type { FindOptions, FindResult, Found, Facets } from './find/index.js';
11
13
  export { sheet } from './sheet/index.js';
package/src/index.js CHANGED
@@ -3,7 +3,9 @@
3
3
  *
4
4
  * Check logomarks the way the Logomarks set is checked: the house rules
5
5
  * (`check`, `fix`), the nearest live marks (`similar`), and review cards to
6
- * look at (`sheet`); searching the live set (`findMarks`). Then build a brand on one: the mark itself (`loadMark`,
6
+ * look at (`sheet`); searching the live set (`findMarks`). The live set is
7
+ * downloaded from logomarks.ai and cached: `await ensureIndex()` once before
8
+ * the functions that read it (`buildIndex` is how the site builds it). Then build a brand on one: the mark itself (`loadMark`,
7
9
  * `paint`, `markPng`), edits (`glitchMark`, `editMark`, `combineMarks`), the
8
10
  * brand kit and social pack (`kit`, `social`, from `@logomarks/brand-kit`),
9
11
  * mockups (`mockup`) and covers (`cover`). The same functions run the `logomarks` command and its MCP
@@ -14,6 +16,7 @@ export { check, fix } from './checks/index.js';
14
16
  export { FINDINGS } from './checks/finding.js';
15
17
  export { similar } from './similar/nearest.js';
16
18
  export { loadIndex } from './similar/store.js';
19
+ export { ensureIndex, IndexError, INDEX_FORMAT, DEFAULT_INDEX_URL, cacheDir, buildIndex, encodeIndex, decodeIndex } from './live/index.js';
17
20
  export { findMarks, facets } from './find/index.js';
18
21
  export { sheet } from './sheet/index.js';
19
22
  export { fitMark } from '@logomarks/core/fit';
@@ -0,0 +1,36 @@
1
+ import type { SearchOptions } from '@arraypress/search';
2
+ import type { MarkAbout } from '../similar/store.js';
3
+ import type { Manifest } from './remote.js';
4
+
5
+ /** A mark as the builder takes it: the site's mark, or the monorepo's data joined up. */
6
+ export interface BuildMark extends Omit<MarkAbout, 'industries' | 'subject'> {
7
+ slug: string;
8
+ name: string;
9
+ /** The mark's SVG (trimmed when stored). */
10
+ svg: string;
11
+ /** What it depicts: subject ids. */
12
+ subject?: readonly string[];
13
+ /** The kinds of business it suits, best fit first. */
14
+ industries?: string[];
15
+ }
16
+
17
+ /** What the index is built from. */
18
+ export interface BuildInput {
19
+ /** Every live mark, in the board's order. */
20
+ marks: readonly BuildMark[];
21
+ /** The board's search: each mark as a search record (same order) and the search options (with the synonyms). */
22
+ search: { records: readonly object[]; options: SearchOptions<object> };
23
+ /** The build date (YYYY-MM-DD; today). */
24
+ built?: string;
25
+ }
26
+
27
+ /** A built index: what the site serves as `/cli/index.bin` and `/cli/index.json`. */
28
+ export interface BuiltIndex {
29
+ /** The file's bytes. */
30
+ bytes: Uint8Array<ArrayBuffer>;
31
+ /** The manifest: format, counts, size, sha256 and build date. */
32
+ manifest: Manifest & { letters: number };
33
+ }
34
+
35
+ /** Builds the live set's index (renders every mark's signatures: about 30 s for 4,000 marks) and its manifest. */
36
+ export function buildIndex(input: BuildInput): Promise<BuiltIndex>;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @module logomarks/live/build
3
+ *
4
+ * Builds the live set's index from the Logomarks data: each mark (in the
5
+ * board's order) with its SVG and what it is, its 48 px outline coverage, a
6
+ * letter signature for badges (their holes) and letter or number marks
7
+ * (their ink), and the board's search, packed (@arraypress/search), so `find`
8
+ * answers as logomarks.ai does. Returns the bytes and the manifest the site
9
+ * serves beside them (`/cli/index.bin`, `/cli/index.json`). Plain data in, so
10
+ * the site's build and the monorepo's `npm run index` share it.
11
+ */
12
+
13
+ import { pack } from '@arraypress/search';
14
+ import { outlineCoverage, letterSignatures } from '../similar/signature.js';
15
+ import { encodeIndex, INDEX_FORMAT } from './format.js';
16
+ import { sha256 } from './remote.js';
17
+
18
+ /**
19
+ * Whether a mark is a badge, whose letter is its holes (every family: Badge, Pixel Badge, Stencil Badge …).
20
+ *
21
+ * @param {string} slug The mark's slug.
22
+ * @returns {boolean} Whether it is.
23
+ */
24
+ const isBadge = (slug) => /(^|-)badge-/.test(slug);
25
+
26
+ /**
27
+ * The index and its manifest.
28
+ *
29
+ * @param {import('./build.js').BuildInput} input The marks, in board order, and the search to pack.
30
+ * @returns {Promise<import('./build.js').BuiltIndex>} The file's bytes and the manifest that describes them.
31
+ */
32
+ export async function buildIndex({ marks: list, search, built = new Date().toISOString().slice(0, 10) }) {
33
+ /** @type {import('../similar/store.js').IndexInput['marks']} */
34
+ const marks = [];
35
+ /** @type {Float32Array[]} */
36
+ const coverages = [];
37
+ /** @type {import('../similar/store.js').IndexInput['letters']} */
38
+ const letters = [];
39
+ for (const m of list) {
40
+ const svg = m.svg.trim();
41
+ const i = marks.push({
42
+ slug: m.slug, name: m.name, svg, tags: m.tags, collection: m.collection, style: m.style, mood: m.mood,
43
+ ...(m.letter && { letter: m.letter }), subject: m.subject ?? [], description: m.description, industries: m.industries ?? [],
44
+ }) - 1;
45
+ coverages.push(await outlineCoverage(svg));
46
+ const lettery = m.tags.includes('letter') || m.tags.includes('number');
47
+ if (isBadge(m.slug) || lettery) {
48
+ const { holes, ink } = await letterSignatures(svg);
49
+ if (isBadge(m.slug) && holes) letters.push({ mark: i, kind: 'hole', sig: holes });
50
+ else if (lettery) letters.push({ mark: i, kind: 'ink', sig: ink });
51
+ }
52
+ }
53
+ const bytes = new Uint8Array(encodeIndex({ marks, coverages, letters, search: pack(search.records, search.options) }, { built }));
54
+ return { bytes, manifest: { format: INDEX_FORMAT, marks: marks.length, letters: letters.length, bytes: bytes.length, sha256: sha256(bytes), built } };
55
+ }
@@ -0,0 +1,25 @@
1
+ import type { Manifest } from './remote.js';
2
+
3
+ /** What the cache keeps beside the index: the manifest it matched, where it came from, and when it was last checked. */
4
+ export interface CacheRecord extends Manifest {
5
+ /** The index's URL. */
6
+ url: string;
7
+ /** When the site was last asked (ms since the epoch). */
8
+ checked: number;
9
+ }
10
+
11
+ /** A cached index this version can read. */
12
+ export interface Cached {
13
+ /** The index file. */
14
+ file: string;
15
+ record: CacheRecord;
16
+ }
17
+
18
+ /** The cache's two files: `index.bin` and its record, `index.json`. */
19
+ export function cacheFiles(dir: string): { index: string; record: string };
20
+ /** The cached index, or null when there's none in a format this version reads. */
21
+ export function readCache(dir: string): Cached | null;
22
+ /** Saves the record alone (atomically). */
23
+ export function saveRecord(dir: string, record: CacheRecord): void;
24
+ /** Saves a verified index and its record (atomically); returns the index's path. */
25
+ export function saveCache(dir: string, bytes: Uint8Array, record: CacheRecord): string;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * @module logomarks/live/cache
3
+ *
4
+ * The cached index in the user's cache folder: `index.bin`, and beside it
5
+ * `index.json`, the manifest it was checked against with where it came from
6
+ * and when it was last checked. Writes are atomic (a temp file, then a
7
+ * rename), so two runs at once never leave half a file.
8
+ */
9
+
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import { INDEX_FORMAT } from './format.js';
13
+
14
+ /**
15
+ * The cache's two files.
16
+ *
17
+ * @param {string} dir The cache folder.
18
+ * @returns {{ index: string, record: string }} The index and its record.
19
+ */
20
+ export const cacheFiles = (dir) => ({ index: path.join(dir, 'index.bin'), record: path.join(dir, 'index.json') });
21
+
22
+ /**
23
+ * The cached index, when there is one this version can read.
24
+ *
25
+ * @param {string} dir The cache folder.
26
+ * @returns {import('./cache.js').Cached | null} The file and its record, or null.
27
+ */
28
+ export function readCache(dir) {
29
+ const { index, record } = cacheFiles(dir);
30
+ try {
31
+ const r = JSON.parse(fs.readFileSync(record, 'utf8'));
32
+ if (r.format !== INDEX_FORMAT || !fs.existsSync(index)) return null;
33
+ return { file: index, record: r };
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Writes a file atomically: to a temp file beside it, then renamed over it.
41
+ *
42
+ * @param {string} file Where.
43
+ * @param {string | Uint8Array} data What.
44
+ */
45
+ function writeAtomic(file, data) {
46
+ fs.mkdirSync(path.dirname(file), { recursive: true });
47
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
48
+ fs.writeFileSync(tmp, data);
49
+ fs.renameSync(tmp, file);
50
+ }
51
+
52
+ /**
53
+ * Saves the record alone (the index is unchanged; it was checked again).
54
+ *
55
+ * @param {string} dir The cache folder.
56
+ * @param {import('./cache.js').CacheRecord} record The manifest, where it came from and when it was checked.
57
+ */
58
+ export function saveRecord(dir, record) {
59
+ writeAtomic(cacheFiles(dir).record, `${JSON.stringify(record, null, 2)}\n`);
60
+ }
61
+
62
+ /**
63
+ * Saves a verified index and its record.
64
+ *
65
+ * @param {string} dir The cache folder.
66
+ * @param {Uint8Array} bytes The index.
67
+ * @param {import('./cache.js').CacheRecord} record The manifest, where it came from and when it was checked.
68
+ * @returns {string} The index's path.
69
+ */
70
+ export function saveCache(dir, bytes, record) {
71
+ const { index } = cacheFiles(dir);
72
+ writeAtomic(index, bytes);
73
+ saveRecord(dir, record);
74
+ return index;
75
+ }
@@ -0,0 +1,9 @@
1
+ /** Makes a file the process's index (what `loadIndex()` reads). */
2
+ export function useIndexFile(file: string): void;
3
+ /** Records why the index couldn't be had, for `loadIndex()` to throw when a command needs it. */
4
+ export function indexUnavailable(error: unknown): void;
5
+ /**
6
+ * The index file this process reads: the one `ensureIndex` settled on, else `LOGOMARKS_INDEX`, else the cached one.
7
+ * Throws the recorded reason, or an `IndexError` (`missing`), when there's none.
8
+ */
9
+ export function currentIndexFile(env?: NodeJS.ProcessEnv): string;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @module logomarks/live/current
3
+ *
4
+ * Which index file this process reads when `loadIndex()` is called with none:
5
+ * the one `ensureIndex` settled on; else `LOGOMARKS_INDEX`; else the cached
6
+ * one, when there is one. With none, the reason `ensureIndex` failed, or a
7
+ * message saying how to get it.
8
+ */
9
+
10
+ import { IndexError } from './errors.js';
11
+ import { cacheDir, localIndex } from './paths.js';
12
+ import { readCache } from './cache.js';
13
+
14
+ /** The file `ensureIndex` settled on. */
15
+ let chosen = /** @type {string | undefined} */ (undefined);
16
+
17
+ /** Why `prepareIndex` couldn't settle on one. */
18
+ let failure = /** @type {unknown} */ (undefined);
19
+
20
+ /**
21
+ * Makes a file the process's index.
22
+ *
23
+ * @param {string} file The index file.
24
+ */
25
+ export function useIndexFile(file) {
26
+ chosen = file;
27
+ failure = undefined;
28
+ }
29
+
30
+ /**
31
+ * Records why the index couldn't be had, for `loadIndex()` to throw when a command needs it.
32
+ *
33
+ * @param {unknown} error The reason.
34
+ */
35
+ export function indexUnavailable(error) {
36
+ failure = error;
37
+ }
38
+
39
+ /**
40
+ * The index file this process reads.
41
+ *
42
+ * @param {NodeJS.ProcessEnv} [env] The environment.
43
+ * @returns {string} The file.
44
+ * @throws {unknown} Why there's none: the recorded reason, or an `IndexError` (`missing`).
45
+ */
46
+ export function currentIndexFile(env = process.env) {
47
+ if (chosen) return chosen;
48
+ const local = localIndex(env);
49
+ if (local) return local;
50
+ if (failure) throw failure;
51
+ const cached = readCache(cacheDir({ env }));
52
+ if (cached) return cached.file;
53
+ throw new IndexError('missing', "the Logomarks index isn't downloaded yet: run logomarks update (it's cached after that), or call await ensureIndex() first");
54
+ }
@@ -0,0 +1,43 @@
1
+ import type { CacheRecord } from './cache.js';
2
+
3
+ /** How to get the live set. */
4
+ export interface EnsureOptions {
5
+ /** Ask the site even when the cache is fresh, and fail rather than fall back to the cache (`logomarks update`). */
6
+ force?: boolean;
7
+ /** Use the cache alone (default: `LOGOMARKS_OFFLINE`). */
8
+ offline?: boolean;
9
+ /** The environment read for `LOGOMARKS_INDEX`, `LOGOMARKS_INDEX_URL`, `LOGOMARKS_OFFLINE` and the cache folder. */
10
+ env?: NodeJS.ProcessEnv;
11
+ /** The index's URL (default: `LOGOMARKS_INDEX_URL`, or https://logomarks.ai/cli/index.bin). */
12
+ url?: string;
13
+ /** The cache folder (default: the user's, `cacheDir()`). */
14
+ dir?: string;
15
+ /** How long a checked index counts as fresh, in ms (a day). */
16
+ maxAge?: number;
17
+ /** How long one request may take, in ms (30 s). */
18
+ timeout?: number;
19
+ /** The time now, in ms (for tests). */
20
+ now?: number;
21
+ /** Told when the site couldn't be reached or sent a bad file and the cache is used instead. */
22
+ warn?: (message: string) => void;
23
+ }
24
+
25
+ /** The index file settled on, and how it was had. */
26
+ export interface IndexStatus {
27
+ /** The index file. */
28
+ file: string;
29
+ /** `file`: `LOGOMARKS_INDEX`; `cache`: the cached one; `download`: just downloaded and cached. */
30
+ source: 'file' | 'cache' | 'download';
31
+ /** Whether the site was asked this time. */
32
+ checked: boolean;
33
+ /** The manifest it matched (absent for `LOGOMARKS_INDEX`). */
34
+ manifest?: CacheRecord;
35
+ }
36
+
37
+ /**
38
+ * The live set's index file: `LOGOMARKS_INDEX`; else the cached one, re-checked at most once a day; else downloaded,
39
+ * verified by sha256 and cached. It becomes the file `loadIndex()` reads. Throws an `IndexError` when there's none.
40
+ */
41
+ export function ensureIndex(o?: EnsureOptions): Promise<IndexStatus>;
42
+ /** Runs `ensureIndex` once per process and never throws: `loadIndex()` throws the failure when something needs the set. */
43
+ export function prepareIndex(o?: EnsureOptions): Promise<IndexStatus | undefined>;