@zenera/cli 1.1.10 → 1.1.12

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 (65) hide show
  1. package/README.md +69 -15
  2. package/dist/banner.js +69 -47
  3. package/dist/cache.d.ts +98 -0
  4. package/dist/cache.js +301 -0
  5. package/dist/catalog.d.ts +3 -0
  6. package/dist/catalog.js +35 -11
  7. package/dist/commands/cache.d.ts +7 -0
  8. package/dist/commands/cache.js +245 -0
  9. package/dist/commands/check.js +6 -3
  10. package/dist/commands/index.js +5 -0
  11. package/dist/commands/init.js +8 -4
  12. package/dist/commands/key.js +68 -14
  13. package/dist/commands/memory.d.ts +3 -0
  14. package/dist/commands/memory.js +375 -0
  15. package/dist/commands/models.js +17 -1
  16. package/dist/commands/run.js +11 -3
  17. package/dist/commands/sandbox.js +70 -23
  18. package/dist/engine.js +14 -9
  19. package/dist/home.d.ts +2 -2
  20. package/dist/home.js +2 -2
  21. package/dist/keys.d.ts +22 -0
  22. package/dist/keys.js +105 -2
  23. package/dist/lib.d.ts +1 -0
  24. package/dist/lib.js +1 -0
  25. package/dist/liveness.js +11 -0
  26. package/dist/main.js +9 -0
  27. package/dist/podman.js +43 -9
  28. package/dist/projects.js +10 -2
  29. package/dist/resolve.d.ts +4 -0
  30. package/dist/resolve.js +43 -17
  31. package/dist/scaffold.js +22 -1
  32. package/dist/term.d.ts +23 -2
  33. package/dist/term.js +215 -8
  34. package/dist/tui/app.d.ts +10 -0
  35. package/dist/tui/app.js +478 -54
  36. package/dist/tui/theme.d.ts +13 -2
  37. package/dist/tui/theme.js +26 -13
  38. package/dist/tui/wrap.d.ts +92 -0
  39. package/dist/tui/wrap.js +147 -2
  40. package/dist/validate.d.ts +2 -0
  41. package/dist/validate.js +87 -2
  42. package/package.json +2 -2
  43. package/templates/editor/.github/copilot-instructions.md +200 -55
  44. package/templates/editor/.github/prompts/new-agent.prompt.md +5 -2
  45. package/templates/editor/.github/prompts/review-project.prompt.md +5 -0
  46. package/templates/editor/.github/prompts/sync-with-spec.prompt.md +11 -4
  47. package/templates/editor/.github/skills/zen-cli/SKILL.md +3 -0
  48. package/templates/editor/.github/skills/zen-cli/references/faker.md +18 -8
  49. package/templates/editor/.github/skills/zen-cli/references/keys.md +7 -7
  50. package/templates/editor/.github/skills/zen-cli/references/memory.md +113 -0
  51. package/templates/editor/.github/skills/zen-cli/references/rag.md +2 -2
  52. package/templates/editor/.github/skills/zen-cli/references/run.md +3 -1
  53. package/templates/editor/.github/skills/zen-memory/SKILL.md +555 -0
  54. package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +575 -0
  55. package/templates/editor/.github/skills/{api-schema-index → zen-rag-schema}/SKILL.md +4 -4
  56. package/templates/editor/.vscode/settings.json +4 -2
  57. package/templates/parts/exa.prompt.md.tmpl +4 -0
  58. package/templates/parts/exa.spec.md.tmpl +2 -0
  59. package/templates/project/INSTRUCTIONS.md +8 -0
  60. package/templates/project/SPECIFICATION.md.tmpl +47 -0
  61. package/templates/project/agents/prompts/{default.md → default.md.tmpl} +4 -0
  62. package/templates/project/agents.yaml.tmpl +4 -0
  63. package/templates/project/gitignore +7 -1
  64. package/templates/project/scripts/_setup.sh +101 -0
  65. package/templates/project/SPECIFICATION.md +0 -6
package/README.md CHANGED
@@ -97,6 +97,7 @@ agents:
97
97
  description: Turns a summary into a file on disk.
98
98
  system: agents/prompts/writer.md
99
99
  tools: [workspace:*]
100
+ handoffs: [reader] # a hand-off does not return by itself; give it a way back
100
101
  ```
101
102
 
102
103
  `agents/prompts/reader.md`:
@@ -133,17 +134,27 @@ Credentials live in `$HOME`, so the project never contains a secret.
133
134
 
134
135
  ```
135
136
  my-project/
137
+ SPECIFICATION.md what this is for — the intent
136
138
  INSTRUCTIONS.md house rules, prepended to every agent
137
139
  agents.yaml who exists, what they may reach for
138
140
  agents/
139
141
  prompts/<name>.md each agent's own brief
140
142
  skills/<name>/ knowledge loaded on demand, not always-on
143
+ assets/ reference material, read-only at /assets
144
+ sandbox/Dockerfile the container commands run in
145
+ scripts/_setup.sh the one command that initialises the project
141
146
  sessions/ one workspace, memory and trajectory each
142
147
  <id>/
143
148
  workspace/ what the agents can read and write
144
149
  runs/<id>/ input, output, state, report.html, meta
145
150
  ```
146
151
 
152
+ `SPECIFICATION.md` comes first because the rest implements it. The one `zen init`
153
+ writes is a true specification of the project it just scaffolded; replace it with
154
+ what you are building, then run `/sync-with-spec` in your editor to make the
155
+ files match. See
156
+ [docs/specification.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/specification.md).
157
+
147
158
  `zen init` also writes `.github/copilot-instructions.md` — a standing brief that
148
159
  explains this runtime to whatever coding agent you have open in that folder, so
149
160
  you are not expected to hand-author `agents.yaml`.
@@ -159,8 +170,10 @@ you are not expected to hand-author `agents.yaml`.
159
170
  | `models` | What this machine can use — list, search, test, pick. |
160
171
  | `run` | Runs the project — the TUI on a terminal, a single answer otherwise. |
161
172
  | `inspect` | Opens or rebuilds a run's `report.html`. |
173
+ | `memory` | What the agents remember — size, listing, one node, or a whole page. |
162
174
  | `check` | Validates the project and every file it names, and asks the models. |
163
175
  | `sandbox` | Checks and prepares the container that command-line tools run in. |
176
+ | `cache` | What work has been kept, and getting rid of it. |
164
177
  | `version` | CLI, library and Node versions. |
165
178
 
166
179
  Commands can also come from a package installed alongside this one, so a new
@@ -254,7 +267,7 @@ zen models test vertex:gemini-embedding-001 # one real call, one verdict
254
267
  zen models pick --embedding # the first ref that answers, on stdout
255
268
  ```
256
269
 
257
- Listings are cached for a day in `~/.zenera/neo/catalog`. When a provider cannot
270
+ Listings are cached for a day in `~/.zenera/neo/cache`. When a provider cannot
258
271
  be asked the last listing is used and said to be stale; only if there was never
259
272
  one does a short built-in list stand in.
260
273
 
@@ -292,7 +305,7 @@ The usual one is a **service-account JSON file** — give its path, not its
292
305
  contents. Run the command with nothing piped and it asks:
293
306
 
294
307
  ```sh
295
- zen key add vertex --location us-central1
308
+ zen key add vertex --gcp-location us-central1
296
309
  # Paste the key, or a path to the file: /Users/you/keys/vertex-sa.json
297
310
  ```
298
311
 
@@ -300,23 +313,29 @@ The prompt is read by `zen`, not by your shell, so give a full path there — `~
300
313
  is not expanded. In a script, pipe the path in instead:
301
314
 
302
315
  ```sh
303
- echo ~/keys/vertex-sa.json | zen key add vertex --location us-central1
316
+ echo ~/keys/vertex-sa.json | zen key add vertex --gcp-location us-central1
304
317
  ```
305
318
 
306
319
  The file is copied into `~/.zenera/neo/keys/`, where only you can read it, so
307
320
  moving or cleaning up the original later cannot break it.
308
321
 
309
- - `--location <region>` is worth setting. It must be `global` or a **concrete
310
- region**; multi-region names like `us` are rejected with a 404. `global`
311
- routes across regions and pays about ten seconds of cold start on the first
312
- request each process makes — a region answers in about two.
313
- - `--project <id>` is only needed when the `project_id` inside the file is not
322
+ - `--gcp-location <region>` is worth setting. It takes a concrete region, or one
323
+ of the endpoints that route across regions: `us` and `eu` pool capacity while
324
+ keeping processing inside that territory, `global` takes whatever is free and
325
+ promises no residency. `global` pays about ten seconds of cold start on the
326
+ first request each process makes — a region answers in about two.
327
+ - Which models a location serves is per model, and not guessable. In one
328
+ project, `gemini-embedding-2` answered at `us` but 404'd at `us-central1`,
329
+ while `gemini-2.5-flash` did the opposite. New models often reach `global`,
330
+ `us` and `eu` first. `zen models test vertex:<model>` is what settles it —
331
+ `zen key add` only establishes that the credential itself works.
332
+ - `--gcp-project <id>` is only needed when the `project_id` inside the file is not
314
333
  the project you want.
315
334
 
316
335
  The alternative is an **express-mode API key** — a single secret, stored under
317
336
  `VERTEX_API_KEY`. It is the Vertex console's way of handing out access without a
318
- service account, and it needs neither a project nor a region, so `--project` and
319
- `--location` mean nothing there and are not stored.
337
+ service account, and it needs neither a project nor a region, so `--gcp-project`
338
+ and `--gcp-location` mean nothing there and are not stored.
320
339
 
321
340
  ### Gemini, three ways
322
341
 
@@ -336,15 +355,15 @@ and a region, because the file says which project it belongs to but never which
336
355
  region to call:
337
356
 
338
357
  ```sh
339
- echo ~/keys/vertex-sa.json | zen key add vertex --location us-central1
358
+ echo ~/keys/vertex-sa.json | zen key add vertex --gcp-location us-central1
340
359
  ```
341
360
 
342
- Add `--project` only when the `project_id` inside the file is not the one you
361
+ Add `--gcp-project` only when the `project_id` inside the file is not the one you
343
362
  want to bill:
344
363
 
345
364
  ```sh
346
365
  echo ~/keys/vertex-sa.json \
347
- | zen key add vertex --project other-project --location europe-west4
366
+ | zen key add vertex --gcp-project other-project --gcp-location europe-west4
348
367
  ```
349
368
 
350
369
  **Vertex, express mode** — paste the key at the prompt; no flags apply:
@@ -356,8 +375,8 @@ zen key add vertex
356
375
  Holding several at once is the ordinary case. Name them and switch:
357
376
 
358
377
  ```sh
359
- echo ~/keys/prod-sa.json | zen key add vertex/prod --location us-central1
360
- echo ~/keys/dev-sa.json | zen key add vertex/dev --location global
378
+ echo ~/keys/prod-sa.json | zen key add vertex/prod --gcp-location us-central1
379
+ echo ~/keys/dev-sa.json | zen key add vertex/dev --gcp-location global
361
380
  zen key add vertex/express # the express key, same provider
362
381
 
363
382
  zen key use vertex/dev # which one the next run uses
@@ -413,6 +432,32 @@ clear where a working provider actually comes from.
413
432
  - **Keyring** — `~/.zenera/neo`, readable only by you. Keys are copied into the
414
433
  environment just before a run, so an environment variable you set yourself
415
434
  always wins and a project checked out on a machine without `zen` still runs.
435
+ - **Cache** — `~/.zenera/neo/cache`, one place for work already done: vectors,
436
+ parses, model listings, generated mocks. Shared by every project on the
437
+ machine, and never evicted by anything but you.
438
+
439
+ ## What has already been paid for
440
+
441
+ Embedding a paragraph, parsing a document, asking a provider what it serves —
442
+ all expensive, all perfectly repeatable. They are kept in one store,
443
+ `~/.zenera/neo/cache/<kind>/`, keyed by every input that produced them. Change
444
+ an input and you are asking a different question, which is why nothing in there
445
+ is ever invalidated: it is simply never asked for again.
446
+
447
+ ```
448
+ zen cache ls # what is kept, by kind
449
+ zen cache ls --kind vectors # and what is in one of them
450
+ zen cache prune --older-than 30d # drop what has gone unread for a month
451
+ zen cache prune --max-size 2GB # or keep it under a ceiling
452
+ zen cache clear --kind docs-parse # throw one kind away
453
+ ```
454
+
455
+ Age is when an entry was last _used_, not when it was written, so a vector a
456
+ weekly rebuild reads is never old. Nothing evicts on its own — a store that
457
+ quietly deletes things is only ever noticed when it has deleted the wrong one —
458
+ so retention is a decision made out loud, here. Nothing in it is precious
459
+ either: every entry is work that can be done again, and the only cost of
460
+ removing one is paying for it a second time.
416
461
 
417
462
  ## The library underneath
418
463
 
@@ -425,11 +470,20 @@ terminal:
425
470
 
426
471
  ## Documentation
427
472
 
473
+ [docs/specification.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/specification.md)
474
+ — writing the specification the project implements ·
428
475
  [docs/projects.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/projects.md)
429
476
  — the folder a project is ·
430
477
  [docs/agents-yaml.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/agents-yaml.md)
431
478
  — every key in the configuration file.
432
479
 
480
+ [docs/knowledge.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/knowledge.md)
481
+ — a searchable document index ·
482
+ [docs/integrations.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/integrations.md)
483
+ — an API description, searchable and mockable ·
484
+ [docs/memory.md](https://github.com/andreyryabov/ZeneraNeo/blob/main/docs/memory.md)
485
+ — what agents keep between sessions.
486
+
433
487
  ## License
434
488
 
435
489
  Early days and moving fast — issues, questions and pull requests are welcome.
package/dist/banner.js CHANGED
@@ -8,64 +8,80 @@ import { dim, note } from "./term.js";
8
8
  // terminal — a pipeline asking for the answer gets the answer, and a CI log is
9
9
  // not decorated with block letters.
10
10
  //
11
- // The font is here rather than pulled in as a dependency because it is a
12
- // kilobyte of constant and the whole CLI is otherwise Node's own. Two things
13
- // about its shape are deliberate: the stroke is **two cells wide**, because a
14
- // one-cell stem in a dotted fill reads as noise rather than as a letter, and
15
- // the ink is the dotted block (U+2592) rather than a solid one, which makes a
16
- // thick stroke a screen-print instead of a slab.
11
+ // The font is here rather than pulled in as a dependency because it is a couple
12
+ // of kilobytes of constant and the whole CLI is otherwise Node's own. It is the
13
+ // shadowed block face: a solid stroke lit from the top left with a box-drawing
14
+ // bevel down its right side and along its foot, so the letters read as raised
15
+ // rather than as a texture. Glyphs are variable width and carry their own
16
+ // spacing, so they abut directly — the gap belongs to the letter, not the join.
17
17
  //
18
18
  // A terminal too narrow for the art gets the wordmark on one line. A banner
19
19
  // that wraps is worse than no banner.
20
20
  // ---------------------------------------------------------------------------
21
- const HEIGHT = 5;
22
- const WIDTH = 6;
21
+ const HEIGHT = 6;
22
+ /** Between the two words. Wide enough that they read as two, not as one. */
23
+ const GAP = 3;
24
+ /** A left margin, so the art does not start against the edge of the screen. */
25
+ const INDENT = 2;
23
26
  const FONT = {
24
- A: [' ▒▒▒▒ ', '▒▒ ▒▒', '▒▒▒▒▒▒', '▒▒ ▒▒', '▒▒ ▒▒'],
25
- B: ['▒▒▒▒▒ ', '▒▒ ▒▒', '▒▒▒▒▒ ', '▒▒ ▒▒', '▒▒▒▒▒ '],
26
- C: [' ▒▒▒▒▒', '▒▒ ', '▒▒ ', '▒▒ ', ' ▒▒▒▒▒'],
27
- D: ['▒▒▒▒▒ ', '▒▒ ▒▒', '▒▒ ▒▒', '▒▒ ▒▒', '▒▒▒▒▒ '],
28
- E: ['▒▒▒▒▒▒', '▒▒ ', '▒▒▒▒▒ ', '▒▒ ', '▒▒▒▒▒▒'],
29
- F: ['▒▒▒▒▒▒', '▒▒ ', '▒▒▒▒▒ ', '▒▒ ', '▒▒ '],
30
- G: [' ▒▒▒▒▒', '▒▒ ', '▒▒ ▒▒▒', '▒▒ ▒▒', ' ▒▒▒▒▒'],
31
- H: ['▒▒ ▒▒', '▒▒ ▒▒', '▒▒▒▒▒▒', '▒▒ ▒▒', '▒▒ ▒▒'],
32
- I: ['▒▒▒▒▒▒', ' ▒▒ ', ' ▒▒ ', ' ▒▒ ', '▒▒▒▒▒▒'],
33
- J: ['▒▒▒▒▒▒', ' ▒▒ ', ' ▒▒ ', '▒▒ ▒▒ ', ' ▒▒▒ '],
34
- K: ['▒▒ ▒▒', '▒▒ ▒▒ ', '▒▒▒▒ ', '▒▒ ▒▒ ', '▒▒ ▒▒'],
35
- L: ['▒▒ ', '▒▒ ', '▒▒ ', '▒▒ ', '▒▒▒▒▒▒'],
36
- M: ['▒▒ ▒▒', '▒▒▒▒▒▒', '▒▒▒▒▒▒', '▒▒ ▒▒', '▒▒ ▒▒'],
37
- N: ['▒▒ ▒▒', '▒▒▒ ▒▒', '▒▒▒▒▒▒', '▒▒ ▒▒▒', '▒▒ ▒▒'],
38
- O: [' ▒▒▒▒ ', '▒▒ ▒▒', '▒▒ ▒▒', '▒▒ ▒▒', ' ▒▒▒▒ '],
39
- P: ['▒▒▒▒▒ ', '▒▒ ▒▒', '▒▒▒▒▒ ', '▒▒ ', '▒▒ '],
40
- Q: [' ▒▒▒▒ ', '▒▒ ▒▒', '▒▒ ▒▒', '▒▒ ▒▒ ', ' ▒▒ ▒▒'],
41
- R: ['▒▒▒▒▒ ', '▒▒ ▒▒', '▒▒▒▒▒ ', '▒▒ ▒▒ ', '▒▒ ▒▒'],
42
- S: [' ▒▒▒▒▒', '▒▒ ', ' ▒▒▒▒ ', ' ▒▒', '▒▒▒▒▒ '],
43
- T: ['▒▒▒▒▒▒', ' ▒▒ ', ' ▒▒ ', ' ▒▒ ', ' ▒▒ '],
44
- U: ['▒▒ ▒▒', '▒▒ ▒▒', '▒▒ ▒▒', '▒▒ ▒▒', ' ▒▒▒▒ '],
45
- V: ['▒▒ ▒▒', '▒▒ ▒▒', '▒▒ ▒▒', ' ▒▒▒▒ ', ' ▒▒ '],
46
- W: ['▒▒ ▒▒', '▒▒ ▒▒', '▒▒▒▒▒▒', '▒▒▒▒▒▒', '▒▒ ▒▒'],
47
- X: ['▒▒ ▒▒', ' ▒▒▒▒ ', ' ▒▒ ', ' ▒▒▒▒ ', '▒▒ ▒▒'],
48
- Y: ['▒▒ ▒▒', ' ▒▒▒▒ ', ' ▒▒ ', ' ▒▒ ', ' ▒▒ '],
49
- Z: ['▒▒▒▒▒▒', ' ▒▒ ', ' ▒▒ ', ' ▒▒ ', '▒▒▒▒▒▒'],
27
+ A: [' █████╗ ', '██╔══██╗', '███████║', '██╔══██║', '██║ ██║', '╚═╝ ╚═╝'],
28
+ B: ['██████╗ ', '██╔══██╗', '██████╔╝', '██╔══██╗', '██████╔╝', '╚═════╝ '],
29
+ C: [' ██████╗', '██╔════╝', '██║ ', '██║ ', '╚██████╗', ' ╚═════╝'],
30
+ D: ['██████╗ ', '██╔══██╗', '██║ ██║', '██║ ██║', '██████╔╝', '╚═════╝ '],
31
+ E: ['███████╗', '██╔════╝', '█████╗ ', '██╔══╝ ', '███████╗', '╚══════╝'],
32
+ F: ['███████╗', '██╔════╝', '█████╗ ', '██╔══╝ ', '██║ ', '╚═╝ '],
33
+ G: [' ██████╗ ', '██╔════╝ ', '██║ ███╗', '██║ ██║', '╚██████╔╝', ' ╚═════╝ '],
34
+ H: ['██╗ ██╗', '██║ ██║', '███████║', '██╔══██║', '██║ ██║', '╚═╝ ╚═╝'],
35
+ I: ['██╗', '██║', '██║', '██║', '██║', '╚═╝'],
36
+ J: [' ██╗', ' ██║', ' ██║', '██ ██║', '╚█████╔╝', ' ╚════╝ '],
37
+ K: ['██╗ ██╗', '██║ ██╔╝', '█████╔╝ ', '██╔═██╗ ', '██║ ██╗', '╚═╝ ╚═╝'],
38
+ L: ['██╗ ', '██║ ', '██║ ', '██║ ', '███████╗', '╚══════╝'],
39
+ M: ['███╗ ███╗', '████╗ ████║', '██╔████╔██║', '██║╚██╔╝██║', '██║ ╚═╝ ██║', '╚═╝ ╚═╝'],
40
+ N: ['███╗ ██╗', '████╗ ██║', '██╔██╗ ██║', '██║╚██╗██║', '██║ ╚████║', '╚═╝ ╚═══╝'],
41
+ O: [' ██████╗ ', '██╔═══██╗', '██║ ██║', '██║ ██║', '╚██████╔╝', ' ╚═════╝ '],
42
+ P: ['██████╗ ', '██╔══██╗', '██████╔╝', '██╔═══╝ ', '██║ ', '╚═╝ '],
43
+ Q: [' ██████╗ ', '██╔═══██╗', '██║ ██║', '██║▄▄ ██║', '╚██████╔╝', ' ╚══▀▀═╝ '],
44
+ R: ['██████╗ ', '██╔══██╗', '██████╔╝', '██╔══██╗', '██║ ██║', '╚═╝ ╚═╝'],
45
+ S: ['███████╗', '██╔════╝', '███████╗', '╚════██║', '███████║', '╚══════╝'],
46
+ T: ['████████╗', '╚══██╔══╝', ' ██║ ', ' ██║ ', ' ██║ ', ' ╚═╝ '],
47
+ U: ['██╗ ██╗', '██║ ██║', '██║ ██║', '██║ ██║', '╚██████╔╝', ' ╚═════╝ '],
48
+ V: ['██╗ ██╗', '██║ ██║', '██║ ██║', '╚██╗ ██╔╝', ' ╚████╔╝ ', ' ╚═══╝ '],
49
+ W: ['██╗ ██╗', '██║ ██║', '██║ █╗ ██║', '██║███╗██║', '╚███╔███╔╝', ' ╚══╝╚══╝ '],
50
+ X: ['██╗ ██╗', '╚██╗██╔╝', ' ╚███╔╝ ', ' ██╔██╗ ', '██╔╝ ██╗', '╚═╝ ╚═╝'],
51
+ Y: ['██╗ ██╗', '╚██╗ ██╔╝', ' ╚████╔╝ ', ' ╚██╔╝ ', ' ██║ ', ' ╚═╝ '],
52
+ Z: ['███████╗', '╚══███╔╝', ' ███╔╝ ', ' ███╔╝ ', '███████╗', '╚══════╝'],
50
53
  };
51
- const BLANK = ' '.repeat(WIDTH);
52
- /** One word as five rows of equal length — so two words line up when joined. */
54
+ const BLANK = [' ', ' ', ' ', ' ', ' ', ' '];
55
+ /** One word as six rows of equal length — so two words line up when joined. */
53
56
  function big(word) {
54
57
  const rows = new Array(HEIGHT).fill('');
55
58
  for (const ch of word.toUpperCase()) {
56
- const glyph = FONT[ch];
59
+ const glyph = FONT[ch] ?? BLANK;
57
60
  for (let r = 0; r < HEIGHT; r++) {
58
- rows[r] += `${glyph?.[r] ?? BLANK} `;
61
+ rows[r] += glyph[r];
59
62
  }
60
63
  }
61
64
  return rows;
62
65
  }
63
66
  /** A wordmark rather than a sentence, so the letters are set apart. */
64
67
  const spaced = (s) => [...s.toUpperCase()].join(' ');
65
- /** The name, in the brightest thing the terminal has. */
66
- const bright = (s) => styleText(['bold', 'whiteBright'], s);
67
- /** The half that carries the colour. */
68
- const neon = (s) => styleText(['bold', 'magentaBright'], s);
68
+ /** Whether anything should be coloured at all — `styleText`'s own answer. */
69
+ const styling = () => styleText('dim', '.') !== '.';
70
+ const RESET = '\u001b[0m';
71
+ const HEAD = { face: '\u001b[1;38;5;231m', shade: '\u001b[0;38;5;244m' };
72
+ const ACCENT = { face: '\u001b[1;38;5;208m', shade: '\u001b[0;38;5;130m' };
73
+ /** Runs of stroke and runs of bevel, alternating; spaces stay unstyled. */
74
+ const RUNS = /[█▀▄]+|[^█▀▄ ]+/g;
75
+ const isStroke = (run) => /[█▀▄]/.test(run);
76
+ /**
77
+ * Two tones per word — without the darker bevel the letter is a flat outline
78
+ * and the shadow reads as part of the stroke.
79
+ */
80
+ const paint = (text, tone) => styling()
81
+ ? text.replace(RUNS, (run) => `${isStroke(run) ? tone.face : tone.shade}${run}${RESET}`)
82
+ : text;
83
+ /** The wordmark has no bevel to shade, so it is all face. */
84
+ const flat = (text, tone) => styling() ? `${tone.face}${text}${RESET}` : text;
69
85
  export const NEO_BANNER = {
70
86
  head: 'Zenera',
71
87
  accent: 'Neo',
@@ -76,17 +92,23 @@ export function bannerLines(text, columns = process.stderr.columns || 80) {
76
92
  // Trimmed: the widest accent row is the banner's right edge, and a pad left
77
93
  // inside a styled string cannot be trimmed away later.
78
94
  const accent = big(text.accent).map((row) => row.trimEnd());
79
- const width = 2 + head[0].length + Math.max(...accent.map((row) => row.length));
80
- const foot = ` ${dim(spaced(text.subtitle))}`;
95
+ const margin = ' '.repeat(INDENT);
96
+ const width = INDENT + head[0].length + GAP + Math.max(...accent.map((row) => row.length));
81
97
  if (width > columns) {
82
- return [` ${bright(text.head.toUpperCase())} ${neon(text.accent.toUpperCase())}`, foot];
98
+ return [
99
+ `${margin}${flat(text.head.toUpperCase(), HEAD)} ${flat(text.accent.toUpperCase(), ACCENT)}`,
100
+ `${margin}${dim(spaced(text.subtitle))}`,
101
+ ];
83
102
  }
84
103
  const lines = [];
85
104
  for (let r = 0; r < HEIGHT; r++) {
86
- lines.push(` ${bright(head[r])} ${neon(accent[r])}`);
105
+ lines.push(`${margin}${paint(head[r], HEAD)}${' '.repeat(GAP)}${paint(accent[r], ACCENT)}`);
87
106
  }
107
+ // Rules on both sides, centred under the art: the subtitle is a caption,
108
+ // and without them a short line of spaced capitals floats.
109
+ const caption = `─── ${spaced(text.subtitle)} ───`;
88
110
  lines.push('');
89
- lines.push(foot);
111
+ lines.push(`${' '.repeat(Math.max(INDENT, Math.round((width - caption.length) / 2)))}${dim(caption)}`);
90
112
  return lines;
91
113
  }
92
114
  /** Narration, and only for someone watching. */
@@ -0,0 +1,98 @@
1
+ export interface CacheOptions {
2
+ /** where the store lives; defaults to `~/.zenera/neo/cache` */
3
+ dir?: string;
4
+ /** file mode, 0600 unless the value is public */
5
+ mode?: number;
6
+ }
7
+ export interface CacheEntry<T> {
8
+ value: T;
9
+ /** when it was written, for callers that have an opinion about freshness */
10
+ storedAt: string;
11
+ }
12
+ /** What a `Cache` does, so that `NO_CACHE` can be one without being one. */
13
+ export interface CacheStore {
14
+ readonly kind: string;
15
+ readonly hits: number;
16
+ readonly misses: number;
17
+ entry<T>(key: string): CacheEntry<T> | undefined;
18
+ get<T>(key: string): T | undefined;
19
+ put(key: string, value: unknown): void;
20
+ delete(key: string): void;
21
+ commit(): void;
22
+ }
23
+ /** Joins the parts of a composite key. NUL cannot occur in any of them. */
24
+ export declare function cacheKey(...parts: readonly (string | number | undefined)[]): string;
25
+ export declare class Cache implements CacheStore {
26
+ #private;
27
+ readonly kind: string;
28
+ constructor(kind: string, options?: CacheOptions);
29
+ get hits(): number;
30
+ get misses(): number;
31
+ entry<T>(key: string): CacheEntry<T> | undefined;
32
+ get<T>(key: string): T | undefined;
33
+ put(key: string, value: unknown): void;
34
+ delete(key: string): void;
35
+ commit(): void;
36
+ }
37
+ /** Remembers nothing, for `--no-cache` and for anywhere a store cannot be opened. */
38
+ export declare const NO_CACHE: CacheStore;
39
+ export interface CacheKind {
40
+ kind: string;
41
+ entries: number;
42
+ bytes: number;
43
+ /** epoch ms of the least and most recently used entry; absent when empty */
44
+ oldest?: number;
45
+ newest?: number;
46
+ }
47
+ export interface CacheItem {
48
+ kind: string;
49
+ key: string;
50
+ bytes: number;
51
+ /** epoch ms, last used */
52
+ usedAt: number;
53
+ storedAt?: string;
54
+ /** what was stored — the file is parsed anyway to recover the key */
55
+ value?: unknown;
56
+ }
57
+ /** A row per kind, for `zen cache ls`. One stat walk; nothing is parsed. */
58
+ export declare function kinds(dir?: string): CacheKind[];
59
+ /**
60
+ * The entries of one kind, most recently used first. Reading a key means
61
+ * parsing the file, so the sort happens on stat data and only what will be
62
+ * shown is opened.
63
+ */
64
+ export declare function items(kind: string, options?: {
65
+ dir?: string;
66
+ limit?: number;
67
+ }): {
68
+ rows: CacheItem[];
69
+ found: number;
70
+ };
71
+ export interface SweepOptions {
72
+ dir?: string;
73
+ /** only this kind */
74
+ kind?: string;
75
+ /** anything unused for longer than this */
76
+ olderThanMs?: number;
77
+ /** a ceiling on what is left, met by removing the least recently used */
78
+ maxBytes?: number;
79
+ }
80
+ export interface Swept {
81
+ kind: string;
82
+ removed: number;
83
+ bytes: number;
84
+ }
85
+ /**
86
+ * Removes what the options describe and reports what went, by kind.
87
+ *
88
+ * `maxBytes` applies to the whole selection rather than to each kind
89
+ * separately: the disk is one thing, and least-recently-used across the store
90
+ * is the only ordering that means anything on it.
91
+ */
92
+ export declare function sweep(options?: SweepOptions): Swept[];
93
+ /** Everything, or one kind of everything. */
94
+ export declare function clear(options?: {
95
+ dir?: string;
96
+ kind?: string;
97
+ }): void;
98
+ //# sourceMappingURL=cache.d.ts.map