@zenera/cli 1.1.18 → 1.1.19
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/dist/banner.js +8 -4
- package/dist/term.d.ts +14 -0
- package/dist/term.js +66 -1
- package/dist/tui/app.js +286 -151
- package/dist/tui/theme.d.ts +26 -7
- package/dist/tui/theme.js +62 -36
- package/dist/tui/wrap.d.ts +40 -15
- package/dist/tui/wrap.js +190 -17
- package/dist/validate.js +3 -16
- package/package.json +2 -2
- package/templates/editor/.github/copilot-instructions.md +2 -2
- package/templates/editor/.github/skills/zen-cli/references/check.md +0 -2
- package/templates/editor/.github/skills/zen-cli/references/projects.md +1 -0
- package/templates/project/agents/tools-instructions.md +8 -0
package/dist/banner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { dim, note, styled } from "./term.js";
|
|
1
|
+
import { appearance, dim, note, styled } from "./term.js";
|
|
2
2
|
// ---------------------------------------------------------------------------
|
|
3
3
|
// The banner
|
|
4
4
|
//
|
|
@@ -65,7 +65,10 @@ function big(word) {
|
|
|
65
65
|
/** A wordmark rather than a sentence, so the letters are set apart. */
|
|
66
66
|
const spaced = (s) => [...s.toUpperCase()].join(' ');
|
|
67
67
|
const RESET = '\u001b[0m';
|
|
68
|
-
const
|
|
68
|
+
const HEADS = {
|
|
69
|
+
dark: { face: '\u001b[1;38;5;231m', shade: '\u001b[0;38;5;244m' },
|
|
70
|
+
light: { face: '\u001b[1;38;5;235m', shade: '\u001b[0;38;5;245m' },
|
|
71
|
+
};
|
|
69
72
|
/** One per sub-brand, so two of them are told apart before the word is read. */
|
|
70
73
|
const ACCENTS = {
|
|
71
74
|
orange: { face: '\u001b[1;38;5;208m', shade: '\u001b[0;38;5;130m' },
|
|
@@ -95,17 +98,18 @@ export function bannerLines(text, columns = process.stderr.columns || 80) {
|
|
|
95
98
|
// inside a styled string cannot be trimmed away later.
|
|
96
99
|
const accent = big(text.accent).map((row) => row.trimEnd());
|
|
97
100
|
const tone = ACCENTS[text.hue ?? 'orange'];
|
|
101
|
+
const ink = HEADS[appearance()];
|
|
98
102
|
const margin = ' '.repeat(INDENT);
|
|
99
103
|
const width = INDENT + head[0].length + GAP + Math.max(...accent.map((row) => row.length));
|
|
100
104
|
if (width > columns) {
|
|
101
105
|
return [
|
|
102
|
-
`${margin}${flat(text.head.toUpperCase(),
|
|
106
|
+
`${margin}${flat(text.head.toUpperCase(), ink)} ${flat(text.accent.toUpperCase(), tone)}`,
|
|
103
107
|
`${margin}${dim(spaced(text.subtitle))}`,
|
|
104
108
|
];
|
|
105
109
|
}
|
|
106
110
|
const lines = [];
|
|
107
111
|
for (let r = 0; r < HEIGHT; r++) {
|
|
108
|
-
lines.push(`${margin}${paint(head[r],
|
|
112
|
+
lines.push(`${margin}${paint(head[r], ink)}${' '.repeat(GAP)}${paint(accent[r], tone)}`);
|
|
109
113
|
}
|
|
110
114
|
// Rules on both sides, centred under the art: the subtitle is a caption,
|
|
111
115
|
// and without them a short line of spaced capitals floats.
|
package/dist/term.d.ts
CHANGED
|
@@ -20,7 +20,21 @@ export declare class CliError extends Error {
|
|
|
20
20
|
export declare const usageError: (m: string, hint?: string) => CliError;
|
|
21
21
|
export declare const invalidError: (m: string, hint?: string) => CliError;
|
|
22
22
|
export declare const credentialError: (m: string, hint?: string) => CliError;
|
|
23
|
+
export type Appearance = 'dark' | 'light';
|
|
24
|
+
/** One grey per background. The only 256-colour indices in the CLI. */
|
|
25
|
+
export declare const CHROME: Record<Appearance, number>;
|
|
26
|
+
/** What the terminal looks like, as far as can be known without asking it. */
|
|
27
|
+
export declare function appearance(): Appearance;
|
|
23
28
|
export declare const bold: (s: string) => string;
|
|
29
|
+
/**
|
|
30
|
+
* One step quieter, which is half of everything this tool prints: paths,
|
|
31
|
+
* counts, hints, the second column of every table.
|
|
32
|
+
*
|
|
33
|
+
* A stated grey rather than SGR 2. `dim` is not a colour, it is a request, and
|
|
34
|
+
* how far it is honoured is the terminal's own business — iTerm2 takes a step,
|
|
35
|
+
* Apple Terminal goes most of the way to the background and takes the quiet
|
|
36
|
+
* half of the output with it.
|
|
37
|
+
*/
|
|
24
38
|
export declare const dim: (s: string) => string;
|
|
25
39
|
export declare const red: (s: string) => string;
|
|
26
40
|
export declare const green: (s: string) => string;
|
package/dist/term.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
1
2
|
import { createInterface } from 'node:readline/promises';
|
|
2
3
|
import { styleText } from 'node:util';
|
|
3
4
|
// ---------------------------------------------------------------------------
|
|
@@ -36,11 +37,75 @@ export class CliError extends Error {
|
|
|
36
37
|
export const usageError = (m, hint) => new CliError(m, EXIT.usage, hint);
|
|
37
38
|
export const invalidError = (m, hint) => new CliError(m, EXIT.invalid, hint);
|
|
38
39
|
export const credentialError = (m, hint) => new CliError(m, EXIT.credentials, hint);
|
|
40
|
+
/** One grey per background. The only 256-colour indices in the CLI. */
|
|
41
|
+
export const CHROME = { dark: 245, light: 242 };
|
|
42
|
+
let sniffed;
|
|
43
|
+
/** What the terminal looks like, as far as can be known without asking it. */
|
|
44
|
+
export function appearance() {
|
|
45
|
+
sniffed ??= fromEnv() ?? fromColorFgBg() ?? fromAppleTerminal() ?? 'dark';
|
|
46
|
+
return sniffed;
|
|
47
|
+
}
|
|
48
|
+
function fromEnv() {
|
|
49
|
+
const v = process.env['ZENERA_THEME']?.trim().toLowerCase();
|
|
50
|
+
return v === 'dark' || v === 'light' ? v : undefined;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* `COLORFGBG` is `fg;bg` or `fg;<something>;bg`; the background is the last
|
|
54
|
+
* field. Anything non-numeric (notably `default`) tells us nothing.
|
|
55
|
+
*/
|
|
56
|
+
function fromColorFgBg() {
|
|
57
|
+
const parts = process.env['COLORFGBG']?.split(';');
|
|
58
|
+
const bg = parts?.[parts.length - 1];
|
|
59
|
+
if (bg === undefined || !/^\d+$/.test(bg)) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
const n = Number(bg);
|
|
63
|
+
return n === 7 || n >= 9 ? 'light' : 'dark';
|
|
64
|
+
}
|
|
65
|
+
/** The shipped Apple Terminal profiles that are paper rather than ink. */
|
|
66
|
+
const APPLE_LIGHT = new Set(['basic', 'man page', 'novel', 'silver aerogel']);
|
|
67
|
+
/**
|
|
68
|
+
* Ask `defaults` which profile Terminal.app opens with. A subprocess during
|
|
69
|
+
* startup is a real cost, so it is paid last, once per process, and only on
|
|
70
|
+
* the one terminal that leaves us nothing else to go on. A custom profile is
|
|
71
|
+
* not in the table and falls through to the guess.
|
|
72
|
+
*/
|
|
73
|
+
function fromAppleTerminal() {
|
|
74
|
+
if (process.env['TERM_PROGRAM'] !== 'Apple_Terminal') {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const name = execFileSync('defaults', ['read', 'com.apple.Terminal', 'Default Window Settings'], { encoding: 'utf8', timeout: 300, stdio: ['ignore', 'pipe', 'ignore'] })
|
|
79
|
+
.trim()
|
|
80
|
+
.toLowerCase();
|
|
81
|
+
return APPLE_LIGHT.has(name) ? 'light' : undefined;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
39
87
|
// ---------------------------------------------------------------------------
|
|
40
88
|
// Styling
|
|
41
89
|
// ---------------------------------------------------------------------------
|
|
42
90
|
export const bold = (s) => styleText('bold', s);
|
|
43
|
-
|
|
91
|
+
/**
|
|
92
|
+
* One step quieter, which is half of everything this tool prints: paths,
|
|
93
|
+
* counts, hints, the second column of every table.
|
|
94
|
+
*
|
|
95
|
+
* A stated grey rather than SGR 2. `dim` is not a colour, it is a request, and
|
|
96
|
+
* how far it is honoured is the terminal's own business — iTerm2 takes a step,
|
|
97
|
+
* Apple Terminal goes most of the way to the background and takes the quiet
|
|
98
|
+
* half of the output with it.
|
|
99
|
+
*/
|
|
100
|
+
export const dim = (s) => {
|
|
101
|
+
if (!styled()) {
|
|
102
|
+
return s;
|
|
103
|
+
}
|
|
104
|
+
const open = `\u001b[38;5;${CHROME[appearance()]}m`;
|
|
105
|
+
// A colour nested inside closes with `39m`, which would drop the grey for
|
|
106
|
+
// whatever follows it.
|
|
107
|
+
return `${open}${s.replaceAll('\u001b[39m', `\u001b[39m${open}`)}\u001b[39m`;
|
|
108
|
+
};
|
|
44
109
|
export const red = (s) => styleText('red', s);
|
|
45
110
|
export const green = (s) => styleText('green', s);
|
|
46
111
|
export const yellow = (s) => styleText('yellow', s);
|