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 +252 -251
- package/dist/api.js +12 -0
- package/dist/cli.js +45 -22
- package/dist/init.js +219 -52
- package/dist/rules.js +1 -1
- package/dist/tour-init.js +23 -1
- package/package.json +1 -1
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
|
-
| `
|
|
221
|
-
| `
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
|
235
|
-
|
|
|
236
|
-
|
|
|
237
|
-
|
|
|
238
|
-
|
|
|
239
|
-
|
|
|
240
|
-
|
|
|
241
|
-
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
211
|
-
|
|
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 (
|
|
214
|
-
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
240
|
-
const idx = parseInt(answer, 10) - 1;
|
|
241
|
-
if (!isNaN(idx) && idx >= 0 && idx < spaces.length) {
|
|
389
|
+
else {
|
|
242
390
|
try {
|
|
243
|
-
await connectFn(
|
|
244
|
-
io.print(` Connected ${repoSlug} to space "${
|
|
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
|
-
|
|
252
|
-
|
|
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
|
|
268
|
-
|
|
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
|
-
|
|
294
|
-
|
|
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 {
|
|
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