docugate 0.2.6 → 0.2.9
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 +55 -12
- package/dist/init.js +85 -11
- package/dist/tour-init.js +31 -8
- package/dist/tour-install.js +58 -0
- 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';
|
|
@@ -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
|
}
|
|
@@ -288,6 +287,10 @@ async function main() {
|
|
|
288
287
|
// already / no-op
|
|
289
288
|
console.log(` ${dim('unchanged')} ${result.target}`);
|
|
290
289
|
}
|
|
290
|
+
if (result.hookAction === 'inserted')
|
|
291
|
+
console.log(` ${green('installed')} package.json ${dim(`(${result.hook}: the tour server starts with your app)`)}`);
|
|
292
|
+
if (result.hookAction === 'removed')
|
|
293
|
+
console.log(` ${green('removed')} package.json ${dim(`(${result.hook})`)}`);
|
|
291
294
|
return;
|
|
292
295
|
}
|
|
293
296
|
case 'init': {
|
|
@@ -344,7 +347,16 @@ ${dim(`Bob: ${parts.join(', ')}`)}`);
|
|
|
344
347
|
return;
|
|
345
348
|
}
|
|
346
349
|
case 'serve': {
|
|
347
|
-
|
|
350
|
+
const port = parseInt(one('port') ?? '4178', 10);
|
|
351
|
+
if (args.bools.has('background')) {
|
|
352
|
+
// Run from the app's predev script: start (or reuse) the server,
|
|
353
|
+
// say so in one line, and hand the terminal back to the app.
|
|
354
|
+
mkdirSync(join(root, '.docugate', 'tour'), { recursive: true });
|
|
355
|
+
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 docugate tour serve to see why.`);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
await serveTour(root, port);
|
|
348
360
|
return;
|
|
349
361
|
}
|
|
350
362
|
case 'export': {
|
|
@@ -369,6 +381,37 @@ main().catch((error) => {
|
|
|
369
381
|
console.error(`${red('error')} ${error.message}`);
|
|
370
382
|
process.exit(1);
|
|
371
383
|
});
|
|
384
|
+
/**
|
|
385
|
+
* Starts `docugate tour serve` detached, so it outlives this command. When one
|
|
386
|
+
* is already answering on the port, that one is kept. True when a server is
|
|
387
|
+
* answering afterwards.
|
|
388
|
+
*/
|
|
389
|
+
async function startServerInBackground(root, port) {
|
|
390
|
+
const answering = async () => {
|
|
391
|
+
try {
|
|
392
|
+
const res = await fetch(`http://127.0.0.1:${port}/pill.js`, { signal: AbortSignal.timeout(800) });
|
|
393
|
+
return res.ok;
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
if (await answering())
|
|
400
|
+
return true;
|
|
401
|
+
const child = spawn(process.execPath, [process.argv[1], 'tour', 'serve', '--port', String(port)], {
|
|
402
|
+
cwd: root,
|
|
403
|
+
detached: true,
|
|
404
|
+
stdio: 'ignore',
|
|
405
|
+
windowsHide: true,
|
|
406
|
+
});
|
|
407
|
+
child.unref();
|
|
408
|
+
for (let i = 0; i < 20; i++) {
|
|
409
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
410
|
+
if (await answering())
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
372
415
|
/**
|
|
373
416
|
* Serves the tour to the pill until Ctrl-C. Starts even when there is no tour
|
|
374
417
|
* 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,13 +182,15 @@ 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;
|
|
174
189
|
}
|
|
175
190
|
useBobKeyForThisRun(key);
|
|
176
|
-
|
|
191
|
+
// Say it arrived, without showing it: the last four characters and the length.
|
|
192
|
+
io.print(` ${green('✓')} Key received (…${key.slice(-4)}, ${key.length} characters). Using it for this run only.`);
|
|
193
|
+
io.print(' To keep it for next time, set it in your environment:');
|
|
177
194
|
io.print(process.platform === 'win32'
|
|
178
195
|
? ' [Environment]::SetEnvironmentVariable("BOB_API_KEY", "<key>", "User") (then open a new terminal)'
|
|
179
196
|
: ' export BOB_API_KEY=<key> (add it to your shell profile)');
|
|
@@ -231,16 +248,36 @@ export function guessRole(root) {
|
|
|
231
248
|
return 'backend';
|
|
232
249
|
return undefined;
|
|
233
250
|
}
|
|
251
|
+
// ── Look ───────────────────────────────────────────────────────────────────
|
|
252
|
+
// Color only in a real terminal, and never when NO_COLOR is set, so logs and
|
|
253
|
+
// CI stay plain text.
|
|
254
|
+
const colorOn = () => Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
255
|
+
const paint = (code) => (text) => (colorOn() ? `\x1b[${code}m${text}\x1b[0m` : text);
|
|
256
|
+
const bold = paint('1');
|
|
257
|
+
const dimmed = paint('2');
|
|
258
|
+
const gold = paint('38;2;244;196;63');
|
|
259
|
+
const cream = paint('38;2;245;240;227');
|
|
260
|
+
const green = paint('32');
|
|
261
|
+
/** DocuGate's owl, drawn small: cream eyes, gold beak. */
|
|
262
|
+
export function banner() {
|
|
263
|
+
return [
|
|
264
|
+
'',
|
|
265
|
+
` ${dimmed('╭─────╮')}`,
|
|
266
|
+
` ${dimmed('│')} ${cream('◉ ◉')} ${dimmed('│')} ${bold('DocuGate')}`,
|
|
267
|
+
` ${dimmed('│')} ${gold('▾')} ${dimmed('│')} ${dimmed('Tours and docs for your codebase')}`,
|
|
268
|
+
` ${dimmed('╰─────╯')}`,
|
|
269
|
+
];
|
|
270
|
+
}
|
|
234
271
|
/**
|
|
235
272
|
* A numbered choice: prints the options, and takes only a number. Enter picks
|
|
236
273
|
* the default. Nobody types a word during setup.
|
|
237
274
|
*/
|
|
238
275
|
export async function choose(io, question, options, fallback = 1) {
|
|
239
276
|
io.print('');
|
|
240
|
-
io.print(` ${question}`);
|
|
241
|
-
options.forEach((option, i) => io.print(` ${i + 1} ${option}`));
|
|
277
|
+
io.print(` ${bold(question)}`);
|
|
278
|
+
options.forEach((option, i) => io.print(` ${gold(String(i + 1))} ${option}`));
|
|
242
279
|
for (;;) {
|
|
243
|
-
const answer = (await io.ask(` Choose 1-${options.length} [${fallback}]
|
|
280
|
+
const answer = (await io.ask(` ${dimmed(`Choose 1-${options.length} [${fallback}]`)}:`, String(fallback))).trim();
|
|
244
281
|
const n = Number(answer);
|
|
245
282
|
if (Number.isInteger(n) && n >= 1 && n <= options.length)
|
|
246
283
|
return n;
|
|
@@ -275,6 +312,9 @@ export async function initFlow(root, options = {}) {
|
|
|
275
312
|
// One project gets one setup and one tour. Run inside frontend/ of a
|
|
276
313
|
// project already set up at its root, and init points there instead of
|
|
277
314
|
// starting a second one.
|
|
315
|
+
if (io.isTTY)
|
|
316
|
+
for (const line of banner())
|
|
317
|
+
io.print(line);
|
|
278
318
|
const parent = setUpAbove(root);
|
|
279
319
|
if (parent) {
|
|
280
320
|
io.print(` This project is already set up in ${parent}.`);
|
|
@@ -475,7 +515,7 @@ export async function initFlow(root, options = {}) {
|
|
|
475
515
|
'IBM Bob (uses Bobcoins)',
|
|
476
516
|
'IBM watsonx (coming soon)',
|
|
477
517
|
'Skip, I will write it in the browser',
|
|
478
|
-
]
|
|
518
|
+
]);
|
|
479
519
|
if (pick === 2)
|
|
480
520
|
io.print(' IBM watsonx is coming soon. Skipped for now: run docugate tour init when you are ready.');
|
|
481
521
|
runTour = pick === 1;
|
|
@@ -511,21 +551,24 @@ export async function initFlow(root, options = {}) {
|
|
|
511
551
|
try {
|
|
512
552
|
tourResult = tourInit(root, { runBob: options.runBob });
|
|
513
553
|
stop();
|
|
514
|
-
io.print(` IBM Bob wrote the tour.`);
|
|
554
|
+
io.print(` ${green('✓')} IBM Bob wrote the tour (${tourResult.added.length} screen${tourResult.added.length === 1 ? '' : 's'}).`);
|
|
515
555
|
}
|
|
516
556
|
catch (error) {
|
|
517
557
|
stop();
|
|
518
558
|
// Bob missing or failed — describe what went wrong, then continue.
|
|
519
559
|
if (error instanceof BobKeyMissingError) {
|
|
520
560
|
tourError =
|
|
521
|
-
|
|
561
|
+
`✗ IBM Bob did not accept this key. Check it is an Inference key from bob.ibm.com, then run docugate tour init.`;
|
|
522
562
|
}
|
|
523
563
|
else if (error instanceof BobMissingError) {
|
|
524
564
|
tourError =
|
|
525
565
|
`Bob Shell is not installed. Install it, sign in, then run docugate tour init.`;
|
|
526
566
|
}
|
|
527
567
|
else {
|
|
528
|
-
|
|
568
|
+
// One calm line; the technical detail only when asked for.
|
|
569
|
+
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.`;
|
|
570
|
+
if (process.env.DOCUGATE_DEBUG)
|
|
571
|
+
io.print(` detail ${error.message}`);
|
|
529
572
|
}
|
|
530
573
|
io.print(` warning ${tourError}`);
|
|
531
574
|
}
|
|
@@ -571,7 +614,38 @@ export async function initFlow(root, options = {}) {
|
|
|
571
614
|
io.print(` installed pill in ${installResult.target}`);
|
|
572
615
|
if (installResult?.action === 'already')
|
|
573
616
|
io.print(` pill already in ${installResult.target}`);
|
|
617
|
+
const hasPill = installResult?.action === 'inserted' || installResult?.action === 'already';
|
|
618
|
+
const final = loadConfig(root).config;
|
|
619
|
+
const freshness = FRESHNESS.find((f) => f.value === final.freshness);
|
|
620
|
+
const rows = [
|
|
621
|
+
['Repository', role ? role.charAt(0).toUpperCase() + role.slice(1) : 'not set'],
|
|
622
|
+
['Docs', final.space ?? 'no space yet'],
|
|
623
|
+
['Checks', freshness ? freshness.label.split(' (')[0] : 'not set'],
|
|
624
|
+
];
|
|
625
|
+
if (role !== 'backend') {
|
|
626
|
+
rows.push(['Tour', tourResult ? `IBM Bob wrote ${tourResult.added.length} screen${tourResult.added.length === 1 ? '' : 's'}` : 'add steps in the browser']);
|
|
627
|
+
rows.push(['Tour button', hasPill && installResult?.target ? installResult.target : 'not added']);
|
|
628
|
+
if (installResult?.hook)
|
|
629
|
+
rows.push(['Starts with', `npm run ${installResult.hook.replace(/^pre/, '')}`]);
|
|
630
|
+
}
|
|
631
|
+
io.print(``);
|
|
632
|
+
io.print(` ${dimmed('╭')} ${bold('DocuGate setup')}`);
|
|
633
|
+
rows.forEach(([label, value], i) => {
|
|
634
|
+
const branch = i === rows.length - 1 ? '╰' : '├';
|
|
635
|
+
io.print(` ${dimmed(branch)} ${label.padEnd(12)} ${green(value)}`);
|
|
636
|
+
});
|
|
574
637
|
io.print(``);
|
|
575
|
-
|
|
638
|
+
if (role === 'backend') {
|
|
639
|
+
io.print(` ${green('✓')} ${bold('DocuGate is set up for this backend.')}`);
|
|
640
|
+
}
|
|
641
|
+
else if (hasPill) {
|
|
642
|
+
io.print(` ${green('✓')} ${bold('DocuGate is set up.')}`);
|
|
643
|
+
const run = installResult?.hook ? `npm run ${installResult.hook.replace(/^pre/, '')}` : 'npm run dev';
|
|
644
|
+
io.print(` Start your project as usual (${gold(run)}) and open it:`);
|
|
645
|
+
io.print(` the ${gold('Tour')} button is in the bottom-right corner.`);
|
|
646
|
+
}
|
|
647
|
+
else {
|
|
648
|
+
io.print(` ${green('✓')} ${bold('DocuGate is set up.')} Run docugate tour install to add the Tour button to your app.`);
|
|
649
|
+
}
|
|
576
650
|
return { ...base, role, tourResult, tourError, installResult, connectError };
|
|
577
651
|
}
|
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-install.js
CHANGED
|
@@ -24,6 +24,59 @@ export const CANDIDATES = [
|
|
|
24
24
|
'app/index.html',
|
|
25
25
|
'public/index.html',
|
|
26
26
|
];
|
|
27
|
+
/** What the app's own start script runs first, so the tour server comes up with it. */
|
|
28
|
+
export const SERVE_HOOK = 'docugate tour serve --background';
|
|
29
|
+
/**
|
|
30
|
+
* Adds `docugate tour serve --background` as the `predev` script (or
|
|
31
|
+
* `prestart`, for apps started with npm start), so running the project the
|
|
32
|
+
* usual way also starts the tour server. npm runs pre-scripts by itself, on
|
|
33
|
+
* every platform. An existing pre-script keeps running first. Only this
|
|
34
|
+
* repository's package.json, and only its scripts, are touched.
|
|
35
|
+
*/
|
|
36
|
+
export function installStartHook(root, options = {}) {
|
|
37
|
+
const path = join(root, 'package.json');
|
|
38
|
+
if (!existsSync(path))
|
|
39
|
+
return { action: 'none' };
|
|
40
|
+
const text = readFileSync(path, 'utf8');
|
|
41
|
+
let pkg;
|
|
42
|
+
try {
|
|
43
|
+
pkg = JSON.parse(text);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return { action: 'none' };
|
|
47
|
+
}
|
|
48
|
+
const scripts = pkg.scripts ?? {};
|
|
49
|
+
const base = scripts.dev ? 'dev' : scripts.start ? 'start' : null;
|
|
50
|
+
if (!base)
|
|
51
|
+
return { action: 'none' };
|
|
52
|
+
const hook = `pre${base}`;
|
|
53
|
+
const port = options.port ?? DEFAULT_PORT;
|
|
54
|
+
const command = port === DEFAULT_PORT ? SERVE_HOOK : `${SERVE_HOOK} --port ${port}`;
|
|
55
|
+
const existing = scripts[hook];
|
|
56
|
+
if (options.remove) {
|
|
57
|
+
if (!existing?.includes(SERVE_HOOK))
|
|
58
|
+
return { hook, action: 'already' };
|
|
59
|
+
const rest = existing
|
|
60
|
+
.split('&&')
|
|
61
|
+
.map((part) => part.trim())
|
|
62
|
+
.filter((part) => !part.startsWith(SERVE_HOOK))
|
|
63
|
+
.join(' && ');
|
|
64
|
+
if (rest)
|
|
65
|
+
scripts[hook] = rest;
|
|
66
|
+
else
|
|
67
|
+
delete scripts[hook];
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
if (existing?.includes(SERVE_HOOK))
|
|
71
|
+
return { hook, action: 'already' };
|
|
72
|
+
scripts[hook] = existing ? `${existing} && ${command}` : command;
|
|
73
|
+
}
|
|
74
|
+
pkg.scripts = scripts;
|
|
75
|
+
const indent = text.match(/^[ \t]+(?=")/m)?.[0] ?? ' ';
|
|
76
|
+
const eol = text.includes('\r\n') ? '\r\n' : '\n';
|
|
77
|
+
writeFileSync(path, JSON.stringify(pkg, null, indent).replace(/\n/g, eol) + eol);
|
|
78
|
+
return { hook, action: options.remove ? 'removed' : 'inserted' };
|
|
79
|
+
}
|
|
27
80
|
/** Returns true if any candidate file already contains the pill snippet. */
|
|
28
81
|
export function hasPillSnippet(root) {
|
|
29
82
|
for (const rel of CANDIDATES) {
|
|
@@ -100,6 +153,11 @@ function removeSnippet(text, open, close) {
|
|
|
100
153
|
* already present is a no-op.
|
|
101
154
|
*/
|
|
102
155
|
export function tourInstall(root, options = {}) {
|
|
156
|
+
const result = installSnippet(root, options);
|
|
157
|
+
const started = installStartHook(root, options);
|
|
158
|
+
return { ...result, hook: started.hook, hookAction: started.action };
|
|
159
|
+
}
|
|
160
|
+
function installSnippet(root, options) {
|
|
103
161
|
const port = options.port ?? DEFAULT_PORT;
|
|
104
162
|
const remove = options.remove ?? false;
|
|
105
163
|
for (const rel of CANDIDATES) {
|
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