docugate 0.4.0 → 0.4.2
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 +14 -7
- package/dist/api.js +20 -3
- package/dist/cli.js +4 -2
- package/dist/commands.js +6 -4
- package/dist/developers.js +1 -1
- package/dist/init.js +1 -1
- package/dist/keys/core.js +1 -1
- package/dist/project.d.ts +50 -3
- package/dist/project.js +152 -29
- package/dist/sync/run.js +1 -1
- package/dist/tour-server.js +8 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -147,7 +147,7 @@ On Pro it carries on:
|
|
|
147
147
|
7. Asks where the API console sends requests from: the reader's browser, or through DocuGate with no CORS setup.
|
|
148
148
|
8. Switches on API keys, with a scope per OpenAPI tag to start from, makes the verify secret, offers to put it in `.env` when git ignores that file, and prints the lines your Express, Hono or Fastify backend needs.
|
|
149
149
|
|
|
150
|
-
Everything it sets stays editable in the space's settings on [trydocugate.site](https://
|
|
150
|
+
Everything it sets stays editable in the space's settings on [trydocugate.site](https://trydocugate.site/dashboard).
|
|
151
151
|
|
|
152
152
|
## Docs that keep up with your code
|
|
153
153
|
|
|
@@ -363,7 +363,7 @@ Pin the version in CI, as above. Without a pin, the rules being enforced can cha
|
|
|
363
363
|
| `docugate openapi` | `--from`, `--out`, `--include`, `--exclude`, `--redact`, `--check` | Prepare an OpenAPI spec for the API reference. |
|
|
364
364
|
| `docugate login` | | Sign in to DocuGate with GitHub. |
|
|
365
365
|
| `docugate logout` | | Sign out and delete saved credentials. |
|
|
366
|
-
| `docugate connect` | `--
|
|
366
|
+
| `docugate connect` | `--project`, `--section`, `--frequency`, `--type`, `--backend-dir`, `--no-audit`, `--yes` | Connect this codebase to a project, and publish its docs with the project's. |
|
|
367
367
|
| `docugate audit` | `--fail-on`, `--json` | Check this project’s packages for known vulnerabilities. |
|
|
368
368
|
| `docugate whoami` | | Print the currently signed-in user. |
|
|
369
369
|
| `docugate tour init` | `--max-cost <n>`, `--team-id <id>` | Write the first tour with IBM Bob (uses Bob Shell). |
|
|
@@ -403,13 +403,20 @@ Or try it without installing anything: https://docugate-demo.vercel.app/invoices
|
|
|
403
403
|
|
|
404
404
|
---
|
|
405
405
|
|
|
406
|
-
##
|
|
406
|
+
## One project, several codebases
|
|
407
407
|
|
|
408
|
-
A
|
|
408
|
+
A project is one set of docs fed by its codebases: a frontend and a backend, in two repositories or as two folders of one. Run `docugate connect` in each of them:
|
|
409
|
+
|
|
410
|
+
```bash
|
|
411
|
+
cd server && npx docugate connect # pick the project, or start a new one
|
|
412
|
+
cd ../public && npx docugate connect # pick the same project
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
The first codebase's docs sit at the top of the project's space; each one after it gets a section named after what it is (`frontend/`, `backend/`), so two pages with the same name cannot clash. A docs folder the space already reads is not added twice. Change the sections, or add repositories by hand, in a space's settings on [trydocugate.site](https://trydocugate.site/dashboard). Feeding one project from several codebases needs Pro or an organization plan.
|
|
409
416
|
|
|
410
417
|
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.
|
|
411
418
|
|
|
412
|
-
See [Merging repositories](https://
|
|
419
|
+
See [Merging repositories](https://trydocugate.site/docs/merging-repositories).
|
|
413
420
|
|
|
414
421
|
---
|
|
415
422
|
|
|
@@ -464,7 +471,7 @@ const verdict = await verifyKey(key, { secret: process.env.DOCUGATE_VERIFY_SECRE
|
|
|
464
471
|
if (!verdict.valid) return deny(verdict.code) // REVOKED, EXPIRED, USAGE_EXCEEDED, ...
|
|
465
472
|
```
|
|
466
473
|
|
|
467
|
-
`verifyKey` answers for every key, valid or not, and throws a `KeyServiceError` only when it could not get an answer. By default every request asks DocuGate, which is what makes a revoked key stop at once; `cacheSeconds` trades that for speed, and `failOpen` decides what happens when DocuGate cannot be reached (by default, the request is refused). The options, the exact request, and what each result means are in [API keys](https://
|
|
474
|
+
`verifyKey` answers for every key, valid or not, and throws a `KeyServiceError` only when it could not get an answer. By default every request asks DocuGate, which is what makes a revoked key stop at once; `cacheSeconds` trades that for speed, and `failOpen` decides what happens when DocuGate cannot be reached (by default, the request is refused). The options, the exact request, and what each result means are in [API keys](https://trydocugate.site/docs/api-keys).
|
|
468
475
|
|
|
469
476
|
---
|
|
470
477
|
|
|
@@ -481,7 +488,7 @@ One per repository, at its root.
|
|
|
481
488
|
| `sync` | Settings for `docugate sync`; see [Docs that keep up with your code](#docs-that-keep-up-with-your-code). The DocuGate site ignores it. |
|
|
482
489
|
| `sidebar` | File and folder names in the order the sidebar should show them. Orders this repository's own pages, not the whole space. |
|
|
483
490
|
| `api.generate` | Defaults for `docugate openapi`. |
|
|
484
|
-
| `api.spec` | Where the OpenAPI file is, for a repository that does not keep it at `openapi.json` in the root or the docs folder. DocuGate draws an [API reference](https://
|
|
491
|
+
| `api.spec` | Where the OpenAPI file is, for a repository that does not keep it at `openapi.json` in the root or the docs folder. DocuGate draws an [API reference](https://trydocugate.site/docs/api-reference) from it. |
|
|
485
492
|
|
|
486
493
|
---
|
|
487
494
|
|
package/dist/api.js
CHANGED
|
@@ -22,7 +22,7 @@ export function detectAgent(env = process.env) {
|
|
|
22
22
|
return 'Terminal';
|
|
23
23
|
}
|
|
24
24
|
/** Module-level default, captured at import time (used by CLI commands). */
|
|
25
|
-
export const BASE_URL = process.env.DOCUGATE_URL ?? 'https://
|
|
25
|
+
export const BASE_URL = process.env.DOCUGATE_URL ?? 'https://trydocugate.site';
|
|
26
26
|
const ERROR_MESSAGES = {
|
|
27
27
|
pro_required_sources: 'Connecting more than one repository to a space needs DocuGate Pro',
|
|
28
28
|
space_limit: 'The free plan has one space. Connect this repository to it, or upgrade to Pro',
|
|
@@ -32,6 +32,13 @@ const ERROR_MESSAGES = {
|
|
|
32
32
|
repo_exists: 'You already have a repository with this name on GitHub',
|
|
33
33
|
app_permission: "DocuGate isn't allowed to create repositories on your account yet. Accept the DocuGate app's updated permissions at https://github.com/settings/installations, then try again",
|
|
34
34
|
github_required: 'Sign in with GitHub to create a space',
|
|
35
|
+
source_collision: 'Some pages would share an address with pages the space already has. Put them under a section',
|
|
36
|
+
duplicate_source: 'The space already reads that folder of that repository',
|
|
37
|
+
duplicate_mount: 'Another repository already uses that section. Choose another name',
|
|
38
|
+
bad_mount: 'A section is a short name: letters, numbers and dashes, up to three levels',
|
|
39
|
+
bad_path: 'DocuGate could not read which folder of the repository this is',
|
|
40
|
+
bad_space: 'That project is not one you can connect to',
|
|
41
|
+
forbidden: 'You can only change a space you own, or one whose repository you can push to',
|
|
35
42
|
};
|
|
36
43
|
function friendlyError(code) {
|
|
37
44
|
return ERROR_MESSAGES[code] ?? `DocuGate said: ${code}`;
|
|
@@ -53,8 +60,18 @@ export async function apiFetch(path, init = {}) {
|
|
|
53
60
|
headers['x-docugate-version'] = CLI_VERSION;
|
|
54
61
|
}
|
|
55
62
|
// Re-read the env at call time so tests that set DOCUGATE_URL after import work.
|
|
56
|
-
const base = process.env.DOCUGATE_URL ?? 'https://
|
|
57
|
-
|
|
63
|
+
const base = process.env.DOCUGATE_URL ?? 'https://trydocugate.site';
|
|
64
|
+
let res = await fetch(`${base}${path}`, { ...init, headers, redirect: 'manual' });
|
|
65
|
+
// fetch drops the Authorization header when a redirect changes host, so a
|
|
66
|
+
// move between www.trydocugate.site and trydocugate.site read as signed out.
|
|
67
|
+
// Followed here instead, once, and only between two names of the same site.
|
|
68
|
+
const moved = res.status >= 301 && res.status <= 308 ? res.headers.get('location') : null;
|
|
69
|
+
if (moved) {
|
|
70
|
+
const to = new URL(moved, base);
|
|
71
|
+
const bare = (host) => host.replace(/^www\./, '');
|
|
72
|
+
if (to.protocol === 'https:' && bare(to.host) === bare(new URL(base).host))
|
|
73
|
+
res = await fetch(to, { ...init, headers, redirect: 'manual' });
|
|
74
|
+
}
|
|
58
75
|
// Renew token if the server sent a fresh one.
|
|
59
76
|
const renewed = res.headers.get('x-docugate-token');
|
|
60
77
|
if (renewed && !envToken)
|
package/dist/cli.js
CHANGED
|
@@ -23,7 +23,7 @@ import { startTourServer } from './tour-server.js';
|
|
|
23
23
|
import { login, clearCredentials, loadCredentials } from './auth.js';
|
|
24
24
|
import { BASE_URL, getSession } from './api.js';
|
|
25
25
|
import { dim, gold, green, hint, isMintty, plural, red, suggest, yellow } from './ui.js';
|
|
26
|
-
const SITE = 'https://
|
|
26
|
+
const SITE = 'https://trydocugate.site';
|
|
27
27
|
/** True while the launch menu is running: its commands do not print their own "Next". */
|
|
28
28
|
let inMenu = false;
|
|
29
29
|
function fail(message, tip) {
|
|
@@ -213,7 +213,9 @@ async function run(argv) {
|
|
|
213
213
|
}
|
|
214
214
|
case 'connect': {
|
|
215
215
|
process.exitCode = await connectCommand(root, {
|
|
216
|
-
space
|
|
216
|
+
// --space is what it was called before a project could have several codebases.
|
|
217
|
+
project: one('project') ?? one('space'),
|
|
218
|
+
section: one('section'),
|
|
217
219
|
frequency: one('frequency'),
|
|
218
220
|
type: one('type'),
|
|
219
221
|
backendDir: one('backend-dir'),
|
package/dist/commands.js
CHANGED
|
@@ -166,18 +166,20 @@ export const COMMANDS = [
|
|
|
166
166
|
{
|
|
167
167
|
name: 'connect',
|
|
168
168
|
group: 'Account',
|
|
169
|
-
summary: 'Connect this
|
|
170
|
-
description: '
|
|
169
|
+
summary: 'Connect this codebase to a project, and publish its docs with the project’s',
|
|
170
|
+
description: 'Run it in each codebase of a project: the backend, then the frontend, whether they are two repositories or two folders of one. ' +
|
|
171
|
+
'It asks which project the codebase belongs to, or starts a new one, and adds its docs folder to the project’s space, under a section of its own when the project already has docs. ' +
|
|
171
172
|
'It shares the repository name, the tool running the CLI, the outcome of each sync, and the names and versions of public packages in your lockfiles. Never your code.',
|
|
172
173
|
flags: [
|
|
173
|
-
{ name: '
|
|
174
|
+
{ name: 'project', value: 'name', description: 'The project to join, by slug, owner/slug or name (--space still works)' },
|
|
175
|
+
{ name: 'section', value: 'name', description: 'The section its docs go under (default: frontend, backend… after the first codebase)' },
|
|
174
176
|
{ name: 'frequency', value: 'when', description: 'How often docs are checked: push, daily, weekly or off' },
|
|
175
177
|
{ name: 'type', value: 'kind', description: 'frontend, backend, serverless or monorepo (default: detected)' },
|
|
176
178
|
{ name: 'backend-dir', value: 'folder', description: 'In a monorepo, the folder that holds the backend' },
|
|
177
179
|
{ name: 'no-audit', description: 'Do not send package names to check for vulnerabilities' },
|
|
178
180
|
YES,
|
|
179
181
|
],
|
|
180
|
-
examples: ['docugate connect', 'docugate connect --
|
|
182
|
+
examples: ['docugate connect', 'docugate connect --project payments --type backend --yes', 'docugate connect --project payments --type frontend --section web --yes'],
|
|
181
183
|
account: 'required',
|
|
182
184
|
},
|
|
183
185
|
{
|
package/dist/developers.js
CHANGED
|
@@ -13,7 +13,7 @@ import { bold, dim, gold, green, plural, yellow } from './ui.js';
|
|
|
13
13
|
// console sends requests, switches on API keys and hands over the verify
|
|
14
14
|
// secret with the lines the backend needs. Every step says what it is about to
|
|
15
15
|
// do, and nothing that already exists is overwritten.
|
|
16
|
-
const SITE = 'https://
|
|
16
|
+
const SITE = 'https://trydocugate.site';
|
|
17
17
|
/** Folders never worth walking for a spec. */
|
|
18
18
|
const SKIP = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'out', 'coverage', 'vendor', '.venv', 'venv', 'target']);
|
|
19
19
|
/** File names that are, by convention, an API description. */
|
package/dist/init.js
CHANGED
|
@@ -480,7 +480,7 @@ export async function initFlow(root, options = {}) {
|
|
|
480
480
|
saveRole(root, role);
|
|
481
481
|
}
|
|
482
482
|
// ── Step 3: sign in and connect this repository ───────────────────────────
|
|
483
|
-
const BASE_URL = options.baseUrl ?? process.env.DOCUGATE_URL ?? 'https://
|
|
483
|
+
const BASE_URL = options.baseUrl ?? process.env.DOCUGATE_URL ?? 'https://trydocugate.site';
|
|
484
484
|
let connectError;
|
|
485
485
|
// In CI (no TTY) skip sign-in entirely (hint already printed in the early-return branch above).
|
|
486
486
|
if (!io.isTTY) {
|
package/dist/keys/core.js
CHANGED
|
@@ -9,7 +9,7 @@ import { looksLikeKey } from './format.js';
|
|
|
9
9
|
* request. This file is that question and what to do with the answer. The
|
|
10
10
|
* adapters for Express, Hono and Fastify are a few lines on top of it.
|
|
11
11
|
*/
|
|
12
|
-
const DEFAULT_BASE_URL = 'https://
|
|
12
|
+
const DEFAULT_BASE_URL = 'https://trydocugate.site';
|
|
13
13
|
/** DocuGate could not be asked, or refused to answer. Never means a key is bad. */
|
|
14
14
|
export class KeyServiceError extends Error {
|
|
15
15
|
constructor(message, options) {
|
package/dist/project.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export type Finding = {
|
|
|
25
25
|
};
|
|
26
26
|
/** `owner/name` from the git remote, or the folder's name without one. */
|
|
27
27
|
export declare function detectRepo(root: string): string;
|
|
28
|
-
/** The token for this
|
|
28
|
+
/** The token for this codebase: CI's secret first, then what `connect` saved for this folder, then for its repository. */
|
|
29
29
|
export declare function projectToken(root: string, env?: NodeJS.ProcessEnv): string | null;
|
|
30
30
|
/** A suggestion only. The person confirms it, and the dashboard can change it. */
|
|
31
31
|
export declare function detectType(root: string): ProjectType;
|
|
@@ -61,12 +61,59 @@ export type AuditArgs = {
|
|
|
61
61
|
/** `docugate audit`. Returns the exit code. */
|
|
62
62
|
export declare function auditCommand(root: string, args: AuditArgs, env?: NodeJS.ProcessEnv): Promise<number>;
|
|
63
63
|
export type ConnectArgs = {
|
|
64
|
-
|
|
64
|
+
/** The project to join: its slug, `owner/slug`, or name. */
|
|
65
|
+
project?: string;
|
|
65
66
|
frequency?: string;
|
|
66
67
|
type?: string;
|
|
67
68
|
backendDir?: string;
|
|
69
|
+
/** The section this codebase's docs go under in the project's space. Empty is the top level. */
|
|
70
|
+
section?: string;
|
|
68
71
|
noAudit: boolean;
|
|
69
72
|
yes: boolean;
|
|
70
73
|
};
|
|
71
|
-
/** `
|
|
74
|
+
/** A project `connect` can join: a space, with the codebases and docs folders already feeding it. */
|
|
75
|
+
export type ProjectChoice = {
|
|
76
|
+
id: string;
|
|
77
|
+
name: string;
|
|
78
|
+
owner: string;
|
|
79
|
+
slug: string;
|
|
80
|
+
org: string | null;
|
|
81
|
+
sources: {
|
|
82
|
+
repo: string;
|
|
83
|
+
docsDir: string;
|
|
84
|
+
mount: string;
|
|
85
|
+
}[];
|
|
86
|
+
codebases: {
|
|
87
|
+
repo: string;
|
|
88
|
+
path: string;
|
|
89
|
+
type: string;
|
|
90
|
+
}[];
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Which codebase this folder is: its repository, and where in it. A frontend and
|
|
94
|
+
* a backend side by side in one repository are two codebases, told apart by
|
|
95
|
+
* their folder. `path` is `''` at the top of the repository.
|
|
96
|
+
*/
|
|
97
|
+
export declare function codebaseOf(root: string, git?: (args: string[], cwd: string) => string): {
|
|
98
|
+
repo: string;
|
|
99
|
+
path: string;
|
|
100
|
+
};
|
|
101
|
+
/** Where a codebase's project token is kept: the repository, and the folder when it is not the whole of it. */
|
|
102
|
+
export declare const tokenKey: (repo: string, path: string) => string;
|
|
103
|
+
/**
|
|
104
|
+
* The section a codebase's docs go under. The first codebase of a project is the
|
|
105
|
+
* top level; each one after it gets a section named after what it is, so two
|
|
106
|
+
* pages called "Authentication" cannot clash.
|
|
107
|
+
*/
|
|
108
|
+
export declare function sectionFor(type: ProjectType, sources: {
|
|
109
|
+
mount: string;
|
|
110
|
+
}[]): string;
|
|
111
|
+
/** The project someone named: by slug, `owner/slug` or name, in any case. */
|
|
112
|
+
export declare function pickProject(choices: ProjectChoice[], wanted: string): ProjectChoice | undefined;
|
|
113
|
+
/**
|
|
114
|
+
* `docugate connect`, run in each codebase of a project: the backend, then the
|
|
115
|
+
* frontend, whether they are two repositories or two folders of one. It joins
|
|
116
|
+
* the codebase to the project, and adds its docs to the project's space so they
|
|
117
|
+
* are published with the rest. Returns the exit code.
|
|
118
|
+
*/
|
|
72
119
|
export declare function connectCommand(root: string, args: ConnectArgs): Promise<number>;
|
package/dist/project.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import { basename, join } from 'node:path';
|
|
3
|
+
import { basename, join, relative, resolve, sep } from 'node:path';
|
|
4
4
|
import { createInterface } from 'node:readline/promises';
|
|
5
5
|
import { apiFetch } from './api.js';
|
|
6
|
+
import { login } from './auth.js';
|
|
6
7
|
import { packageVersion } from './banner.js';
|
|
7
8
|
import { loadConfig } from './config.js';
|
|
8
9
|
import { guessRole } from './init.js';
|
|
@@ -17,7 +18,7 @@ import { bold, dim, green, plural, red, yellow } from './ui.js';
|
|
|
17
18
|
export const PROJECT_TYPES = ['frontend', 'backend', 'serverless', 'monorepo'];
|
|
18
19
|
export const FREQUENCIES = ['push', 'daily', 'weekly', 'off'];
|
|
19
20
|
const TOKENS_FILE = () => join(process.env.DOCUGATE_HOME ?? join(homedir(), '.docugate'), 'projects.json');
|
|
20
|
-
const base = () => process.env.DOCUGATE_URL ?? 'https://
|
|
21
|
+
const base = () => process.env.DOCUGATE_URL ?? 'https://trydocugate.site';
|
|
21
22
|
function readTokens() {
|
|
22
23
|
try {
|
|
23
24
|
return JSON.parse(readFileSync(TOKENS_FILE(), 'utf8'));
|
|
@@ -38,9 +39,13 @@ export function detectRepo(root) {
|
|
|
38
39
|
}
|
|
39
40
|
return basename(root);
|
|
40
41
|
}
|
|
41
|
-
/** The token for this
|
|
42
|
+
/** The token for this codebase: CI's secret first, then what `connect` saved for this folder, then for its repository. */
|
|
42
43
|
export function projectToken(root, env = process.env) {
|
|
43
|
-
|
|
44
|
+
if (env.DOCUGATE_PROJECT_TOKEN)
|
|
45
|
+
return env.DOCUGATE_PROJECT_TOKEN;
|
|
46
|
+
const { repo, path } = codebaseOf(root);
|
|
47
|
+
const tokens = readTokens();
|
|
48
|
+
return tokens[tokenKey(repo, path)] || tokens[repo] || null;
|
|
44
49
|
}
|
|
45
50
|
/** A suggestion only. The person confirms it, and the dashboard can change it. */
|
|
46
51
|
export function detectType(root) {
|
|
@@ -162,11 +167,56 @@ export async function auditCommand(root, args, env = process.env) {
|
|
|
162
167
|
}
|
|
163
168
|
return 0;
|
|
164
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Which codebase this folder is: its repository, and where in it. A frontend and
|
|
172
|
+
* a backend side by side in one repository are two codebases, told apart by
|
|
173
|
+
* their folder. `path` is `''` at the top of the repository.
|
|
174
|
+
*/
|
|
175
|
+
export function codebaseOf(root, git = defaultGit) {
|
|
176
|
+
const repo = detectRepo(root);
|
|
177
|
+
try {
|
|
178
|
+
const top = git(['rev-parse', '--show-toplevel'], root).trim();
|
|
179
|
+
const path = relative(resolve(top), resolve(root)).split(sep).join('/');
|
|
180
|
+
return { repo, path: path.startsWith('..') ? '' : path };
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return { repo, path: '' };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** Where a codebase's project token is kept: the repository, and the folder when it is not the whole of it. */
|
|
187
|
+
export const tokenKey = (repo, path) => (path ? `${repo}#${path}` : repo);
|
|
188
|
+
/**
|
|
189
|
+
* The section a codebase's docs go under. The first codebase of a project is the
|
|
190
|
+
* top level; each one after it gets a section named after what it is, so two
|
|
191
|
+
* pages called "Authentication" cannot clash.
|
|
192
|
+
*/
|
|
193
|
+
export function sectionFor(type, sources) {
|
|
194
|
+
if (!sources.length)
|
|
195
|
+
return '';
|
|
196
|
+
const wanted = type === 'monorepo' ? 'app' : type;
|
|
197
|
+
const taken = new Set(sources.map((s) => s.mount));
|
|
198
|
+
if (!taken.has(wanted))
|
|
199
|
+
return wanted;
|
|
200
|
+
for (let n = 2;; n++)
|
|
201
|
+
if (!taken.has(`${wanted}-${n}`))
|
|
202
|
+
return `${wanted}-${n}`;
|
|
203
|
+
}
|
|
204
|
+
/** The project someone named: by slug, `owner/slug` or name, in any case. */
|
|
205
|
+
export function pickProject(choices, wanted) {
|
|
206
|
+
const w = wanted.trim().toLowerCase();
|
|
207
|
+
return choices.find((p) => `${p.owner}/${p.slug}`.toLowerCase() === w) ?? choices.find((p) => p.slug.toLowerCase() === w) ?? choices.find((p) => p.name.toLowerCase() === w);
|
|
208
|
+
}
|
|
165
209
|
async function choose(question, options, fallback, ask) {
|
|
166
210
|
const answer = (await ask(` ${question} ${dim(`(${options.join(', ')})`)} [${fallback}] `)).trim().toLowerCase();
|
|
167
211
|
return options.includes(answer) ? answer : fallback;
|
|
168
212
|
}
|
|
169
|
-
|
|
213
|
+
const codeOf = (error) => error.code;
|
|
214
|
+
/**
|
|
215
|
+
* `docugate connect`, run in each codebase of a project: the backend, then the
|
|
216
|
+
* frontend, whether they are two repositories or two folders of one. It joins
|
|
217
|
+
* the codebase to the project, and adds its docs to the project's space so they
|
|
218
|
+
* are published with the rest. Returns the exit code.
|
|
219
|
+
*/
|
|
170
220
|
export async function connectCommand(root, args) {
|
|
171
221
|
if (args.type && !PROJECT_TYPES.includes(args.type)) {
|
|
172
222
|
console.error(`${red('error')} --type must be one of ${PROJECT_TYPES.join(', ')}.`);
|
|
@@ -176,36 +226,68 @@ export async function connectCommand(root, args) {
|
|
|
176
226
|
console.error(`${red('error')} --frequency must be one of ${FREQUENCIES.join(', ')}.`);
|
|
177
227
|
return 2;
|
|
178
228
|
}
|
|
179
|
-
const repo =
|
|
229
|
+
const { repo, path } = codebaseOf(root);
|
|
180
230
|
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !process.env.CI && !args.yes;
|
|
181
231
|
const rl = interactive ? createInterface({ input: process.stdin, output: process.stdout }) : null;
|
|
182
232
|
const ask = async (q) => (rl ? rl.question(q) : '');
|
|
233
|
+
const where = path ? `${repo} ${dim(`(${path}/)`)}` : repo;
|
|
183
234
|
try {
|
|
184
|
-
|
|
235
|
+
// Signed in first: everything after this is done as someone.
|
|
236
|
+
const { user } = (await (await apiFetch('/api/auth/session')).json().catch(() => ({ user: null })));
|
|
237
|
+
if (!user) {
|
|
238
|
+
if (!rl || !/^(|y|yes)$/i.test((await ask(` Sign in to DocuGate first? ${dim('(Y/n)')} `)).trim())) {
|
|
239
|
+
console.error(`${red('error')} Not signed in. Run docugate login, then docugate connect again.`);
|
|
240
|
+
return 1;
|
|
241
|
+
}
|
|
242
|
+
rl.pause();
|
|
243
|
+
await login(base());
|
|
244
|
+
rl.resume();
|
|
245
|
+
}
|
|
246
|
+
const { projects: choices } = (await (await apiFetch('/api/projects/choices')).json());
|
|
185
247
|
console.log();
|
|
186
|
-
console.log(` ${bold('Connect')} ${
|
|
248
|
+
console.log(` ${bold('Connect')} ${where}`);
|
|
187
249
|
console.log(` ${dim('Shared with DocuGate: this repository’s name, the tool running the CLI, the outcome of each sync, and the names,')}`);
|
|
188
250
|
console.log(` ${dim('versions and lockfile paths of public packages, to check them for known vulnerabilities. Never your code.')}`);
|
|
189
251
|
console.log();
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
252
|
+
// The docs this codebase brings, as the repository sees the folder.
|
|
253
|
+
const config = loadConfig(root);
|
|
254
|
+
const docsDir = [path, config.docsDir].filter(Boolean).join('/');
|
|
255
|
+
const hasDocs = existsSync(join(root, config.docsDir));
|
|
256
|
+
let project;
|
|
257
|
+
if (args.project) {
|
|
258
|
+
project = pickProject(choices, args.project);
|
|
259
|
+
if (!project) {
|
|
260
|
+
console.error(`${red('error')} No project called "${args.project}" that you can connect to. Run docugate connect without --project to see them.`);
|
|
195
261
|
return 1;
|
|
196
262
|
}
|
|
197
|
-
spaceId = match.id;
|
|
198
263
|
}
|
|
199
|
-
else if (rl &&
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
264
|
+
else if (rl && choices.length) {
|
|
265
|
+
console.log(` ${bold('Which project is this codebase part of?')}`);
|
|
266
|
+
choices.forEach((p, i) => {
|
|
267
|
+
const parts = p.codebases.map((c) => `${c.type}: ${c.repo}${c.path ? `/${c.path}` : ''}`);
|
|
268
|
+
console.log(` ${i + 1}. ${p.name}${p.org ? dim(` (${p.org})`) : ''} ${dim(parts.length ? `· ${parts.join(', ')}` : '· no codebase yet')}`);
|
|
269
|
+
});
|
|
270
|
+
console.log(` ${dim('n. A new project, from this codebase’s docs')}`);
|
|
271
|
+
const answer = (await ask(` ${dim('(number, n, or empty for none yet)')} `)).trim().toLowerCase();
|
|
272
|
+
if (answer === 'n') {
|
|
273
|
+
if (!hasDocs) {
|
|
274
|
+
console.log(` ${yellow('No docs folder here yet')} (${config.docsDir}/). Run docugate init in this folder to start one, then connect again.`);
|
|
275
|
+
return 1;
|
|
276
|
+
}
|
|
277
|
+
const name = (await ask(` What is the project called? [${repo.split('/').at(-1)}] `)).trim() || repo.split('/').at(-1);
|
|
278
|
+
const res = await apiFetch('/api/spaces', { method: 'POST', body: JSON.stringify({ repo, name, docsDir }) });
|
|
279
|
+
const { space } = (await res.json());
|
|
280
|
+
project = { ...space, org: null, sources: [{ repo, docsDir, mount: '' }], codebases: [] };
|
|
281
|
+
console.log(` ${green('✓')} Created ${space.name}, published from ${docsDir}/.`);
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
project = choices[Number(answer) - 1];
|
|
285
|
+
}
|
|
203
286
|
}
|
|
204
|
-
else if (
|
|
205
|
-
|
|
287
|
+
else if (choices.length === 1) {
|
|
288
|
+
project = choices[0];
|
|
206
289
|
}
|
|
207
|
-
const
|
|
208
|
-
const type = args.type ?? (await choose('What kind of project is this?', PROJECT_TYPES, guessed, ask));
|
|
290
|
+
const type = args.type ?? (await choose('What is this codebase?', PROJECT_TYPES, detectType(root), ask));
|
|
209
291
|
let backendDir = args.backendDir ?? '';
|
|
210
292
|
if (type === 'monorepo' && !backendDir) {
|
|
211
293
|
const found = detectBackendDir(root);
|
|
@@ -214,20 +296,61 @@ export async function connectCommand(root, args) {
|
|
|
214
296
|
const frequency = args.frequency ?? (await choose('How often should docs be checked?', FREQUENCIES, 'weekly', ask));
|
|
215
297
|
const res = await apiFetch('/api/projects', {
|
|
216
298
|
method: 'POST',
|
|
217
|
-
body: JSON.stringify({ repo, type, backendDir, frequency, spaceId, audit: !args.noAudit }),
|
|
299
|
+
body: JSON.stringify({ repo, path, type, backendDir, frequency, spaceId: project?.id ?? null, audit: !args.noAudit }),
|
|
218
300
|
});
|
|
219
301
|
const { token } = (await res.json());
|
|
220
|
-
const
|
|
221
|
-
mkdirSync(join(
|
|
222
|
-
writeFileSync(
|
|
302
|
+
const file = TOKENS_FILE();
|
|
303
|
+
mkdirSync(join(file, '..'), { recursive: true });
|
|
304
|
+
writeFileSync(file, JSON.stringify({ ...readTokens(), [tokenKey(repo, path)]: token }, null, 2) + '\n', { mode: 0o600 });
|
|
305
|
+
// Its docs join the project's, unless the space already reads that folder.
|
|
306
|
+
let section = null;
|
|
307
|
+
const read = project?.sources.some((s) => s.repo.toLowerCase() === repo.toLowerCase() && s.docsDir === docsDir);
|
|
308
|
+
if (project && hasDocs && !read) {
|
|
309
|
+
section = args.section ?? sectionFor(type, project.sources);
|
|
310
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
311
|
+
try {
|
|
312
|
+
await apiFetch(`/api/spaces/${project.id}/sources`, {
|
|
313
|
+
method: 'POST',
|
|
314
|
+
body: JSON.stringify({ repo, docsDir, mount: section, label: path ? `${repo.split('/').at(-1)}/${path}` : repo.split('/').at(-1) }),
|
|
315
|
+
});
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
const code = codeOf(error);
|
|
320
|
+
// Two pages at one address: a section of its own settles it.
|
|
321
|
+
if (code === 'source_collision' && attempt === 0) {
|
|
322
|
+
const fallback = section || (type === 'monorepo' ? 'app' : type);
|
|
323
|
+
section = rl ? (await ask(` Some of its pages share an address with pages already there. Put them under a section? [${fallback}] `)).trim() || fallback : fallback;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
section = null;
|
|
327
|
+
if (code === 'pro_required_sources')
|
|
328
|
+
console.log(` ${yellow('Its docs were not added.')} One project fed by several codebases needs Pro or an organization plan: ${base()}/pricing`);
|
|
329
|
+
else if (code === 'no_repo_access')
|
|
330
|
+
console.log(` ${yellow('Its docs were not added.')} Install the DocuGate GitHub App on ${repo} (${base()}/api/auth/github?install=1), then run docugate connect again.`);
|
|
331
|
+
else
|
|
332
|
+
console.log(` ${yellow('Its docs were not added.')} ${error.message}`);
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
223
337
|
console.log();
|
|
224
|
-
|
|
338
|
+
if (project) {
|
|
339
|
+
const docs = section === null ? (read ? 'Its docs were already published.' : hasDocs ? '' : `It has no docs folder (${config.docsDir}/) to publish yet.`) : section ? `Its docs appear under ${section}.` : 'Its docs are at the top of the space.';
|
|
340
|
+
console.log(` ${green('Connected')} ${where} (${type}) to ${bold(project.name)}. ${docs}`);
|
|
341
|
+
const others = project.codebases.filter((c) => !(c.repo === repo && c.path === path));
|
|
342
|
+
if (!others.length)
|
|
343
|
+
console.log(` ${dim('Run docugate connect in its other codebases, the frontend or the backend, to add them to the same project.')}`);
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
console.log(` ${green('Connected')} ${where}. Choose its project in the dashboard, under Projects, or run docugate connect again.`);
|
|
347
|
+
}
|
|
225
348
|
console.log();
|
|
226
349
|
if (type === 'frontend')
|
|
227
350
|
console.log(` ${dim('A frontend needs no server package.')}`);
|
|
228
351
|
else {
|
|
229
|
-
const
|
|
230
|
-
console.log(` ${dim(`To check API keys, install the package ${
|
|
352
|
+
const inside = type === 'monorepo' && backendDir ? `in ${backendDir}` : 'in your backend';
|
|
353
|
+
console.log(` ${dim(`To check API keys, install the package ${inside}:`)} npm i docugate`);
|
|
231
354
|
}
|
|
232
355
|
console.log();
|
|
233
356
|
console.log(` For scheduled syncs, add this token as a GitHub secret. It is shown once:`);
|
package/dist/sync/run.js
CHANGED
|
@@ -90,7 +90,7 @@ export async function runSync(root, options = {}, deps = {}) {
|
|
|
90
90
|
writeSyncState(root, { lastSha: changes.to, lastRunAt: (deps.now ?? new Date()).toISOString() });
|
|
91
91
|
if (options.pr) {
|
|
92
92
|
const prFn = deps.prFn ?? defaultPr({ git, env });
|
|
93
|
-
const siteUrl = env.DOCUGATE_URL ?? 'https://
|
|
93
|
+
const siteUrl = env.DOCUGATE_URL ?? 'https://trydocugate.site';
|
|
94
94
|
result.pr = await prFn({
|
|
95
95
|
root,
|
|
96
96
|
title: `docs: sync with code changes (${changes.commits.length} commit${changes.commits.length === 1 ? '' : 's'})`,
|
package/dist/tour-server.js
CHANGED
|
@@ -104,7 +104,7 @@ export function publishedDocsUrl(root) {
|
|
|
104
104
|
const space = loadConfig(root).config.space;
|
|
105
105
|
if (!space || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(space))
|
|
106
106
|
return undefined;
|
|
107
|
-
return `${process.env.DOCUGATE_URL ?? 'https://
|
|
107
|
+
return `${process.env.DOCUGATE_URL ?? 'https://trydocugate.site'}/${space}`;
|
|
108
108
|
}
|
|
109
109
|
function handleGetEvents(req, res, root) {
|
|
110
110
|
const tourDir = join(root, '.docugate', 'tour');
|
|
@@ -379,13 +379,13 @@ export function startTourServer(root, port) {
|
|
|
379
379
|
// Somebody opened the tour server itself. Say what it is and where to go.
|
|
380
380
|
const screens = loadTours(root).length;
|
|
381
381
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
382
|
-
res.end(`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>DocuGate tour server</title>
|
|
383
|
-
<style>body{margin:0;min-height:100vh;display:grid;place-items:center;background:#0F1014;color:#F5F0E3;font:15px/1.6 system-ui,sans-serif}
|
|
384
|
-
main{max-width:440px;padding:32px;border:1px solid #25273a;border-radius:16px;background:#15173a}
|
|
385
|
-
h1{font-size:20px;margin:0 0 8px}p{margin:0 0 12px;color:#aaa597}b{color:#F4C43F;font-weight:600}
|
|
386
|
-
.dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:#45d483;margin-right:8px}</style></head>
|
|
387
|
-
<body><main><h1><span class="dot"></span>The DocuGate tour server is running</h1>
|
|
388
|
-
<p>This is not a page to open. It feeds the Tour button in your app (${screens} screen${screens === 1 ? '' : 's'} in <b>.docugate/tour/</b>).</p>
|
|
382
|
+
res.end(`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>DocuGate tour server</title>
|
|
383
|
+
<style>body{margin:0;min-height:100vh;display:grid;place-items:center;background:#0F1014;color:#F5F0E3;font:15px/1.6 system-ui,sans-serif}
|
|
384
|
+
main{max-width:440px;padding:32px;border:1px solid #25273a;border-radius:16px;background:#15173a}
|
|
385
|
+
h1{font-size:20px;margin:0 0 8px}p{margin:0 0 12px;color:#aaa597}b{color:#F4C43F;font-weight:600}
|
|
386
|
+
.dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:#45d483;margin-right:8px}</style></head>
|
|
387
|
+
<body><main><h1><span class="dot"></span>The DocuGate tour server is running</h1>
|
|
388
|
+
<p>This is not a page to open. It feeds the Tour button in your app (${screens} screen${screens === 1 ? '' : 's'} in <b>.docugate/tour/</b>).</p>
|
|
389
389
|
<p>Open <b>your app</b> instead, for example <b>http://localhost:5173</b> or <b>http://localhost:3000</b>. The Tour button is in the bottom-right corner.</p></main></body></html>`);
|
|
390
390
|
}
|
|
391
391
|
else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docugate",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Command-line tools for DocuGate: set up, check and prepare the documentation DocuGate reads straight from your repository. Also checks DocuGate API keys from Express, Hono and Fastify.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|