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.
Files changed (81) hide show
  1. package/README.md +200 -57
  2. package/dist/api.d.ts +44 -0
  3. package/dist/api.js +11 -5
  4. package/dist/args.d.ts +29 -0
  5. package/dist/args.js +121 -0
  6. package/dist/auth.d.ts +21 -0
  7. package/dist/banner.d.ts +13 -0
  8. package/dist/banner.js +62 -0
  9. package/dist/check.d.ts +22 -0
  10. package/dist/check.js +4 -3
  11. package/dist/cli.d.ts +2 -0
  12. package/dist/cli.js +201 -165
  13. package/dist/commands.d.ts +35 -0
  14. package/dist/commands.js +293 -0
  15. package/dist/config.d.ts +22 -0
  16. package/dist/config.js +43 -3
  17. package/dist/developers.d.ts +35 -0
  18. package/dist/developers.js +439 -0
  19. package/dist/init.d.ts +130 -0
  20. package/dist/init.js +93 -32
  21. package/dist/keys/core.d.ts +108 -0
  22. package/dist/keys/core.js +142 -0
  23. package/dist/keys/express.d.ts +21 -0
  24. package/dist/keys/express.js +27 -0
  25. package/dist/keys/fastify.d.ts +21 -0
  26. package/dist/keys/fastify.js +21 -0
  27. package/dist/keys/format.d.ts +11 -0
  28. package/dist/keys/format.js +46 -0
  29. package/dist/keys/hono.d.ts +19 -0
  30. package/dist/keys/hono.js +21 -0
  31. package/dist/keys/index.d.ts +13 -0
  32. package/dist/keys/index.js +12 -0
  33. package/dist/logo-pixels.d.ts +4 -0
  34. package/dist/logo-pixels.js +35 -0
  35. package/dist/logo.d.ts +19 -0
  36. package/dist/logo.js +68 -0
  37. package/dist/menu.d.ts +16 -0
  38. package/dist/menu.js +157 -0
  39. package/dist/openapi.d.ts +38 -0
  40. package/dist/openapi.js +2 -1
  41. package/dist/rules.d.ts +69 -0
  42. package/dist/rules.js +2 -1
  43. package/dist/select.d.ts +75 -0
  44. package/dist/select.js +215 -0
  45. package/dist/session.d.ts +46 -0
  46. package/dist/session.js +54 -0
  47. package/dist/spinner.d.ts +12 -0
  48. package/dist/spinner.js +12 -12
  49. package/dist/status.d.ts +46 -0
  50. package/dist/status.js +166 -0
  51. package/dist/sync/ai.d.ts +44 -0
  52. package/dist/sync/ai.js +147 -0
  53. package/dist/sync/apply.d.ts +25 -0
  54. package/dist/sync/apply.js +104 -0
  55. package/dist/sync/changes.d.ts +58 -0
  56. package/dist/sync/changes.js +167 -0
  57. package/dist/sync/command.d.ts +12 -0
  58. package/dist/sync/command.js +236 -0
  59. package/dist/sync/guard.d.ts +15 -0
  60. package/dist/sync/guard.js +21 -0
  61. package/dist/sync/hook.d.ts +10 -0
  62. package/dist/sync/hook.js +62 -0
  63. package/dist/sync/impact.d.ts +23 -0
  64. package/dist/sync/impact.js +93 -0
  65. package/dist/sync/pr.d.ts +37 -0
  66. package/dist/sync/pr.js +106 -0
  67. package/dist/sync/run.d.ts +50 -0
  68. package/dist/sync/run.js +124 -0
  69. package/dist/sync/state.d.ts +8 -0
  70. package/dist/sync/state.js +22 -0
  71. package/dist/sync/workflow.d.ts +20 -0
  72. package/dist/sync/workflow.js +84 -0
  73. package/dist/text.d.ts +11 -0
  74. package/dist/text.js +16 -0
  75. package/dist/tour-init.d.ts +61 -0
  76. package/dist/tour-install.d.ts +38 -0
  77. package/dist/tour-server.d.ts +21 -0
  78. package/dist/tour.d.ts +60 -0
  79. package/dist/ui.d.ts +29 -0
  80. package/dist/ui.js +63 -0
  81. package/package.json +67 -47
package/README.md CHANGED
@@ -1,11 +1,18 @@
1
1
  # docugate
2
2
 
3
- DocuGate publishes documentation straight from the markdown in your GitHub repositories and shows a walkthrough pill on your running app so any developer can see what each element on screen is and where its data comes from.
3
+ DocuGate publishes documentation straight from the markdown in your GitHub repositories. This package is its command line, and it does three things:
4
+
5
+ - **Keeps the docs true.** `docugate sync` reads what changed in your code, works out which pages that made wrong, and opens a pull request with the fix. Nobody has to remember to update the docs. [More](#docs-that-keep-up-with-your-code).
6
+ - **Explains your app.** A walkthrough pill on your running app shows any developer what each element on screen is and where its data comes from. [More](#tours).
7
+ - **Checks the result.** `docugate check` reads your docs and tours the way DocuGate will, so problems show up in CI instead of on the published page.
4
8
 
5
9
  ```sh
6
10
  npm i -g docugate
11
+ docugate # opens DocuGate in your terminal: pick what to do next
7
12
  ```
8
13
 
14
+ Type `docugate` on its own and it opens, the way `claude` does: where your account, docs and space stand, and every next step in a list, with the one worth doing now under Enter. Arrow keys pick; in Git Bash and other terminals without them, type the number.
15
+
9
16
  ---
10
17
 
11
18
  ## Quick start, step by step
@@ -13,29 +20,30 @@ npm i -g docugate
13
20
  Follow these in order. Every question during setup is a numbered choice: type
14
21
  the number and press Enter, or just press Enter for the suggested one.
15
22
 
16
- **1. Install DocuGate, once per computer.**
23
+ **1. Open your project's folder.**
17
24
 
18
25
  ```sh
19
- npm i -g docugate
26
+ cd my-app
20
27
  ```
21
28
 
22
- This gives you the `docugate` command in every folder. To pin a version for a
23
- team instead, run `npm i -D docugate` inside the project and put `npx` in front
24
- of each command below.
29
+ Use the root of the repository: the folder with `.git` and `package.json` in
30
+ it. If your frontend and backend are two repositories, do steps 1 to 3 in each.
25
31
 
26
- **2. Open your project's folder.**
32
+ **2. Install DocuGate in the project.**
27
33
 
28
34
  ```sh
29
- cd my-app
35
+ npm i docugate
30
36
  ```
31
37
 
32
- Use the root of the repository: the folder with `.git` in it. If your frontend
33
- and backend are two repositories, do steps 2 to 4 in each one.
38
+ Like any other package: everyone who clones the project gets the same version,
39
+ and CI too. (A repository with no `package.json`, like a Go or Python backend,
40
+ can install the command on the computer instead with `npm i -g docugate`, and
41
+ then drop the `npx` from the commands below.)
34
42
 
35
43
  **3. Set everything up.**
36
44
 
37
45
  ```sh
38
- docugate init
46
+ npx docugate init
39
47
  ```
40
48
 
41
49
  It asks, one at a time:
@@ -45,10 +53,9 @@ It asks, one at a time:
45
53
  | What is in this repository? | **1** Frontend, **2** Backend, or **3** Both (a `frontend/` and a `backend/` folder in one repository). It suggests one from your folders. |
46
54
  | Sign in to DocuGate? | **1** opens your browser to sign in with GitHub. You come back to your editor when it is done. **2** skips it; `docugate login` does it later. |
47
55
  | Where should the documentation live? | **1** creates a new space, with a new GitHub repository on your account to hold its markdown (`<project>-docs`, with a first page). Or pick one of your spaces. The pill then links to the published docs. |
48
- | How often should DocuGate check your docs still match the code? | On every push, daily (suggested, fewer AI runs), weekly, or never. |
56
+ | How often should DocuGate check your docs still match the code? | On every push, daily (suggested, fewer AI runs), weekly, or never. The answer is saved as `freshness`; `docugate sync init` then adds the GitHub Action that does the checking. |
49
57
  | Set up the tour: which AI should write it? | **1** IBM Bob reads your code and writes the tour. **2** IBM watsonx is coming soon. **3** skips it; you can write the tour in the browser. Frontend and both only. |
50
58
  | Add the DocuGate pill to your app? | **1** adds a few lines to your `index.html` or Next.js layout that load the pill in development only. |
51
- | Start the tour server now? | **1** starts it, so the pill shows right away. |
52
59
 
53
60
  IBM Bob reads its API key from the `BOB_API_KEY` environment variable. If you
54
61
  saved it under another name (say `IBM_BOB_KEY`), setup finds it and asks before
@@ -61,24 +68,20 @@ export BOB_API_KEY=<key> # macOS,
61
68
  ```
62
69
 
63
70
  Setup writes `docugate.json` and `.docugate/tour/`. Commit both, so the next
64
- person who clones the repository needs no setup.
65
-
66
- **4. Start the tour server while you work.**
67
-
68
- ```sh
69
- docugate tour serve
70
- ```
71
-
72
- Leave it running in its own terminal. The pill only appears while it runs,
73
- which is why the pill never shows in production. If you started it at the end
74
- of step 3, it is already running.
71
+ person who clones the repository needs no setup. When setup finishes it starts
72
+ the tour server in the background, so the pill is there the first time you open
73
+ the app (`--no-serve` skips that).
75
74
 
76
- **5. Run your app as usual, in another terminal, and open it.**
75
+ **4. Run your app as usual, and open it.**
77
76
 
78
77
  ```sh
79
78
  npm run dev
80
79
  ```
81
80
 
81
+ Setup added a `predev` script, so `npm run dev` starts the DocuGate tour server
82
+ first (one line, "DocuGate tour ready") and then your app. The pill only
83
+ appears while that server runs, which is why it never shows in production.
84
+
82
85
  The DocuGate pill sits in the bottom-right corner. Click it:
83
86
 
84
87
  - **Walkthrough** lists the page's steps with your progress. Click a step to
@@ -89,10 +92,10 @@ The DocuGate pill sits in the bottom-right corner. Click it:
89
92
 
90
93
  Every change is saved straight into `.docugate/tour/`, so it shows up in `git diff`.
91
94
 
92
- **6. Keep the tour true.**
95
+ **5. Keep the tour true.**
93
96
 
94
97
  ```sh
95
- docugate check
98
+ npx docugate check
96
99
  ```
97
100
 
98
101
  It checks every step against the code and warns about any that went stale,
@@ -102,24 +105,146 @@ for example after a rename. Put it in CI so a stale tour fails the build (see
102
105
  **Later, when you need them:**
103
106
 
104
107
  ```sh
105
- docugate login # sign in again, or on a new computer
106
- docugate whoami # who am I signed in as?
107
- docugate tour init # let IBM Bob write tours for screens that have none
108
- docugate tour install # add the pill again (--remove takes it out)
109
- docugate logout # disconnect this computer
108
+ npx docugate # the menu: pick what to do next
109
+ npx docugate status # where things stand, and what to do next
110
+ npx docugate sync # update the docs from what changed in the code
111
+ npx docugate login # sign in again, or on a new computer
112
+ npx docugate whoami # who am I signed in as?
113
+ npx docugate tour init # let IBM Bob write tours for screens that have none
114
+ npx docugate tour install # add the pill again (--remove takes it out)
115
+ npx docugate tour serve # start the tour server by hand
116
+ npx docugate logout # disconnect this computer
110
117
  ```
111
118
 
112
119
  **Something not working?**
113
120
 
114
121
  | You see | Do this |
115
122
  | --- | --- |
116
- | No pill in the app | Check `docugate tour serve` is running, then refresh the page. |
123
+ | No pill in the app | Start the app with `npm run dev` (it starts the tour server), or run `npx docugate tour serve`, then refresh the page. |
117
124
  | Bob can't run | Choose IBM Bob again in `docugate init` to paste a key, or install Bob Shell: https://bob.ibm.com/docs/shell/getting-started/install-and-setup |
118
125
  | Port 4178 is busy | `docugate tour serve --port 4179` and `docugate tour install --port 4179` |
119
126
  | "Already set up in ..." | You ran it in a subfolder. Run commands from the folder it names. |
120
127
 
121
128
  ---
122
129
 
130
+ ## A developer space
131
+
132
+ ```sh
133
+ docugate developers
134
+ ```
135
+
136
+ One guided run sets up the docs developers read before they build on your API:
137
+
138
+ 1. Signs you in, if you are not.
139
+ 2. Finds your OpenAPI file (`openapi.json`, `.yaml`, `swagger.*`) and says how many endpoints it describes.
140
+ 3. Writes the first pages, `index.md`, `quickstart.md`, `authentication.md` and `errors.md`, in `docs/developers/`, where they are missing. It never overwrites a file.
141
+ 4. Offers to commit and push them, since a space reads GitHub, not your disk.
142
+ 5. Creates a public space for them, or uses the one that already reads this repository, and saves it in `docugate.json`.
143
+
144
+ On Pro it carries on:
145
+
146
+ 6. Claims a subdomain such as `acme.trydocugate.site`, live at once.
147
+ 7. Asks where the API console sends requests from: the reader's browser, or through DocuGate with no CORS setup.
148
+ 8. Switches on API keys, with a scope per OpenAPI tag to start from, makes the verify secret, offers to put it in `.env` when git ignores that file, and prints the lines your Express, Hono or Fastify backend needs.
149
+
150
+ Everything it sets stays editable in the space's settings on [trydocugate.site](https://www.trydocugate.site/dashboard).
151
+
152
+ ## Docs that keep up with your code
153
+
154
+ Documentation goes stale because updating it is a chore nobody owns. `docugate sync` does the chore: it reads what changed in the code, finds the pages those changes made wrong, asks an AI model to correct them, and gives you the edits to review.
155
+
156
+ ```sh
157
+ npx docugate sync --dry-run # free and instant: which pages are probably out of date
158
+ npx docugate sync # write the edits into your docs folder
159
+ npx docugate sync --pr # push them and open a pull request instead
160
+ ```
161
+
162
+ `sync` needs an AI key of your own, in `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` (set `DOCUGATE_AI_BASE_URL` to use another OpenAI-compatible service). It talks to the provider directly: nothing goes through DocuGate's servers, and it works without a DocuGate account.
163
+
164
+ **What it does, step by step**
165
+
166
+ 1. **Reads the changes** from git: the commits and a trimmed diff of each changed file, since the last sync. The first run looks back a day, a week or one commit, depending on `freshness`. `--since 7d`, a branch or a commit overrides it. Documentation, lockfiles, build output, binaries and anything that looks like a secret (`.env`, `*.pem`, files named `secret` or `credentials`) are never read.
167
+ 2. **Finds the pages that are affected.** A page counts when it names a changed file, mentions something the change added or removed (a function, a route, an environment variable), or is pinned to that code in `sync.map`. This part is free, and it is all `--dry-run` does.
168
+ 3. **Asks the model** for the new text of those pages, and only those. It is told to change what the diff requires and nothing else, to invent nothing, and to treat the code as data rather than instructions.
169
+ 4. **Checks the answer before writing it.** An edit is dropped unless it is a markdown page inside your docs folder, within 300 pages, without front matter. After writing, `docugate check` runs again, and an edit that breaks a link is put back as it was.
170
+ 5. **Hands it over.** With `--pr` the edits go on one branch, `docugate/sync`, and one pull request that lists every page and why it changed. Each run updates that same pull request rather than opening another. Nothing is ever merged for you, and closing the pull request changes nothing. The sync moves forward only when you merge it.
171
+
172
+ **Make it automatic**
173
+
174
+ ```sh
175
+ npx docugate sync init
176
+ ```
177
+
178
+ This writes `.github/workflows/docugate-sync.yml`, timed by `freshness` in `docugate.json`:
179
+
180
+ | `freshness` | When it runs |
181
+ | --- | --- |
182
+ | `push` | On every push to your default branch. |
183
+ | `daily` | Every day. |
184
+ | `weekly` | Every Monday (the default). |
185
+ | `off` | Never. No workflow is written. |
186
+
187
+ Do two things on GitHub, once: add your AI key as a repository secret named `ANTHROPIC_API_KEY` (or `OPENAI_API_KEY`), and under Settings, Actions, General, tick "Allow GitHub Actions to create and approve pull requests". `sync init` prints both steps. The workflow is pinned to the version that wrote it, so a new release never changes what your CI does by surprise; run `sync init --force` to upgrade it. A sync's own commits never start another sync.
188
+
189
+ **A heads-up on your own machine**
190
+
191
+ ```sh
192
+ npx docugate sync hook install
193
+ ```
194
+
195
+ After every commit, a git hook runs `sync --dry-run` and, if pages look out of date, says so in one line. It sends nothing, changes nothing and never fails a commit. `sync hook remove` takes it out.
196
+
197
+ **Settings**, in `docugate.json`:
198
+
199
+ ```json
200
+ {
201
+ "freshness": "weekly",
202
+ "sync": {
203
+ "provider": "anthropic",
204
+ "model": "claude-sonnet-5-5",
205
+ "include": ["server/**", "src/**"],
206
+ "exclude": ["**/*.test.ts"],
207
+ "map": { "docs/api.md": ["server/routes/**"] },
208
+ "maxFiles": 10,
209
+ "digest": false
210
+ }
211
+ }
212
+ ```
213
+
214
+ | Key | Meaning |
215
+ | --- | --- |
216
+ | `provider` | `anthropic` or `openai`. Defaults to whichever key is set. |
217
+ | `model` | The model to use. |
218
+ | `include` / `exclude` | Path globs for the code worth documenting. `*` stays within a folder, `**` crosses folders. |
219
+ | `map` | Pin a page to the code it documents, when its text never names the files. DocuGate does not read front matter, so this is where that link lives. |
220
+ | `maxFiles` | The most pages one run may change (10). |
221
+ | `digest` | Also write a short "what changed" page, `changes/<date>.md`, for each run. |
222
+
223
+ `sync` also lists tour stops that no longer match the code. It only reports them: a tour is yours, and DocuGate never edits one.
224
+
225
+ **When it says no**
226
+
227
+ | You see | Do this |
228
+ | --- | --- |
229
+ | "No AI key found" | Set `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` where you run it, or as an Actions secret. |
230
+ | "shallow clone" | In GitHub Actions, set `fetch-depth: 0` on the checkout step. `sync init` already does. |
231
+ | Pushed, but no pull request | Allow Actions to create pull requests (above), or set `GITHUB_TOKEN` when running it by hand. |
232
+ | "Skipped: the changes only touch documentation" | Working as intended. A merged sync, or a docs-only change, never starts a sync. |
233
+
234
+ ---
235
+
236
+ ## Moving around
237
+
238
+ Run `docugate` in a terminal and you get a menu: arrow keys to move, type to search, Enter to run, Esc to leave. It shows where you are (signed in or not, how many pages, how far the docs are behind the code) and puts the step worth taking next under Enter, with a dot beside it. Not connected to DocuGate yet? Connecting your account is that step. When a command finishes, the menu comes back.
239
+
240
+ - `docugate status` prints the same picture: your account, your docs, the sync (last run, whether the workflow and hook are on, how many commits and pages are behind), your tour, and one line saying what to do next. `--json` is for scripts.
241
+ - `docugate help <command>`, or `docugate <command> --help`, shows one command's options and examples.
242
+ - Mistype and it helps: `docugate chekc` suggests `check`, and an option a command does not have is an error with the nearest real one, not silently ignored.
243
+ - `docugate completion powershell|bash|zsh` prints a completion script.
244
+ - In CI, in a pipe, or with `--no-input`, nothing is ever asked: the menu does not open, the help prints as plain text, and a command that needs an answer fails with what to do. To use your DocuGate account in CI, set `DOCUGATE_TOKEN`.
245
+
246
+ ---
247
+
123
248
  ## Tours
124
249
 
125
250
  A tour helps a developer who is new to a codebase see what each element on screen is and where its data comes from. For each screen there is one markdown file in `.docugate/tour/`. Each file has front matter with a `route` and a `title`, then one `##` section per stop.
@@ -224,18 +349,29 @@ Pin the version in CI, as above. Without a pin, the rules being enforced can cha
224
349
 
225
350
  | Command | Main flags | What it does |
226
351
  | --- | --- | --- |
227
- | `docugate init` | `--dir`, `--title`, `--role frontend\|backend\|both`, `--yes` | Set up this repository for DocuGate (interactive). |
352
+ | `docugate` | | In a terminal: the menu. Elsewhere: the help. |
353
+ | `docugate status` | `--json` | Where things stand: account, docs, sync and tour, and what to do next. |
354
+ | `docugate sync` | `--dry-run`, `--since <when>`, `--pr`, `--yes`, `--max-files <n>`, `--model <name>`, `--quiet` | Update the docs from what changed in the code. |
355
+ | `docugate sync init` | `--freshness push\|daily\|weekly\|off`, `--force` | Add the GitHub Action that runs sync on a schedule or on every push. |
356
+ | `docugate sync hook install` | | Tell you after each commit which pages the code left behind. |
357
+ | `docugate sync hook remove` | | Remove that hook. |
358
+ | `docugate init` | `--dir`, `--title`, `--role frontend\|backend\|both`, `--yes`, `--no-serve` | Set up this repository for DocuGate (interactive). |
359
+ | `docugate developers` | `--dir`, `--yes` | Set up a developer space: API reference, guides and keys (interactive). |
228
360
  | `docugate check` | `--dir`, `--strict` | Check docs and tour files the way DocuGate will read them. |
361
+ | `docugate help [command]` | | Every command, or one command's options and examples. |
362
+ | `docugate completion <shell>` | | Print a completion script for bash, zsh or powershell. |
229
363
  | `docugate openapi` | `--from`, `--out`, `--include`, `--exclude`, `--redact`, `--check` | Prepare an OpenAPI spec for the API reference. |
230
364
  | `docugate login` | | Sign in to DocuGate with GitHub. |
231
365
  | `docugate logout` | | Sign out and delete saved credentials. |
232
366
  | `docugate whoami` | | Print the currently signed-in user. |
233
367
  | `docugate tour init` | `--max-cost <n>`, `--team-id <id>` | Write the first tour with IBM Bob (uses Bob Shell). |
234
- | `docugate tour serve` | `--port <n>` (default 4178) | Start the local server that serves tour data for the pill. |
368
+ | `docugate tour serve` | `--port <n>` (default 4178), `--background` | Start the local server that serves tour data for the pill. |
235
369
  | `docugate tour install` | `--port <n>`, `--remove` | Add (or remove) the pill loader from your app's HTML or layout. |
236
370
  | `docugate tour export <file>` | | Write every tour screen to a single JSON file. |
237
371
 
238
- Run every command from the root of your repository.
372
+ Run every command from the root of your repository. Every command also takes
373
+ `--help`, and `--no-input` (which CI implies) to make sure it never stops to
374
+ ask a question: it fails with what to do instead.
239
375
 
240
376
  ---
241
377
 
@@ -307,6 +443,29 @@ Instead of passing flags every time, set the same options in `docugate.json`:
307
443
 
308
444
  ---
309
445
 
446
+ ## Checking API keys from your backend
447
+
448
+ If you publish an API's docs on DocuGate Enterprise, the people reading them can generate an API key on the page. Your backend asks DocuGate whether each key is valid, with a secret from your space's settings, so a key you revoke stops on its next request. This package carries that check. It is separate from the command and has no dependencies of its own.
449
+
450
+ ```ts
451
+ import { requireKey } from 'docugate/express' // or 'docugate/hono', 'docugate/fastify'
452
+
453
+ app.use('/api', requireKey({ secret: process.env.DOCUGATE_VERIFY_SECRET! }))
454
+ ```
455
+
456
+ The verified key is on `req.docugateKey` (Express and Fastify) or `c.get('docugateKey')` (Hono): who holds it, its scopes, and what is left of its allowance. Anything else, in any framework, can call the check directly:
457
+
458
+ ```ts
459
+ import { verifyKey } from 'docugate/keys'
460
+
461
+ const verdict = await verifyKey(key, { secret: process.env.DOCUGATE_VERIFY_SECRET! })
462
+ if (!verdict.valid) return deny(verdict.code) // REVOKED, EXPIRED, USAGE_EXCEEDED, ...
463
+ ```
464
+
465
+ `verifyKey` answers for every key, valid or not, and throws a `KeyServiceError` only when it could not get an answer. By default every request asks DocuGate, which is what makes a revoked key stop at once; `cacheSeconds` trades that for speed, and `failOpen` decides what happens when DocuGate cannot be reached (by default, the request is refused). The options, the exact request, and what each result means are in [API keys](https://www.trydocugate.site/docs/api-keys).
466
+
467
+ ---
468
+
310
469
  ## `docugate.json`
311
470
 
312
471
  One per repository, at its root.
@@ -316,33 +475,17 @@ One per repository, at its root.
316
475
  | `docsDir` | The folder DocuGate reads. Defaults to `docs`. |
317
476
  | `title` | The space's title. |
318
477
  | `role` | Repository role: `frontend`, `backend`, or `both`. Set by `docugate init`. |
319
- | `freshness` | How often DocuGate checks that the docs still match the code: `push`, `daily`, `weekly` or `off`. Set by `docugate init`; the scheduled check itself is coming soon. |
478
+ | `freshness` | How often the docs are checked against the code: `push`, `daily`, `weekly` or `off`. Set by `docugate init` or `docugate sync init`, which writes the GitHub Action to match. |
479
+ | `sync` | Settings for `docugate sync`; see [Docs that keep up with your code](#docs-that-keep-up-with-your-code). The DocuGate site ignores it. |
320
480
  | `sidebar` | File and folder names in the order the sidebar should show them. Orders this repository's own pages, not the whole space. |
321
481
  | `api.generate` | Defaults for `docugate openapi`. |
482
+ | `api.spec` | Where the OpenAPI file is, for a repository that does not keep it at `openapi.json` in the root or the docs folder. DocuGate draws an [API reference](https://www.trydocugate.site/docs/api-reference) from it. |
322
483
 
323
484
  ---
324
485
 
325
- ## IBM Bob 2.0 hackathon
326
-
327
- This repository is DocuGate's entry to the IBM Bob 2.0 hackathon (lablab.ai, September 25 to 27, 2026). To be clear about what was built when:
328
-
329
- **Before the event:** the CLI that was already in DocuGate's private repository: `docugate init`, `docugate check`, and `docugate openapi`. Moved here with its history.
330
-
331
- **During the event, with IBM Bob.** Each task has its session summary and export in `bob_sessions/`, and a plan where Bob planned first:
332
-
333
- | Task | What Bob built |
334
- | --- | --- |
335
- | 01 | The first tour: Bob read the Ledgerly demo and wrote `.docugate/tour/invoice-detail.md`, tracing each value to its endpoint and backend file. |
336
- | 02 | The tour format in code (`parseTourFile`, `serializeTourFile`, `loadTours`, `matchRoute`) and the tour checks in `docugate check`. |
337
- | 03 | `docugate tour serve` (tour data, live change events, saving stops) and `docugate tour export`. |
338
- | 04 | The pill: walkthrough, inspector and Add to tour, in a shadow DOM with no dependencies. |
339
- | 05 | One command: `docugate init` walks through the whole setup, and `docugate tour install` adds the pill to the app. |
340
- | 06 | `docugate login`, `logout` and `whoami`, and connecting the repository to a space during `init`. |
341
- | 07 | This README. |
342
-
343
- `docugate tour init` also runs IBM Bob itself, through Bob Shell. It installs a Tour Writer mode that may only edit `.docugate/tour/`, asks Bob to read the app's code, and never changes a tour file the user already has.
486
+ ## Built with IBM Bob
344
487
 
345
- **During the event, without Bob:** the Ledgerly demo app; the website side of `docugate login` on trydocugate.site; and fixes found while testing, such as Windows short paths in `tour serve` and the sign-in link on Windows.
488
+ The tour and pill began as DocuGate's entry to the IBM Bob 2.0 hackathon (lablab.ai, September 25 to 27, 2026), and IBM Bob still writes the first tour (`docugate tour init` runs it through Bob Shell, in a Tour Writer mode that may only edit `.docugate/tour/` and never changes a tour file you already have). Demo video: https://youtu.be/mgLUA-0r5MM. Live demo: https://docugate-demo.vercel.app/invoices/inv_1004
346
489
 
347
490
  ---
348
491
 
package/dist/api.d.ts ADDED
@@ -0,0 +1,44 @@
1
+ /** Module-level default, captured at import time (used by CLI commands). */
2
+ export declare const BASE_URL: string;
3
+ export declare function apiFetch(path: string, init?: RequestInit): Promise<Response>;
4
+ export interface DocugateUser {
5
+ githubLogin: string;
6
+ name: string;
7
+ email: string;
8
+ plan: string;
9
+ }
10
+ export interface SessionResponse {
11
+ user: DocugateUser | null;
12
+ }
13
+ export interface Space {
14
+ id: string;
15
+ owner: string;
16
+ slug: string;
17
+ name: string;
18
+ /** The repositories the space reads from. */
19
+ sources?: Array<{
20
+ repo: string;
21
+ }>;
22
+ }
23
+ export interface SpacesResponse {
24
+ spaces: Space[];
25
+ }
26
+ export declare function getSession(): Promise<SessionResponse>;
27
+ export declare function getSpaces(): Promise<SpacesResponse>;
28
+ export declare function createSpace(repo: string, name: string, docsDir: string, visibility?: 'public' | 'repo-access' | 'allowlist'): Promise<Space>;
29
+ /** Creates an empty repository on the person's GitHub account, as them. */
30
+ export interface CreatedRepo {
31
+ repo: string;
32
+ url: string;
33
+ cloneUrl: string;
34
+ /** For a docs repository: whether docs/index.md was written into it. */
35
+ seeded?: boolean;
36
+ }
37
+ /**
38
+ * Creates a repository on the person's GitHub account, as them. With `docs`,
39
+ * it is a documentation repository that starts with docs/index.md.
40
+ */
41
+ export declare function createRepo(name: string, isPrivate: boolean, docs?: {
42
+ title: string;
43
+ }): Promise<CreatedRepo>;
44
+ export declare function connectRepo(spaceId: string, repo: string, docsDir: string): Promise<void>;
package/dist/api.js CHANGED
@@ -15,7 +15,9 @@ function friendlyError(code) {
15
15
  return ERROR_MESSAGES[code] ?? `DocuGate said: ${code}`;
16
16
  }
17
17
  export async function apiFetch(path, init = {}) {
18
- const creds = loadCredentials();
18
+ // DOCUGATE_TOKEN is for CI, where there is no browser to sign in with.
19
+ const envToken = process.env.DOCUGATE_TOKEN;
20
+ const creds = envToken ? { token: envToken } : loadCredentials();
19
21
  const headers = {
20
22
  'Content-Type': 'application/json',
21
23
  ...init.headers,
@@ -28,26 +30,30 @@ export async function apiFetch(path, init = {}) {
28
30
  const res = await fetch(`${base}${path}`, { ...init, headers });
29
31
  // Renew token if the server sent a fresh one.
30
32
  const renewed = res.headers.get('x-docugate-token');
31
- if (renewed)
33
+ if (renewed && !envToken)
32
34
  saveCredentials(renewed);
33
35
  if (res.status === 401) {
34
36
  throw new Error('Run docugate login');
35
37
  }
36
38
  if (!res.ok) {
37
39
  let code;
40
+ let detail;
38
41
  try {
39
42
  const body = await res.clone().json();
40
43
  if (body && typeof body === 'object' && !Array.isArray(body)) {
41
44
  const b = body;
42
45
  if (typeof b.error === 'string')
43
46
  code = b.error;
47
+ if (typeof b.detail === 'string' && b.detail)
48
+ detail = b.detail;
44
49
  }
45
50
  }
46
51
  catch {
47
52
  // ignore JSON parse failure
48
53
  }
54
+ // GitHub's own reason, when the server passed it on, after ours.
49
55
  if (code)
50
- throw Object.assign(new Error(friendlyError(code)), { code });
56
+ throw Object.assign(new Error(detail ? `${friendlyError(code)} (GitHub: ${detail})` : friendlyError(code)), { code, detail });
51
57
  throw new Error(`DocuGate request failed with status ${res.status}`);
52
58
  }
53
59
  return res;
@@ -60,10 +66,10 @@ export async function getSpaces() {
60
66
  const res = await apiFetch('/api/spaces/mine');
61
67
  return res.json();
62
68
  }
63
- export async function createSpace(repo, name, docsDir) {
69
+ export async function createSpace(repo, name, docsDir, visibility) {
64
70
  const res = await apiFetch('/api/spaces', {
65
71
  method: 'POST',
66
- body: JSON.stringify({ repo, name, docsDir }),
72
+ body: JSON.stringify({ repo, name, docsDir, ...(visibility ? { visibility } : {}) }),
67
73
  });
68
74
  return (await res.json()).space;
69
75
  }
package/dist/args.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ import type { Command } from './commands.js';
2
+ /** A mistake in how the command was typed. Exit code 2, with a hint if there is one. */
3
+ export declare class UsageError extends Error {
4
+ hint?: string | undefined;
5
+ constructor(message: string, hint?: string | undefined);
6
+ }
7
+ export type Parsed = {
8
+ /** The command that was matched, if any. */
9
+ spec?: Command;
10
+ /** `tour` or `sync hook` typed with nothing after it: a group, not a command. */
11
+ group?: string;
12
+ /** The words of the command, split: `sync hook install` is sync / hook / install. */
13
+ command?: string;
14
+ sub?: string;
15
+ sub2?: string;
16
+ /** The trailing argument some commands take, like the file of `tour export`. */
17
+ positional?: string;
18
+ flags: Map<string, string[]>;
19
+ bools: Set<string>;
20
+ help: boolean;
21
+ version: boolean;
22
+ };
23
+ /**
24
+ * Reads the command line against the registry: it finds the command (including
25
+ * two- and three-word ones), and rejects an unknown command or option with the
26
+ * nearest valid one, rather than ignoring it.
27
+ */
28
+ export declare function parseArgs(argv: string[]): Parsed;
29
+ export declare const GLOBAL_FLAG_NAMES: string[];
package/dist/args.js ADDED
@@ -0,0 +1,121 @@
1
+ import { COMMANDS, findCommand, flagsOf, GLOBAL_FLAGS, VALUE_FLAG_NAMES } from './commands.js';
2
+ import { suggest } from './ui.js';
3
+ /** A mistake in how the command was typed. Exit code 2, with a hint if there is one. */
4
+ export class UsageError extends Error {
5
+ hint;
6
+ constructor(message, hint) {
7
+ super(message);
8
+ this.hint = hint;
9
+ }
10
+ }
11
+ const names = (list) => list.map((f) => f.name);
12
+ /**
13
+ * Reads the command line against the registry: it finds the command (including
14
+ * two- and three-word ones), and rejects an unknown command or option with the
15
+ * nearest valid one, rather than ignoring it.
16
+ */
17
+ export function parseArgs(argv) {
18
+ const flags = new Map();
19
+ const bools = new Set();
20
+ const words = [];
21
+ let help = false;
22
+ let version = false;
23
+ for (let i = 0; i < argv.length; i++) {
24
+ const arg = argv[i];
25
+ if (arg === '-h')
26
+ help = true;
27
+ else if (arg === '-v')
28
+ version = true;
29
+ else if (arg.startsWith('--')) {
30
+ const [name, inline] = arg.slice(2).split(/=(.*)/s, 2);
31
+ if (name === 'help')
32
+ help = true;
33
+ else if (name === 'version')
34
+ version = true;
35
+ else if (VALUE_FLAG_NAMES.has(name)) {
36
+ const value = inline ?? argv[++i];
37
+ if (value === undefined)
38
+ throw new UsageError(`--${name} needs a value.`);
39
+ flags.set(name, [...(flags.get(name) ?? []), value]);
40
+ }
41
+ else
42
+ bools.add(name);
43
+ }
44
+ else if (arg.startsWith('-') && arg.length > 1) {
45
+ throw new UsageError(`Unknown option ${arg}.`, 'Options start with two dashes, like --yes. -h and -v are the only short ones.');
46
+ }
47
+ else
48
+ words.push(arg);
49
+ }
50
+ const parsed = { flags, bools, help, version };
51
+ if (!words.length)
52
+ return parsed;
53
+ // `tour` or `sync hook` on their own: not a command, but the start of several.
54
+ const typed = words.join(' ');
55
+ if (words.length <= 2 && !findCommand(typed) && COMMANDS.some((c) => c.name.startsWith(`${typed} `))) {
56
+ return { ...parsed, group: typed, command: words[0], sub: words[1] };
57
+ }
58
+ // The longest run of words that is a command.
59
+ let spec;
60
+ let used = 0;
61
+ for (let n = Math.min(3, words.length); n >= 1; n--) {
62
+ const found = findCommand(words.slice(0, n).join(' '));
63
+ if (found) {
64
+ spec = found;
65
+ used = n;
66
+ break;
67
+ }
68
+ }
69
+ if (!spec) {
70
+ // `tour` or `sync hook` alone: a group of commands.
71
+ for (let n = Math.min(2, words.length); n >= 1; n--) {
72
+ const prefix = words.slice(0, n).join(' ');
73
+ if (words.length === n && COMMANDS.some((c) => c.name.startsWith(`${prefix} `)))
74
+ return { ...parsed, group: prefix, command: words[0], sub: words[1] };
75
+ }
76
+ // A known group with a sub-command that does not exist: `tour nope`.
77
+ const subs = [...new Set(COMMANDS.filter((c) => c.name.startsWith(`${words[0]} `)).map((c) => c.name.split(' ')[1]))];
78
+ if (subs.length) {
79
+ const close = suggest(words[1], subs);
80
+ throw new UsageError(`Unknown ${words[0]} command "${words[1]}".`, close ? `Did you mean "${words[0]} ${close}"?` : `Try: ${subs.map((s) => `${words[0]} ${s}`).join(', ')}.`);
81
+ }
82
+ const top = [...new Set(COMMANDS.map((c) => c.name.split(' ')[0]))];
83
+ const close = suggest(words[0], top);
84
+ throw new UsageError(`Unknown command "${words[0]}".`, close ? `Did you mean "${close}"?` : 'Run `docugate --help` to see every command.');
85
+ }
86
+ const rest = words.slice(used);
87
+ if (rest.length > (spec.positional ? 1 : 0)) {
88
+ const subs = COMMANDS.filter((c) => c.name.startsWith(`${spec.name} `)).map((c) => c.name.slice(spec.name.length + 1).split(' ')[0]);
89
+ if (!spec.positional && subs.length) {
90
+ const close = suggest(rest[0], [...new Set(subs)]);
91
+ throw new UsageError(`Unknown command "${spec.name} ${rest[0]}".`, close ? `Did you mean "${spec.name} ${close}"?` : `Try: ${[...new Set(subs)].map((s) => `${spec.name} ${s}`).join(', ')}.`);
92
+ }
93
+ throw new UsageError(`Unexpected argument "${rest[spec.positional ? 1 : 0]}".`);
94
+ }
95
+ // Options this command does not have are mistakes, not noise.
96
+ const allowed = flagsOf(spec);
97
+ const valueNames = new Set(spec.flags.filter((f) => f.value).map((f) => f.name));
98
+ for (const name of [...flags.keys(), ...bools]) {
99
+ if (!allowed.some((f) => f.name === name)) {
100
+ const close = suggest(name, names(allowed));
101
+ throw new UsageError(`"${spec.name}" has no option --${name}.`, close ? `Did you mean --${close}?` : `Run \`docugate help ${spec.name}\` to see its options.`);
102
+ }
103
+ if (valueNames.has(name) === false && flags.has(name)) {
104
+ throw new UsageError(`--${name} does not take a value for "${spec.name}".`);
105
+ }
106
+ }
107
+ for (const name of valueNames) {
108
+ if (bools.has(name))
109
+ throw new UsageError(`--${name} needs a value.`);
110
+ }
111
+ const parts = spec.name.split(' ');
112
+ return {
113
+ ...parsed,
114
+ spec,
115
+ command: parts[0],
116
+ sub: parts[1],
117
+ sub2: parts[2] ?? rest[0],
118
+ positional: rest[0],
119
+ };
120
+ }
121
+ export const GLOBAL_FLAG_NAMES = names(GLOBAL_FLAGS);
package/dist/auth.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ export type Credentials = {
2
+ token: string;
3
+ };
4
+ export declare function credentialsPath(): string;
5
+ export declare function loadCredentials(): Credentials | null;
6
+ export declare function saveCredentials(token: string): void;
7
+ export declare function clearCredentials(): void;
8
+ export interface LoginOptions {
9
+ /** Override the browser-open function (used in tests). */
10
+ openBrowser?: (url: string) => void;
11
+ /** Override the timeout in ms (used in tests; default 5 minutes). */
12
+ timeoutMs?: number;
13
+ }
14
+ /**
15
+ * Opens the browser at `baseUrl/api/auth/cli?port=…&state=…`, starts a local
16
+ * HTTP server to receive the callback, saves the token, and resolves once the
17
+ * user completes sign-in. Rejects after `timeoutMs` (default 5 minutes).
18
+ */
19
+ export declare function login(baseUrl: string, opts?: LoginOptions): Promise<{
20
+ token: string;
21
+ }>;