docugate 0.4.1 → 0.4.4

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
@@ -147,19 +147,21 @@ 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://www.trydocugate.site/dashboard).
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
 
154
154
  Documentation goes stale because updating it is a chore nobody owns. `docugate sync` does the chore: it reads what changed in the code, finds the pages those changes made wrong, asks an AI model to correct them, and gives you the edits to review.
155
155
 
156
+ The easy way is the menu: run `docugate` and pick **Update the docs with recent changes**. It asks which changes to catch up on (since the last update, today, this week, this month, or since a commit you pick), shows the pages they touch, then writes the edits into your files or a pull request. The first time, it asks for an AI key and keeps it on your computer. That is `docugate sync --ask`. The commands below are the same thing for scripts, agents and CI.
157
+
156
158
  ```sh
157
159
  npx docugate sync --dry-run # free and instant: which pages are probably out of date
158
160
  npx docugate sync # write the edits into your docs folder
159
161
  npx docugate sync --pr # push them and open a pull request instead
160
162
  ```
161
163
 
162
- `sync` needs an AI key of your own, in `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` (set `DOCUGATE_AI_BASE_URL` to use another OpenAI-compatible service). It talks to the provider directly: nothing goes through DocuGate's servers, and it works without a DocuGate account.
164
+ `sync` needs an AI key of your own, in `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` (set `DOCUGATE_AI_BASE_URL` to use another OpenAI-compatible service), or the one you pasted in the menu, kept in `~/.docugate/ai.json`. It talks to the provider directly: nothing goes through DocuGate's servers, and it works without a DocuGate account.
163
165
 
164
166
  **What it does, step by step**
165
167
 
@@ -351,7 +353,7 @@ Pin the version in CI, as above. Without a pin, the rules being enforced can cha
351
353
  | --- | --- | --- |
352
354
  | `docugate` | | In a terminal: the menu. Elsewhere: the help. |
353
355
  | `docugate status` | `--json` | Where things stand: account, docs, sync and tour, and what to do next. |
354
- | `docugate sync` | `--dry-run`, `--since <when>`, `--pr`, `--yes`, `--max-files <n>`, `--model <name>`, `--quiet` | Update the docs from what changed in the code. |
356
+ | `docugate sync` | `--ask`, `--dry-run`, `--since <when>`, `--pr`, `--yes`, `--max-files <n>`, `--model <name>`, `--quiet` | Update the docs from what changed in the code. |
355
357
  | `docugate sync init` | `--freshness push\|daily\|weekly\|off`, `--force` | Add the GitHub Action that runs sync on a schedule or on every push. |
356
358
  | `docugate sync hook install` | | Tell you after each commit which pages the code left behind. |
357
359
  | `docugate sync hook remove` | | Remove that hook. |
@@ -412,11 +414,11 @@ cd server && npx docugate connect # pick the project, or start a new one
412
414
  cd ../public && npx docugate connect # pick the same project
413
415
  ```
414
416
 
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://www.trydocugate.site/dashboard). Feeding one project from several codebases needs Pro or an organization plan.
417
+ 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.
416
418
 
417
419
  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.
418
420
 
419
- See [Merging repositories](https://www.trydocugate.site/docs/merging-repositories).
421
+ See [Merging repositories](https://trydocugate.site/docs/merging-repositories).
420
422
 
421
423
  ---
422
424
 
@@ -471,7 +473,7 @@ const verdict = await verifyKey(key, { secret: process.env.DOCUGATE_VERIFY_SECRE
471
473
  if (!verdict.valid) return deny(verdict.code) // REVOKED, EXPIRED, USAGE_EXCEEDED, ...
472
474
  ```
473
475
 
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://www.trydocugate.site/docs/api-keys).
476
+ `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).
475
477
 
476
478
  ---
477
479
 
@@ -488,7 +490,7 @@ One per repository, at its root.
488
490
  | `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. |
489
491
  | `sidebar` | File and folder names in the order the sidebar should show them. Orders this repository's own pages, not the whole space. |
490
492
  | `api.generate` | Defaults for `docugate openapi`. |
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://www.trydocugate.site/docs/api-reference) from it. |
493
+ | `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. |
492
494
 
493
495
  ---
494
496
 
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://www.trydocugate.site';
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',
@@ -60,8 +60,18 @@ export async function apiFetch(path, init = {}) {
60
60
  headers['x-docugate-version'] = CLI_VERSION;
61
61
  }
62
62
  // Re-read the env at call time so tests that set DOCUGATE_URL after import work.
63
- const base = process.env.DOCUGATE_URL ?? 'https://www.trydocugate.site';
64
- const res = await fetch(`${base}${path}`, { ...init, headers });
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
+ }
65
75
  // Renew token if the server sent a fresh one.
66
76
  const renewed = res.headers.get('x-docugate-token');
67
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://www.trydocugate.site';
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) {
package/dist/commands.js CHANGED
@@ -14,9 +14,10 @@ export const COMMANDS = [
14
14
  { name: 'max-files', value: 'n', description: 'Most pages one run may change (default: 10)' },
15
15
  { name: 'model', value: 'name', description: 'Model to use (default: sync.model in docugate.json)' },
16
16
  { name: 'quiet', description: 'Print one line only when pages are out of date (for the git hook)' },
17
+ { name: 'ask', description: 'Ask which changes, show the pages first, then ask where the edits go (what the menu runs)' },
17
18
  YES,
18
19
  ],
19
- examples: ['docugate sync --dry-run', 'docugate sync --since 7d', 'docugate sync --pr --yes'],
20
+ examples: ['docugate sync --dry-run', 'docugate sync --since 7d', 'docugate sync --pr --yes', 'docugate sync --ask'],
20
21
  account: 'none',
21
22
  },
22
23
  {
@@ -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://www.trydocugate.site';
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://www.trydocugate.site';
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://www.trydocugate.site';
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/menu.js CHANGED
@@ -20,6 +20,24 @@ export function buildChoices(s, now = new Date()) {
20
20
  keywords: 'login sign in account github',
21
21
  });
22
22
  }
23
+ if (signedIn && p.git) {
24
+ choices.push({
25
+ value: ['connect'],
26
+ label: p.connected ? 'Connect again, or to another project' : 'Connect this folder to a project',
27
+ status: p.connected ? 'connected' : 'not connected',
28
+ description: 'Links this codebase to a project on DocuGate: its docs, and a backend or frontend beside it. The dashboard then shows its packages, security and updates.',
29
+ keywords: 'connect link project codebase frontend backend',
30
+ });
31
+ }
32
+ if (p.connected) {
33
+ choices.push({
34
+ value: ['audit'],
35
+ label: 'Check packages for vulnerabilities',
36
+ status: '',
37
+ description: 'Reads your lockfiles and checks every package against the published advisories. The results show in the dashboard under Security.',
38
+ keywords: 'audit security packages vulnerabilities osv',
39
+ });
40
+ }
23
41
  choices.push({
24
42
  value: ['developers'],
25
43
  label: 'Set up a developer space',
@@ -33,10 +51,10 @@ export function buildChoices(s, now = new Date()) {
33
51
  description: 'A short guided setup: where the docs live, the kind of repository, and linking a DocuGate space. It never overwrites your files.',
34
52
  keywords: 'init setup configure',
35
53
  }, {
36
- value: ['sync'],
37
- label: 'Update the docs from the code',
54
+ value: ['sync', '--ask'],
55
+ label: 'Update the docs with recent changes',
38
56
  status: y.commits === null ? '' : y.commits === 0 ? 'up to date' : `${plural(y.commits, 'commit')} behind`,
39
- description: 'Reads what changed in the code, asks your AI model for the docs edits, and writes them. Add --pr from the command line to open a pull request instead.',
57
+ description: 'Pick which changes (since the last update, today, this week), see the pages they touch, then write the edits into the files or a pull request. Uses your own AI key.',
40
58
  keywords: 'sync update docs changes ai',
41
59
  disabled: needsDocs,
42
60
  }, {
@@ -48,7 +66,7 @@ export function buildChoices(s, now = new Date()) {
48
66
  disabled: needsDocs,
49
67
  }, {
50
68
  value: ['sync', 'init'],
51
- label: 'Keep the docs current automatically',
69
+ label: 'Update the docs automatically on GitHub',
52
70
  status: y.workflow ? `on · ${p.freshness}` : 'not set up',
53
71
  description: `Adds a GitHub Action that runs sync ${p.freshness === 'push' ? 'on every push' : p.freshness === 'daily' ? 'every day' : 'every week'} and opens a pull request with the edits. You review and merge.`,
54
72
  keywords: 'sync init github action workflow schedule freshness',
@@ -156,16 +174,17 @@ export function buildMenu(s, section) {
156
174
  if (section === 'more') {
157
175
  const hook = all.find((c) => c.value[0] === 'sync' && c.value[1] === 'hook');
158
176
  return [
159
- ...pick('sync --dry-run', 'sync init', ...(hook ? [hook.value.join(' ')] : []), 'status', 'openapi', 'help'),
177
+ ...pick('developers', ...(s.project.setUp ? ['init'] : []), ...(s.project.connected ? ['connect'] : []), 'sync --dry-run', 'sync init', ...(hook ? [hook.value.join(' ')] : [])),
178
+ { value: [...GO.tours], label: 'Tours' },
179
+ ...pick('status', 'openapi', 'help', 'logout'),
160
180
  { value: [...GO.back], label: 'Back' },
161
181
  ];
162
182
  }
183
+ // The few things people come for. Everything else is one step away, under More.
163
184
  const signedIn = s.account.state === 'signed-in';
164
185
  return [
165
- ...pick('developers', 'init', 'sync', 'check'),
166
- { value: [...GO.tours], label: 'Tours' },
186
+ ...pick(...(signedIn ? [] : ['login']), ...(s.project.connected ? [] : ['connect']), ...(s.project.setUp ? [] : ['init']), 'sync --ask', 'audit', 'check'),
167
187
  { value: [...GO.more], label: 'More' },
168
- ...pick(signedIn ? 'logout' : 'login'),
169
188
  { value: [...GO.quit], label: 'Quit' },
170
189
  ];
171
190
  }
package/dist/project.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { Choice } from './select.js';
1
2
  export declare const PROJECT_TYPES: readonly ["frontend", "backend", "serverless", "monorepo"];
2
3
  export type ProjectType = (typeof PROJECT_TYPES)[number];
3
4
  export declare const FREQUENCIES: readonly ["push", "daily", "weekly", "off"];
@@ -110,6 +111,13 @@ export declare function sectionFor(type: ProjectType, sources: {
110
111
  }[]): string;
111
112
  /** The project someone named: by slug, `owner/slug` or name, in any case. */
112
113
  export declare function pickProject(choices: ProjectChoice[], wanted: string): ProjectChoice | undefined;
114
+ /**
115
+ * One choice from a list: the arrow-key picker in a terminal that can draw it,
116
+ * else a typed answer. Null when the person pressed escape.
117
+ */
118
+ export declare function pickOne<T extends string>(title: string, choices: Choice<T>[], fallback: T, ask: (q: string) => Promise<string>): Promise<T | null>;
119
+ /** A typed answer. Each question has its own reader, so the picker owns the keyboard in between. */
120
+ export declare function askLine(question: string): Promise<string>;
113
121
  /**
114
122
  * `docugate connect`, run in each codebase of a project: the backend, then the
115
123
  * frontend, whether they are two repositories or two folders of one. It joins
package/dist/project.js CHANGED
@@ -10,6 +10,7 @@ import { guessRole } from './init.js';
10
10
  import { scanLockfiles } from './lockfiles.js';
11
11
  import { defaultGit } from './sync/changes.js';
12
12
  import { repoFromRemote } from './sync/pr.js';
13
+ import { canPick, select } from './select.js';
13
14
  import { bold, dim, green, plural, red, yellow } from './ui.js';
14
15
  // A repository connected to a DocuGate account. `docugate connect` links it, and
15
16
  // from then on the dashboard decides which docs it updates, how often and what
@@ -18,7 +19,7 @@ import { bold, dim, green, plural, red, yellow } from './ui.js';
18
19
  export const PROJECT_TYPES = ['frontend', 'backend', 'serverless', 'monorepo'];
19
20
  export const FREQUENCIES = ['push', 'daily', 'weekly', 'off'];
20
21
  const TOKENS_FILE = () => join(process.env.DOCUGATE_HOME ?? join(homedir(), '.docugate'), 'projects.json');
21
- const base = () => process.env.DOCUGATE_URL ?? 'https://www.trydocugate.site';
22
+ const base = () => process.env.DOCUGATE_URL ?? 'https://trydocugate.site';
22
23
  function readTokens() {
23
24
  try {
24
25
  return JSON.parse(readFileSync(TOKENS_FILE(), 'utf8'));
@@ -60,9 +61,12 @@ export function detectType(root) {
60
61
  const role = guessRole(root);
61
62
  if (workspaces || role === 'both')
62
63
  return 'monorepo';
64
+ // Checked before the hosting files: a frontend on Vercel or Netlify has one too.
65
+ if (role === 'frontend')
66
+ return 'frontend';
63
67
  if (['serverless.yml', 'serverless.ts', 'wrangler.toml', 'wrangler.jsonc', 'netlify.toml', 'vercel.json', 'template.yaml'].some(has))
64
68
  return 'serverless';
65
- return role === 'frontend' ? 'frontend' : 'backend';
69
+ return 'backend';
66
70
  }
67
71
  /** Where the backend lives in a monorepo, which is where the key-check package installs. */
68
72
  export function detectBackendDir(root) {
@@ -206,9 +210,39 @@ export function pickProject(choices, wanted) {
206
210
  const w = wanted.trim().toLowerCase();
207
211
  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
212
  }
209
- async function choose(question, options, fallback, ask) {
210
- const answer = (await ask(` ${question} ${dim(`(${options.join(', ')})`)} [${fallback}] `)).trim().toLowerCase();
211
- return options.includes(answer) ? answer : fallback;
213
+ /** What each kind of codebase and each cadence means, for the picker. */
214
+ const TYPE_HINT = {
215
+ frontend: 'What people see and click: a web or mobile app',
216
+ backend: 'An API or server the frontend calls',
217
+ serverless: 'Functions on Lambda, Workers or similar, with no server of their own',
218
+ monorepo: 'A frontend and a backend in this one folder',
219
+ };
220
+ const FREQUENCY_HINT = {
221
+ push: 'On every push to the default branch',
222
+ daily: 'Once a day',
223
+ weekly: 'Once a week',
224
+ off: 'Only when you run docugate sync',
225
+ };
226
+ /**
227
+ * One choice from a list: the arrow-key picker in a terminal that can draw it,
228
+ * else a typed answer. Null when the person pressed escape.
229
+ */
230
+ export async function pickOne(title, choices, fallback, ask) {
231
+ if (canPick())
232
+ return select({ title, choices, rows: Math.min(choices.length, 10), legend: false });
233
+ const values = choices.map((c) => c.value);
234
+ const answer = (await ask(` ${title} ${dim(`(${values.join(', ')})`)} [${fallback}] `)).trim().toLowerCase();
235
+ return values.includes(answer) ? answer : fallback;
236
+ }
237
+ /** A typed answer. Each question has its own reader, so the picker owns the keyboard in between. */
238
+ export async function askLine(question) {
239
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
240
+ try {
241
+ return await rl.question(question);
242
+ }
243
+ finally {
244
+ rl.close();
245
+ }
212
246
  }
213
247
  const codeOf = (error) => error.code;
214
248
  /**
@@ -228,20 +262,17 @@ export async function connectCommand(root, args) {
228
262
  }
229
263
  const { repo, path } = codebaseOf(root);
230
264
  const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !process.env.CI && !args.yes;
231
- const rl = interactive ? createInterface({ input: process.stdin, output: process.stdout }) : null;
232
- const ask = async (q) => (rl ? rl.question(q) : '');
265
+ const ask = async (q) => (interactive ? askLine(q) : '');
233
266
  const where = path ? `${repo} ${dim(`(${path}/)`)}` : repo;
234
267
  try {
235
268
  // Signed in first: everything after this is done as someone.
236
269
  const { user } = (await (await apiFetch('/api/auth/session')).json().catch(() => ({ user: null })));
237
270
  if (!user) {
238
- if (!rl || !/^(|y|yes)$/i.test((await ask(` Sign in to DocuGate first? ${dim('(Y/n)')} `)).trim())) {
271
+ if (!interactive || !/^(|y|yes)$/i.test((await ask(` Sign in to DocuGate first? ${dim('(Y/n)')} `)).trim())) {
239
272
  console.error(`${red('error')} Not signed in. Run docugate login, then docugate connect again.`);
240
273
  return 1;
241
274
  }
242
- rl.pause();
243
275
  await login(base());
244
- rl.resume();
245
276
  }
246
277
  const { projects: choices } = (await (await apiFetch('/api/projects/choices')).json());
247
278
  console.log();
@@ -261,15 +292,28 @@ export async function connectCommand(root, args) {
261
292
  return 1;
262
293
  }
263
294
  }
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') {
295
+ else if (interactive) {
296
+ // The codebases already in a project say what it is better than its name does.
297
+ const options = [
298
+ ...choices.map((p) => {
299
+ const here = p.codebases.some((c) => c.repo.toLowerCase() === repo.toLowerCase() && c.path === path);
300
+ const parts = p.codebases.map((c) => `${c.type} ${c.repo.split('/').at(-1)}${c.path ? `/${c.path}` : ''}`);
301
+ return {
302
+ value: p.id,
303
+ label: p.name,
304
+ status: [p.org, here ? 'already has this codebase' : ''].filter(Boolean).join(' · '),
305
+ description: parts.length ? `Fed by ${parts.join(', ')}` : 'No codebase yet',
306
+ keywords: `${p.owner}/${p.slug} ${parts.join(' ')}`,
307
+ recommended: here,
308
+ };
309
+ }),
310
+ { value: 'new', label: 'A new project', description: hasDocs ? `Its docs come from ${docsDir}/ in this codebase` : `Needs a docs folder here first (${config.docsDir}/). Run docugate init`, disabled: hasDocs ? undefined : 'no docs folder here yet' },
311
+ { value: 'none', label: 'Not yet', description: 'Connect it now and choose its project later, in the dashboard' },
312
+ ];
313
+ const answer = await pickOne('Which project is this codebase part of?', options, 'none', ask);
314
+ if (answer === null)
315
+ return 1;
316
+ if (answer === 'new') {
273
317
  if (!hasDocs) {
274
318
  console.log(` ${yellow('No docs folder here yet')} (${config.docsDir}/). Run docugate init in this folder to start one, then connect again.`);
275
319
  return 1;
@@ -281,19 +325,30 @@ export async function connectCommand(root, args) {
281
325
  console.log(` ${green('✓')} Created ${space.name}, published from ${docsDir}/.`);
282
326
  }
283
327
  else {
284
- project = choices[Number(answer) - 1];
328
+ project = choices.find((p) => p.id === answer);
285
329
  }
286
330
  }
287
331
  else if (choices.length === 1) {
288
332
  project = choices[0];
289
333
  }
290
- const type = args.type ?? (await choose('What is this codebase?', PROJECT_TYPES, detectType(root), ask));
334
+ const guessed = detectType(root);
335
+ const type = args.type ??
336
+ (interactive
337
+ ? await pickOne('What is this codebase?', PROJECT_TYPES.map((t) => ({ value: t, label: t, description: TYPE_HINT[t], recommended: t === guessed, status: t === guessed ? 'detected' : undefined })), guessed, ask)
338
+ : guessed);
339
+ if (type === null)
340
+ return 1;
291
341
  let backendDir = args.backendDir ?? '';
292
342
  if (type === 'monorepo' && !backendDir) {
293
343
  const found = detectBackendDir(root);
294
344
  backendDir = (await ask(` Which folder holds the backend? [${found || 'none'}] `)).trim() || found;
295
345
  }
296
- const frequency = args.frequency ?? (await choose('How often should docs be checked?', FREQUENCIES, 'weekly', ask));
346
+ const frequency = args.frequency ??
347
+ (interactive
348
+ ? await pickOne('How often should its docs be checked?', FREQUENCIES.map((f) => ({ value: f, label: f, description: FREQUENCY_HINT[f], recommended: f === 'weekly' })), 'weekly', ask)
349
+ : 'weekly');
350
+ if (frequency === null)
351
+ return 1;
297
352
  const res = await apiFetch('/api/projects', {
298
353
  method: 'POST',
299
354
  body: JSON.stringify({ repo, path, type, backendDir, frequency, spaceId: project?.id ?? null, audit: !args.noAudit }),
@@ -320,7 +375,7 @@ export async function connectCommand(root, args) {
320
375
  // Two pages at one address: a section of its own settles it.
321
376
  if (code === 'source_collision' && attempt === 0) {
322
377
  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;
378
+ section = interactive ? (await ask(` Some of its pages share an address with pages already there. Put them under a section? [${fallback}] `)).trim() || fallback : fallback;
324
379
  continue;
325
380
  }
326
381
  section = null;
@@ -346,16 +401,24 @@ export async function connectCommand(root, args) {
346
401
  console.log(` ${green('Connected')} ${where}. Choose its project in the dashboard, under Projects, or run docugate connect again.`);
347
402
  }
348
403
  console.log();
404
+ // The first check now, so the dashboard's Security column is filled from the start.
405
+ // After this, each docugate sync checks again, and docugate audit does on demand.
406
+ if (!args.noAudit) {
407
+ const answer = await runAudit(root, token);
408
+ console.log(` ${bold('Dependency check')} ${dim(`${plural(answer.files.length, 'lockfile')} read`)}`);
409
+ printFindings(answer);
410
+ console.log();
411
+ }
349
412
  if (type === 'frontend')
350
413
  console.log(` ${dim('A frontend needs no server package.')}`);
351
414
  else {
352
415
  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`);
416
+ console.log(` ${dim(`Only if you use DocuGate API keys: install the package ${inside} with`)} npm i docugate`);
354
417
  }
355
418
  console.log();
356
- console.log(` For scheduled syncs, add this token as a GitHub secret. It is shown once:`);
357
- console.log(` gh secret set DOCUGATE_PROJECT_TOKEN --body ${token}`);
358
- console.log(` ${dim('Then run docugate sync init --force so the workflow follows the dashboard.')}`);
419
+ // Nothing else to set up: updates run from this computer, and report to the dashboard on their own.
420
+ console.log(` ${green('Done.')} Whenever you want this codebase's changes in the docs, run ${bold('docugate')} and pick ${bold('Update the docs')}.`);
421
+ console.log(` ${dim('To have GitHub do it on every push instead: docugate, then More, then Update the docs automatically on GitHub.')}`);
359
422
  console.log();
360
423
  return 0;
361
424
  }
@@ -363,7 +426,4 @@ export async function connectCommand(root, args) {
363
426
  console.error(`${red('error')} ${error.message}`);
364
427
  return 1;
365
428
  }
366
- finally {
367
- rl?.close();
368
- }
369
429
  }
package/dist/status.d.ts CHANGED
@@ -10,6 +10,8 @@ export type Status = {
10
10
  project: {
11
11
  git: boolean;
12
12
  setUp: boolean;
13
+ /** Linked to a DocuGate project with `docugate connect`, so the dashboard sees it. */
14
+ connected: boolean;
13
15
  docsDir: string;
14
16
  pages: number;
15
17
  role?: string;
package/dist/status.js CHANGED
@@ -2,6 +2,8 @@ import { loadConfig } from './config.js';
2
2
  import { accountState } from './session.js';
3
3
  import { collectChanges, defaultGit } from './sync/changes.js';
4
4
  import { hookInstalled } from './sync/hook.js';
5
+ import { withSavedKey } from './sync/ai.js';
6
+ import { projectToken } from './project.js';
5
7
  import { loadPages, scoreImpact } from './sync/impact.js';
6
8
  import { readSyncState } from './sync/state.js';
7
9
  import { workflowInstalled } from './sync/workflow.js';
@@ -66,6 +68,7 @@ export async function gatherStatus(root, deps = {}) {
66
68
  project: {
67
69
  git: inGit,
68
70
  setUp: loaded.found,
71
+ connected: Boolean(projectToken(root, env)),
69
72
  docsDir,
70
73
  pages: pages.length,
71
74
  role: loaded.config.role,
@@ -75,7 +78,10 @@ export async function gatherStatus(root, deps = {}) {
75
78
  sync: {
76
79
  workflow: workflowInstalled(root),
77
80
  hook: inGit && hookInstalled(root, git),
78
- key: env.ANTHROPIC_API_KEY ? 'anthropic' : env.OPENAI_API_KEY ? 'openai' : null,
81
+ key: (() => {
82
+ const withKey = withSavedKey(env);
83
+ return withKey.ANTHROPIC_API_KEY ? 'anthropic' : withKey.OPENAI_API_KEY ? 'openai' : null;
84
+ })(),
79
85
  lastRunAt: state.lastRunAt,
80
86
  commits,
81
87
  affected,
@@ -90,14 +96,15 @@ export function nextStep(s) {
90
96
  if (s.account.state === 'signed-out') {
91
97
  return { command: 'login', why: 'Connect your DocuGate account so your docs can be published' };
92
98
  }
99
+ // Connected first: it is what makes the dashboard see this codebase, its packages and its runs.
100
+ if (s.account.state === 'signed-in' && s.project.git && !s.project.connected) {
101
+ return { command: 'connect', why: 'Link this folder to a project, so the dashboard sees it' };
102
+ }
93
103
  if (!s.project.setUp)
94
104
  return { command: 'init', why: 'Set this repository up for DocuGate' };
95
- if (s.project.git && s.project.freshness !== 'off' && !s.sync.workflow) {
96
- return { command: 'sync init', why: `Keep the docs current automatically (${s.project.freshness})` };
97
- }
98
105
  if (s.project.git && s.sync.commits && s.sync.commits > 0) {
99
106
  return {
100
- command: 'sync',
107
+ command: 'sync --ask',
101
108
  why: s.sync.affected
102
109
  ? `${plural(s.sync.affected, 'page')} may be out of date after ${plural(s.sync.commits, 'commit')}`
103
110
  : `${plural(s.sync.commits, 'commit')} since the last sync`,
@@ -148,7 +155,7 @@ export function renderStatus(s, now = new Date()) {
148
155
  p.freshness === 'off' ? 'automatic sync off' : `checks ${p.freshness}`,
149
156
  y.workflow ? 'workflow installed' : 'no workflow',
150
157
  y.hook ? 'hook on' : null,
151
- y.key ? `${y.key === 'anthropic' ? 'Anthropic' : 'OpenAI'} key found` : 'no AI key in this shell',
158
+ y.key ? `${y.key === 'anthropic' ? 'Anthropic' : 'OpenAI'} key found` : 'no AI key yet',
152
159
  ].filter(Boolean);
153
160
  lines.push(row('Sync', y.workflow ? ok : off, `${bits.join(dim(' · '))}`));
154
161
  const behind = y.commits === null
package/dist/sync/ai.d.ts CHANGED
@@ -26,6 +26,16 @@ export type AiRequest = {
26
26
  export type AiFn = (request: AiRequest) => Promise<string>;
27
27
  /** Which provider and key to use: the configured one, else whichever key is set. */
28
28
  export declare function resolveProvider(env: NodeJS.ProcessEnv, sync?: SyncConfig, modelOverride?: string): Provider | null;
29
+ export type SavedKey = {
30
+ provider: 'anthropic' | 'openai';
31
+ key: string;
32
+ };
33
+ export declare function readSavedKey(): SavedKey | null;
34
+ export declare function saveKey(saved: SavedKey): void;
35
+ /** Which provider a pasted key belongs to, from its shape. */
36
+ export declare function providerOfKey(key: string): SavedKey['provider'];
37
+ /** The environment, with the saved key filled in when it has none of its own. */
38
+ export declare function withSavedKey(env: NodeJS.ProcessEnv, saved?: SavedKey | null): NodeJS.ProcessEnv;
29
39
  export declare const KEY_HELP: string;
30
40
  /** The call to a real provider. */
31
41
  export declare function providerAiFn(provider: Provider): AiFn;
package/dist/sync/ai.js CHANGED
@@ -1,4 +1,7 @@
1
1
  import { randomBytes } from 'node:crypto';
2
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
2
5
  import { SyncError } from './changes.js';
3
6
  const DEFAULT_MODELS = { anthropic: 'claude-sonnet-5-5', openai: 'gpt-4o' };
4
7
  /** Which provider and key to use: the configured one, else whichever key is set. */
@@ -20,8 +23,44 @@ export function resolveProvider(env, sync = {}, modelOverride) {
20
23
  : (env.DOCUGATE_AI_BASE_URL ?? env.OPENAI_BASE_URL ?? 'https://api.openai.com/v1'),
21
24
  };
22
25
  }
26
+ /*
27
+ * A key pasted once in the menu, kept on this computer next to the sign-in, so
28
+ * nobody has to export it in every terminal. The environment still wins: CI
29
+ * and a developer's own shell setup are never overridden.
30
+ */
31
+ const KEY_FILE = () => join(process.env.DOCUGATE_HOME ?? join(homedir(), '.docugate'), 'ai.json');
32
+ export function readSavedKey() {
33
+ try {
34
+ const saved = JSON.parse(readFileSync(KEY_FILE(), 'utf8'));
35
+ return (saved.provider === 'anthropic' || saved.provider === 'openai') && typeof saved.key === 'string' && saved.key ? saved : null;
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ export function saveKey(saved) {
42
+ const file = KEY_FILE();
43
+ mkdirSync(dirname(file), { recursive: true });
44
+ writeFileSync(file, JSON.stringify(saved), { mode: 0o600 });
45
+ try {
46
+ chmodSync(file, 0o600);
47
+ }
48
+ catch {
49
+ // Windows keeps its own permissions.
50
+ }
51
+ }
52
+ /** Which provider a pasted key belongs to, from its shape. */
53
+ export function providerOfKey(key) {
54
+ return key.trim().startsWith('sk-ant-') ? 'anthropic' : 'openai';
55
+ }
56
+ /** The environment, with the saved key filled in when it has none of its own. */
57
+ export function withSavedKey(env, saved = readSavedKey()) {
58
+ if (env.ANTHROPIC_API_KEY || env.OPENAI_API_KEY || !saved)
59
+ return env;
60
+ return { ...env, [saved.provider === 'anthropic' ? 'ANTHROPIC_API_KEY' : 'OPENAI_API_KEY']: saved.key };
61
+ }
23
62
  export const KEY_HELP = 'Set ANTHROPIC_API_KEY (or OPENAI_API_KEY, optionally with DOCUGATE_AI_BASE_URL for another OpenAI-compatible service) ' +
24
- 'in your environment, or as a secret in GitHub Actions.';
63
+ 'in your environment, or as a secret in GitHub Actions. Or run `docugate`, pick Update the docs, and paste a key once.';
25
64
  /** The call to a real provider. */
26
65
  export function providerAiFn(provider) {
27
66
  return async ({ system, user, maxTokens }) => {
@@ -0,0 +1,26 @@
1
+ import type { Choice } from '../select.js';
2
+ import type { GitFn } from './changes.js';
3
+ import type { SavedKey } from './ai.js';
4
+ export type Since = 'last' | '1d' | '7d' | '30d' | 'commit';
5
+ export type Destination = 'files' | 'pr' | 'stop';
6
+ export type AskDeps = {
7
+ /** One arrow-key question. Tests answer it themselves. */
8
+ pick: <T extends string>(title: string, choices: Choice<T>[], fallback: T) => Promise<T | null>;
9
+ /** A typed answer, for the AI key. */
10
+ line: (question: string) => Promise<string>;
11
+ git?: GitFn;
12
+ now?: Date;
13
+ save?: (key: SavedKey) => void;
14
+ };
15
+ /** Which changes to read. Null when the person backed out. */
16
+ export declare function askSince(root: string, lastRunAt: string | undefined, deps: AskDeps): Promise<string | null>;
17
+ /** The latest commits, newest first, for picking a starting point. */
18
+ export declare function recentCommits(root: string, git: GitFn, count?: number): {
19
+ sha: string;
20
+ subject: string;
21
+ when: string;
22
+ }[];
23
+ /** Where the edits go, after the preview. */
24
+ export declare function askDestination(pages: number, canOpenPr: boolean, deps: AskDeps): Promise<Destination | null>;
25
+ /** Asks for an AI key once and keeps it on this computer. Null when none was given. */
26
+ export declare function askForKey(deps: AskDeps): Promise<SavedKey | null>;
@@ -0,0 +1,55 @@
1
+ import { ago } from '../status.js';
2
+ import { dim } from '../ui.js';
3
+ import { defaultGit } from './changes.js';
4
+ import { providerOfKey, saveKey } from './ai.js';
5
+ /** Which changes to read. Null when the person backed out. */
6
+ export async function askSince(root, lastRunAt, deps) {
7
+ const since = await deps.pick('Which changes should the docs catch up on?', [
8
+ { value: 'last', label: 'Since the last update', status: lastRunAt ? ago(lastRunAt, deps.now) : 'none yet', recommended: true },
9
+ { value: '1d', label: 'Today' },
10
+ { value: '7d', label: 'This week' },
11
+ { value: '30d', label: 'This month' },
12
+ { value: 'commit', label: 'Since a commit I pick' },
13
+ ], 'last');
14
+ if (since !== 'commit')
15
+ return since;
16
+ const commits = recentCommits(root, deps.git ?? defaultGit);
17
+ if (!commits.length)
18
+ return 'last';
19
+ return deps.pick('Since which commit? Everything after it is read.', commits.map((c, i) => ({ value: c.sha, label: c.subject, status: `${c.sha} · ${c.when}`, recommended: i === 0 })), commits[0].sha);
20
+ }
21
+ /** The latest commits, newest first, for picking a starting point. */
22
+ export function recentCommits(root, git, count = 15) {
23
+ try {
24
+ return git(['log', '--no-merges', `-${count}`, '--format=%h%x1f%s%x1f%ar'], root)
25
+ .trim()
26
+ .split('\n')
27
+ .filter(Boolean)
28
+ .map((line) => {
29
+ const [sha, subject, when] = line.split('\x1f');
30
+ return { sha, subject: subject.length > 60 ? `${subject.slice(0, 57)}...` : subject, when };
31
+ });
32
+ }
33
+ catch {
34
+ return [];
35
+ }
36
+ }
37
+ /** Where the edits go, after the preview. */
38
+ export async function askDestination(pages, canOpenPr, deps) {
39
+ return deps.pick(pages ? `Write the edits for ${pages === 1 ? 'this page' : 'these pages'}?` : 'No page names these changes. Ask the AI anyway?', [
40
+ { value: 'files', label: 'Yes, into the files here', description: 'You read them, then commit them like any change.', recommended: true },
41
+ ...(canOpenPr ? [{ value: 'pr', label: 'Yes, as a pull request', description: 'Pushed to the docugate/sync branch for someone to review.' }] : []),
42
+ { value: 'stop', label: 'Not now' },
43
+ ], 'files');
44
+ }
45
+ /** Asks for an AI key once and keeps it on this computer. Null when none was given. */
46
+ export async function askForKey(deps) {
47
+ console.log(`\n ${dim('Writing the edits uses your own AI model. The key stays on this computer and never goes through DocuGate.')}`);
48
+ const key = (await deps.line(' Paste an Anthropic or OpenAI key (Enter to skip): ')).trim();
49
+ if (!key)
50
+ return null;
51
+ const saved = { provider: providerOfKey(key), key };
52
+ (deps.save ?? saveKey)(saved);
53
+ console.log(` ${dim(`Saved. Later runs use it without asking.`)}`);
54
+ return saved;
55
+ }
@@ -10,8 +10,11 @@ import { defaultGit, SyncError } from './changes.js';
10
10
  import { installHook, removeHook } from './hook.js';
11
11
  import { runSync } from './run.js';
12
12
  import { repoFromRemote } from './pr.js';
13
- import { printFindings, projectToken, readSettings, reportRun, runAudit } from '../project.js';
13
+ import { askLine, pickOne, printFindings, projectToken, readSettings, reportRun, runAudit } from '../project.js';
14
14
  import { writeWorkflow, WORKFLOW_FILE } from './workflow.js';
15
+ import { KEY_HELP, resolveProvider, withSavedKey } from './ai.js';
16
+ import { askDestination, askForKey, askSince } from './ask.js';
17
+ import { readSyncState } from './state.js';
15
18
  const out = (line = '') => console.log(line);
16
19
  /** `docugate sync ...`. Returns the exit code. */
17
20
  export async function syncCommand(root, args, deps = {}) {
@@ -24,6 +27,9 @@ export async function syncCommand(root, args, deps = {}) {
24
27
  if (args.sub)
25
28
  throw new SyncError(`Unknown sync command "${args.sub}". Try: sync, sync init, sync hook install, sync hook remove.`);
26
29
  const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !process.env.CI && !args.bools.has('no-input');
30
+ deps = { ...deps, env: withSavedKey(deps.env ?? process.env) };
31
+ if (args.bools.has('ask') && interactive)
32
+ return askedSync(root, args, deps);
27
33
  const options = {
28
34
  since: args.since,
29
35
  dryRun: args.bools.has('dry-run'),
@@ -78,6 +84,44 @@ export async function syncCommand(root, args, deps = {}) {
78
84
  throw error;
79
85
  }
80
86
  }
87
+ /**
88
+ * The menu's "Update the docs": which changes, a free preview of the pages they
89
+ * touch, then where the edits go. The answers become an ordinary sync.
90
+ */
91
+ async function askedSync(root, args, deps) {
92
+ const askDeps = { pick: (title, choices, fallback) => pickOne(title, choices, fallback, askLine), line: askLine };
93
+ const since = await askSince(root, readSyncState(root).lastRunAt, askDeps);
94
+ if (!since)
95
+ return 0;
96
+ const preview = await runSync(root, { since, dryRun: true }, deps);
97
+ report(preview, false, false);
98
+ if (preview.skipped)
99
+ return 0;
100
+ let env = deps.env ?? process.env;
101
+ if (!resolveProvider(env, loadConfig(root).config.sync)) {
102
+ const saved = await askForKey(askDeps);
103
+ if (!saved) {
104
+ out(` ${dim(KEY_HELP)}`);
105
+ return 0;
106
+ }
107
+ env = withSavedKey(env, saved);
108
+ }
109
+ let remote = '';
110
+ try {
111
+ remote = repoFromRemote(defaultGit(['remote', 'get-url', 'origin'], root)) ?? '';
112
+ }
113
+ catch {
114
+ // No remote: the edits can only go into the files.
115
+ }
116
+ const destination = await askDestination(preview.impact?.pages.length ?? 0, Boolean(remote), askDeps);
117
+ if (!destination || destination === 'stop')
118
+ return 0;
119
+ const bools = new Set([...args.bools].filter((b) => b !== 'ask'));
120
+ bools.add('yes');
121
+ if (destination === 'pr')
122
+ bools.add('pr');
123
+ return syncCommand(root, { ...args, since, bools }, { ...deps, env });
124
+ }
81
125
  function headCommit(root) {
82
126
  try {
83
127
  return defaultGit(['rev-parse', '--short', 'HEAD'], root).trim();
@@ -100,7 +144,7 @@ async function ask(question) {
100
144
  rl.close();
101
145
  }
102
146
  }
103
- function report(r, quiet) {
147
+ function report(r, quiet, next = true) {
104
148
  const affected = r.impact?.pages ?? [];
105
149
  if (quiet) {
106
150
  if (affected.length) {
@@ -138,7 +182,8 @@ function report(r, quiet) {
138
182
  }
139
183
  staleTour(r);
140
184
  out();
141
- out(` ${dim('Next:')} docugate sync`);
185
+ if (next)
186
+ out(` ${dim('Next:')} docugate sync`);
142
187
  out();
143
188
  return 0;
144
189
  }
@@ -228,12 +273,17 @@ function syncInit(root, args) {
228
273
  /* no remote yet */
229
274
  }
230
275
  const key = loaded.config.sync?.provider === 'openai' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY';
276
+ const managed = Boolean(projectToken(root));
231
277
  out();
232
- out(` ${bold('Two things to do on GitHub, once:')}`);
278
+ out(` ${bold(`${managed ? 'Three' : 'Two'} things to do on GitHub, once:`)}`);
233
279
  out(` 1. Add your AI key as a repository secret named ${gold(key)}`);
234
280
  out(` ${dim(repo ? `https://github.com/${repo}/settings/secrets/actions` : 'Settings > Secrets and variables > Actions')}`);
235
281
  out(` 2. Allow Actions to open pull requests: Settings > Actions > General > Workflow permissions`);
236
282
  out(` ${dim('and tick "Allow GitHub Actions to create and approve pull requests"')}`);
283
+ if (managed) {
284
+ out(` 3. Add a secret named ${gold('DOCUGATE_PROJECT_TOKEN')}, so the dashboard sees each run`);
285
+ out(` ${dim("Make one on the codebase's page in the dashboard: Settings, Access, Replace the token")}`);
286
+ }
237
287
  out();
238
288
  out(` Then commit the workflow. ${dim('Next:')} docugate sync hook install ${dim('(optional: a heads-up after each commit)')}`);
239
289
  out();
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://www.trydocugate.site';
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'})`,
@@ -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://www.trydocugate.site'}/${space}`;
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.1",
3
+ "version": "0.4.4",
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": {