docugate 0.2.6 → 0.2.8
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/cli.js +102 -72
- package/dist/init.js +77 -8
- package/dist/tour-init.js +31 -8
- package/dist/tour-server.js +13 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
2
3
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
4
|
import { join } from 'node:path';
|
|
4
5
|
import { check } from './check.js';
|
|
5
6
|
import { loadConfig } from './config.js';
|
|
6
|
-
import {
|
|
7
|
+
import { initFlow } from './init.js';
|
|
7
8
|
import { buildOpenApi, isCurrent, writeOpenApi } from './openapi.js';
|
|
8
9
|
import { BobKeyMissingError, BobMissingError, tourInit } from './tour-init.js';
|
|
9
10
|
import { tourInstall, hasPillSnippet, pillSnippetLine, DEFAULT_PORT } from './tour-install.js';
|
|
@@ -12,57 +13,57 @@ import { startTourServer } from './tour-server.js';
|
|
|
12
13
|
import { login, clearCredentials, loadCredentials } from './auth.js';
|
|
13
14
|
import { BASE_URL, getSession } from './api.js';
|
|
14
15
|
const SITE = 'https://www.trydocugate.site';
|
|
15
|
-
const HELP = `docugate: set up and check the docs DocuGate reads from your repository
|
|
16
|
-
|
|
17
|
-
Usage
|
|
18
|
-
docugate <command> [options]
|
|
19
|
-
|
|
20
|
-
Commands
|
|
21
|
-
init Set up this repository for DocuGate (interactive)
|
|
22
|
-
login Sign in to DocuGate with GitHub
|
|
23
|
-
logout Sign out and delete saved credentials
|
|
24
|
-
whoami Print the currently signed-in user
|
|
25
|
-
check Check the docs the way DocuGate will read them
|
|
26
|
-
openapi Prepare an OpenAPI spec for DocuGate's API reference
|
|
27
|
-
tour init Write the first tour of this app with IBM Bob
|
|
28
|
-
tour install Add the pill loader to your app
|
|
29
|
-
tour serve Start a local server that serves tour data for the pill
|
|
30
|
-
tour export Write every tour screen to a single JSON file
|
|
31
|
-
|
|
32
|
-
init
|
|
33
|
-
--dir <folder> Docs folder (default: docs)
|
|
34
|
-
--title <text> Space title (default: this folder's name)
|
|
35
|
-
--role <role> Repository role: frontend, backend, or both
|
|
36
|
-
--yes Accept every default without asking
|
|
37
|
-
|
|
38
|
-
check
|
|
39
|
-
--dir <folder> Check this folder instead of docugate.json's docsDir
|
|
40
|
-
--strict Treat warnings as errors
|
|
41
|
-
|
|
42
|
-
openapi
|
|
43
|
-
--from <file|url> The spec to read (or api.generate.from in docugate.json)
|
|
44
|
-
--out <file> Where to write it (default: openapi.json)
|
|
45
|
-
--include <glob> Keep only paths that match; repeatable
|
|
46
|
-
--exclude <glob> Drop paths that match; repeatable
|
|
47
|
-
--redact <name> Remove a property from every schema and example; repeatable
|
|
48
|
-
--check Fail if the file is out of date, instead of writing it
|
|
49
|
-
|
|
50
|
-
tour init
|
|
51
|
-
--max-cost <n> Most Bobcoins the run may spend (default: 3)
|
|
52
|
-
--team-id <id> Bob team to run under, for API keys that need one
|
|
53
|
-
|
|
54
|
-
tour install
|
|
55
|
-
--port <n> Port the pill server listens on (default: 4178)
|
|
56
|
-
--remove Remove the pill snippet instead of adding it
|
|
57
|
-
|
|
58
|
-
tour serve
|
|
59
|
-
--port <n> Port to listen on (default: 4178)
|
|
60
|
-
|
|
61
|
-
tour export <file>
|
|
62
|
-
Write all tour screens to <file> as JSON.
|
|
63
|
-
|
|
64
|
-
Run it from the root of your repository.
|
|
65
|
-
${SITE}
|
|
16
|
+
const HELP = `docugate: set up and check the docs DocuGate reads from your repository
|
|
17
|
+
|
|
18
|
+
Usage
|
|
19
|
+
docugate <command> [options]
|
|
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}
|
|
66
67
|
`;
|
|
67
68
|
const color = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
68
69
|
const paint = (code, text) => (color ? `\x1b[${code}m${text}\x1b[0m` : text);
|
|
@@ -206,18 +207,16 @@ async function main() {
|
|
|
206
207
|
yes: args.bools.has('yes'),
|
|
207
208
|
port,
|
|
208
209
|
});
|
|
209
|
-
// The
|
|
210
|
-
//
|
|
210
|
+
// The Tour button only shows while the tour server runs, so setup starts
|
|
211
|
+
// it in the background instead of asking: the next thing the person
|
|
212
|
+
// does is open their app, and the button is there.
|
|
211
213
|
const where = result.alreadySetUp ?? root;
|
|
212
214
|
const hasTour = result.role === 'frontend' || result.role === 'both';
|
|
213
|
-
if (hasTour &&
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
]);
|
|
219
|
-
if (pick === 1)
|
|
220
|
-
await serveTour(where, port);
|
|
215
|
+
if (hasTour && !args.bools.has('no-serve')) {
|
|
216
|
+
const started = await startServerInBackground(where, port);
|
|
217
|
+
console.log(dim(started
|
|
218
|
+
? ` The tour server is running in the background (port ${port}). After a restart: docugate tour serve`
|
|
219
|
+
: ` Start the tour server with docugate tour serve, then open your app.`));
|
|
221
220
|
}
|
|
222
221
|
return;
|
|
223
222
|
}
|
|
@@ -298,28 +297,28 @@ async function main() {
|
|
|
298
297
|
}
|
|
299
298
|
catch (error) {
|
|
300
299
|
if (error instanceof BobKeyMissingError) {
|
|
301
|
-
console.error(`${red('error')} Bob Shell needs an API key to run on its own (your IDE sign-in is not used here).
|
|
300
|
+
console.error(`${red('error')} Bob Shell needs an API key to run on its own (your IDE sign-in is not used here).
|
|
302
301
|
` +
|
|
303
|
-
`1. At https://bob.ibm.com, open your instance, then API keys, and create an Inference key.
|
|
302
|
+
`1. At https://bob.ibm.com, open your instance, then API keys, and create an Inference key.
|
|
304
303
|
` +
|
|
305
|
-
`2. Save it as the BOB_API_KEY environment variable, then open a new terminal:
|
|
304
|
+
`2. Save it as the BOB_API_KEY environment variable, then open a new terminal:
|
|
306
305
|
` +
|
|
307
|
-
` PowerShell [Environment]::SetEnvironmentVariable("BOB_API_KEY", "<key>", "User")
|
|
306
|
+
` PowerShell [Environment]::SetEnvironmentVariable("BOB_API_KEY", "<key>", "User")
|
|
308
307
|
` +
|
|
309
|
-
` macOS/Linux export BOB_API_KEY=<key> (add it to your shell profile)
|
|
308
|
+
` macOS/Linux export BOB_API_KEY=<key> (add it to your shell profile)
|
|
310
309
|
` +
|
|
311
310
|
dim('Keep the key out of your repository. https://bob.ibm.com/docs/ide/account/api-keys'));
|
|
312
311
|
process.exit(2);
|
|
313
312
|
}
|
|
314
313
|
if (!(error instanceof BobMissingError))
|
|
315
314
|
throw error;
|
|
316
|
-
console.error(`${red('error')} Bob Shell is not installed, and tours are written by IBM Bob.
|
|
315
|
+
console.error(`${red('error')} Bob Shell is not installed, and tours are written by IBM Bob.
|
|
317
316
|
` +
|
|
318
|
-
`Install it (Node 24 or later), sign in, then run this again:
|
|
317
|
+
`Install it (Node 24 or later), sign in, then run this again:
|
|
319
318
|
` +
|
|
320
|
-
` Windows powershell -c "irm -Uri https://bob.ibm.com/download/bobshell.ps1 | iex"
|
|
319
|
+
` Windows powershell -c "irm -Uri https://bob.ibm.com/download/bobshell.ps1 | iex"
|
|
321
320
|
` +
|
|
322
|
-
` macOS/Linux curl -fsSL https://bob.ibm.com/download/bobshell.sh | bash
|
|
321
|
+
` macOS/Linux curl -fsSL https://bob.ibm.com/download/bobshell.sh | bash
|
|
323
322
|
` +
|
|
324
323
|
dim('https://bob.ibm.com/docs/shell/getting-started/install-and-setup'));
|
|
325
324
|
process.exit(2);
|
|
@@ -338,7 +337,7 @@ async function main() {
|
|
|
338
337
|
stats?.durationMs !== undefined ? `${Math.round(stats.durationMs / 1000)}s` : '',
|
|
339
338
|
stats?.cost !== undefined ? `${stats.cost} Bobcoins` : '',
|
|
340
339
|
].filter(Boolean);
|
|
341
|
-
console.log(`
|
|
340
|
+
console.log(`
|
|
342
341
|
${dim(`Bob: ${parts.join(', ')}`)}`);
|
|
343
342
|
console.log('Next: read the new files, fix anything Bob got wrong, and commit them. They are yours now.');
|
|
344
343
|
return;
|
|
@@ -369,6 +368,37 @@ main().catch((error) => {
|
|
|
369
368
|
console.error(`${red('error')} ${error.message}`);
|
|
370
369
|
process.exit(1);
|
|
371
370
|
});
|
|
371
|
+
/**
|
|
372
|
+
* Starts `docugate tour serve` detached, so it outlives this command. When one
|
|
373
|
+
* is already answering on the port, that one is kept. True when a server is
|
|
374
|
+
* answering afterwards.
|
|
375
|
+
*/
|
|
376
|
+
async function startServerInBackground(root, port) {
|
|
377
|
+
const answering = async () => {
|
|
378
|
+
try {
|
|
379
|
+
const res = await fetch(`http://127.0.0.1:${port}/pill.js`, { signal: AbortSignal.timeout(800) });
|
|
380
|
+
return res.ok;
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
if (await answering())
|
|
387
|
+
return true;
|
|
388
|
+
const child = spawn(process.execPath, [process.argv[1], 'tour', 'serve', '--port', String(port)], {
|
|
389
|
+
cwd: root,
|
|
390
|
+
detached: true,
|
|
391
|
+
stdio: 'ignore',
|
|
392
|
+
windowsHide: true,
|
|
393
|
+
});
|
|
394
|
+
child.unref();
|
|
395
|
+
for (let i = 0; i < 20; i++) {
|
|
396
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
397
|
+
if (await answering())
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
372
402
|
/**
|
|
373
403
|
* Serves the tour to the pill until Ctrl-C. Starts even when there is no tour
|
|
374
404
|
* yet: the pill then shows on every page, and its inspector writes the first
|
package/dist/init.js
CHANGED
|
@@ -53,6 +53,21 @@ export function makeDefaultIO() {
|
|
|
53
53
|
});
|
|
54
54
|
});
|
|
55
55
|
},
|
|
56
|
+
askSecret(question, defaultAnswer) {
|
|
57
|
+
// Keys must never be shown on screen, where screenshots and recordings
|
|
58
|
+
// pick them up: print the question, then echo nothing while typing.
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
61
|
+
const muted = rl;
|
|
62
|
+
process.stdout.write(`${question} `);
|
|
63
|
+
muted._writeToOutput = () => undefined;
|
|
64
|
+
rl.question('', (answer) => {
|
|
65
|
+
rl.close();
|
|
66
|
+
process.stdout.write('[hidden]\n');
|
|
67
|
+
resolve(answer.trim() || defaultAnswer);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
},
|
|
56
71
|
print(line) {
|
|
57
72
|
console.log(line);
|
|
58
73
|
},
|
|
@@ -114,7 +129,7 @@ async function createDocsSpace(io, projectName, baseUrl, options) {
|
|
|
114
129
|
if (fix !== 1)
|
|
115
130
|
return undefined;
|
|
116
131
|
(options.openUrl ?? openInBrowser)(`${baseUrl}/api/auth/github?install=1`);
|
|
117
|
-
await io.ask(` On GitHub, add ${repo.repo} (or choose All repositories) and save. Then press Enter here
|
|
132
|
+
await io.ask(` On GitHub, add ${repo.repo} (or choose All repositories) and save. Then press Enter here:`, '');
|
|
118
133
|
}
|
|
119
134
|
}
|
|
120
135
|
return undefined;
|
|
@@ -167,7 +182,7 @@ async function askForBobKey(io, openUrl) {
|
|
|
167
182
|
io.print(` In your Bob instance: API keys, then create an Inference key.`);
|
|
168
183
|
io.print(` ${BOB_KEYS_URL}`);
|
|
169
184
|
(openUrl ?? openInBrowser)(BOB_KEYS_URL);
|
|
170
|
-
const key = (await io.ask(' Paste the key and press Enter (used for this run only, never saved)', '')).trim();
|
|
185
|
+
const key = (await (io.askSecret ?? io.ask)(' Paste the key and press Enter (hidden, used for this run only, never saved):', '')).trim();
|
|
171
186
|
if (!key) {
|
|
172
187
|
io.print(' No key pasted. Skipped the tour: run docugate tour init when you have one.');
|
|
173
188
|
return false;
|
|
@@ -231,16 +246,36 @@ export function guessRole(root) {
|
|
|
231
246
|
return 'backend';
|
|
232
247
|
return undefined;
|
|
233
248
|
}
|
|
249
|
+
// ── Look ───────────────────────────────────────────────────────────────────
|
|
250
|
+
// Color only in a real terminal, and never when NO_COLOR is set, so logs and
|
|
251
|
+
// CI stay plain text.
|
|
252
|
+
const colorOn = () => Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
253
|
+
const paint = (code) => (text) => (colorOn() ? `\x1b[${code}m${text}\x1b[0m` : text);
|
|
254
|
+
const bold = paint('1');
|
|
255
|
+
const dimmed = paint('2');
|
|
256
|
+
const gold = paint('38;2;244;196;63');
|
|
257
|
+
const cream = paint('38;2;245;240;227');
|
|
258
|
+
const green = paint('32');
|
|
259
|
+
/** DocuGate's owl, drawn small: cream eyes, gold beak. */
|
|
260
|
+
export function banner() {
|
|
261
|
+
return [
|
|
262
|
+
'',
|
|
263
|
+
` ${dimmed('╭─────╮')}`,
|
|
264
|
+
` ${dimmed('│')} ${cream('◉ ◉')} ${dimmed('│')} ${bold('DocuGate')}`,
|
|
265
|
+
` ${dimmed('│')} ${gold('▾')} ${dimmed('│')} ${dimmed('Tours and docs for your codebase')}`,
|
|
266
|
+
` ${dimmed('╰─────╯')}`,
|
|
267
|
+
];
|
|
268
|
+
}
|
|
234
269
|
/**
|
|
235
270
|
* A numbered choice: prints the options, and takes only a number. Enter picks
|
|
236
271
|
* the default. Nobody types a word during setup.
|
|
237
272
|
*/
|
|
238
273
|
export async function choose(io, question, options, fallback = 1) {
|
|
239
274
|
io.print('');
|
|
240
|
-
io.print(` ${question}`);
|
|
241
|
-
options.forEach((option, i) => io.print(` ${i + 1} ${option}`));
|
|
275
|
+
io.print(` ${bold(question)}`);
|
|
276
|
+
options.forEach((option, i) => io.print(` ${gold(String(i + 1))} ${option}`));
|
|
242
277
|
for (;;) {
|
|
243
|
-
const answer = (await io.ask(` Choose 1-${options.length} [${fallback}]
|
|
278
|
+
const answer = (await io.ask(` ${dimmed(`Choose 1-${options.length} [${fallback}]`)}:`, String(fallback))).trim();
|
|
244
279
|
const n = Number(answer);
|
|
245
280
|
if (Number.isInteger(n) && n >= 1 && n <= options.length)
|
|
246
281
|
return n;
|
|
@@ -275,6 +310,9 @@ export async function initFlow(root, options = {}) {
|
|
|
275
310
|
// One project gets one setup and one tour. Run inside frontend/ of a
|
|
276
311
|
// project already set up at its root, and init points there instead of
|
|
277
312
|
// starting a second one.
|
|
313
|
+
if (io.isTTY)
|
|
314
|
+
for (const line of banner())
|
|
315
|
+
io.print(line);
|
|
278
316
|
const parent = setUpAbove(root);
|
|
279
317
|
if (parent) {
|
|
280
318
|
io.print(` This project is already set up in ${parent}.`);
|
|
@@ -475,7 +513,7 @@ export async function initFlow(root, options = {}) {
|
|
|
475
513
|
'IBM Bob (uses Bobcoins)',
|
|
476
514
|
'IBM watsonx (coming soon)',
|
|
477
515
|
'Skip, I will write it in the browser',
|
|
478
|
-
]
|
|
516
|
+
]);
|
|
479
517
|
if (pick === 2)
|
|
480
518
|
io.print(' IBM watsonx is coming soon. Skipped for now: run docugate tour init when you are ready.');
|
|
481
519
|
runTour = pick === 1;
|
|
@@ -525,7 +563,10 @@ export async function initFlow(root, options = {}) {
|
|
|
525
563
|
`Bob Shell is not installed. Install it, sign in, then run docugate tour init.`;
|
|
526
564
|
}
|
|
527
565
|
else {
|
|
528
|
-
|
|
566
|
+
// One calm line; the technical detail only when asked for.
|
|
567
|
+
tourError = `IBM Bob couldn't write the tour this time. The pill still works: add steps in the browser, or run docugate tour init later.`;
|
|
568
|
+
if (process.env.DOCUGATE_DEBUG)
|
|
569
|
+
io.print(` detail ${error.message}`);
|
|
529
570
|
}
|
|
530
571
|
io.print(` warning ${tourError}`);
|
|
531
572
|
}
|
|
@@ -571,7 +612,35 @@ export async function initFlow(root, options = {}) {
|
|
|
571
612
|
io.print(` installed pill in ${installResult.target}`);
|
|
572
613
|
if (installResult?.action === 'already')
|
|
573
614
|
io.print(` pill already in ${installResult.target}`);
|
|
615
|
+
const hasPill = installResult?.action === 'inserted' || installResult?.action === 'already';
|
|
616
|
+
const final = loadConfig(root).config;
|
|
617
|
+
const freshness = FRESHNESS.find((f) => f.value === final.freshness);
|
|
618
|
+
const rows = [
|
|
619
|
+
['Repository', role ? role.charAt(0).toUpperCase() + role.slice(1) : 'not set'],
|
|
620
|
+
['Docs', final.space ?? 'no space yet'],
|
|
621
|
+
['Checks', freshness ? freshness.label.split(' (')[0] : 'not set'],
|
|
622
|
+
];
|
|
623
|
+
if (role !== 'backend') {
|
|
624
|
+
rows.push(['Tour', tourResult ? `IBM Bob wrote ${tourResult.added.length} screen${tourResult.added.length === 1 ? '' : 's'}` : 'add steps in the browser']);
|
|
625
|
+
rows.push(['Tour button', hasPill && installResult?.target ? installResult.target : 'not added']);
|
|
626
|
+
}
|
|
627
|
+
io.print(``);
|
|
628
|
+
io.print(` ${dimmed('╭')} ${bold('DocuGate setup')}`);
|
|
629
|
+
rows.forEach(([label, value], i) => {
|
|
630
|
+
const branch = i === rows.length - 1 ? '╰' : '├';
|
|
631
|
+
io.print(` ${dimmed(branch)} ${label.padEnd(12)} ${green(value)}`);
|
|
632
|
+
});
|
|
574
633
|
io.print(``);
|
|
575
|
-
|
|
634
|
+
if (role === 'backend') {
|
|
635
|
+
io.print(` ${green('✓')} ${bold('DocuGate is set up for this backend.')}`);
|
|
636
|
+
}
|
|
637
|
+
else if (hasPill) {
|
|
638
|
+
io.print(` ${green('✓')} ${bold('DocuGate is set up.')}`);
|
|
639
|
+
io.print(` Start your project as usual (for example ${gold('npm run dev')}) and open it:`);
|
|
640
|
+
io.print(` the ${gold('Tour')} button is in the bottom-right corner.`);
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
io.print(` ${green('✓')} ${bold('DocuGate is set up.')} Run docugate tour install to add the Tour button to your app.`);
|
|
644
|
+
}
|
|
576
645
|
return { ...base, role, tourResult, tourError, installResult, connectError };
|
|
577
646
|
}
|
package/dist/tour-init.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { join } from 'node:path';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
5
|
import { isMap, isSeq, parseDocument } from 'yaml';
|
|
6
6
|
// `docugate tour init` writes the first tour of an app by asking IBM Bob
|
|
7
7
|
// (through Bob Shell) to read the code. Bob does the reading; this file makes
|
|
@@ -86,14 +86,37 @@ export function readBobKey() {
|
|
|
86
86
|
export function useBobKeyForThisRun(key) {
|
|
87
87
|
process.env.BOB_API_KEY = key;
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* On Windows, `bob` is an npm .cmd shim, and a .cmd only runs through cmd.exe,
|
|
91
|
+
* which splits a path at its spaces ("C:\Users\GUSENGA Thierry") and cannot
|
|
92
|
+
* carry the prompt's line breaks. The shim only starts Node on Bob Shell's
|
|
93
|
+
* script, so run that script with Node directly: no shell, nothing mangled.
|
|
94
|
+
*/
|
|
95
|
+
export function bobScript(pathEnv = process.env.PATH ?? '') {
|
|
96
|
+
for (const dir of pathEnv.split(';').filter(Boolean)) {
|
|
97
|
+
const shim = join(dir, 'bob.cmd');
|
|
98
|
+
if (!existsSync(shim))
|
|
99
|
+
continue;
|
|
100
|
+
const match = readFileSync(shim, 'utf8').match(/"%dp0%\\([^"]+\.js)"/);
|
|
101
|
+
if (!match)
|
|
102
|
+
continue;
|
|
103
|
+
const script = join(dir, match[1]);
|
|
104
|
+
if (existsSync(script))
|
|
105
|
+
return script;
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
89
109
|
const defaultRunBob = (args, cwd) => {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
encoding: 'utf8',
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
110
|
+
const script = process.platform === 'win32' ? bobScript() : undefined;
|
|
111
|
+
const result = script
|
|
112
|
+
? spawnSync(process.execPath, [script, ...args], { cwd, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 })
|
|
113
|
+
: spawnSync('bob', args, {
|
|
114
|
+
cwd,
|
|
115
|
+
encoding: 'utf8',
|
|
116
|
+
// No shim script found on Windows: fall back to the shell.
|
|
117
|
+
shell: process.platform === 'win32',
|
|
118
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
119
|
+
});
|
|
97
120
|
if (result.error && result.error.code === 'ENOENT') {
|
|
98
121
|
throw new BobMissingError('Bob Shell is not installed.');
|
|
99
122
|
}
|
package/dist/tour-server.js
CHANGED
|
@@ -375,6 +375,19 @@ export function startTourServer(root, port) {
|
|
|
375
375
|
else if (req.method === 'GET' && path === '/pill.js') {
|
|
376
376
|
handleGetPill(res);
|
|
377
377
|
}
|
|
378
|
+
else if (req.method === 'GET' && path === '/') {
|
|
379
|
+
// Somebody opened the tour server itself. Say what it is and where to go.
|
|
380
|
+
const screens = loadTours(root).length;
|
|
381
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
382
|
+
res.end(`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>DocuGate tour server</title>
|
|
383
|
+
<style>body{margin:0;min-height:100vh;display:grid;place-items:center;background:#0F1014;color:#F5F0E3;font:15px/1.6 system-ui,sans-serif}
|
|
384
|
+
main{max-width:440px;padding:32px;border:1px solid #25273a;border-radius:16px;background:#15173a}
|
|
385
|
+
h1{font-size:20px;margin:0 0 8px}p{margin:0 0 12px;color:#aaa597}b{color:#F4C43F;font-weight:600}
|
|
386
|
+
.dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:#45d483;margin-right:8px}</style></head>
|
|
387
|
+
<body><main><h1><span class="dot"></span>The DocuGate tour server is running</h1>
|
|
388
|
+
<p>This is not a page to open. It feeds the Tour button in your app (${screens} screen${screens === 1 ? '' : 's'} in <b>.docugate/tour/</b>).</p>
|
|
389
|
+
<p>Open <b>your app</b> instead, for example <b>http://localhost:5173</b> or <b>http://localhost:3000</b>. The Tour button is in the bottom-right corner.</p></main></body></html>`);
|
|
390
|
+
}
|
|
378
391
|
else {
|
|
379
392
|
sendText(res, 404, 'Not found');
|
|
380
393
|
}
|
package/package.json
CHANGED