@zenera/cli 1.1.19 → 1.1.20
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 +6 -0
- package/dist/banner.d.ts +8 -0
- package/dist/banner.js +2 -0
- package/dist/commands/cache.js +1 -0
- package/dist/commands/check.js +34 -2
- package/dist/commands/index.js +1 -1
- package/dist/commands/init.js +1 -1
- package/dist/commands/meta.js +1 -1
- package/dist/commands/open.js +1 -1
- package/dist/main.js +8 -1
- package/dist/meta.js +19 -7
- package/dist/scaffold.d.ts +37 -0
- package/dist/scaffold.js +67 -3
- package/dist/term.d.ts +2 -0
- package/dist/term.js +25 -0
- package/dist/tui/app.js +237 -65
- package/dist/tui/theme.d.ts +10 -0
- package/dist/tui/theme.js +4 -0
- package/dist/tui/wrap.d.ts +100 -18
- package/dist/tui/wrap.js +500 -27
- package/dist/validate.js +115 -9
- package/package.json +2 -2
- package/templates/editor/.github/copilot-instructions.md +33 -8
- package/templates/editor/.github/skills/zen-cli/references/projects.md +19 -8
- package/templates/editor/.github/skills/zen-memory/SKILL.md +77 -33
- package/templates/editor/.github/skills/zen-memory/scripts/check-instructions.sh +2 -1
- package/templates/editor/.vscode/agents.schema.json +611 -0
- package/templates/editor/.vscode/extensions.json +3 -0
- package/templates/editor/.vscode/settings.json +8 -0
- package/templates/project/agents/fork-instructions.md +151 -0
- package/templates/project/agents/memory-instructions.md +120 -8
package/README.md
CHANGED
|
@@ -377,6 +377,12 @@ clones it types `zen init repo-notes` once - a directory that is already a
|
|
|
377
377
|
project is registered, not rebuilt, and nothing in it is touched - and the name
|
|
378
378
|
is a command on their machine too.
|
|
379
379
|
|
|
380
|
+
When a newer `zen` is installed, `zen check --fix` brings a project's copies of
|
|
381
|
+
_our_ files up to date: the memory and tool house rules under `agents/`, and the
|
|
382
|
+
`.vscode/` and `.github/` trees. They restate how the version in hand behaves,
|
|
383
|
+
so a copy left behind by an upgrade is worse than none. Nothing that describes
|
|
384
|
+
_your_ project is touched.
|
|
385
|
+
|
|
380
386
|
## A project is a folder
|
|
381
387
|
|
|
382
388
|
Prompts, agent wiring, skills and tool selections are all files - Markdown and
|
package/dist/banner.d.ts
CHANGED
|
@@ -4,6 +4,10 @@ declare const ACCENTS: {
|
|
|
4
4
|
readonly face: "\u001B[1;38;5;208m";
|
|
5
5
|
readonly shade: "\u001B[0;38;5;130m";
|
|
6
6
|
};
|
|
7
|
+
readonly green: {
|
|
8
|
+
readonly face: "\u001B[1;38;5;41m";
|
|
9
|
+
readonly shade: "\u001B[0;38;5;29m";
|
|
10
|
+
};
|
|
7
11
|
readonly cyan: {
|
|
8
12
|
readonly face: "\u001B[1;38;5;51m";
|
|
9
13
|
readonly shade: "\u001B[0;38;5;37m";
|
|
@@ -12,6 +16,10 @@ declare const ACCENTS: {
|
|
|
12
16
|
readonly face: "\u001B[1;38;5;121m";
|
|
13
17
|
readonly shade: "\u001B[0;38;5;78m";
|
|
14
18
|
};
|
|
19
|
+
readonly gold: {
|
|
20
|
+
readonly face: "\u001B[1;38;5;220m";
|
|
21
|
+
readonly shade: "\u001B[0;38;5;178m";
|
|
22
|
+
};
|
|
15
23
|
};
|
|
16
24
|
export type Hue = keyof typeof ACCENTS;
|
|
17
25
|
export interface BannerText {
|
package/dist/banner.js
CHANGED
|
@@ -72,8 +72,10 @@ const HEADS = {
|
|
|
72
72
|
/** One per sub-brand, so two of them are told apart before the word is read. */
|
|
73
73
|
const ACCENTS = {
|
|
74
74
|
orange: { face: '\u001b[1;38;5;208m', shade: '\u001b[0;38;5;130m' },
|
|
75
|
+
green: { face: '\u001b[1;38;5;41m', shade: '\u001b[0;38;5;29m' },
|
|
75
76
|
cyan: { face: '\u001b[1;38;5;51m', shade: '\u001b[0;38;5;37m' },
|
|
76
77
|
mint: { face: '\u001b[1;38;5;121m', shade: '\u001b[0;38;5;78m' },
|
|
78
|
+
gold: { face: '\u001b[1;38;5;220m', shade: '\u001b[0;38;5;178m' },
|
|
77
79
|
};
|
|
78
80
|
/** Runs of stroke and runs of bevel, alternating; spaces stay unstyled. */
|
|
79
81
|
const RUNS = /[█▀▄]+|[^█▀▄ ]+/g;
|
package/dist/commands/cache.js
CHANGED
|
@@ -20,6 +20,7 @@ const LISTED = 20;
|
|
|
20
20
|
export const cache = {
|
|
21
21
|
summary: 'What work has been kept, and getting rid of it.',
|
|
22
22
|
usage: USAGE,
|
|
23
|
+
banner: { head: 'Zenera', accent: 'Cache', subtitle: 'Stored Work', hue: 'gold' },
|
|
23
24
|
details: [
|
|
24
25
|
' ls What is stored, by kind. Changes nothing.',
|
|
25
26
|
' prune Remove what the filters name.',
|
package/dist/commands/check.js
CHANGED
|
@@ -6,9 +6,10 @@ import { KeyStore } from "../keys.js";
|
|
|
6
6
|
import { duration } from "../narrate.js";
|
|
7
7
|
import { Registry } from "../projects.js";
|
|
8
8
|
import { project as resolveProject } from "../resolve.js";
|
|
9
|
+
import { refreshShared } from "../scaffold.js";
|
|
9
10
|
import { bold, count, cyan, dim, green, invalidError, json, progress, red, table, usageError, write, writeAll, yellow, } from "../term.js";
|
|
10
11
|
import { validateProject, } from "../validate.js";
|
|
11
|
-
const USAGE = 'zen check [name|dir] [--project <name|dir>] [--no-sandbox] [--no-models] [--strict] [--quiet]';
|
|
12
|
+
const USAGE = 'zen check [name|dir] [--project <name|dir>] [--fix] [--no-sandbox] [--no-models] [--strict] [--quiet]';
|
|
12
13
|
// ---------------------------------------------------------------------------
|
|
13
14
|
// zen check
|
|
14
15
|
//
|
|
@@ -52,6 +53,14 @@ export const check = {
|
|
|
52
53
|
'Unlike a run, it does not stop at the first problem — the report lists',
|
|
53
54
|
'everything it found, each with a code and the fix for it.',
|
|
54
55
|
'',
|
|
56
|
+
'--fix rewrites the files that are ours rather than the project\u2019s: the',
|
|
57
|
+
'memory and tool house rules under agents/, and the editor tree',
|
|
58
|
+
'(.vscode/ and .github/). They restate how this version of zen behaves,',
|
|
59
|
+
'so a copy left behind by an upgrade is worse than none — they are',
|
|
60
|
+
'replaced whether or not they were edited, and the report that follows is',
|
|
61
|
+
'of the project as repaired. Nothing else is touched: agents.yaml, the',
|
|
62
|
+
'prompts, the specification and agents/instructions.md are yours.',
|
|
63
|
+
'',
|
|
55
64
|
'The argument is a directory if one is there and a registered project',
|
|
56
65
|
'name otherwise; with neither, the project you are standing in.',
|
|
57
66
|
'',
|
|
@@ -61,6 +70,7 @@ export const check = {
|
|
|
61
70
|
run: async (ctx) => {
|
|
62
71
|
const { values, positionals } = parse(ctx.args, {
|
|
63
72
|
project: { type: 'string' },
|
|
73
|
+
fix: { type: 'boolean' },
|
|
64
74
|
'no-sandbox': { type: 'boolean' },
|
|
65
75
|
'no-models': { type: 'boolean' },
|
|
66
76
|
strict: { type: 'boolean' },
|
|
@@ -74,6 +84,11 @@ export const check = {
|
|
|
74
84
|
// read here and handed to the check rather than looked up inside it.
|
|
75
85
|
const entry = (await Registry.open()).findPath(dir);
|
|
76
86
|
const name = entry?.name ?? basename(dir);
|
|
87
|
+
// Before the report rather than after it, so what it says is true of
|
|
88
|
+
// the files that are there now and the exit code answers the repaired
|
|
89
|
+
// project. A fix that left an error behind in the output would be read
|
|
90
|
+
// as a fix that failed.
|
|
91
|
+
const fixed = values.fix ? refreshShared(dir) : [];
|
|
77
92
|
// Materialised first, so the credential verdicts are the ones a run
|
|
78
93
|
// would reach: a key in the environment and a key in the keyring are
|
|
79
94
|
// the same key by the time the library asks. The project's own `.env`
|
|
@@ -99,13 +114,15 @@ export const check = {
|
|
|
99
114
|
});
|
|
100
115
|
bar.done();
|
|
101
116
|
if (ctx.json) {
|
|
102
|
-
json(report);
|
|
117
|
+
json({ ...report, fixed });
|
|
103
118
|
}
|
|
104
119
|
else if (values.quiet) {
|
|
120
|
+
writeAll(fixedLines(fixed));
|
|
105
121
|
writeAll(findingLines(report.findings));
|
|
106
122
|
write(verdict(report, Boolean(values.strict)));
|
|
107
123
|
}
|
|
108
124
|
else {
|
|
125
|
+
writeAll(fixedLines(fixed));
|
|
109
126
|
writeAll(render(report));
|
|
110
127
|
}
|
|
111
128
|
const failed = report.counts.errors > 0 || (Boolean(values.strict) && report.counts.warnings > 0);
|
|
@@ -303,6 +320,21 @@ function agentBlock(agent) {
|
|
|
303
320
|
}
|
|
304
321
|
return [head, ...table(rows)];
|
|
305
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* What `--fix` replaced, said before the report so the two read in the order
|
|
325
|
+
* they happened. Silent when nothing was asked for, so the only difference
|
|
326
|
+
* between `zen check` and `zen check --fix` on a healthy project is these lines.
|
|
327
|
+
*/
|
|
328
|
+
function fixedLines(fixed) {
|
|
329
|
+
if (fixed.length === 0) {
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
return [
|
|
333
|
+
`${bold('Replaced')} ${dim(`${count(fixed.length, 'file')} \u2014 ours, not the project's`)}`,
|
|
334
|
+
...fixed.map((f) => ` ${green('wrote')} ${f}`),
|
|
335
|
+
'',
|
|
336
|
+
];
|
|
337
|
+
}
|
|
306
338
|
/**
|
|
307
339
|
* One finding, three lines at most: what and where, then why, then the fix.
|
|
308
340
|
* The code is printed because it is the part a script — or a model asked to
|
package/dist/commands/index.js
CHANGED
|
@@ -59,7 +59,7 @@ export const EXTERNAL = {
|
|
|
59
59
|
summary: 'Retrieval over a corpus: index it, then ask it something.',
|
|
60
60
|
usage: 'zen rag <subject> <command> [args...]',
|
|
61
61
|
install: 'npm i -g @zenera/rag',
|
|
62
|
-
banner: { head: 'Zenera', accent: 'Rag', subtitle: 'Corpus Retrieval' },
|
|
62
|
+
banner: { head: 'Zenera', accent: 'Rag', subtitle: 'Corpus Retrieval', hue: 'green' },
|
|
63
63
|
},
|
|
64
64
|
};
|
|
65
65
|
//# sourceMappingURL=index.js.map
|
package/dist/commands/init.js
CHANGED
|
@@ -130,7 +130,7 @@ export const init = {
|
|
|
130
130
|
'Writes SPECIFICATION.md, agents.yaml, agents/ (house rules, the memory',
|
|
131
131
|
'rules and one role prompt) and scripts/, then records the directory so',
|
|
132
132
|
'`zen list` and `zen open` can find it by name. Editor files',
|
|
133
|
-
'(.vscode/
|
|
133
|
+
'(.vscode/ and the .github/ tree) are written alongside,',
|
|
134
134
|
'replacing any already there.',
|
|
135
135
|
'',
|
|
136
136
|
'A directory that is already a project is adopted rather than rebuilt:',
|
package/dist/commands/meta.js
CHANGED
|
@@ -56,7 +56,7 @@ export const meta = {
|
|
|
56
56
|
'Nothing it needs is put on a command line: every credential reaches it',
|
|
57
57
|
'through the environment, where other processes cannot read it.',
|
|
58
58
|
'',
|
|
59
|
-
'The editor files `zen init` writes (.vscode/
|
|
59
|
+
'The editor files `zen init` writes (.vscode/ and the .github/',
|
|
60
60
|
'tree) are refreshed in the project first, the way `zen open` refreshes',
|
|
61
61
|
'them: the agent is about to read that brief, and it describes this version',
|
|
62
62
|
'of zen. Edits to them do not survive.',
|
package/dist/commands/open.js
CHANGED
|
@@ -20,7 +20,7 @@ export const open = {
|
|
|
20
20
|
'Editor: --editor, then $ZENERA_EDITOR, then the editor this terminal',
|
|
21
21
|
'belongs to, then $VISUAL or $EDITOR, then a known editor on PATH or',
|
|
22
22
|
'installed, then the platform opener.',
|
|
23
|
-
'The editor files `zen init` writes (.vscode/
|
|
23
|
+
'The editor files `zen init` writes (.vscode/ and the',
|
|
24
24
|
'.github/ tree) are refreshed in the directory being opened; edits to',
|
|
25
25
|
'them do not survive.',
|
|
26
26
|
'VS Code and its forks are launched with --disable-workspace-trust so',
|
package/dist/main.js
CHANGED
|
@@ -6,7 +6,7 @@ import { NEO_BANNER, printBanner } from "./banner.js";
|
|
|
6
6
|
import { ALIASES, COMMANDS, EXTERNAL } from "./commands/index.js";
|
|
7
7
|
import { cliManifest, versionOf } from "./commands/version.js";
|
|
8
8
|
import { hasExternal, loadExternal } from "./external.js";
|
|
9
|
-
import { CliError, EXIT, bold, cyan, dim, fail, note, pad, write } from "./term.js";
|
|
9
|
+
import { CliError, EXIT, bold, cyan, dim, fail, note, pad, title, write } from "./term.js";
|
|
10
10
|
/** What the user typed: `zen`, `zn` or `zenera` all arrive here. */
|
|
11
11
|
const NAME = invokedAs('zen');
|
|
12
12
|
/** Usage lines are written against `zen`. Say them back in the reader's word. */
|
|
@@ -22,6 +22,8 @@ const spell = (usage) => (NAME === 'zen' ? usage : usage.replace(/^zen\b/, NAME)
|
|
|
22
22
|
// inside `run`. Everything on this path is Node's own, which is why `zen --help`
|
|
23
23
|
// starts instantly and why the CLI adds no weight to the library.
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
25
|
+
/** Commands whose name is not simply its first letter capitalised. */
|
|
26
|
+
const TITLES = { rag: 'RAG' };
|
|
25
27
|
const GLOBAL = {
|
|
26
28
|
help: { type: 'boolean', short: 'h' },
|
|
27
29
|
version: { type: 'boolean', short: 'v' },
|
|
@@ -54,6 +56,11 @@ async function main(argv) {
|
|
|
54
56
|
const name = parts.name ? (ALIASES[parts.name] ?? parts.name) : undefined;
|
|
55
57
|
const command = name ? COMMANDS[name] : undefined;
|
|
56
58
|
const external = name && !command ? EXTERNAL[name] : undefined;
|
|
59
|
+
// Named before anything is printed, so a run that takes an hour is
|
|
60
|
+
// identifiable in `ps` and on its own tab for the whole of it.
|
|
61
|
+
if (name && (command || external)) {
|
|
62
|
+
title(`Zen ${TITLES[name] ?? name.replace(/^./, (c) => c.toUpperCase())}`);
|
|
63
|
+
}
|
|
57
64
|
// Narration, so `--json` and every pipe are untouched by it. A command
|
|
58
65
|
// living in another package brings its own brand, but only once it is
|
|
59
66
|
// actually there — a banner over "not installed" is a claim about nothing.
|
package/dist/meta.js
CHANGED
|
@@ -6,7 +6,7 @@ import { invokedAs } from "./args.js";
|
|
|
6
6
|
import { paths, readJson, writeJson } from "./home.js";
|
|
7
7
|
import { envOf } from "./keys.js";
|
|
8
8
|
import { credentialError, cut, cyan, dim, note, pad, plain, red, styled, usageError, write, yellow, } from "./term.js";
|
|
9
|
-
import {
|
|
9
|
+
import { blocksOf, boxWidth, textOf, wrap } from "./tui/wrap.js";
|
|
10
10
|
export const PROMPT_DIR = '.github/prompts';
|
|
11
11
|
const PROMPT_SUFFIX = '.prompt.md';
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
@@ -492,19 +492,31 @@ export function answerBox(text, columns = process.stdout.columns ?? 80) {
|
|
|
492
492
|
if (!process.stdout.isTTY) {
|
|
493
493
|
return text.split('\n');
|
|
494
494
|
}
|
|
495
|
-
const outer =
|
|
495
|
+
const outer = boxWidth(text, columns);
|
|
496
496
|
const inner = outer - 4;
|
|
497
497
|
const body = [];
|
|
498
|
-
|
|
499
|
-
|
|
498
|
+
let previous;
|
|
499
|
+
for (const block of blocksOf(text)) {
|
|
500
|
+
if (previous && !(block.kind === 'item' && previous.kind === 'item')) {
|
|
501
|
+
body.push('');
|
|
502
|
+
}
|
|
503
|
+
previous = block;
|
|
504
|
+
if (block.kind === 'code') {
|
|
500
505
|
// Indentation is the meaning of a fenced block, so it is cut rather
|
|
501
506
|
// than reflowed.
|
|
502
|
-
body.push(dim(`\u250c\u2500${
|
|
503
|
-
body.push(...
|
|
507
|
+
body.push(dim(`\u250c\u2500${block.title ? ` ${block.title}` : ''}`));
|
|
508
|
+
body.push(...(block.lines ?? []).map((l) => `${dim('\u2502')} ${cut(l, inner - 2)}`));
|
|
504
509
|
body.push(dim('\u2514\u2500'));
|
|
505
510
|
}
|
|
511
|
+
else if (block.lines || block.kind === 'table') {
|
|
512
|
+
body.push(...textOf(block)
|
|
513
|
+
.split('\n')
|
|
514
|
+
.map((l) => cut(l, inner)));
|
|
515
|
+
}
|
|
506
516
|
else {
|
|
507
|
-
|
|
517
|
+
// Plain, not styled: `wrap` counts characters, and a bold run
|
|
518
|
+
// measured with its escape codes in it folds the line early.
|
|
519
|
+
body.push(...wrap(textOf(block, inner), inner));
|
|
508
520
|
}
|
|
509
521
|
}
|
|
510
522
|
const rule = BOX.h.repeat(outer - 2);
|
package/dist/scaffold.d.ts
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The project files that are ours rather than the project's.
|
|
3
|
+
*
|
|
4
|
+
* Everything else under `templates/project/` describes *this project* and is
|
|
5
|
+
* written once. These describe *this version of `zen`* — how the memory graph
|
|
6
|
+
* works, how its tools are called, what survives a join — and land under
|
|
7
|
+
* `agents/` only because that is where house rules have to be to reach a
|
|
8
|
+
* prompt. A copy that has fallen behind the runtime is worse than none, so
|
|
9
|
+
* `zen check --fix` replaces them, and nothing in them is worth editing in
|
|
10
|
+
* place: project policy on any of those subjects goes in a topic file of its
|
|
11
|
+
* own beside them.
|
|
12
|
+
*/
|
|
13
|
+
export declare const SHARED_RULES: readonly string[];
|
|
1
14
|
/**
|
|
2
15
|
* Writes the editor's files under `dir`, replacing what is there. They are
|
|
3
16
|
* ours: they say how the editor is to treat a directory the agents write into,
|
|
@@ -6,6 +19,30 @@
|
|
|
6
19
|
* none. Returns the relative paths written.
|
|
7
20
|
*/
|
|
8
21
|
export declare function editorFiles(dir: string): string[];
|
|
22
|
+
/**
|
|
23
|
+
* Replaces every file in a project that nobody is meant to be maintaining:
|
|
24
|
+
* `SHARED_RULES` in the project tree, and the editor tree whole. Returns the
|
|
25
|
+
* relative paths written, project files first.
|
|
26
|
+
*
|
|
27
|
+
* This is the other half of `keep: true`. A scaffold never overwrites, which is
|
|
28
|
+
* right for the files a project goes on to make its own and wrong for the ones
|
|
29
|
+
* that only ever restate how this version of `zen` behaves — those a project
|
|
30
|
+
* upgrades into, and before this there was no way to get them except by hand.
|
|
31
|
+
*/
|
|
32
|
+
export declare function refreshShared(dir: string): string[];
|
|
33
|
+
/**
|
|
34
|
+
* Which of a project's `SHARED_RULES` are not the bytes this `zen` ships.
|
|
35
|
+
*
|
|
36
|
+
* The comparison is the whole file rather than anything read out of it,
|
|
37
|
+
* because there is no reading that would have caught the copies that went
|
|
38
|
+
* stale in practice: prose describing a tool that was renamed, a section about
|
|
39
|
+
* behaviour that changed, frontmatter added after the copy was written. All of
|
|
40
|
+
* it parses, all of it loads, and all of it is wrong. A file that is not there
|
|
41
|
+
* is not stale — what must exist is checked where it is required, not here.
|
|
42
|
+
*
|
|
43
|
+
* Paths come back with forward slashes, the way a report names a file.
|
|
44
|
+
*/
|
|
45
|
+
export declare function staleShared(dir: string): string[];
|
|
9
46
|
export interface ScaffoldOptions {
|
|
10
47
|
/** the project directory */
|
|
11
48
|
dir: string;
|
package/dist/scaffold.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { chmodSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from 'node:fs';
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
2
|
+
import { dirname, join, sep } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
// ---------------------------------------------------------------------------
|
|
5
5
|
// Scaffolding
|
|
@@ -11,7 +11,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
11
11
|
//
|
|
12
12
|
// templates/project/ the project's own files. Written once and edited from
|
|
13
13
|
// then on, so anything already there is left alone.
|
|
14
|
-
// templates/editor/ ours: `.vscode
|
|
14
|
+
// templates/editor/ ours: the `.vscode/` and `.github/` trees,
|
|
15
15
|
// which describe this version of `zen` to the editor and
|
|
16
16
|
// are replaced every time.
|
|
17
17
|
// templates/parts/ fragments spliced into a template above.
|
|
@@ -35,6 +35,23 @@ const TEMPLATES = fileURLToPath(new URL('../templates', import.meta.url));
|
|
|
35
35
|
const TEMPLATE = '.tmpl';
|
|
36
36
|
/** How to use the memory graph: a house rules file, landing under `agents/`. */
|
|
37
37
|
const MEMORY_RULES = join('agents', 'memory-instructions.md');
|
|
38
|
+
/** How tools are called: the same, and equally not the project's to maintain. */
|
|
39
|
+
const TOOL_RULES = join('agents', 'tools-instructions.md');
|
|
40
|
+
/** How forking behaves: the same again, reaching only agents that declare `fork:`. */
|
|
41
|
+
const FORK_RULES = join('agents', 'fork-instructions.md');
|
|
42
|
+
/**
|
|
43
|
+
* The project files that are ours rather than the project's.
|
|
44
|
+
*
|
|
45
|
+
* Everything else under `templates/project/` describes *this project* and is
|
|
46
|
+
* written once. These describe *this version of `zen`* — how the memory graph
|
|
47
|
+
* works, how its tools are called, what survives a join — and land under
|
|
48
|
+
* `agents/` only because that is where house rules have to be to reach a
|
|
49
|
+
* prompt. A copy that has fallen behind the runtime is worse than none, so
|
|
50
|
+
* `zen check --fix` replaces them, and nothing in them is worth editing in
|
|
51
|
+
* place: project policy on any of those subjects goes in a topic file of its
|
|
52
|
+
* own beside them.
|
|
53
|
+
*/
|
|
54
|
+
export const SHARED_RULES = [FORK_RULES, MEMORY_RULES, TOOL_RULES];
|
|
38
55
|
/** The same bytes, kept in the `zen-memory` skill to restore or diff against. */
|
|
39
56
|
const MEMORY_REFERENCE = join('.github', 'skills', 'zen-memory', 'references', 'memory-instructions.md');
|
|
40
57
|
/**
|
|
@@ -194,7 +211,7 @@ function copyTree(from, dir, rel, opts) {
|
|
|
194
211
|
export function editorFiles(dir) {
|
|
195
212
|
const written = copyTree(join(TEMPLATES, 'editor'), dir, '', {});
|
|
196
213
|
// The one file that belongs to both trees. It is a project file first —
|
|
197
|
-
// house rules
|
|
214
|
+
// house rules reaching every agent that can see the graph — and it is
|
|
198
215
|
// written once and edited from then on, like every other project file. But
|
|
199
216
|
// a project whose copy drifted, or that deleted it and turned memory back
|
|
200
217
|
// on later, needs somewhere to get the current text from, and the editor
|
|
@@ -208,6 +225,53 @@ export function editorFiles(dir) {
|
|
|
208
225
|
written.push(MEMORY_REFERENCE);
|
|
209
226
|
return written;
|
|
210
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Replaces every file in a project that nobody is meant to be maintaining:
|
|
230
|
+
* `SHARED_RULES` in the project tree, and the editor tree whole. Returns the
|
|
231
|
+
* relative paths written, project files first.
|
|
232
|
+
*
|
|
233
|
+
* This is the other half of `keep: true`. A scaffold never overwrites, which is
|
|
234
|
+
* right for the files a project goes on to make its own and wrong for the ones
|
|
235
|
+
* that only ever restate how this version of `zen` behaves — those a project
|
|
236
|
+
* upgrades into, and before this there was no way to get them except by hand.
|
|
237
|
+
*/
|
|
238
|
+
export function refreshShared(dir) {
|
|
239
|
+
const written = [];
|
|
240
|
+
for (const rel of SHARED_RULES) {
|
|
241
|
+
const path = join(dir, rel);
|
|
242
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
243
|
+
copyFileSync(join(TEMPLATES, 'project', rel), path);
|
|
244
|
+
written.push(rel);
|
|
245
|
+
}
|
|
246
|
+
written.push(...editorFiles(dir));
|
|
247
|
+
return written;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Which of a project's `SHARED_RULES` are not the bytes this `zen` ships.
|
|
251
|
+
*
|
|
252
|
+
* The comparison is the whole file rather than anything read out of it,
|
|
253
|
+
* because there is no reading that would have caught the copies that went
|
|
254
|
+
* stale in practice: prose describing a tool that was renamed, a section about
|
|
255
|
+
* behaviour that changed, frontmatter added after the copy was written. All of
|
|
256
|
+
* it parses, all of it loads, and all of it is wrong. A file that is not there
|
|
257
|
+
* is not stale — what must exist is checked where it is required, not here.
|
|
258
|
+
*
|
|
259
|
+
* Paths come back with forward slashes, the way a report names a file.
|
|
260
|
+
*/
|
|
261
|
+
export function staleShared(dir) {
|
|
262
|
+
const stale = [];
|
|
263
|
+
for (const rel of SHARED_RULES) {
|
|
264
|
+
const path = join(dir, rel);
|
|
265
|
+
if (!existsSync(path)) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const ours = readFileSync(join(TEMPLATES, 'project', rel), 'utf8');
|
|
269
|
+
if (readFileSync(path, 'utf8') !== ours) {
|
|
270
|
+
stale.push(rel.split(sep).join('/'));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return stale;
|
|
274
|
+
}
|
|
211
275
|
/**
|
|
212
276
|
* Writes a project. Never overwrites the project's own files — a second `init`
|
|
213
277
|
* over a directory fills in what is missing and leaves the rest alone — but the
|
package/dist/term.d.ts
CHANGED
|
@@ -66,6 +66,8 @@ export declare function progress(): {
|
|
|
66
66
|
done: () => void;
|
|
67
67
|
};
|
|
68
68
|
export declare function warn(message: string): void;
|
|
69
|
+
/** What this run calls itself, in the process table and on the terminal's tab. */
|
|
70
|
+
export declare function title(text: string): void;
|
|
69
71
|
export declare function fail(message: string, hint?: string): void;
|
|
70
72
|
export declare function isInteractive(): boolean;
|
|
71
73
|
export declare function ask(question: string, fallback?: string): Promise<string>;
|
package/dist/term.js
CHANGED
|
@@ -186,6 +186,31 @@ export function progress() {
|
|
|
186
186
|
export function warn(message) {
|
|
187
187
|
note(`${yellow('warning')} ${message}`);
|
|
188
188
|
}
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// Naming the run
|
|
191
|
+
//
|
|
192
|
+
// Two different things answer to the word "title", and a long run wants both.
|
|
193
|
+
// `process.title` is what the process table says, which is how `ps` and
|
|
194
|
+
// Activity Monitor tell one `zen` from another. The tab is the terminal's own,
|
|
195
|
+
// and nothing a process does to itself reaches it — macOS reads the tab name
|
|
196
|
+
// from the executable — so it takes an OSC sequence, written to stderr with the
|
|
197
|
+
// rest of the narration and only when there is a terminal to read it.
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
let restores = false;
|
|
200
|
+
/** What this run calls itself, in the process table and on the terminal's tab. */
|
|
201
|
+
export function title(text) {
|
|
202
|
+
process.title = text;
|
|
203
|
+
if (!process.stderr.isTTY) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// Push what the shell had there first, so leaving puts it back.
|
|
207
|
+
if (!restores) {
|
|
208
|
+
restores = true;
|
|
209
|
+
process.stderr.write('\u001b[22;0t');
|
|
210
|
+
process.on('exit', () => process.stderr.write('\u001b[23;0t'));
|
|
211
|
+
}
|
|
212
|
+
process.stderr.write(`\u001b]0;${text}\u0007`);
|
|
213
|
+
}
|
|
189
214
|
export function fail(message, hint) {
|
|
190
215
|
note(`${red('error')} ${message}`);
|
|
191
216
|
if (hint) {
|