docugate 0.2.9 → 0.3.0
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 +200 -57
- package/dist/api.d.ts +44 -0
- package/dist/api.js +11 -5
- package/dist/args.d.ts +29 -0
- package/dist/args.js +121 -0
- package/dist/auth.d.ts +21 -0
- package/dist/banner.d.ts +13 -0
- package/dist/banner.js +62 -0
- package/dist/check.d.ts +22 -0
- package/dist/check.js +4 -3
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +201 -165
- package/dist/commands.d.ts +35 -0
- package/dist/commands.js +293 -0
- package/dist/config.d.ts +22 -0
- package/dist/config.js +43 -3
- package/dist/developers.d.ts +35 -0
- package/dist/developers.js +439 -0
- package/dist/init.d.ts +130 -0
- package/dist/init.js +93 -32
- package/dist/keys/core.d.ts +108 -0
- package/dist/keys/core.js +142 -0
- package/dist/keys/express.d.ts +21 -0
- package/dist/keys/express.js +27 -0
- package/dist/keys/fastify.d.ts +21 -0
- package/dist/keys/fastify.js +21 -0
- package/dist/keys/format.d.ts +11 -0
- package/dist/keys/format.js +46 -0
- package/dist/keys/hono.d.ts +19 -0
- package/dist/keys/hono.js +21 -0
- package/dist/keys/index.d.ts +13 -0
- package/dist/keys/index.js +12 -0
- package/dist/logo-pixels.d.ts +4 -0
- package/dist/logo-pixels.js +35 -0
- package/dist/logo.d.ts +19 -0
- package/dist/logo.js +68 -0
- package/dist/menu.d.ts +16 -0
- package/dist/menu.js +157 -0
- package/dist/openapi.d.ts +38 -0
- package/dist/openapi.js +2 -1
- package/dist/rules.d.ts +69 -0
- package/dist/rules.js +2 -1
- package/dist/select.d.ts +75 -0
- package/dist/select.js +215 -0
- package/dist/session.d.ts +46 -0
- package/dist/session.js +54 -0
- package/dist/spinner.d.ts +12 -0
- package/dist/spinner.js +12 -12
- package/dist/status.d.ts +46 -0
- package/dist/status.js +166 -0
- package/dist/sync/ai.d.ts +44 -0
- package/dist/sync/ai.js +147 -0
- package/dist/sync/apply.d.ts +25 -0
- package/dist/sync/apply.js +104 -0
- package/dist/sync/changes.d.ts +58 -0
- package/dist/sync/changes.js +167 -0
- package/dist/sync/command.d.ts +12 -0
- package/dist/sync/command.js +236 -0
- package/dist/sync/guard.d.ts +15 -0
- package/dist/sync/guard.js +21 -0
- package/dist/sync/hook.d.ts +10 -0
- package/dist/sync/hook.js +62 -0
- package/dist/sync/impact.d.ts +23 -0
- package/dist/sync/impact.js +93 -0
- package/dist/sync/pr.d.ts +37 -0
- package/dist/sync/pr.js +106 -0
- package/dist/sync/run.d.ts +50 -0
- package/dist/sync/run.js +124 -0
- package/dist/sync/state.d.ts +8 -0
- package/dist/sync/state.js +22 -0
- package/dist/sync/workflow.d.ts +20 -0
- package/dist/sync/workflow.js +84 -0
- package/dist/text.d.ts +11 -0
- package/dist/text.js +16 -0
- package/dist/tour-init.d.ts +61 -0
- package/dist/tour-install.d.ts +38 -0
- package/dist/tour-server.d.ts +21 -0
- package/dist/tour.d.ts +60 -0
- package/dist/ui.d.ts +29 -0
- package/dist/ui.js +63 -0
- package/package.json +67 -47
package/dist/banner.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const TAGLINE = "Tours and docs for your codebase";
|
|
2
|
+
/** The wordmark as five plain lines, no color. */
|
|
3
|
+
export declare function wordmark(text?: string, gap?: number): string[];
|
|
4
|
+
export declare function packageVersion(): string;
|
|
5
|
+
export type BannerOptions = {
|
|
6
|
+
/** Terminal width; unknown is treated as narrow. */
|
|
7
|
+
columns?: number;
|
|
8
|
+
color?: boolean;
|
|
9
|
+
version?: string;
|
|
10
|
+
env?: NodeJS.ProcessEnv;
|
|
11
|
+
};
|
|
12
|
+
/** The banner as lines to print, with a blank line above and below. */
|
|
13
|
+
export declare function banner(options?: BannerOptions): string[];
|
package/dist/banner.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { colorDepth, fg, logoBanner } from './logo.js';
|
|
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.
|
|
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.
|
|
13
|
+
const GLYPHS = {
|
|
14
|
+
D: ['████ ', '█ █', '█ █', '█ █', '████ '],
|
|
15
|
+
O: [' ███ ', '█ █', '█ █', '█ █', ' ███ '],
|
|
16
|
+
C: [' ████', '█ ', '█ ', '█ ', ' ████'],
|
|
17
|
+
U: ['█ █', '█ █', '█ █', '█ █', ' ███ '],
|
|
18
|
+
G: [' ████', '█ ', '█ ██', '█ █', ' ████'],
|
|
19
|
+
A: [' ███ ', '█ █', '█████', '█ █', '█ █'],
|
|
20
|
+
T: ['█████', ' █ ', ' █ ', ' █ ', ' █ '],
|
|
21
|
+
E: ['█████', '█ ', '████ ', '█ ', '█████'],
|
|
22
|
+
};
|
|
23
|
+
// Cream at the top, through gold, to deep gold at the bottom.
|
|
24
|
+
const SHADES = [
|
|
25
|
+
[245, 240, 227],
|
|
26
|
+
[246, 222, 150],
|
|
27
|
+
[244, 196, 63],
|
|
28
|
+
[230, 175, 46],
|
|
29
|
+
[217, 154, 30],
|
|
30
|
+
];
|
|
31
|
+
/** 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) {
|
|
37
|
+
const letters = [...text.toUpperCase()].map((c) => GLYPHS[c]);
|
|
38
|
+
if (letters.some((g) => !g))
|
|
39
|
+
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());
|
|
41
|
+
}
|
|
42
|
+
export function packageVersion() {
|
|
43
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
44
|
+
return pkg.version;
|
|
45
|
+
}
|
|
46
|
+
/** The banner as lines to print, with a blank line above and below. */
|
|
47
|
+
export function banner(options = {}) {
|
|
48
|
+
const {
|
|
49
|
+
// mintty reports no width to a piped console; COLUMNS, or a usual window, stands in.
|
|
50
|
+
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
|
+
}
|
|
56
|
+
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}` : '')), ''];
|
|
62
|
+
}
|
package/dist/check.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Issue } from './config.js';
|
|
2
|
+
export type CheckResult = {
|
|
3
|
+
docsDir: string;
|
|
4
|
+
pages: number;
|
|
5
|
+
errors: Issue[];
|
|
6
|
+
warnings: Issue[];
|
|
7
|
+
};
|
|
8
|
+
/** Every file under `dir`, as forward-slash paths relative to it. */
|
|
9
|
+
export declare function walk(dir: string): string[];
|
|
10
|
+
/**
|
|
11
|
+
* Checks a repository's docs the way DocuGate will read them, so problems turn
|
|
12
|
+
* up on the command line or in CI rather than on the published page.
|
|
13
|
+
*
|
|
14
|
+
* Errors are things DocuGate will get wrong or refuse. Warnings are things it
|
|
15
|
+
* handles, but probably not the way the author meant.
|
|
16
|
+
*/
|
|
17
|
+
export declare function check(root: string, dirOverride?: string): CheckResult;
|
|
18
|
+
/**
|
|
19
|
+
* Run tour checks against every tour file found under `.docugate/tour/`.
|
|
20
|
+
* Issues are warnings: the docs still publish; the tour may just be wrong.
|
|
21
|
+
*/
|
|
22
|
+
export declare function checkTours(root: string): Issue[];
|
package/dist/check.js
CHANGED
|
@@ -2,11 +2,12 @@ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
|
2
2
|
import { join, relative, sep } from 'node:path';
|
|
3
3
|
import { loadConfig } from './config.js';
|
|
4
4
|
import { MAX_FILES, firstHeading, landingPath, pathToSlug, resolveRelativeLink } from './rules.js';
|
|
5
|
+
import { readText } from './text.js';
|
|
5
6
|
import { loadTours } from './tour.js';
|
|
6
7
|
/** Never part of a repository on GitHub, and huge when present locally. */
|
|
7
8
|
const SKIP = new Set(['node_modules', '.git']);
|
|
8
9
|
/** Every file under `dir`, as forward-slash paths relative to it. */
|
|
9
|
-
function walk(dir) {
|
|
10
|
+
export function walk(dir) {
|
|
10
11
|
const out = [];
|
|
11
12
|
const visit = (current) => {
|
|
12
13
|
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
@@ -95,7 +96,7 @@ export function check(root, dirOverride) {
|
|
|
95
96
|
if (landing)
|
|
96
97
|
slugs.add('');
|
|
97
98
|
for (const page of pages) {
|
|
98
|
-
const content =
|
|
99
|
+
const content = readText(join(base, page)).replace(/\r\n/g, '\n');
|
|
99
100
|
if (content.startsWith('---\n')) {
|
|
100
101
|
warnings.push({
|
|
101
102
|
file: shown(page),
|
|
@@ -183,7 +184,7 @@ function importsIn(text) {
|
|
|
183
184
|
* Run tour checks against every tour file found under `.docugate/tour/`.
|
|
184
185
|
* Issues are warnings: the docs still publish; the tour may just be wrong.
|
|
185
186
|
*/
|
|
186
|
-
function checkTours(root) {
|
|
187
|
+
export function checkTours(root) {
|
|
187
188
|
const warnings = [];
|
|
188
189
|
let tours;
|
|
189
190
|
try {
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
CHANGED
|
@@ -1,143 +1,170 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
-
import {
|
|
3
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
+
import { createInterface } from 'node:readline/promises';
|
|
6
|
+
import { parseArgs, UsageError } from './args.js';
|
|
7
|
+
import { banner, packageVersion } from './banner.js';
|
|
5
8
|
import { check } from './check.js';
|
|
9
|
+
import { COMMANDS, findCommand, renderCommandHelp, renderCompletion, renderGroupHelp, renderHelp } from './commands.js';
|
|
6
10
|
import { loadConfig } from './config.js';
|
|
7
|
-
import {
|
|
11
|
+
import { developersFlow } from './developers.js';
|
|
12
|
+
import { initFlow, makeDefaultIO } from './init.js';
|
|
13
|
+
import { runMenu } from './menu.js';
|
|
8
14
|
import { buildOpenApi, isCurrent, writeOpenApi } from './openapi.js';
|
|
15
|
+
import { canPick } from './select.js';
|
|
16
|
+
import { gatherStatus, renderStatus } from './status.js';
|
|
17
|
+
import { syncCommand } from './sync/command.js';
|
|
9
18
|
import { BobKeyMissingError, BobMissingError, tourInit } from './tour-init.js';
|
|
10
19
|
import { tourInstall, hasPillSnippet, pillSnippetLine, DEFAULT_PORT } from './tour-install.js';
|
|
11
20
|
import { loadTours } from './tour.js';
|
|
12
21
|
import { startTourServer } from './tour-server.js';
|
|
13
22
|
import { login, clearCredentials, loadCredentials } from './auth.js';
|
|
14
23
|
import { BASE_URL, getSession } from './api.js';
|
|
24
|
+
import { dim, gold, green, hint, isMintty, plural, red, suggest, yellow } from './ui.js';
|
|
15
25
|
const SITE = 'https://www.trydocugate.site';
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
Commands
|
|
22
|
-
init Set up this repository for DocuGate (interactive)
|
|
23
|
-
login Sign in to DocuGate with GitHub
|
|
24
|
-
logout Sign out and delete saved credentials
|
|
25
|
-
whoami Print the currently signed-in user
|
|
26
|
-
check Check the docs the way DocuGate will read them
|
|
27
|
-
openapi Prepare an OpenAPI spec for DocuGate's API reference
|
|
28
|
-
tour init Write the first tour of this app with IBM Bob
|
|
29
|
-
tour install Add the pill loader to your app
|
|
30
|
-
tour serve Start a local server that serves tour data for the pill
|
|
31
|
-
tour export Write every tour screen to a single JSON file
|
|
32
|
-
|
|
33
|
-
init
|
|
34
|
-
--dir <folder> Docs folder (default: docs)
|
|
35
|
-
--title <text> Space title (default: this folder's name)
|
|
36
|
-
--role <role> Repository role: frontend, backend, or both
|
|
37
|
-
--yes Accept every default without asking
|
|
38
|
-
|
|
39
|
-
check
|
|
40
|
-
--dir <folder> Check this folder instead of docugate.json's docsDir
|
|
41
|
-
--strict Treat warnings as errors
|
|
42
|
-
|
|
43
|
-
openapi
|
|
44
|
-
--from <file|url> The spec to read (or api.generate.from in docugate.json)
|
|
45
|
-
--out <file> Where to write it (default: openapi.json)
|
|
46
|
-
--include <glob> Keep only paths that match; repeatable
|
|
47
|
-
--exclude <glob> Drop paths that match; repeatable
|
|
48
|
-
--redact <name> Remove a property from every schema and example; repeatable
|
|
49
|
-
--check Fail if the file is out of date, instead of writing it
|
|
50
|
-
|
|
51
|
-
tour init
|
|
52
|
-
--max-cost <n> Most Bobcoins the run may spend (default: 3)
|
|
53
|
-
--team-id <id> Bob team to run under, for API keys that need one
|
|
54
|
-
|
|
55
|
-
tour install
|
|
56
|
-
--port <n> Port the pill server listens on (default: 4178)
|
|
57
|
-
--remove Remove the pill snippet instead of adding it
|
|
58
|
-
|
|
59
|
-
tour serve
|
|
60
|
-
--port <n> Port to listen on (default: 4178)
|
|
61
|
-
|
|
62
|
-
tour export <file>
|
|
63
|
-
Write all tour screens to <file> as JSON.
|
|
64
|
-
|
|
65
|
-
Run it from the root of your repository.
|
|
66
|
-
${SITE}
|
|
67
|
-
`;
|
|
68
|
-
const color = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
69
|
-
const paint = (code, text) => (color ? `\x1b[${code}m${text}\x1b[0m` : text);
|
|
70
|
-
const red = (t) => paint(31, t);
|
|
71
|
-
const yellow = (t) => paint(33, t);
|
|
72
|
-
const green = (t) => paint(32, t);
|
|
73
|
-
const dim = (t) => paint(2, t);
|
|
74
|
-
const VALUE_FLAGS = new Set(['dir', 'title', 'role', 'from', 'out', 'include', 'exclude', 'redact', 'max-cost', 'team-id', 'port']);
|
|
75
|
-
function parse(argv) {
|
|
76
|
-
const flags = new Map();
|
|
77
|
-
const bools = new Set();
|
|
78
|
-
let command;
|
|
79
|
-
let sub;
|
|
80
|
-
let sub2;
|
|
81
|
-
for (let i = 0; i < argv.length; i++) {
|
|
82
|
-
const arg = argv[i];
|
|
83
|
-
if (arg === '-h')
|
|
84
|
-
bools.add('help');
|
|
85
|
-
else if (arg === '-v')
|
|
86
|
-
bools.add('version');
|
|
87
|
-
else if (arg.startsWith('--')) {
|
|
88
|
-
const [name, inline] = arg.slice(2).split(/=(.*)/s, 2);
|
|
89
|
-
if (VALUE_FLAGS.has(name)) {
|
|
90
|
-
const value = inline ?? argv[++i];
|
|
91
|
-
if (value === undefined)
|
|
92
|
-
fail(`--${name} needs a value.`);
|
|
93
|
-
flags.set(name, [...(flags.get(name) ?? []), value]);
|
|
94
|
-
}
|
|
95
|
-
else
|
|
96
|
-
bools.add(name);
|
|
97
|
-
}
|
|
98
|
-
else if (!command)
|
|
99
|
-
command = arg;
|
|
100
|
-
else if (command === 'tour' && !sub)
|
|
101
|
-
sub = arg;
|
|
102
|
-
else if (command === 'tour' && sub === 'export' && !sub2)
|
|
103
|
-
sub2 = arg;
|
|
104
|
-
else
|
|
105
|
-
fail(`Unexpected argument "${arg}".`);
|
|
106
|
-
}
|
|
107
|
-
return { command, sub, sub2, flags, bools };
|
|
108
|
-
}
|
|
109
|
-
function fail(message) {
|
|
110
|
-
// Phrased without a command name on purpose: this is the message somebody
|
|
111
|
-
// sees when they are already stuck, and it would be wrong for half of them.
|
|
112
|
-
// `docugate --help` is not a thing you can run if you reached this through
|
|
113
|
-
// npx, and `npx docugate --help` is not one if you installed it.
|
|
114
|
-
console.error(`${red('error')} ${message}\n${dim('See --help for usage.')}`);
|
|
115
|
-
process.exit(2);
|
|
116
|
-
}
|
|
117
|
-
function version() {
|
|
118
|
-
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
119
|
-
return pkg.version;
|
|
26
|
+
/** True while the launch menu is running: its commands do not print their own "Next". */
|
|
27
|
+
let inMenu = false;
|
|
28
|
+
function fail(message, tip) {
|
|
29
|
+
throw new UsageError(message, tip);
|
|
120
30
|
}
|
|
121
31
|
function report(label, issues, paintLabel) {
|
|
122
32
|
for (const issue of issues) {
|
|
123
33
|
console.log(` ${paintLabel(label.padEnd(5))} ${issue.file ? `${issue.file} ` : ''}${issue.message}`);
|
|
124
34
|
}
|
|
125
35
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
36
|
+
function printHelp() {
|
|
37
|
+
// The banner only in a real terminal: piped help stays plain text.
|
|
38
|
+
if (process.stdout.isTTY)
|
|
39
|
+
for (const line of banner())
|
|
40
|
+
console.log(line);
|
|
41
|
+
console.log(renderHelp(SITE));
|
|
42
|
+
}
|
|
43
|
+
/** After a command in a terminal: the one thing worth doing next. */
|
|
44
|
+
async function closing(root) {
|
|
45
|
+
if (inMenu || !process.stdout.isTTY || process.env.CI)
|
|
46
|
+
return;
|
|
47
|
+
try {
|
|
48
|
+
const status = await gatherStatus(root, { timeoutMs: 1500 });
|
|
49
|
+
if (status.next.done)
|
|
50
|
+
return;
|
|
51
|
+
console.log(`\n ${dim('Next:')} ${gold(hint(status.next.command))} ${dim(`— ${status.next.why}`)}\n`);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
/* a hint is never worth an error */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function reportError(error) {
|
|
58
|
+
if (error instanceof UsageError) {
|
|
59
|
+
// Phrased without a command name on purpose: this is the message somebody
|
|
60
|
+
// sees when they are already stuck, and it would be wrong for half of them.
|
|
61
|
+
console.error(`${red('error')} ${error.message}\n${dim(error.hint ?? 'See --help for usage.')}`);
|
|
62
|
+
process.exitCode = 2;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
console.error(`${red('error')} ${error.message}`);
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Whether somebody is at the keyboard. A real terminal says so; Git Bash's
|
|
71
|
+
* mintty window on Windows does not, because Node sees its console as a pipe,
|
|
72
|
+
* so it is recognised by the name it puts in TERM_PROGRAM instead. There the menu is the numbered
|
|
73
|
+
* one, which needs no raw keys.
|
|
74
|
+
*/
|
|
75
|
+
function isInteractive() {
|
|
76
|
+
if (process.stdin.isTTY && process.stdout.isTTY)
|
|
77
|
+
return true;
|
|
78
|
+
// mintty names itself; a script or a pipe run from Git Bash does not.
|
|
79
|
+
return isMintty() && !process.env.CI;
|
|
80
|
+
}
|
|
81
|
+
/** The launch menu: pick a step, run it, come back. */
|
|
82
|
+
async function menuLoop(root) {
|
|
83
|
+
inMenu = true;
|
|
84
|
+
// The menu only opens for somebody at the keyboard, so the logo always shows.
|
|
85
|
+
for (const line of banner())
|
|
86
|
+
console.log(line);
|
|
87
|
+
for (;;) {
|
|
88
|
+
const status = await gatherStatus(root);
|
|
89
|
+
const action = await runMenu(status, { numbered: !canPick() });
|
|
90
|
+
if (!action)
|
|
91
|
+
return;
|
|
92
|
+
try {
|
|
93
|
+
await run(action);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
reportError(error);
|
|
97
|
+
}
|
|
98
|
+
process.exitCode = 0;
|
|
99
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
100
|
+
const again = (await rl.question(dim('\n Press Enter for the menu, or q to quit. '))).trim().toLowerCase() !== 'q';
|
|
101
|
+
rl.close();
|
|
102
|
+
if (!again)
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function run(argv) {
|
|
107
|
+
const args = parseArgs(argv);
|
|
129
108
|
const one = (name) => args.flags.get(name)?.at(-1);
|
|
130
109
|
const many = (name) => args.flags.get(name) ?? [];
|
|
131
110
|
const root = process.cwd();
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (args.
|
|
135
|
-
return console.log(
|
|
111
|
+
// No question is ever asked in CI, in a pipe, or when told not to.
|
|
112
|
+
const noInput = args.bools.has('no-input') || Boolean(process.env.CI);
|
|
113
|
+
if (args.version)
|
|
114
|
+
return console.log(packageVersion());
|
|
115
|
+
if (args.help) {
|
|
116
|
+
if (args.spec)
|
|
117
|
+
return console.log(renderCommandHelp(args.spec));
|
|
118
|
+
if (args.group)
|
|
119
|
+
return console.log(renderGroupHelp(args.group));
|
|
120
|
+
return printHelp();
|
|
121
|
+
}
|
|
122
|
+
if (args.group) {
|
|
123
|
+
// A group on its own is a command that was not finished: show the choices, and say it failed.
|
|
124
|
+
console.log(renderGroupHelp(args.group));
|
|
125
|
+
process.exitCode = 2;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (!args.command) {
|
|
129
|
+
// `docugate` on its own opens the app, the way `claude` does: a person typed
|
|
130
|
+
// it. Help is for a pipe, CI, or --no-input, where nobody can answer.
|
|
131
|
+
if (!inMenu && !noInput && isInteractive())
|
|
132
|
+
return menuLoop(root);
|
|
133
|
+
return printHelp();
|
|
134
|
+
}
|
|
136
135
|
switch (args.command) {
|
|
136
|
+
case 'help': {
|
|
137
|
+
const topic = args.positional;
|
|
138
|
+
if (!topic)
|
|
139
|
+
return printHelp();
|
|
140
|
+
const spec = findCommand(topic);
|
|
141
|
+
if (spec)
|
|
142
|
+
return console.log(renderCommandHelp(spec));
|
|
143
|
+
if (COMMANDS.some((c) => c.name.startsWith(`${topic} `)))
|
|
144
|
+
return console.log(renderGroupHelp(topic));
|
|
145
|
+
const close = suggest(topic, COMMANDS.map((c) => c.name));
|
|
146
|
+
return fail(`There is no command "${topic}".`, close ? `Did you mean "${close}"?` : 'Run `docugate help` to see every command.');
|
|
147
|
+
}
|
|
148
|
+
case 'completion': {
|
|
149
|
+
const script = renderCompletion(args.positional ?? '');
|
|
150
|
+
if (!script)
|
|
151
|
+
fail('Say which shell: bash, zsh or powershell.');
|
|
152
|
+
process.stdout.write(script);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
case 'status': {
|
|
156
|
+
const status = await gatherStatus(root);
|
|
157
|
+
if (args.bools.has('json'))
|
|
158
|
+
return console.log(JSON.stringify(status, null, 2));
|
|
159
|
+
console.log();
|
|
160
|
+
for (const line of renderStatus(status))
|
|
161
|
+
console.log(line);
|
|
162
|
+
console.log();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
137
165
|
case 'login': {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
// Already signed in — confirm with the server.
|
|
166
|
+
if (loadCredentials()) {
|
|
167
|
+
// Already signed in: confirm with the server.
|
|
141
168
|
try {
|
|
142
169
|
const { user } = await getSession();
|
|
143
170
|
if (user) {
|
|
@@ -149,21 +176,16 @@ async function main() {
|
|
|
149
176
|
// server unreachable; proceed to sign in again
|
|
150
177
|
}
|
|
151
178
|
}
|
|
152
|
-
|
|
179
|
+
await login(BASE_URL);
|
|
153
180
|
try {
|
|
154
181
|
const { user } = await getSession();
|
|
155
|
-
|
|
156
|
-
console.log(`Signed in as @${user.githubLogin}`);
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
console.log('Signed in.');
|
|
160
|
-
}
|
|
182
|
+
console.log(user ? `Signed in as @${user.githubLogin}` : 'Signed in.');
|
|
161
183
|
}
|
|
162
184
|
catch {
|
|
163
185
|
// token saved; just confirm
|
|
164
186
|
console.log('Signed in.');
|
|
165
187
|
}
|
|
166
|
-
|
|
188
|
+
await closing(root);
|
|
167
189
|
return;
|
|
168
190
|
}
|
|
169
191
|
case 'logout': {
|
|
@@ -172,9 +194,8 @@ async function main() {
|
|
|
172
194
|
return;
|
|
173
195
|
}
|
|
174
196
|
case 'whoami': {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
console.log('Not signed in. Run docugate login.');
|
|
197
|
+
if (!loadCredentials() && !process.env.DOCUGATE_TOKEN) {
|
|
198
|
+
console.log(`Not signed in. Run ${hint('login')}.`);
|
|
178
199
|
return;
|
|
179
200
|
}
|
|
180
201
|
let session;
|
|
@@ -186,12 +207,13 @@ async function main() {
|
|
|
186
207
|
process.exitCode = 1;
|
|
187
208
|
return;
|
|
188
209
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
210
|
+
console.log(session.user ? `@${session.user.githubLogin}` : `Not signed in. Run ${hint('login')}.`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
case 'developers': {
|
|
214
|
+
if (noInput && !args.bools.has('yes'))
|
|
215
|
+
fail('developers asks questions. Run it in a terminal, or add --yes to take every default.');
|
|
216
|
+
await developersFlow(root, { dir: one('dir'), yes: args.bools.has('yes') });
|
|
195
217
|
return;
|
|
196
218
|
}
|
|
197
219
|
case 'init': {
|
|
@@ -206,6 +228,8 @@ async function main() {
|
|
|
206
228
|
role: roleFlag,
|
|
207
229
|
yes: args.bools.has('yes'),
|
|
208
230
|
port,
|
|
231
|
+
// Under --no-input or in CI nothing may be asked, even in a terminal.
|
|
232
|
+
io: noInput ? { ...makeDefaultIO(), isTTY: false } : undefined,
|
|
209
233
|
});
|
|
210
234
|
// The Tour button only shows while the tour server runs, so setup starts
|
|
211
235
|
// it in the background instead of asking: the next thing the person
|
|
@@ -215,11 +239,23 @@ async function main() {
|
|
|
215
239
|
if (hasTour && !args.bools.has('no-serve')) {
|
|
216
240
|
const started = await startServerInBackground(where, port);
|
|
217
241
|
console.log(dim(started
|
|
218
|
-
? ` The tour server is running in the background (port ${port}). After a restart:
|
|
219
|
-
: ` Start the tour server with
|
|
242
|
+
? ` The tour server is running in the background (port ${port}). After a restart: ${hint('tour serve')}`
|
|
243
|
+
: ` Start the tour server with ${hint('tour serve')}, then open your app.`));
|
|
220
244
|
}
|
|
221
245
|
return;
|
|
222
246
|
}
|
|
247
|
+
case 'sync': {
|
|
248
|
+
process.exitCode = await syncCommand(root, {
|
|
249
|
+
sub: args.sub,
|
|
250
|
+
sub2: args.sub2,
|
|
251
|
+
since: one('since'),
|
|
252
|
+
model: one('model'),
|
|
253
|
+
maxFiles: one('max-files'),
|
|
254
|
+
freshness: one('freshness'),
|
|
255
|
+
bools: args.bools,
|
|
256
|
+
});
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
223
259
|
case 'check': {
|
|
224
260
|
const result = check(root, one('dir'));
|
|
225
261
|
const strict = args.bools.has('strict');
|
|
@@ -233,6 +269,8 @@ async function main() {
|
|
|
233
269
|
console.log(`\n${plural(result.errors.length, 'error')}, ${plural(result.warnings.length, 'warning')}`);
|
|
234
270
|
if (failed)
|
|
235
271
|
process.exitCode = 1;
|
|
272
|
+
else
|
|
273
|
+
await closing(root);
|
|
236
274
|
return;
|
|
237
275
|
}
|
|
238
276
|
case 'openapi': {
|
|
@@ -257,7 +295,7 @@ async function main() {
|
|
|
257
295
|
}
|
|
258
296
|
else {
|
|
259
297
|
console.log(`${red('out of date')} ${output} does not match ${from}.`);
|
|
260
|
-
console.log(dim(
|
|
298
|
+
console.log(dim(`Run ${hint('openapi')} and commit the result.`));
|
|
261
299
|
process.exitCode = 1;
|
|
262
300
|
}
|
|
263
301
|
return;
|
|
@@ -291,6 +329,7 @@ async function main() {
|
|
|
291
329
|
console.log(` ${green('installed')} package.json ${dim(`(${result.hook}: the tour server starts with your app)`)}`);
|
|
292
330
|
if (result.hookAction === 'removed')
|
|
293
331
|
console.log(` ${green('removed')} package.json ${dim(`(${result.hook})`)}`);
|
|
332
|
+
await closing(root);
|
|
294
333
|
return;
|
|
295
334
|
}
|
|
296
335
|
case 'init': {
|
|
@@ -301,31 +340,24 @@ async function main() {
|
|
|
301
340
|
}
|
|
302
341
|
catch (error) {
|
|
303
342
|
if (error instanceof BobKeyMissingError) {
|
|
304
|
-
console.error(`${red('error')} Bob Shell needs an API key to run on its own (your IDE sign-in is not used here)
|
|
305
|
-
` +
|
|
306
|
-
`
|
|
307
|
-
` +
|
|
308
|
-
`
|
|
309
|
-
` +
|
|
310
|
-
` PowerShell [Environment]::SetEnvironmentVariable("BOB_API_KEY", "<key>", "User")
|
|
311
|
-
` +
|
|
312
|
-
` macOS/Linux export BOB_API_KEY=<key> (add it to your shell profile)
|
|
313
|
-
` +
|
|
343
|
+
console.error(`${red('error')} Bob Shell needs an API key to run on its own (your IDE sign-in is not used here).\n` +
|
|
344
|
+
`1. At https://bob.ibm.com, open your instance, then API keys, and create an Inference key.\n` +
|
|
345
|
+
`2. Save it as the BOB_API_KEY environment variable, then open a new terminal:\n` +
|
|
346
|
+
` PowerShell [Environment]::SetEnvironmentVariable("BOB_API_KEY", "<key>", "User")\n` +
|
|
347
|
+
` macOS/Linux export BOB_API_KEY=<key> (add it to your shell profile)\n` +
|
|
314
348
|
dim('Keep the key out of your repository. https://bob.ibm.com/docs/ide/account/api-keys'));
|
|
315
|
-
process.
|
|
349
|
+
process.exitCode = 2;
|
|
350
|
+
return;
|
|
316
351
|
}
|
|
317
352
|
if (!(error instanceof BobMissingError))
|
|
318
353
|
throw error;
|
|
319
|
-
console.error(`${red('error')} Bob Shell is not installed, and tours are written by IBM Bob
|
|
320
|
-
` +
|
|
321
|
-
`
|
|
322
|
-
` +
|
|
323
|
-
` Windows powershell -c "irm -Uri https://bob.ibm.com/download/bobshell.ps1 | iex"
|
|
324
|
-
` +
|
|
325
|
-
` macOS/Linux curl -fsSL https://bob.ibm.com/download/bobshell.sh | bash
|
|
326
|
-
` +
|
|
354
|
+
console.error(`${red('error')} Bob Shell is not installed, and tours are written by IBM Bob.\n` +
|
|
355
|
+
`Install it (Node 24 or later), sign in, then run this again:\n` +
|
|
356
|
+
` Windows powershell -c "irm -Uri https://bob.ibm.com/download/bobshell.ps1 | iex"\n` +
|
|
357
|
+
` macOS/Linux curl -fsSL https://bob.ibm.com/download/bobshell.sh | bash\n` +
|
|
327
358
|
dim('https://bob.ibm.com/docs/shell/getting-started/install-and-setup'));
|
|
328
|
-
process.
|
|
359
|
+
process.exitCode = 2;
|
|
360
|
+
return;
|
|
329
361
|
}
|
|
330
362
|
for (const f of result.setup)
|
|
331
363
|
console.log(` ${green('created')} ${f}`);
|
|
@@ -341,8 +373,7 @@ async function main() {
|
|
|
341
373
|
stats?.durationMs !== undefined ? `${Math.round(stats.durationMs / 1000)}s` : '',
|
|
342
374
|
stats?.cost !== undefined ? `${stats.cost} Bobcoins` : '',
|
|
343
375
|
].filter(Boolean);
|
|
344
|
-
console.log(`
|
|
345
|
-
${dim(`Bob: ${parts.join(', ')}`)}`);
|
|
376
|
+
console.log(`\n${dim(`Bob: ${parts.join(', ')}`)}`);
|
|
346
377
|
console.log('Next: read the new files, fix anything Bob got wrong, and commit them. They are yours now.');
|
|
347
378
|
return;
|
|
348
379
|
}
|
|
@@ -353,14 +384,14 @@ ${dim(`Bob: ${parts.join(', ')}`)}`);
|
|
|
353
384
|
// say so in one line, and hand the terminal back to the app.
|
|
354
385
|
mkdirSync(join(root, '.docugate', 'tour'), { recursive: true });
|
|
355
386
|
const up = await startServerInBackground(root, port);
|
|
356
|
-
console.log(up ? `${green('DocuGate')} tour ready on port ${port}` : `${yellow('DocuGate')} could not start the tour server. Run
|
|
387
|
+
console.log(up ? `${green('DocuGate')} tour ready on port ${port}` : `${yellow('DocuGate')} could not start the tour server. Run ${hint('tour serve')} to see why.`);
|
|
357
388
|
return;
|
|
358
389
|
}
|
|
359
390
|
await serveTour(root, port);
|
|
360
391
|
return;
|
|
361
392
|
}
|
|
362
393
|
case 'export': {
|
|
363
|
-
const outFile = args.
|
|
394
|
+
const outFile = args.positional;
|
|
364
395
|
if (!outFile)
|
|
365
396
|
fail('Say where to write: docugate tour export <file>.');
|
|
366
397
|
const screens = loadTours(root);
|
|
@@ -370,17 +401,22 @@ ${dim(`Bob: ${parts.join(', ')}`)}`);
|
|
|
370
401
|
return;
|
|
371
402
|
}
|
|
372
403
|
default:
|
|
373
|
-
fail(
|
|
404
|
+
fail('Say which: docugate tour init, tour install, tour serve, or tour export.');
|
|
374
405
|
}
|
|
375
406
|
}
|
|
376
407
|
default:
|
|
377
408
|
fail(`Unknown command "${args.command}".`);
|
|
378
409
|
}
|
|
379
410
|
}
|
|
380
|
-
main()
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
411
|
+
main();
|
|
412
|
+
async function main() {
|
|
413
|
+
try {
|
|
414
|
+
await run(process.argv.slice(2));
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
reportError(error);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
384
420
|
/**
|
|
385
421
|
* Starts `docugate tour serve` detached, so it outlives this command. When one
|
|
386
422
|
* is already answering on the port, that one is kept. True when a server is
|