docugate 0.2.8 → 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 CHANGED
@@ -13,57 +13,57 @@ import { startTourServer } from './tour-server.js';
13
13
  import { login, clearCredentials, loadCredentials } from './auth.js';
14
14
  import { BASE_URL, getSession } from './api.js';
15
15
  const SITE = 'https://www.trydocugate.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}
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}
67
67
  `;
68
68
  const color = process.stdout.isTTY && !process.env.NO_COLOR;
69
69
  const paint = (code, text) => (color ? `\x1b[${code}m${text}\x1b[0m` : text);
@@ -287,6 +287,10 @@ async function main() {
287
287
  // already / no-op
288
288
  console.log(` ${dim('unchanged')} ${result.target}`);
289
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})`)}`);
290
294
  return;
291
295
  }
292
296
  case 'init': {
@@ -297,28 +301,28 @@ async function main() {
297
301
  }
298
302
  catch (error) {
299
303
  if (error instanceof BobKeyMissingError) {
300
- console.error(`${red('error')} Bob Shell needs an API key to run on its own (your IDE sign-in is not used here).
304
+ console.error(`${red('error')} Bob Shell needs an API key to run on its own (your IDE sign-in is not used here).
301
305
  ` +
302
- `1. At https://bob.ibm.com, open your instance, then API keys, and create an Inference key.
306
+ `1. At https://bob.ibm.com, open your instance, then API keys, and create an Inference key.
303
307
  ` +
304
- `2. Save it as the BOB_API_KEY environment variable, then open a new terminal:
308
+ `2. Save it as the BOB_API_KEY environment variable, then open a new terminal:
305
309
  ` +
306
- ` PowerShell [Environment]::SetEnvironmentVariable("BOB_API_KEY", "<key>", "User")
310
+ ` PowerShell [Environment]::SetEnvironmentVariable("BOB_API_KEY", "<key>", "User")
307
311
  ` +
308
- ` macOS/Linux export BOB_API_KEY=<key> (add it to your shell profile)
312
+ ` macOS/Linux export BOB_API_KEY=<key> (add it to your shell profile)
309
313
  ` +
310
314
  dim('Keep the key out of your repository. https://bob.ibm.com/docs/ide/account/api-keys'));
311
315
  process.exit(2);
312
316
  }
313
317
  if (!(error instanceof BobMissingError))
314
318
  throw error;
315
- console.error(`${red('error')} Bob Shell is not installed, and tours are written by IBM Bob.
319
+ console.error(`${red('error')} Bob Shell is not installed, and tours are written by IBM Bob.
316
320
  ` +
317
- `Install it (Node 24 or later), sign in, then run this again:
321
+ `Install it (Node 24 or later), sign in, then run this again:
318
322
  ` +
319
- ` Windows powershell -c "irm -Uri https://bob.ibm.com/download/bobshell.ps1 | iex"
323
+ ` Windows powershell -c "irm -Uri https://bob.ibm.com/download/bobshell.ps1 | iex"
320
324
  ` +
321
- ` macOS/Linux curl -fsSL https://bob.ibm.com/download/bobshell.sh | bash
325
+ ` macOS/Linux curl -fsSL https://bob.ibm.com/download/bobshell.sh | bash
322
326
  ` +
323
327
  dim('https://bob.ibm.com/docs/shell/getting-started/install-and-setup'));
324
328
  process.exit(2);
@@ -337,13 +341,22 @@ async function main() {
337
341
  stats?.durationMs !== undefined ? `${Math.round(stats.durationMs / 1000)}s` : '',
338
342
  stats?.cost !== undefined ? `${stats.cost} Bobcoins` : '',
339
343
  ].filter(Boolean);
340
- console.log(`
344
+ console.log(`
341
345
  ${dim(`Bob: ${parts.join(', ')}`)}`);
342
346
  console.log('Next: read the new files, fix anything Bob got wrong, and commit them. They are yours now.');
343
347
  return;
344
348
  }
345
349
  case 'serve': {
346
- await serveTour(root, parseInt(one('port') ?? '4178', 10));
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);
347
360
  return;
348
361
  }
349
362
  case 'export': {
package/dist/init.js CHANGED
@@ -188,7 +188,9 @@ async function askForBobKey(io, openUrl) {
188
188
  return false;
189
189
  }
190
190
  useBobKeyForThisRun(key);
191
- io.print(' Using the key for this run only. To keep it for next time, set it in your environment:');
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:');
192
194
  io.print(process.platform === 'win32'
193
195
  ? ' [Environment]::SetEnvironmentVariable("BOB_API_KEY", "<key>", "User") (then open a new terminal)'
194
196
  : ' export BOB_API_KEY=<key> (add it to your shell profile)');
@@ -549,14 +551,14 @@ export async function initFlow(root, options = {}) {
549
551
  try {
550
552
  tourResult = tourInit(root, { runBob: options.runBob });
551
553
  stop();
552
- 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'}).`);
553
555
  }
554
556
  catch (error) {
555
557
  stop();
556
558
  // Bob missing or failed — describe what went wrong, then continue.
557
559
  if (error instanceof BobKeyMissingError) {
558
560
  tourError =
559
- `IBM Bob did not accept the key. Check BOB_API_KEY holds an Inference key from bob.ibm.com, then run docugate tour init.`;
561
+ `✗ IBM Bob did not accept this key. Check it is an Inference key from bob.ibm.com, then run docugate tour init.`;
560
562
  }
561
563
  else if (error instanceof BobMissingError) {
562
564
  tourError =
@@ -623,6 +625,8 @@ export async function initFlow(root, options = {}) {
623
625
  if (role !== 'backend') {
624
626
  rows.push(['Tour', tourResult ? `IBM Bob wrote ${tourResult.added.length} screen${tourResult.added.length === 1 ? '' : 's'}` : 'add steps in the browser']);
625
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/, '')}`]);
626
630
  }
627
631
  io.print(``);
628
632
  io.print(` ${dimmed('╭')} ${bold('DocuGate setup')}`);
@@ -636,7 +640,8 @@ export async function initFlow(root, options = {}) {
636
640
  }
637
641
  else if (hasPill) {
638
642
  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:`);
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:`);
640
645
  io.print(` the ${gold('Tour')} button is in the bottom-right corner.`);
641
646
  }
642
647
  else {
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docugate",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Command-line tools for DocuGate: set up, check and prepare the documentation DocuGate reads straight from your repository.",
5
5
  "type": "module",
6
6
  "bin": {