docugate 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,251 +1,252 @@
1
- # docugate
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.
4
-
5
- ```sh
6
- npm i -g docugate
7
- ```
8
-
9
- ---
10
-
11
- ## Quick start
12
-
13
- `cd` into your app's repository, then run:
14
-
15
- ```sh
16
- docugate init
17
- ```
18
-
19
- It walks you through four steps:
20
-
21
- 1. **Repository role.** Say whether this repository is the frontend, the backend, or both.
22
- 2. **Sign in and connect.** Sign in to DocuGate with GitHub and connect this repository to a space.
23
- 3. **IBM Bob writes the tour.** Bob reads the code and writes one `.docugate/tour/*.md` file for each screen it finds.
24
- 4. **The pill is added to your app.** The dev-only loader snippet is inserted into your HTML or layout file.
25
-
26
- Once that is done:
27
-
28
- ```sh
29
- docugate tour serve
30
- ```
31
-
32
- Start your app in another terminal, open it in the browser, and the pill appears in the corner.
33
-
34
- ---
35
-
36
- ## Tours
37
-
38
- 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.
39
-
40
- **Example stop:**
41
-
42
- ```markdown
43
- ---
44
- route: /invoices/:id
45
- title: Invoice detail
46
- ---
47
-
48
- ## Total
49
- target: [data-tour="invoice-total"]
50
- data: GET /api/invoices/:id → total
51
- code: frontend/src/screens/InvoiceDetail.tsx
52
- source: backend/src/billing.ts
53
- docs: https://example.com/docs/billing
54
-
55
- The amount due: the subtotal plus tax, computed by `totals()` in the backend.
56
- ```
57
-
58
- Fields:
59
-
60
- | Field | Required | Meaning |
61
- | --- | --- | --- |
62
- | `target` | yes | CSS selector for the element. Prefer `[data-tour="…"]`. |
63
- | `data` | no | Request and field: `METHOD /path → field`. |
64
- | `code` | yes | File that renders the element, relative to the repository root. |
65
- | `source` | no | File where the value is computed, when that is elsewhere. |
66
- | `docs` | no | URL to relevant documentation. |
67
-
68
- The prose paragraph after the fields explains what the element is and why its value is what it is.
69
-
70
- Tour files are yours. `docugate tour init` never overwrites a file that already exists.
71
-
72
- ---
73
-
74
- ## The pill
75
-
76
- When `docugate tour serve` is running, the pill appears in the corner of your app. It has two modes:
77
-
78
- - **Walkthrough.** Steps through every stop on the current screen in order, highlighting each element and showing its tour card.
79
- - **Inspector.** Hovering highlights an element; clicking it shows its stop: what it is and where its data comes from. An element with no stop yet offers **Add to tour**, a small form that saves the stop straight into the tour file, and tells you which `data-tour` attribute to add when the element has none.
80
-
81
- The pill stays on every page while the server runs, including pages with no tour yet, so their first stop can be added from the browser. Edits to the tour files show up on the page as you save them.
82
-
83
- `docugate tour install` adds the pill for you. It only loads when the app runs on `localhost` and the tour server answers, so it never shows in production.
84
-
85
- ---
86
-
87
- ## Keeping tours true
88
-
89
- A tour is only useful while it is true, so `docugate check` verifies every claim in `.docugate/tour/` against the code. For each stop it warns when:
90
-
91
- - the `code` file, or the `source` file, no longer exists;
92
- - the `code` file, and the files it imports, no longer mention the endpoint in `data`;
93
- - the `code` file no longer contains the `data-tour` value in `target` (a value passed through a prop counts);
94
- - a required field is missing, or two stops on one screen point at the same element.
95
-
96
- Rename `invoice-total` in a component and `check` tells you which stop went stale.
97
-
98
- It checks your docs folder too:
99
-
100
- - **Errors:** broken links between pages, links that leave the docs folder, two files that become the same URL, an unreadable `docugate.json`, an empty docs folder, or more than 300 pages.
101
- - **Warnings:** pages with no `# Title`, front matter, `.mdx` files, no landing page, and `sidebar` entries that match nothing.
102
-
103
- It exits with 1 when there are errors. Add `--strict` to fail on warnings too, which is how stale tour stops fail a build.
104
-
105
- Use it in CI:
106
-
107
- ```yaml
108
- # .github/workflows/docs.yml
109
- name: Docs
110
- on: [push, pull_request]
111
- jobs:
112
- check:
113
- runs-on: ubuntu-latest
114
- steps:
115
- - uses: actions/checkout@v4
116
- - uses: actions/setup-node@v4
117
- with:
118
- node-version: 22
119
- - run: npx docugate@0.2 check --strict
120
- ```
121
-
122
- Pin the version in CI, as above. Without a pin, the rules being enforced can change between two runs of the same workflow, which turns a green build red with nothing in the commit to explain it.
123
-
124
- ---
125
-
126
- ## Commands
127
-
128
- | Command | Main flags | What it does |
129
- | --- | --- | --- |
130
- | `docugate init` | `--dir`, `--title`, `--role frontend\|backend\|both`, `--yes` | Set up this repository for DocuGate (interactive). |
131
- | `docugate check` | `--dir`, `--strict` | Check docs and tour files the way DocuGate will read them. |
132
- | `docugate openapi` | `--from`, `--out`, `--include`, `--exclude`, `--redact`, `--check` | Prepare an OpenAPI spec for the API reference. |
133
- | `docugate login` | | Sign in to DocuGate with GitHub. |
134
- | `docugate logout` | | Sign out and delete saved credentials. |
135
- | `docugate whoami` | | Print the currently signed-in user. |
136
- | `docugate tour init` | `--max-cost <n>`, `--team-id <id>` | Write the first tour with IBM Bob (uses Bob Shell). |
137
- | `docugate tour serve` | `--port <n>` (default 4178) | Start the local server that serves tour data for the pill. |
138
- | `docugate tour install` | `--port <n>`, `--remove` | Add (or remove) the pill loader from your app's HTML or layout. |
139
- | `docugate tour export <file>` | | Write every tour screen to a single JSON file. |
140
-
141
- Run every command from the root of your repository.
142
-
143
- ---
144
-
145
- ## Try it on the demo
146
-
147
- Clone [docugate-demo](https://github.com/Docugate/docugate-demo), a small full-stack app with a pre-written tour:
148
-
149
- ```sh
150
- git clone https://github.com/Docugate/docugate-demo
151
- cd docugate-demo
152
-
153
- # install dependencies and start the app
154
- npm install
155
- npm run dev
156
- ```
157
-
158
- In a second terminal:
159
-
160
- ```sh
161
- cd docugate-demo
162
- docugate tour serve
163
- ```
164
-
165
- Open the app in the browser. The pill appears in the bottom-right corner.
166
-
167
- ---
168
-
169
- ## Several repositories in one space
170
-
171
- A space can merge docs from a frontend, a backend, and a mobile app into one set of pages. `docugate init` connects the repository it runs in to one of your spaces once you are signed in; you can also add repositories from a space's settings on [trydocugate.site](https://www.trydocugate.site/dashboard).
172
-
173
- Run `init` and `check` in each repository that contributes docs, and answer the role question in each: the tour lives in the frontend, and its `source` lines can point into the backend. Every one gets its own `docugate.json`, naming its own docs folder. `check` reads the repository it is run in, which is why its 300-page limit is per repository rather than per space.
174
-
175
- See [Merging repositories](https://www.trydocugate.site/docs/merging-repositories).
176
-
177
- ---
178
-
179
- ## `openapi`
180
-
181
- Takes the OpenAPI 3 spec your backend already produces and writes the version you want published: only the public paths, without sensitive fields, with keys in a stable order so it reviews cleanly in a pull request.
182
-
183
- ```sh
184
- docugate openapi --from http://localhost:3000/openapi.json \
185
- --exclude "/internal/**" --redact passwordHash --out openapi.json
186
- ```
187
-
188
- - `--from`: a `.json`, `.yaml` or `.yml` file, or a URL serving one.
189
- - `--include` / `--exclude`: path globs; `*` matches within one segment and `**` matches across segments. Both can be repeated.
190
- - `--redact`: a property name to remove from every schema and example. Can be repeated.
191
- - `--check`: do not write anything; exit with 1 if the committed file is out of date. Useful in CI.
192
-
193
- Instead of passing flags every time, set the same options in `docugate.json`:
194
-
195
- ```json
196
- {
197
- "docsDir": "docs",
198
- "api": {
199
- "generate": {
200
- "from": "http://localhost:3000/openapi.json",
201
- "output": "openapi.json",
202
- "exclude": ["/internal/**"],
203
- "redact": ["passwordHash"]
204
- }
205
- }
206
- }
207
- ```
208
-
209
- ---
210
-
211
- ## `docugate.json`
212
-
213
- One per repository, at its root.
214
-
215
- | Key | Meaning |
216
- | --- | --- |
217
- | `docsDir` | The folder DocuGate reads. Defaults to `docs`. |
218
- | `title` | The space's title. |
219
- | `role` | Repository role: `frontend`, `backend`, or `both`. Set by `docugate init`. |
220
- | `sidebar` | File and folder names in the order the sidebar should show them. Orders this repository's own pages, not the whole space. |
221
- | `api.generate` | Defaults for `docugate openapi`. |
222
-
223
- ---
224
-
225
- ## IBM Bob 2.0 hackathon
226
-
227
- 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:
228
-
229
- **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.
230
-
231
- **During the event, with IBM Bob.** Each task has its session summary and export in `bob_sessions/`, and a plan where Bob planned first:
232
-
233
- | Task | What Bob built |
234
- | --- | --- |
235
- | 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. |
236
- | 02 | The tour format in code (`parseTourFile`, `serializeTourFile`, `loadTours`, `matchRoute`) and the tour checks in `docugate check`. |
237
- | 03 | `docugate tour serve` (tour data, live change events, saving stops) and `docugate tour export`. |
238
- | 04 | The pill: walkthrough, inspector and Add to tour, in a shadow DOM with no dependencies. |
239
- | 05 | One command: `docugate init` walks through the whole setup, and `docugate tour install` adds the pill to the app. |
240
- | 06 | `docugate login`, `logout` and `whoami`, and connecting the repository to a space during `init`. |
241
- | 07 | This README. |
242
-
243
- `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.
244
-
245
- **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.
246
-
247
- ---
248
-
249
- ## Licence
250
-
251
- MIT
1
+ # docugate
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.
4
+
5
+ ```sh
6
+ npm i -g docugate
7
+ ```
8
+
9
+ ---
10
+
11
+ ## Quick start
12
+
13
+ `cd` into your app's repository, then run:
14
+
15
+ ```sh
16
+ docugate init
17
+ ```
18
+
19
+ It walks you through four steps:
20
+
21
+ 1. **Repository role.** Say whether this repository is the frontend, the backend, or both.
22
+ 2. **Sign in and connect.** Sign in to DocuGate with GitHub and connect this repository to a space.
23
+ 3. **IBM Bob writes the tour.** Bob reads the code and writes one `.docugate/tour/*.md` file for each screen it finds.
24
+ 4. **The pill is added to your app.** The dev-only loader snippet is inserted into your HTML or layout file.
25
+
26
+ Once that is done:
27
+
28
+ ```sh
29
+ docugate tour serve
30
+ ```
31
+
32
+ Start your app in another terminal, open it in the browser, and the pill appears in the corner.
33
+
34
+ ---
35
+
36
+ ## Tours
37
+
38
+ 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.
39
+
40
+ **Example stop:**
41
+
42
+ ```markdown
43
+ ---
44
+ route: /invoices/:id
45
+ title: Invoice detail
46
+ ---
47
+
48
+ ## Total
49
+ target: [data-tour="invoice-total"]
50
+ data: GET /api/invoices/:id → total
51
+ code: frontend/src/screens/InvoiceDetail.tsx
52
+ source: backend/src/billing.ts
53
+ docs: https://example.com/docs/billing
54
+
55
+ The amount due: the subtotal plus tax, computed by `totals()` in the backend.
56
+ ```
57
+
58
+ Fields:
59
+
60
+ | Field | Required | Meaning |
61
+ | --- | --- | --- |
62
+ | `target` | yes | CSS selector for the element. Prefer `[data-tour="…"]`. |
63
+ | `data` | no | Request and field: `METHOD /path → field`. |
64
+ | `code` | yes | File that renders the element, relative to the repository root. |
65
+ | `source` | no | File where the value is computed, when that is elsewhere. |
66
+ | `docs` | no | URL to relevant documentation. |
67
+
68
+ The prose paragraph after the fields explains what the element is and why its value is what it is.
69
+
70
+ Tour files are yours. `docugate tour init` never overwrites a file that already exists.
71
+
72
+ ---
73
+
74
+ ## The pill
75
+
76
+ When `docugate tour serve` is running, the pill appears in the corner of your app. It has two modes:
77
+
78
+ - **Walkthrough.** Steps through every stop on the current screen in order, highlighting each element and showing its tour card.
79
+ - **Inspector.** Hovering highlights an element; clicking it shows its stop: what it is and where its data comes from. An element with no stop yet offers **Add to tour**, a small form that saves the stop straight into the tour file, and tells you which `data-tour` attribute to add when the element has none.
80
+
81
+ The pill stays on every page while the server runs, including pages with no tour yet, so their first stop can be added from the browser. Edits to the tour files show up on the page as you save them.
82
+
83
+ `docugate tour install` adds the pill for you. It only loads when the app runs on `localhost` and the tour server answers, so it never shows in production.
84
+
85
+ ---
86
+
87
+ ## Keeping tours true
88
+
89
+ A tour is only useful while it is true, so `docugate check` verifies every claim in `.docugate/tour/` against the code. For each stop it warns when:
90
+
91
+ - the `code` file, or the `source` file, no longer exists;
92
+ - the `code` file, and the files it imports, no longer mention the endpoint in `data`;
93
+ - the `code` file no longer contains the `data-tour` value in `target` (a value passed through a prop counts);
94
+ - a required field is missing, or two stops on one screen point at the same element.
95
+
96
+ Rename `invoice-total` in a component and `check` tells you which stop went stale.
97
+
98
+ It checks your docs folder too:
99
+
100
+ - **Errors:** broken links between pages, links that leave the docs folder, two files that become the same URL, an unreadable `docugate.json`, an empty docs folder, or more than 300 pages.
101
+ - **Warnings:** pages with no `# Title`, front matter, `.mdx` files, no landing page, and `sidebar` entries that match nothing.
102
+
103
+ It exits with 1 when there are errors. Add `--strict` to fail on warnings too, which is how stale tour stops fail a build.
104
+
105
+ Use it in CI:
106
+
107
+ ```yaml
108
+ # .github/workflows/docs.yml
109
+ name: Docs
110
+ on: [push, pull_request]
111
+ jobs:
112
+ check:
113
+ runs-on: ubuntu-latest
114
+ steps:
115
+ - uses: actions/checkout@v4
116
+ - uses: actions/setup-node@v4
117
+ with:
118
+ node-version: 22
119
+ - run: npx docugate@0.2 check --strict
120
+ ```
121
+
122
+ Pin the version in CI, as above. Without a pin, the rules being enforced can change between two runs of the same workflow, which turns a green build red with nothing in the commit to explain it.
123
+
124
+ ---
125
+
126
+ ## Commands
127
+
128
+ | Command | Main flags | What it does |
129
+ | --- | --- | --- |
130
+ | `docugate init` | `--dir`, `--title`, `--role frontend\|backend\|both`, `--yes` | Set up this repository for DocuGate (interactive). |
131
+ | `docugate check` | `--dir`, `--strict` | Check docs and tour files the way DocuGate will read them. |
132
+ | `docugate openapi` | `--from`, `--out`, `--include`, `--exclude`, `--redact`, `--check` | Prepare an OpenAPI spec for the API reference. |
133
+ | `docugate login` | | Sign in to DocuGate with GitHub. |
134
+ | `docugate logout` | | Sign out and delete saved credentials. |
135
+ | `docugate whoami` | | Print the currently signed-in user. |
136
+ | `docugate tour init` | `--max-cost <n>`, `--team-id <id>` | Write the first tour with IBM Bob (uses Bob Shell). |
137
+ | `docugate tour serve` | `--port <n>` (default 4178) | Start the local server that serves tour data for the pill. |
138
+ | `docugate tour install` | `--port <n>`, `--remove` | Add (or remove) the pill loader from your app's HTML or layout. |
139
+ | `docugate tour export <file>` | | Write every tour screen to a single JSON file. |
140
+
141
+ Run every command from the root of your repository.
142
+
143
+ ---
144
+
145
+ ## Try it on the demo
146
+
147
+ Clone [docugate-demo](https://github.com/Docugate/docugate-demo), a small full-stack app with a pre-written tour:
148
+
149
+ ```sh
150
+ git clone https://github.com/Docugate/docugate-demo
151
+ cd docugate-demo
152
+
153
+ # install dependencies and start the app
154
+ npm install
155
+ npm run dev
156
+ ```
157
+
158
+ In a second terminal:
159
+
160
+ ```sh
161
+ cd docugate-demo
162
+ docugate tour serve
163
+ ```
164
+
165
+ Open the app in the browser. The pill appears in the bottom-right corner.
166
+
167
+ ---
168
+
169
+ ## Several repositories in one space
170
+
171
+ A space can merge docs from a frontend, a backend, and a mobile app into one set of pages. `docugate init` connects the repository it runs in to one of your spaces once you are signed in; you can also add repositories from a space's settings on [trydocugate.site](https://www.trydocugate.site/dashboard).
172
+
173
+ Run `init` and `check` in each repository that contributes docs, and answer the role question in each: the tour lives in the frontend, and its `source` lines can point into the backend. Every one gets its own `docugate.json`, naming its own docs folder. `check` reads the repository it is run in, which is why its 300-page limit is per repository rather than per space.
174
+
175
+ See [Merging repositories](https://www.trydocugate.site/docs/merging-repositories).
176
+
177
+ ---
178
+
179
+ ## `openapi`
180
+
181
+ Takes the OpenAPI 3 spec your backend already produces and writes the version you want published: only the public paths, without sensitive fields, with keys in a stable order so it reviews cleanly in a pull request.
182
+
183
+ ```sh
184
+ docugate openapi --from http://localhost:3000/openapi.json \
185
+ --exclude "/internal/**" --redact passwordHash --out openapi.json
186
+ ```
187
+
188
+ - `--from`: a `.json`, `.yaml` or `.yml` file, or a URL serving one.
189
+ - `--include` / `--exclude`: path globs; `*` matches within one segment and `**` matches across segments. Both can be repeated.
190
+ - `--redact`: a property name to remove from every schema and example. Can be repeated.
191
+ - `--check`: do not write anything; exit with 1 if the committed file is out of date. Useful in CI.
192
+
193
+ Instead of passing flags every time, set the same options in `docugate.json`:
194
+
195
+ ```json
196
+ {
197
+ "docsDir": "docs",
198
+ "api": {
199
+ "generate": {
200
+ "from": "http://localhost:3000/openapi.json",
201
+ "output": "openapi.json",
202
+ "exclude": ["/internal/**"],
203
+ "redact": ["passwordHash"]
204
+ }
205
+ }
206
+ }
207
+ ```
208
+
209
+ ---
210
+
211
+ ## `docugate.json`
212
+
213
+ One per repository, at its root.
214
+
215
+ | Key | Meaning |
216
+ | --- | --- |
217
+ | `docsDir` | The folder DocuGate reads. Defaults to `docs`. |
218
+ | `title` | The space's title. |
219
+ | `role` | Repository role: `frontend`, `backend`, or `both`. Set by `docugate init`. |
220
+ | `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. |
221
+ | `sidebar` | File and folder names in the order the sidebar should show them. Orders this repository's own pages, not the whole space. |
222
+ | `api.generate` | Defaults for `docugate openapi`. |
223
+
224
+ ---
225
+
226
+ ## IBM Bob 2.0 hackathon
227
+
228
+ 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:
229
+
230
+ **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.
231
+
232
+ **During the event, with IBM Bob.** Each task has its session summary and export in `bob_sessions/`, and a plan where Bob planned first:
233
+
234
+ | Task | What Bob built |
235
+ | --- | --- |
236
+ | 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. |
237
+ | 02 | The tour format in code (`parseTourFile`, `serializeTourFile`, `loadTours`, `matchRoute`) and the tour checks in `docugate check`. |
238
+ | 03 | `docugate tour serve` (tour data, live change events, saving stops) and `docugate tour export`. |
239
+ | 04 | The pill: walkthrough, inspector and Add to tour, in a shadow DOM with no dependencies. |
240
+ | 05 | One command: `docugate init` walks through the whole setup, and `docugate tour install` adds the pill to the app. |
241
+ | 06 | `docugate login`, `logout` and `whoami`, and connecting the repository to a space during `init`. |
242
+ | 07 | This README. |
243
+
244
+ `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.
245
+
246
+ **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.
247
+
248
+ ---
249
+
250
+ ## Licence
251
+
252
+ MIT
package/dist/api.js CHANGED
@@ -3,6 +3,11 @@ import { loadCredentials, saveCredentials } from './auth.js';
3
3
  export const BASE_URL = process.env.DOCUGATE_URL ?? 'https://www.trydocugate.site';
4
4
  const ERROR_MESSAGES = {
5
5
  pro_required_sources: 'Connecting more than one repository to a space needs DocuGate Pro',
6
+ space_limit: 'The free plan has one space. Connect this repository to it, or upgrade to Pro',
7
+ pro_required: 'Private repositories need DocuGate Pro',
8
+ no_repo_access: "DocuGate can't see this repository. Install the DocuGate GitHub App on it first",
9
+ slug_taken: 'You already have a space with this name',
10
+ github_required: 'Sign in with GitHub to create a space',
6
11
  };
7
12
  function friendlyError(code) {
8
13
  return ERROR_MESSAGES[code] ?? `DocuGate said: ${code}`;
@@ -53,6 +58,13 @@ export async function getSpaces() {
53
58
  const res = await apiFetch('/api/spaces/mine');
54
59
  return res.json();
55
60
  }
61
+ export async function createSpace(repo, name, docsDir) {
62
+ const res = await apiFetch('/api/spaces', {
63
+ method: 'POST',
64
+ body: JSON.stringify({ repo, name, docsDir }),
65
+ });
66
+ return (await res.json()).space;
67
+ }
56
68
  export async function connectRepo(spaceId, repo, docsDir) {
57
69
  await apiFetch(`/api/spaces/${spaceId}/sources`, {
58
70
  method: 'POST',
package/dist/cli.js CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
3
4
  import { check } from './check.js';
4
5
  import { loadConfig } from './config.js';
5
- import { initFlow } from './init.js';
6
+ import { choose, initFlow, makeDefaultIO } from './init.js';
6
7
  import { buildOpenApi, isCurrent, writeOpenApi } from './openapi.js';
7
8
  import { BobKeyMissingError, BobMissingError, tourInit } from './tour-init.js';
8
9
  import { tourInstall, hasPillSnippet, pillSnippetLine, DEFAULT_PORT } from './tour-install.js';
@@ -197,13 +198,27 @@ async function main() {
197
198
  if (roleFlag && !['frontend', 'backend', 'both'].includes(roleFlag)) {
198
199
  fail(`--role must be frontend, backend, or both.`);
199
200
  }
200
- await initFlow(root, {
201
+ const port = one('port') !== undefined ? parseInt(one('port'), 10) : DEFAULT_PORT;
202
+ const result = await initFlow(root, {
201
203
  dir: one('dir'),
202
204
  title: one('title'),
203
205
  role: roleFlag,
204
206
  yes: args.bools.has('yes'),
205
- port: one('port') !== undefined ? parseInt(one('port'), 10) : undefined,
207
+ port,
206
208
  });
209
+ // The pill only shows while the tour server runs, so setup ends by
210
+ // offering to start it rather than leaving one more command to remember.
211
+ const where = result.alreadySetUp ?? root;
212
+ const hasTour = result.role === 'frontend' || result.role === 'both';
213
+ if (hasTour && process.stdin.isTTY && !args.bools.has('yes')) {
214
+ const io = makeDefaultIO();
215
+ const pick = await choose(io, 'Start the tour server now, so the pill shows in your app?', [
216
+ 'Yes, start it (leave this terminal open, then run your app as usual)',
217
+ 'Later: docugate tour serve',
218
+ ]);
219
+ if (pick === 1)
220
+ await serveTour(where, port);
221
+ }
207
222
  return;
208
223
  }
209
224
  case 'check': {
@@ -329,24 +344,7 @@ ${dim(`Bob: ${parts.join(', ')}`)}`);
329
344
  return;
330
345
  }
331
346
  case 'serve': {
332
- const tourDir = `${root}/.docugate/tour`;
333
- if (!existsSync(tourDir)) {
334
- console.error(`${red('error')} .docugate/tour/ not found. Run ${green('docugate tour init')} first.`);
335
- process.exit(2);
336
- }
337
- const port = parseInt(one('port') ?? '4178', 10);
338
- const server = await startTourServer(root, port);
339
- const url = `http://localhost:${server.port}`;
340
- const screens = loadTours(root);
341
- console.log(`${green('Tour server')} ${url}`);
342
- console.log(`${plural(screens.length, 'screen')} loaded from .docugate/tour/`);
343
- if (!hasPillSnippet(root)) {
344
- console.log(`${dim('Hint: run')} ${green('docugate tour install')} ${dim('to add the pill to your app automatically.')}`);
345
- }
346
- // Keep the process alive; Ctrl-C will stop it.
347
- process.on('SIGINT', () => { server.close(); process.exit(0); });
348
- process.on('SIGTERM', () => { server.close(); process.exit(0); });
349
- await new Promise(() => { });
347
+ await serveTour(root, parseInt(one('port') ?? '4178', 10));
350
348
  return;
351
349
  }
352
350
  case 'export': {
@@ -371,3 +369,28 @@ main().catch((error) => {
371
369
  console.error(`${red('error')} ${error.message}`);
372
370
  process.exit(1);
373
371
  });
372
+ /**
373
+ * Serves the tour to the pill until Ctrl-C. Starts even when there is no tour
374
+ * yet: the pill then shows on every page, and its inspector writes the first
375
+ * step, so a failed or skipped AI step never leaves the app without it.
376
+ */
377
+ async function serveTour(root, port) {
378
+ mkdirSync(join(root, '.docugate', 'tour'), { recursive: true });
379
+ const server = await startTourServer(root, port);
380
+ const url = `http://localhost:${server.port}`;
381
+ const screens = loadTours(root);
382
+ console.log(`${green('Tour server')} ${url}`);
383
+ if (screens.length) {
384
+ console.log(`${plural(screens.length, 'screen')} loaded from .docugate/tour/`);
385
+ }
386
+ else {
387
+ console.log(`No tour yet. Open your app: the pill shows on every page, and its Inspector adds the first step.`);
388
+ }
389
+ if (!hasPillSnippet(root)) {
390
+ console.log(`${dim('Hint: run')} ${green('docugate tour install')} ${dim('to add the pill to your app automatically.')}`);
391
+ }
392
+ console.log(dim('Leave this running while you work. Ctrl-C stops it.'));
393
+ process.on('SIGINT', () => { server.close(); process.exit(0); });
394
+ process.on('SIGTERM', () => { server.close(); process.exit(0); });
395
+ await new Promise(() => { });
396
+ }
package/dist/init.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { createInterface } from 'node:readline';
2
- import { execFileSync } from 'node:child_process';
2
+ import { execFileSync, spawn } from 'node:child_process';
3
3
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
4
- import { basename, join } from 'node:path';
4
+ import { basename, dirname, join } from 'node:path';
5
5
  import { CONFIG_FILE, loadConfig } from './config.js';
6
- import { tourInit, BobMissingError, BobKeyMissingError } from './tour-init.js';
6
+ import { tourInit, BobMissingError, BobKeyMissingError, readBobKey, saveBobKey } from './tour-init.js';
7
7
  import { tourInstall, pillSnippetLine, DEFAULT_PORT } from './tour-install.js';
8
8
  import { login, loadCredentials } from './auth.js';
9
9
  /**
@@ -39,7 +39,7 @@ export function init(root, opts = {}) {
39
39
  }
40
40
  return { created, kept, docsDir };
41
41
  }
42
- function makeDefaultIO() {
42
+ export function makeDefaultIO() {
43
43
  const isTTY = Boolean(process.stdin.isTTY);
44
44
  return {
45
45
  isTTY,
@@ -57,6 +57,80 @@ function makeDefaultIO() {
57
57
  },
58
58
  };
59
59
  }
60
+ /**
61
+ * The nearest folder above `root`, inside the same Git repository, that has
62
+ * DocuGate set up already (a docugate.json), if any.
63
+ */
64
+ export function setUpAbove(root) {
65
+ if (existsSync(join(root, '.git')))
66
+ return undefined;
67
+ let dir = dirname(root);
68
+ while (dir !== dirname(dir)) {
69
+ if (existsSync(join(dir, CONFIG_FILE)))
70
+ return dir;
71
+ if (existsSync(join(dir, '.git')))
72
+ return undefined;
73
+ dir = dirname(dir);
74
+ }
75
+ return undefined;
76
+ }
77
+ /** Sets one key in docugate.json, keeping everything else as it was. */
78
+ function saveSetting(root, key, value) {
79
+ const path = join(root, CONFIG_FILE);
80
+ let existing = {};
81
+ if (existsSync(path)) {
82
+ try {
83
+ existing = JSON.parse(readFileSync(path, 'utf8'));
84
+ }
85
+ catch {
86
+ // unreadable: start again with this key
87
+ }
88
+ }
89
+ existing[key] = value;
90
+ writeFileSync(path, JSON.stringify(existing, null, 2) + '\n');
91
+ }
92
+ const BOB_KEYS_URL = 'https://bob.ibm.com/docs/ide/account/api-keys';
93
+ /**
94
+ * Bob Shell runs on its own with an API key, not the IDE sign-in. Setup gets
95
+ * one without anyone editing environment variables: open the page that makes
96
+ * it, paste it, and it is kept in ~/.docugate/bob.json, outside the repository.
97
+ */
98
+ async function askForBobKey(io, openUrl) {
99
+ const pick = await choose(io, 'IBM Bob needs an API key to write the tour. Get one now?', [
100
+ 'Yes, open the page to create an Inference key, then paste it here',
101
+ 'Skip the tour for now',
102
+ ]);
103
+ if (pick !== 1)
104
+ return false;
105
+ io.print(` In your Bob instance: API keys, then create an Inference key.`);
106
+ io.print(` ${BOB_KEYS_URL}`);
107
+ (openUrl ?? openInBrowser)(BOB_KEYS_URL);
108
+ const key = (await io.ask(' Paste the key and press Enter (it stays on this computer)', '')).trim();
109
+ if (!key) {
110
+ io.print(' No key pasted. Skipped the tour: run docugate tour init when you have one.');
111
+ return false;
112
+ }
113
+ saveBobKey(key);
114
+ io.print(' Key saved in ~/.docugate/bob.json. It is never written into the repository.');
115
+ return true;
116
+ }
117
+ function openInBrowser(url) {
118
+ const [cmd, args] = process.platform === 'win32' ? ['rundll32', ['url.dll,FileProtocolHandler', url]]
119
+ : process.platform === 'darwin' ? ['open', [url]]
120
+ : ['xdg-open', [url]];
121
+ try {
122
+ spawn(cmd, args, { detached: true, stdio: 'ignore' }).unref();
123
+ }
124
+ catch {
125
+ // the URL is printed above
126
+ }
127
+ }
128
+ export const FRESHNESS = [
129
+ { value: 'push', label: 'On every push' },
130
+ { value: 'daily', label: 'Once a day (recommended: fewer AI runs)' },
131
+ { value: 'weekly', label: 'Once a week' },
132
+ { value: 'off', label: 'Never, I will ask for it' },
133
+ ];
60
134
  function saveRole(root, role) {
61
135
  const path = join(root, CONFIG_FILE);
62
136
  let existing = {};
@@ -71,7 +145,43 @@ function saveRole(root, role) {
71
145
  existing.role = role;
72
146
  writeFileSync(path, JSON.stringify(existing, null, 2) + '\n');
73
147
  }
74
- const VALID_ROLES = new Set(['frontend', 'backend', 'both']);
148
+ const ROLE_CHOICES = [{ role: 'frontend' }, { role: 'backend' }, { role: 'both' }];
149
+ const FRONTEND_DIRS = ['frontend', 'client', 'web'];
150
+ const BACKEND_DIRS = ['backend', 'server', 'api'];
151
+ const FRONTEND_MARKERS = ['index.html', 'vite.config.ts', 'vite.config.js', 'next.config.js', 'next.config.mjs', 'next.config.ts', 'angular.json', 'svelte.config.js'];
152
+ const BACKEND_MARKERS = ['go.mod', 'requirements.txt', 'pyproject.toml', 'manage.py', 'pom.xml', 'build.gradle', 'Gemfile', 'composer.json'];
153
+ /**
154
+ * What the repository probably is, from its folders and files, so the question
155
+ * can suggest an answer. Only a suggestion: the person always confirms it.
156
+ */
157
+ export function guessRole(root) {
158
+ const has = (name) => existsSync(join(root, name));
159
+ const front = FRONTEND_DIRS.some(has);
160
+ const back = BACKEND_DIRS.some(has);
161
+ if (front && back)
162
+ return 'both';
163
+ if (FRONTEND_MARKERS.some(has) || front)
164
+ return 'frontend';
165
+ if (BACKEND_MARKERS.some(has) || back)
166
+ return 'backend';
167
+ return undefined;
168
+ }
169
+ /**
170
+ * A numbered choice: prints the options, and takes only a number. Enter picks
171
+ * the default. Nobody types a word during setup.
172
+ */
173
+ export async function choose(io, question, options, fallback = 1) {
174
+ io.print('');
175
+ io.print(` ${question}`);
176
+ options.forEach((option, i) => io.print(` ${i + 1} ${option}`));
177
+ for (;;) {
178
+ const answer = (await io.ask(` Choose 1-${options.length} [${fallback}]`, String(fallback))).trim();
179
+ const n = Number(answer);
180
+ if (Number.isInteger(n) && n >= 1 && n <= options.length)
181
+ return n;
182
+ io.print(` Type a number from 1 to ${options.length}.`);
183
+ }
184
+ }
75
185
  function defaultGetRemote(root) {
76
186
  try {
77
187
  const remote = execFileSync('git', ['remote', 'get-url', 'origin'], {
@@ -97,6 +207,16 @@ export async function initFlow(root, options = {}) {
97
207
  const io = options.io ?? makeDefaultIO();
98
208
  const yes = options.yes ?? false;
99
209
  const port = options.port ?? DEFAULT_PORT;
210
+ // One project gets one setup and one tour. Run inside frontend/ of a
211
+ // project already set up at its root, and init points there instead of
212
+ // starting a second one.
213
+ const parent = setUpAbove(root);
214
+ if (parent) {
215
+ io.print(` This project is already set up in ${parent}.`);
216
+ io.print(` One project has one tour: run docugate commands from that folder.`);
217
+ const above = loadConfig(parent);
218
+ return { created: [], kept: [], docsDir: above.docsDir, role: above.config.role, alreadySetUp: parent };
219
+ }
100
220
  // ── Step 1: base repo setup (docugate.json + docs/) ──────────────────────
101
221
  const base = init(root, { dir: options.dir, title: options.title });
102
222
  // ── Step 2: determine role ────────────────────────────────────────────────
@@ -116,15 +236,19 @@ export async function initFlow(root, options = {}) {
116
236
  role = 'both';
117
237
  }
118
238
  else {
119
- // Interactive: ask once; re-prompt on invalid answer.
120
- let answer = '';
121
- while (!VALID_ROLES.has(answer)) {
122
- answer = await io.ask('Is this repository the frontend, the backend, or both? [frontend/backend/both]', 'both');
123
- if (!VALID_ROLES.has(answer)) {
124
- io.print(` Please answer frontend, backend, or both.`);
125
- }
126
- }
127
- role = answer;
239
+ // Interactive: a numbered choice with examples, the likely answer
240
+ // suggested from the folders in the repository.
241
+ const guess = guessRole(root);
242
+ const guessNumber = String(ROLE_CHOICES.findIndex((c) => c.role === guess) + 1);
243
+ const n = await choose(io, `What is in this repository?${guess ? ` (it looks like ${guess})` : ''}`, [
244
+ 'Frontend the app people see: React, Vue, Svelte, Next.js, plain HTML',
245
+ 'Backend the API or server: Node, Python, Go, Java, ...',
246
+ 'Both frontend and backend in one repository, for example:\n' +
247
+ ' my-app/\n' +
248
+ ' ├─ frontend/ (or client/, web/)\n' +
249
+ ' └─ backend/ (or server/, api/)',
250
+ ], guessNumber === '0' ? 3 : Number(guessNumber));
251
+ role = ROLE_CHOICES[n - 1].role;
128
252
  }
129
253
  }
130
254
  if (!savedRole) {
@@ -174,8 +298,11 @@ export async function initFlow(root, options = {}) {
174
298
  if (!signedIn) {
175
299
  let doLogin = yes;
176
300
  if (!yes) {
177
- const answer = await io.ask('Sign in to DocuGate now? [Y/n]', 'y');
178
- doLogin = !/^n(o)?$/i.test(answer);
301
+ doLogin =
302
+ (await choose(io, 'Sign in to DocuGate, so this repository can feed a space?', [
303
+ 'Yes, open the browser to sign in',
304
+ 'Skip for now (docugate login does it later)',
305
+ ])) === 1;
179
306
  }
180
307
  if (doLogin) {
181
308
  try {
@@ -207,55 +334,84 @@ export async function initFlow(root, options = {}) {
207
334
  connectError = `Couldn't load spaces: ${err.message}`;
208
335
  io.print(` warning ${connectError}`);
209
336
  }
210
- if (spaces.length === 0 && !connectError) {
211
- io.print(` No spaces found. Create one first: ${BASE_URL}/dashboard/new`);
337
+ const { docsDir: repoDocsDir } = loadConfig(root);
338
+ const hasRepo = (sp) => (sp.sources ?? []).some((src) => src.repo.toLowerCase() === repoSlug.toLowerCase());
339
+ const already = spaces.find(hasRepo);
340
+ if (connectError) {
341
+ // the warning is printed already; carry on with the rest of setup
212
342
  }
213
- else if (spaces.length > 0) {
214
- const { docsDir: repoDocsDir } = loadConfig(root);
215
- if (yes) {
216
- // --yes: auto-connect only when there is exactly one space.
217
- if (spaces.length === 1) {
218
- try {
219
- await connectFn(spaces[0].id, repoSlug, repoDocsDir);
220
- io.print(` Connected ${repoSlug} to space "${spaces[0].name}".`);
221
- }
222
- catch (err) {
223
- connectError = err.message;
224
- io.print(` warning ${connectError}`);
225
- }
343
+ else if (yes) {
344
+ // --yes: connect only when there is no doubt which space is meant.
345
+ if (already) {
346
+ io.print(` ${repoSlug} is already in space "${already.name}".`);
347
+ }
348
+ else if (spaces.length === 1) {
349
+ try {
350
+ await connectFn(spaces[0].id, repoSlug, repoDocsDir);
351
+ io.print(` Connected ${repoSlug} to space "${spaces[0].name}".`);
226
352
  }
227
- else {
228
- io.print(` Multiple spaces found — run docugate init again without --yes to choose:`);
229
- for (const s of spaces)
230
- io.print(` ${s.owner}/${s.slug} ${s.name}`);
353
+ catch (err) {
354
+ connectError = err.message;
355
+ io.print(` warning ${connectError}`);
231
356
  }
232
357
  }
233
- else {
234
- // Interactive: list spaces and ask.
235
- io.print(` Your spaces:`);
236
- for (let i = 0; i < spaces.length; i++) {
237
- io.print(` ${i + 1}. ${spaces[i].name} (${spaces[i].owner}/${spaces[i].slug})`);
358
+ else if (spaces.length > 1) {
359
+ io.print(` Several spaces found: run docugate init again without --yes to choose.`);
360
+ }
361
+ }
362
+ else {
363
+ // Interactive: create a space, or pick one, or skip. Numbers only.
364
+ const choices = [
365
+ `Create a new space for ${repoSlug}`,
366
+ ...spaces.map((sp) => `${sp.name} (${sp.owner}/${sp.slug})${hasRepo(sp) ? ' already reads this repository' : ''}`),
367
+ 'Skip for now',
368
+ ];
369
+ const pick = await choose(io, `Which DocuGate space should document ${repoSlug}?`, choices, already ? spaces.indexOf(already) + 2 : 1);
370
+ if (pick === 1) {
371
+ try {
372
+ const createFn = options.createSpaceFn ?? (async (repo, name, dir) => {
373
+ const { createSpace } = await import('./api.js');
374
+ return createSpace(repo, name, dir);
375
+ });
376
+ const created = await createFn(repoSlug, loadConfig(root).config.title ?? repoSlug.split('/')[1], repoDocsDir);
377
+ io.print(` Created space "${created.name}": ${BASE_URL}/${created.owner}/${created.slug}`);
378
+ }
379
+ catch (err) {
380
+ connectError = err.message;
381
+ io.print(` warning ${connectError}`);
382
+ }
383
+ }
384
+ else if (pick <= spaces.length + 1) {
385
+ const sp = spaces[pick - 2];
386
+ if (hasRepo(sp)) {
387
+ io.print(` ${repoSlug} is already in space "${sp.name}". Nothing to change.`);
238
388
  }
239
- const answer = await io.ask(`Connect ${repoSlug} to which space? [1-${spaces.length}/skip]`, 'skip');
240
- const idx = parseInt(answer, 10) - 1;
241
- if (!isNaN(idx) && idx >= 0 && idx < spaces.length) {
389
+ else {
242
390
  try {
243
- await connectFn(spaces[idx].id, repoSlug, repoDocsDir);
244
- io.print(` Connected ${repoSlug} to space "${spaces[idx].name}".`);
391
+ await connectFn(sp.id, repoSlug, repoDocsDir);
392
+ io.print(` Connected ${repoSlug} to space "${sp.name}".`);
245
393
  }
246
394
  catch (err) {
247
395
  connectError = err.message;
248
396
  io.print(` warning ${connectError}`);
249
397
  }
250
398
  }
251
- else {
252
- io.print(` Skipped connecting to a space.`);
253
- }
399
+ }
400
+ else {
401
+ io.print(` Skipped. docugate init connects it any time.`);
254
402
  }
255
403
  }
256
404
  }
257
405
  }
258
406
  }
407
+ // ── Step 3c: how often the docs are checked against the code ─────────────
408
+ if (io.isTTY && !loaded.config.freshness) {
409
+ const n = yes
410
+ ? 2
411
+ : await choose(io, 'How often should DocuGate check that your docs still match the code?', FRESHNESS.map((f) => f.label), 2);
412
+ saveSetting(root, 'freshness', FRESHNESS[n - 1].value);
413
+ io.print(` Saved: ${FRESHNESS[n - 1].label.split(' (')[0].toLowerCase()}. Change "freshness" in docugate.json any time.`);
414
+ }
259
415
  // ── Step 4: tour steps (frontend / both only) ─────────────────────────────
260
416
  let tourResult;
261
417
  let tourError;
@@ -264,8 +420,16 @@ export async function initFlow(root, options = {}) {
264
420
  // Decide whether to run tour init.
265
421
  let runTour = yes;
266
422
  if (!yes && io.isTTY) {
267
- const answer = await io.ask('Write the tour with IBM Bob? (costs Bobcoins) [y/N]', 'n');
268
- runTour = /^y(es)?$/i.test(answer);
423
+ const pick = await choose(io, 'Set up the tour: which AI should read your code and write it?', [
424
+ 'IBM Bob (uses Bobcoins)',
425
+ 'IBM watsonx (coming soon)',
426
+ 'Skip, I will write it in the browser',
427
+ ], 3);
428
+ if (pick === 2)
429
+ io.print(' IBM watsonx is coming soon. Skipped for now: run docugate tour init when you are ready.');
430
+ runTour = pick === 1;
431
+ if (runTour && !readBobKey() && !options.runBob)
432
+ runTour = await askForBobKey(io, options.openUrl);
269
433
  }
270
434
  if (runTour) {
271
435
  try {
@@ -290,8 +454,11 @@ export async function initFlow(root, options = {}) {
290
454
  // tour install: ask in TTY unless --yes.
291
455
  let runInstall = yes;
292
456
  if (!yes && io.isTTY) {
293
- const answer = await io.ask('Add the pill loader to your app? [Y/n]', 'y');
294
- runInstall = !/^n(o)?$/i.test(answer);
457
+ runInstall =
458
+ (await choose(io, 'Add the DocuGate pill to your app? (development only)', [
459
+ 'Yes, add it',
460
+ 'Skip, I will run docugate tour install later',
461
+ ])) === 1;
295
462
  }
296
463
  if (runInstall) {
297
464
  installResult = tourInstall(root, { port });
package/dist/rules.js CHANGED
@@ -58,4 +58,4 @@ export function firstHeading(content) {
58
58
  const heading = content.match(/^#\s+(.+)$/m);
59
59
  return heading ? heading[1].trim() : null;
60
60
  }
61
- export const KNOWN_KEYS = new Set(['$schema', 'docsDir', 'title', 'role', 'sidebar', 'api']);
61
+ export const KNOWN_KEYS = new Set(['$schema', 'docsDir', 'title', 'role', 'freshness', 'sidebar', 'api']);
package/dist/tour-init.js CHANGED
@@ -1,7 +1,8 @@
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 { homedir } from 'node:os';
5
+ import { dirname, join } from 'node:path';
5
6
  import { isMap, isSeq, parseDocument } from 'yaml';
6
7
  // `docugate tour init` writes the first tour of an app by asking IBM Bob
7
8
  // (through Bob Shell) to read the code. Bob does the reading; this file makes
@@ -75,9 +76,30 @@ export class BobMissingError extends Error {
75
76
  /** Bob Shell's headless mode signs in with an API key, not the IDE's IBMid session. */
76
77
  export class BobKeyMissingError extends Error {
77
78
  }
79
+ /** Where `docugate init` keeps the Bob API key: the home folder, never the repository. */
80
+ export function bobKeyPath() {
81
+ return join(homedir(), '.docugate', 'bob.json');
82
+ }
83
+ /** The Bob API key: BOB_API_KEY if set, else the one saved during setup. */
84
+ export function readBobKey() {
85
+ if (process.env.BOB_API_KEY)
86
+ return process.env.BOB_API_KEY;
87
+ try {
88
+ return JSON.parse(readFileSync(bobKeyPath(), 'utf8')).key || undefined;
89
+ }
90
+ catch {
91
+ return undefined;
92
+ }
93
+ }
94
+ export function saveBobKey(key) {
95
+ mkdirSync(dirname(bobKeyPath()), { recursive: true });
96
+ writeFileSync(bobKeyPath(), JSON.stringify({ key }) + '\n', { mode: 0o600 });
97
+ }
78
98
  const defaultRunBob = (args, cwd) => {
99
+ const key = readBobKey();
79
100
  const result = spawnSync('bob', args, {
80
101
  cwd,
102
+ env: key ? { ...process.env, BOB_API_KEY: key } : process.env,
81
103
  encoding: 'utf8',
82
104
  // `bob` is a .cmd shim on Windows, which only runs through a shell.
83
105
  shell: process.platform === 'win32',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docugate",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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": {