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 CHANGED
@@ -1,5 +1,5 @@
1
1
  export declare const TAGLINE = "Tours and docs for your codebase";
2
- /** The wordmark as five plain lines, no color. */
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, logoBanner } from './logo.js';
2
+ import { colorDepth, fg } from './logo.js';
3
3
  import { colorOn, isMintty } from './ui.js';
4
- // The banner: DOCUGATE in block letters, shaded from cream to gold like the
5
- // owl, with the tagline and version under it. It adapts to where it is drawn:
6
- // beside the owl in a wide terminal, alone in a normal one, as a single line
7
- // in a narrow one, and without color when color is off. It is only ever
8
- // printed to a real terminal, so logs and CI never see it.
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
- const SITE = 'https://www.trydocugate.site';
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 at the top, through gold, to deep gold at the bottom.
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 = 60;
33
- /** Wide enough for the owl beside the wordmark. */
34
- const OWL_MIN_COLUMNS = 96;
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, 2, 3, 4].map((row) => letters.map((g) => g[row]).join(' '.repeat(gap)).trimEnd());
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 ? `\x1b[2m${t}\x1b[0m` : t);
52
- if (columns < WORDMARK_MIN_COLUMNS) {
53
- const full = ` DocuGate · ${TAGLINE} · v${version}`;
54
- return ['', full.length <= columns ? full : ` DocuGate v${version}`, ''];
55
- }
44
+ const dim = (t) => (color ? `${t}` : t);
45
+ if (columns < WORDMARK_MIN_COLUMNS)
46
+ return ['', ` DocuGate ${dim(`v${version}`)}`, ''];
56
47
  const depth = colorDepth(env);
57
- const art = wordmark().map((line, row) => (color ? `${fg(SHADES[row], depth)}${line}\x1b[0m` : line));
58
- const text = [...art, '', dim(TAGLINE), dim(`v${version} · ${SITE}`)];
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}` : 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
- /** Shows the menu and returns what was chosen, or null to quit. */
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
- /** Shows the menu and returns what was chosen, or null to quit. */
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
- return pick({ title: 'What next?', choices, note: menuNote(status), rows: 8 });
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, status) {
142
- const usable = choices.filter((c) => !c.disabled);
143
- console.log(`\n ${bold('What next?')} ${menuNote(status)}\n`);
144
- usable.forEach((c, i) => console.log(` ${gold(String(i + 1).padStart(2))} ${c.label}${c.recommended ? gold(' ●') : ''}${c.status ? dim(` ${c.status}`) : ''}`));
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, usable.findIndex((c) => c.recommended)) + 1;
148
- const answer = (await rl.question(`\n Choose 1-${usable.length}, or q to quit ${dim(`[${def}]`)}: `)).trim().toLowerCase();
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) && usable[n - 1] ? usable[n - 1].value : null;
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): KeyResult<T>;
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.0",
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": {