@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.
- package/README.md +69 -15
- package/dist/banner.js +69 -47
- package/dist/cache.d.ts +98 -0
- package/dist/cache.js +301 -0
- package/dist/catalog.d.ts +3 -0
- package/dist/catalog.js +35 -11
- package/dist/commands/cache.d.ts +7 -0
- package/dist/commands/cache.js +245 -0
- package/dist/commands/check.js +6 -3
- package/dist/commands/index.js +5 -0
- package/dist/commands/init.js +8 -4
- package/dist/commands/key.js +68 -14
- package/dist/commands/memory.d.ts +3 -0
- package/dist/commands/memory.js +375 -0
- package/dist/commands/models.js +17 -1
- package/dist/commands/run.js +11 -3
- package/dist/commands/sandbox.js +70 -23
- package/dist/engine.js +14 -9
- package/dist/home.d.ts +2 -2
- package/dist/home.js +2 -2
- package/dist/keys.d.ts +22 -0
- package/dist/keys.js +105 -2
- package/dist/lib.d.ts +1 -0
- package/dist/lib.js +1 -0
- package/dist/liveness.js +11 -0
- package/dist/main.js +9 -0
- package/dist/podman.js +43 -9
- package/dist/projects.js +10 -2
- package/dist/resolve.d.ts +4 -0
- package/dist/resolve.js +43 -17
- package/dist/scaffold.js +22 -1
- package/dist/term.d.ts +23 -2
- package/dist/term.js +215 -8
- package/dist/tui/app.d.ts +10 -0
- package/dist/tui/app.js +478 -54
- package/dist/tui/theme.d.ts +13 -2
- package/dist/tui/theme.js +26 -13
- package/dist/tui/wrap.d.ts +92 -0
- package/dist/tui/wrap.js +147 -2
- package/dist/validate.d.ts +2 -0
- package/dist/validate.js +87 -2
- package/package.json +2 -2
- package/templates/editor/.github/copilot-instructions.md +200 -55
- package/templates/editor/.github/prompts/new-agent.prompt.md +5 -2
- package/templates/editor/.github/prompts/review-project.prompt.md +5 -0
- package/templates/editor/.github/prompts/sync-with-spec.prompt.md +11 -4
- package/templates/editor/.github/skills/zen-cli/SKILL.md +3 -0
- package/templates/editor/.github/skills/zen-cli/references/faker.md +18 -8
- package/templates/editor/.github/skills/zen-cli/references/keys.md +7 -7
- package/templates/editor/.github/skills/zen-cli/references/memory.md +113 -0
- package/templates/editor/.github/skills/zen-cli/references/rag.md +2 -2
- package/templates/editor/.github/skills/zen-cli/references/run.md +3 -1
- package/templates/editor/.github/skills/zen-memory/SKILL.md +555 -0
- package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +575 -0
- package/templates/editor/.github/skills/{api-schema-index → zen-rag-schema}/SKILL.md +4 -4
- package/templates/editor/.vscode/settings.json +4 -2
- package/templates/parts/exa.prompt.md.tmpl +4 -0
- package/templates/parts/exa.spec.md.tmpl +2 -0
- package/templates/project/INSTRUCTIONS.md +8 -0
- package/templates/project/SPECIFICATION.md.tmpl +47 -0
- package/templates/project/agents/prompts/{default.md → default.md.tmpl} +4 -0
- package/templates/project/agents.yaml.tmpl +4 -0
- package/templates/project/gitignore +7 -1
- package/templates/project/scripts/_setup.sh +101 -0
- 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/
|
|
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
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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`
|
|
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
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
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 =
|
|
22
|
-
|
|
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 = ' '
|
|
52
|
-
/** One word as
|
|
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] +=
|
|
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
|
-
/**
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
const
|
|
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
|
|
80
|
-
const
|
|
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 [
|
|
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(
|
|
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(
|
|
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. */
|
package/dist/cache.d.ts
ADDED
|
@@ -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
|