docugate 0.3.0 → 0.3.1
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.d.ts +1 -1
- package/dist/banner.js +25 -37
- package/dist/menu.d.ts +14 -1
- package/dist/menu.js +68 -12
- package/dist/select.d.ts +7 -1
- package/dist/select.js +36 -3
- package/package.json +1 -1
package/dist/banner.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const TAGLINE = "Tours and docs for your codebase";
|
|
2
|
-
/** The wordmark as
|
|
2
|
+
/** The wordmark as two plain lines, no color. */
|
|
3
3
|
export declare function wordmark(text?: string, gap?: number): string[];
|
|
4
4
|
export declare function packageVersion(): string;
|
|
5
5
|
export type BannerOptions = {
|
package/dist/banner.js
CHANGED
|
@@ -1,43 +1,36 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { colorDepth, fg
|
|
2
|
+
import { colorDepth, fg } from './logo.js';
|
|
3
3
|
import { colorOn, isMintty } from './ui.js';
|
|
4
|
-
// The banner: DOCUGATE in block
|
|
5
|
-
// owl, with the
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// The banner: DOCUGATE in a compact two-row block font, cream over gold
|
|
5
|
+
// like the owl, with the version beside it. Small on purpose: it sits above
|
|
6
|
+
// the menu, and the menu is what the person came for. A narrow terminal gets
|
|
7
|
+
// one line, and without color it is plain text. It is only ever printed to a
|
|
8
|
+
// person at a terminal, so logs and CI never see it.
|
|
9
9
|
export const TAGLINE = 'Tours and docs for your codebase';
|
|
10
|
-
|
|
11
|
-
// A five-row block font, just the letters in the name. Every glyph is five
|
|
12
|
-
// columns wide, so the wordmark's width is simply letters * 5 + gaps.
|
|
10
|
+
// Two rows per letter, three columns wide, drawn with half blocks.
|
|
13
11
|
const GLYPHS = {
|
|
14
|
-
D: ['
|
|
15
|
-
O: ['
|
|
16
|
-
C: ['
|
|
17
|
-
U: ['█
|
|
18
|
-
G: ['
|
|
19
|
-
A: ['
|
|
20
|
-
T: ['
|
|
21
|
-
E: ['
|
|
12
|
+
D: ['█▀▄', '█▄▀'],
|
|
13
|
+
O: ['█▀█', '█▄█'],
|
|
14
|
+
C: ['█▀▀', '█▄▄'],
|
|
15
|
+
U: ['█ █', '█▄█'],
|
|
16
|
+
G: ['█▀▀', '█▄█'],
|
|
17
|
+
A: ['▄▀█', '█▀█'],
|
|
18
|
+
T: ['▀█▀', ' █ '],
|
|
19
|
+
E: ['█▀▀', '██▄'],
|
|
22
20
|
};
|
|
23
|
-
// Cream
|
|
21
|
+
// Cream on the top row, gold on the bottom.
|
|
24
22
|
const SHADES = [
|
|
25
23
|
[245, 240, 227],
|
|
26
|
-
[246, 222, 150],
|
|
27
24
|
[244, 196, 63],
|
|
28
|
-
[230, 175, 46],
|
|
29
|
-
[217, 154, 30],
|
|
30
25
|
];
|
|
31
26
|
/** Narrower than this and the wordmark would wrap, so one line is shown. */
|
|
32
|
-
const WORDMARK_MIN_COLUMNS =
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
/** The wordmark as five plain lines, no color. */
|
|
36
|
-
export function wordmark(text = 'DOCUGATE', gap = 2) {
|
|
27
|
+
const WORDMARK_MIN_COLUMNS = 40;
|
|
28
|
+
/** The wordmark as two plain lines, no color. */
|
|
29
|
+
export function wordmark(text = 'DOCUGATE', gap = 1) {
|
|
37
30
|
const letters = [...text.toUpperCase()].map((c) => GLYPHS[c]);
|
|
38
31
|
if (letters.some((g) => !g))
|
|
39
32
|
throw new Error(`The banner font has no glyph for every letter of "${text}".`);
|
|
40
|
-
return [0, 1
|
|
33
|
+
return [0, 1].map((row) => letters.map((g) => g[row]).join(' '.repeat(gap)).trimEnd());
|
|
41
34
|
}
|
|
42
35
|
export function packageVersion() {
|
|
43
36
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
@@ -48,15 +41,10 @@ export function banner(options = {}) {
|
|
|
48
41
|
const {
|
|
49
42
|
// mintty reports no width to a piped console; COLUMNS, or a usual window, stands in.
|
|
50
43
|
columns = process.stdout.columns ?? (Number(process.env.COLUMNS) || (isMintty() ? 100 : 0)), color = colorOn(), version = packageVersion(), env = process.env, } = options;
|
|
51
|
-
const dim = (t) => (color ?
|
|
52
|
-
if (columns < WORDMARK_MIN_COLUMNS)
|
|
53
|
-
|
|
54
|
-
return ['', full.length <= columns ? full : ` DocuGate v${version}`, ''];
|
|
55
|
-
}
|
|
44
|
+
const dim = (t) => (color ? `[2m${t}[0m` : t);
|
|
45
|
+
if (columns < WORDMARK_MIN_COLUMNS)
|
|
46
|
+
return ['', ` DocuGate ${dim(`v${version}`)}`, ''];
|
|
56
47
|
const depth = colorDepth(env);
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
if (color && columns >= OWL_MIN_COLUMNS)
|
|
60
|
-
return logoBanner(text, env);
|
|
61
|
-
return ['', ...text.map((t) => (t ? ` ${t}` : '')), ''];
|
|
48
|
+
const [top, bottom] = wordmark().map((line, row) => (color ? `${fg(SHADES[row], depth)}${line}[0m` : line));
|
|
49
|
+
return ['', ` ${top}`, ` ${bottom} ${dim(`v${version}`)}`, ''];
|
|
62
50
|
}
|
package/dist/menu.d.ts
CHANGED
|
@@ -7,10 +7,23 @@ export type Action = string[];
|
|
|
7
7
|
export declare function buildChoices(s: Status, now?: Date): Choice<Action>[];
|
|
8
8
|
/** The line under the list: who you are, and the one thing worth doing. */
|
|
9
9
|
export declare function menuNote(s: Status, now?: Date): string;
|
|
10
|
+
/** Who is signed in: the one line the launch screen keeps above the list. */
|
|
11
|
+
export declare function whoNote(s: Status): string;
|
|
12
|
+
export type Section = 'main' | 'tours' | 'more';
|
|
13
|
+
/**
|
|
14
|
+
* The launch menu, short enough to see at once: the few things people come
|
|
15
|
+
* for on the first screen, the rest one step away under Tours and More. A step
|
|
16
|
+
* that cannot run yet is left out rather than shown greyed, so every line is
|
|
17
|
+
* something to do. Labels and rules come from buildChoices, written once.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildMenu(s: Status, section: Section): Choice<Action>[];
|
|
10
20
|
export type MenuDeps = {
|
|
11
21
|
pick?: typeof select<Action>;
|
|
12
22
|
/** For terminals that cannot do arrow keys: a numbered list instead. */
|
|
13
23
|
numbered?: boolean;
|
|
14
24
|
};
|
|
15
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Shows the menu and returns the command chosen, or null to quit. Tours and
|
|
27
|
+
* More open their own short list, with Back to return.
|
|
28
|
+
*/
|
|
16
29
|
export declare function runMenu(status: Status, deps?: MenuDeps): Promise<Action | null>;
|
package/dist/menu.js
CHANGED
|
@@ -130,26 +130,82 @@ export function menuNote(s, now = new Date()) {
|
|
|
130
130
|
}
|
|
131
131
|
return dim(bits.join(' · '));
|
|
132
132
|
}
|
|
133
|
-
/**
|
|
133
|
+
/** Who is signed in: the one line the launch screen keeps above the list. */
|
|
134
|
+
export function whoNote(s) {
|
|
135
|
+
if (s.account.state === 'signed-in')
|
|
136
|
+
return `signed in as ${gold(`@${s.account.login}`)}`;
|
|
137
|
+
return dim(s.account.state === 'offline' ? 'offline' : 'not signed in');
|
|
138
|
+
}
|
|
139
|
+
/** Values that move around the menu rather than run a command. */
|
|
140
|
+
const GO = { tours: ['@tours'], more: ['@more'], back: ['@back'], quit: ['@quit'] };
|
|
141
|
+
/**
|
|
142
|
+
* The launch menu, short enough to see at once: the few things people come
|
|
143
|
+
* for on the first screen, the rest one step away under Tours and More. A step
|
|
144
|
+
* that cannot run yet is left out rather than shown greyed, so every line is
|
|
145
|
+
* something to do. Labels and rules come from buildChoices, written once.
|
|
146
|
+
*/
|
|
147
|
+
export function buildMenu(s, section) {
|
|
148
|
+
const all = buildChoices(s);
|
|
149
|
+
const pick = (...commands) => commands
|
|
150
|
+
.map((command) => all.find((c) => c.value.join(' ') === command))
|
|
151
|
+
.filter((c) => Boolean(c && !c.disabled))
|
|
152
|
+
.map((c) => ({ ...c, status: undefined, recommended: c.recommended }));
|
|
153
|
+
if (section === 'tours') {
|
|
154
|
+
return [...pick('tour init', 'tour install', 'tour serve'), { value: [...GO.back], label: 'Back' }];
|
|
155
|
+
}
|
|
156
|
+
if (section === 'more') {
|
|
157
|
+
const hook = all.find((c) => c.value[0] === 'sync' && c.value[1] === 'hook');
|
|
158
|
+
return [
|
|
159
|
+
...pick('sync --dry-run', 'sync init', ...(hook ? [hook.value.join(' ')] : []), 'status', 'openapi', 'help'),
|
|
160
|
+
{ value: [...GO.back], label: 'Back' },
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
const signedIn = s.account.state === 'signed-in';
|
|
164
|
+
return [
|
|
165
|
+
...pick('developers', 'init', 'sync', 'check'),
|
|
166
|
+
{ value: [...GO.tours], label: 'Tours' },
|
|
167
|
+
{ value: [...GO.more], label: 'More' },
|
|
168
|
+
...pick(signedIn ? 'logout' : 'login'),
|
|
169
|
+
{ value: [...GO.quit], label: 'Quit' },
|
|
170
|
+
];
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Shows the menu and returns the command chosen, or null to quit. Tours and
|
|
174
|
+
* More open their own short list, with Back to return.
|
|
175
|
+
*/
|
|
134
176
|
export async function runMenu(status, deps = {}) {
|
|
135
|
-
const choices = buildChoices(status);
|
|
136
|
-
if (deps.numbered)
|
|
137
|
-
return numbered(choices, status);
|
|
138
177
|
const pick = deps.pick ?? select;
|
|
139
|
-
|
|
178
|
+
let section = 'main';
|
|
179
|
+
for (;;) {
|
|
180
|
+
const choices = buildMenu(status, section);
|
|
181
|
+
const note = section === 'main' ? whoNote(status) : dim(section === 'tours' ? 'Tours' : 'More');
|
|
182
|
+
const chosen = deps.numbered ? await numbered(choices, note) : await pick({ choices, note, minimal: true });
|
|
183
|
+
if (!chosen || chosen[0] === GO.quit[0])
|
|
184
|
+
return null;
|
|
185
|
+
if (chosen[0] === GO.back[0])
|
|
186
|
+
section = 'main';
|
|
187
|
+
else if (chosen[0] === GO.tours[0])
|
|
188
|
+
section = 'tours';
|
|
189
|
+
else if (chosen[0] === GO.more[0])
|
|
190
|
+
section = 'more';
|
|
191
|
+
else
|
|
192
|
+
return chosen;
|
|
193
|
+
}
|
|
140
194
|
}
|
|
141
|
-
async function numbered(choices,
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
195
|
+
async function numbered(choices, note) {
|
|
196
|
+
console.log(`
|
|
197
|
+
${note}
|
|
198
|
+
`);
|
|
199
|
+
choices.forEach((c, i) => console.log(` ${dim(String(i + 1).padStart(2))} ${c.label}`));
|
|
145
200
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
146
201
|
try {
|
|
147
|
-
const def = Math.max(0,
|
|
148
|
-
const answer = (await rl.question(
|
|
202
|
+
const def = Math.max(0, choices.findIndex((c) => c.recommended)) + 1;
|
|
203
|
+
const answer = (await rl.question(`
|
|
204
|
+
${dim(`Choose 1-${choices.length} [${def}]`)}: `)).trim().toLowerCase();
|
|
149
205
|
if (answer === 'q')
|
|
150
206
|
return null;
|
|
151
207
|
const n = answer === '' ? def : Number(answer);
|
|
152
|
-
return Number.isInteger(n) &&
|
|
208
|
+
return Number.isInteger(n) && choices[n - 1] ? choices[n - 1].value : null;
|
|
153
209
|
}
|
|
154
210
|
finally {
|
|
155
211
|
rl.close();
|
package/dist/select.d.ts
CHANGED
|
@@ -41,7 +41,9 @@ export type KeyResult<T> = {
|
|
|
41
41
|
state: PickerState;
|
|
42
42
|
};
|
|
43
43
|
/** One key press, applied to the picker's state. */
|
|
44
|
-
export declare function handleKey<T>(choices: Choice<T>[], state: PickerState, str: string | undefined, key: Key, rows: number
|
|
44
|
+
export declare function handleKey<T>(choices: Choice<T>[], state: PickerState, str: string | undefined, key: Key, rows: number,
|
|
45
|
+
/** False for a short menu: letters do not search, a number picks, q quits. */
|
|
46
|
+
search?: boolean): KeyResult<T>;
|
|
45
47
|
export type RenderOptions = {
|
|
46
48
|
title?: string;
|
|
47
49
|
width: number;
|
|
@@ -49,6 +51,8 @@ export type RenderOptions = {
|
|
|
49
51
|
footer?: string;
|
|
50
52
|
note?: string;
|
|
51
53
|
legend?: boolean;
|
|
54
|
+
/** Just the note and a numbered list: no search, rules, description, legend or key hints. */
|
|
55
|
+
minimal?: boolean;
|
|
52
56
|
};
|
|
53
57
|
/** The picker as lines of text. */
|
|
54
58
|
export declare function renderPicker<T>(choices: Choice<T>[], state: PickerState, o: RenderOptions): string[];
|
|
@@ -65,6 +69,8 @@ export type SelectOptions<T> = {
|
|
|
65
69
|
/** How many choices show at once. */
|
|
66
70
|
rows?: number;
|
|
67
71
|
legend?: boolean;
|
|
72
|
+
/** The short form: see renderPicker. Every choice shows; a number picks one. */
|
|
73
|
+
minimal?: boolean;
|
|
68
74
|
};
|
|
69
75
|
/** Whether there is a real terminal to draw a picker in. */
|
|
70
76
|
export declare const canPick: (io?: PickerIO) => boolean;
|
package/dist/select.js
CHANGED
|
@@ -43,8 +43,21 @@ function step(list, from, dir) {
|
|
|
43
43
|
return clamp(from + dir, 0, Math.max(0, list.length - 1));
|
|
44
44
|
}
|
|
45
45
|
/** One key press, applied to the picker's state. */
|
|
46
|
-
export function handleKey(choices, state, str, key, rows
|
|
46
|
+
export function handleKey(choices, state, str, key, rows,
|
|
47
|
+
/** False for a short menu: letters do not search, a number picks, q quits. */
|
|
48
|
+
search = true) {
|
|
47
49
|
const list = filterChoices(choices, state.query);
|
|
50
|
+
if (!search && str && !key.ctrl && !key.meta) {
|
|
51
|
+
if (str === 'q')
|
|
52
|
+
return { done: 'cancelled' };
|
|
53
|
+
const n = Number(str);
|
|
54
|
+
if (Number.isInteger(n) && n >= 1 && n <= list.length) {
|
|
55
|
+
const choice = list[n - 1];
|
|
56
|
+
return choice.disabled ? { done: 'blocked', reason: choice.disabled, state: { ...state, cursor: n - 1 } } : { done: 'chosen', choice };
|
|
57
|
+
}
|
|
58
|
+
if (key.name !== 'return' && key.name !== 'enter')
|
|
59
|
+
return { state };
|
|
60
|
+
}
|
|
48
61
|
const move = (cursor) => {
|
|
49
62
|
const c = clamp(cursor, 0, Math.max(0, list.length - 1));
|
|
50
63
|
const top = c < state.top ? c : c >= state.top + rows ? c - rows + 1 : state.top;
|
|
@@ -94,9 +107,28 @@ const fit = (text, width) => {
|
|
|
94
107
|
// Over width: cut the plain text. Color is not worth keeping on a line this long.
|
|
95
108
|
return `${plain.slice(0, Math.max(0, width - 1))}…`;
|
|
96
109
|
};
|
|
110
|
+
/**
|
|
111
|
+
* The short form: the note, then every choice on its own numbered line. Short
|
|
112
|
+
* enough never to scroll, and every line cut to the width so none can wrap,
|
|
113
|
+
* which is what kept the redraw in step with what is on screen.
|
|
114
|
+
*/
|
|
115
|
+
function renderMinimal(list, state, o) {
|
|
116
|
+
const lines = [];
|
|
117
|
+
if (o.note)
|
|
118
|
+
lines.push(` ${o.note}`, '');
|
|
119
|
+
list.forEach((c, i) => {
|
|
120
|
+
const at = i === state.cursor;
|
|
121
|
+
const n = dim(String(i + 1).padStart(2));
|
|
122
|
+
const label = at ? bold(gold(c.label)) : c.disabled ? dim(c.label) : c.label;
|
|
123
|
+
lines.push(` ${at ? gold('❯') : ' '} ${n} ${label}`);
|
|
124
|
+
});
|
|
125
|
+
return lines.map((l) => fit(l, o.width - 1));
|
|
126
|
+
}
|
|
97
127
|
/** The picker as lines of text. */
|
|
98
128
|
export function renderPicker(choices, state, o) {
|
|
99
129
|
const list = filterChoices(choices, state.query);
|
|
130
|
+
if (o.minimal)
|
|
131
|
+
return renderMinimal(list, state, o);
|
|
100
132
|
const rule = dim('─'.repeat(Math.max(10, Math.min(o.width - 4, 78))));
|
|
101
133
|
const lines = [];
|
|
102
134
|
const w = o.width - 2;
|
|
@@ -156,7 +188,7 @@ export const canPick = (io = { stdin: process.stdin, stdout: process.stdout }) =
|
|
|
156
188
|
*/
|
|
157
189
|
export function select(options, io = { stdin: process.stdin, stdout: process.stdout }) {
|
|
158
190
|
const { stdin, stdout } = io;
|
|
159
|
-
const rows = Math.max(3, Math.min(options.rows ?? 8, (stdout.rows ?? 24) - 14));
|
|
191
|
+
const rows = options.minimal ? options.choices.length : Math.max(3, Math.min(options.rows ?? 8, (stdout.rows ?? 24) - 14));
|
|
160
192
|
const color = colorOn();
|
|
161
193
|
let state = { query: '', cursor: initialCursor(options.choices), top: 0 };
|
|
162
194
|
state.top = Math.max(0, state.cursor - rows + 1);
|
|
@@ -171,6 +203,7 @@ export function select(options, io = { stdin: process.stdin, stdout: process.std
|
|
|
171
203
|
footer: options.footer,
|
|
172
204
|
note,
|
|
173
205
|
legend: options.legend,
|
|
206
|
+
minimal: options.minimal,
|
|
174
207
|
});
|
|
175
208
|
stdout.write(`${drawn ? `\x1b[${drawn}A\r` : ''}\x1b[0J${hint(lines.join('\n'))}\n`);
|
|
176
209
|
drawn = lines.length;
|
|
@@ -193,7 +226,7 @@ export function select(options, io = { stdin: process.stdin, stdout: process.std
|
|
|
193
226
|
resolve(value);
|
|
194
227
|
};
|
|
195
228
|
const onKey = (str, key = {}) => {
|
|
196
|
-
const result = handleKey(options.choices, state, str, key, rows);
|
|
229
|
+
const result = handleKey(options.choices, state, str, key, rows, !options.minimal);
|
|
197
230
|
if ('done' in result) {
|
|
198
231
|
if (result.done === 'cancelled')
|
|
199
232
|
return finish(null);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docugate",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Command-line tools for DocuGate: set up, check and prepare the documentation DocuGate reads straight from your repository. Also checks DocuGate API keys from Express, Hono and Fastify.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|