docugate 0.3.1 → 0.4.1

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/dist/api.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ /** The tool running the CLI, from what it sets in the environment. */
2
+ export declare function detectAgent(env?: NodeJS.ProcessEnv): string;
1
3
  /** Module-level default, captured at import time (used by CLI commands). */
2
4
  export declare const BASE_URL: string;
3
5
  export declare function apiFetch(path: string, init?: RequestInit): Promise<Response>;
package/dist/api.js CHANGED
@@ -1,4 +1,26 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { basename } from 'node:path';
1
3
  import { loadCredentials, saveCredentials } from './auth.js';
4
+ const CLI_VERSION = (() => {
5
+ try {
6
+ return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version ?? '';
7
+ }
8
+ catch {
9
+ return '';
10
+ }
11
+ })();
12
+ /** The tool running the CLI, from what it sets in the environment. */
13
+ export function detectAgent(env = process.env) {
14
+ if (env.CLAUDECODE)
15
+ return 'Claude Code';
16
+ if (env.CURSOR_TRACE_ID || env.CURSOR_AGENT)
17
+ return 'Cursor';
18
+ if (env.GITHUB_COPILOT_CLI || env.COPILOT_AGENT)
19
+ return 'Copilot';
20
+ if (env.CI)
21
+ return 'CI';
22
+ return 'Terminal';
23
+ }
2
24
  /** Module-level default, captured at import time (used by CLI commands). */
3
25
  export const BASE_URL = process.env.DOCUGATE_URL ?? 'https://www.trydocugate.site';
4
26
  const ERROR_MESSAGES = {
@@ -10,6 +32,13 @@ const ERROR_MESSAGES = {
10
32
  repo_exists: 'You already have a repository with this name on GitHub',
11
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",
12
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',
13
42
  };
14
43
  function friendlyError(code) {
15
44
  return ERROR_MESSAGES[code] ?? `DocuGate said: ${code}`;
@@ -24,6 +53,11 @@ export async function apiFetch(path, init = {}) {
24
53
  };
25
54
  if (creds) {
26
55
  headers['Authorization'] = `Bearer ${creds.token}`;
56
+ // What the dashboard's Agents screen lists: this folder's name and the tool
57
+ // running the CLI. No paths, no file contents.
58
+ headers['x-docugate-project'] = basename(process.cwd());
59
+ headers['x-docugate-agent'] = detectAgent();
60
+ headers['x-docugate-version'] = CLI_VERSION;
27
61
  }
28
62
  // Re-read the env at call time so tests that set DOCUGATE_URL after import work.
29
63
  const base = process.env.DOCUGATE_URL ?? 'https://www.trydocugate.site';
package/dist/cli.js CHANGED
@@ -4,6 +4,7 @@ import { mkdirSync, writeFileSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
  import { createInterface } from 'node:readline/promises';
6
6
  import { parseArgs, UsageError } from './args.js';
7
+ import { auditCommand, connectCommand } from './project.js';
7
8
  import { banner, packageVersion } from './banner.js';
8
9
  import { check } from './check.js';
9
10
  import { COMMANDS, findCommand, renderCommandHelp, renderCompletion, renderGroupHelp, renderHelp } from './commands.js';
@@ -210,6 +211,23 @@ async function run(argv) {
210
211
  console.log(session.user ? `@${session.user.githubLogin}` : `Not signed in. Run ${hint('login')}.`);
211
212
  return;
212
213
  }
214
+ case 'connect': {
215
+ process.exitCode = await connectCommand(root, {
216
+ // --space is what it was called before a project could have several codebases.
217
+ project: one('project') ?? one('space'),
218
+ section: one('section'),
219
+ frequency: one('frequency'),
220
+ type: one('type'),
221
+ backendDir: one('backend-dir'),
222
+ noAudit: args.bools.has('no-audit'),
223
+ yes: args.bools.has('yes') || noInput,
224
+ });
225
+ return;
226
+ }
227
+ case 'audit': {
228
+ process.exitCode = await auditCommand(root, { failOn: one('fail-on'), json: args.bools.has('json') });
229
+ return;
230
+ }
213
231
  case 'developers': {
214
232
  if (noInput && !args.bools.has('yes'))
215
233
  fail('developers asks questions. Run it in a terminal, or add --yes to take every default.');
package/dist/commands.js CHANGED
@@ -163,6 +163,38 @@ export const COMMANDS = [
163
163
  examples: ['docugate logout'],
164
164
  account: 'none',
165
165
  },
166
+ {
167
+ name: 'connect',
168
+ group: 'Account',
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. ' +
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.',
173
+ flags: [
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)' },
176
+ { name: 'frequency', value: 'when', description: 'How often docs are checked: push, daily, weekly or off' },
177
+ { name: 'type', value: 'kind', description: 'frontend, backend, serverless or monorepo (default: detected)' },
178
+ { name: 'backend-dir', value: 'folder', description: 'In a monorepo, the folder that holds the backend' },
179
+ { name: 'no-audit', description: 'Do not send package names to check for vulnerabilities' },
180
+ YES,
181
+ ],
182
+ examples: ['docugate connect', 'docugate connect --project payments --type backend --yes', 'docugate connect --project payments --type frontend --section web --yes'],
183
+ account: 'required',
184
+ },
185
+ {
186
+ name: 'audit',
187
+ group: 'Checks',
188
+ summary: 'Check this project’s packages for known vulnerabilities',
189
+ description: 'Reads your lockfiles without installing or running anything, and checks public packages against the OSV.dev vulnerability database. ' +
190
+ 'Private packages stay on your machine. It only warns, unless you ask it to fail with --fail-on.',
191
+ flags: [
192
+ { name: 'fail-on', value: 'severity', description: 'Exit 1 at this severity or above: critical, high, moderate or low' },
193
+ { name: 'json', description: 'Print the result as JSON' },
194
+ ],
195
+ examples: ['docugate audit', 'docugate audit --fail-on high'],
196
+ account: 'required',
197
+ },
166
198
  {
167
199
  name: 'whoami',
168
200
  group: 'Account',
package/dist/init.d.ts CHANGED
@@ -19,6 +19,7 @@ export declare function init(root: string, opts?: {
19
19
  dir?: string;
20
20
  title?: string;
21
21
  }): InitResult;
22
+ export declare const AGENTS_FILE = "AGENTS.md";
22
23
  export type Role = 'frontend' | 'backend' | 'both';
23
24
  /** Minimal I/O surface so the flow can be driven by tests without a terminal. */
24
25
  export interface IO {
package/dist/init.js CHANGED
@@ -38,12 +38,76 @@ export function init(root, opts = {}) {
38
38
  else {
39
39
  mkdirSync(base, { recursive: true });
40
40
  writeFileSync(join(base, 'index.md'), `# ${title}\n\nWelcome. This page is the landing page of your DocuGate space.\n\n` +
41
+ `New here? Read [Start here](start-here.md) first.\n\n` +
41
42
  `Every \`.md\` file in \`${docsDir}/\` becomes a page, and every folder becomes a\n` +
42
43
  `section in the sidebar. The first \`# heading\` in a file is its title.\n`);
43
44
  created.push(`${docsDir}/index.md`);
45
+ writeFileSync(join(base, 'start-here.md'), startHere(title));
46
+ created.push(`${docsDir}/start-here.md`);
47
+ }
48
+ // For the AI tools that work in this repository: where the docs are, and how
49
+ // they are written. Never touched if the repository already has one.
50
+ if (existsSync(join(root, AGENTS_FILE))) {
51
+ kept.push(AGENTS_FILE);
52
+ }
53
+ else {
54
+ writeFileSync(join(root, AGENTS_FILE), agentsGuide(title, docsDir));
55
+ created.push(AGENTS_FILE);
44
56
  }
45
57
  return { created, kept, docsDir };
46
58
  }
59
+ export const AGENTS_FILE = 'AGENTS.md';
60
+ /** The page a new reader opens first: what is here, and how to find the rest. */
61
+ function startHere(title) {
62
+ return `# Start here
63
+
64
+ This is the documentation for ${title}. Five minutes here saves an hour later.
65
+
66
+ ## What is in these docs
67
+
68
+ - **Guides** walk through one task from start to finish.
69
+ - **Reference** pages list every option, for looking things up.
70
+ - **Runbooks** say what to do when something breaks.
71
+
72
+ Replace this list with the sections your docs actually have.
73
+
74
+ ## Finding things
75
+
76
+ - Press \`/\` to search every page.
77
+ - The sidebar follows the folders in the repository, so a page lives where you
78
+ would look for it.
79
+
80
+ ## Getting help
81
+
82
+ Say where to ask questions: a channel, an email address, or a person.
83
+ `;
84
+ }
85
+ /** AGENTS.md: what an AI coding tool should know before it writes docs here. */
86
+ function agentsGuide(title, docsDir) {
87
+ return `# AGENTS.md
88
+
89
+ Guidance for AI coding tools working in ${title}.
90
+
91
+ ## Documentation
92
+
93
+ - The docs live in \`${docsDir}/\` and are published with DocuGate. Every push
94
+ updates the site; there is no build step.
95
+ - Every \`.md\` file is a page and every folder is a sidebar section. The first
96
+ \`# heading\` in a file is its title.
97
+ - \`${docsDir}/index.md\` is the landing page. \`${docsDir}/start-here.md\` is the
98
+ first page a new reader opens.
99
+ - When you change behaviour, update the page that describes it in the same
100
+ change. Prefer editing an existing page to adding a new one.
101
+ - Run \`docugate check\` before committing: it confirms every page parses and
102
+ every link resolves.
103
+
104
+ ## Writing style
105
+
106
+ - Short sentences, plain words, one idea per paragraph.
107
+ - Name commands, files and settings exactly, in backticks.
108
+ - Lead with the task the reader came to do, then the details.
109
+ `;
110
+ }
47
111
  export function makeDefaultIO() {
48
112
  const isTTY = Boolean(process.stdin.isTTY);
49
113
  return {
@@ -0,0 +1,24 @@
1
+ export type Dependency = {
2
+ ecosystem: 'npm' | 'PyPI' | 'Go';
3
+ name: string;
4
+ version: string;
5
+ /** Listed by the project itself. Null when the lockfile cannot say. */
6
+ direct?: boolean | null;
7
+ /** For an indirect package, the direct one that brings it in, when known. */
8
+ via?: string | null;
9
+ lockfile?: string;
10
+ };
11
+ export type LockfileScan = {
12
+ dependencies: Dependency[];
13
+ files: string[];
14
+ unchecked: string[];
15
+ };
16
+ type Parser = (text: string, ownOwner?: string) => Dependency[];
17
+ export declare const parsePackageLock: Parser;
18
+ export declare const parsePnpmLock: Parser;
19
+ export declare const parseYarnLock: Parser;
20
+ export declare const parseRequirements: Parser;
21
+ export declare const parseGoMod: Parser;
22
+ /** Every lockfile in the project, monorepo folders included, within a depth limit. */
23
+ export declare function scanLockfiles(root: string, ownOwner?: string): LockfileScan;
24
+ export {};
@@ -0,0 +1,203 @@
1
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { join, relative } from 'node:path';
3
+ const PUBLIC_NPM = /^https:\/\/registry\.(npmjs\.org|yarnpkg\.com)\//;
4
+ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'vendor', '.next', '.venv', 'venv', '__pycache__', 'target', 'coverage']);
5
+ const MAX_DEPTH = 4;
6
+ const MAX_DIRS = 2000;
7
+ export const parsePackageLock = (text) => {
8
+ const lock = JSON.parse(text);
9
+ const out = [];
10
+ if (lock.packages) {
11
+ for (const [path, entry] of Object.entries(lock.packages)) {
12
+ const at = path.lastIndexOf('node_modules/');
13
+ if (at < 0 || entry.link || !entry.version || !entry.resolved || !PUBLIC_NPM.test(entry.resolved))
14
+ continue;
15
+ out.push({ ecosystem: 'npm', name: path.slice(at + 'node_modules/'.length), version: entry.version });
16
+ }
17
+ addViaFromLock(lock.packages, out);
18
+ }
19
+ else if (lock.dependencies) {
20
+ collectV1(lock.dependencies, out);
21
+ }
22
+ else {
23
+ throw new Error('no packages listed');
24
+ }
25
+ return out;
26
+ };
27
+ /** Walks down from each direct package, so an indirect one can name who brought it in. */
28
+ function addViaFromLock(packages, out) {
29
+ const root = packages[''];
30
+ const direct = Object.keys({ ...root?.dependencies, ...root?.devDependencies });
31
+ const via = new Map();
32
+ for (const top of direct) {
33
+ const queue = [top];
34
+ const seen = new Set(queue);
35
+ while (queue.length) {
36
+ const name = queue.shift();
37
+ for (const child of Object.keys(packages[`node_modules/${name}`]?.dependencies ?? {})) {
38
+ if (seen.has(child))
39
+ continue;
40
+ seen.add(child);
41
+ if (!via.has(child) && !direct.includes(child))
42
+ via.set(child, top);
43
+ queue.push(child);
44
+ }
45
+ }
46
+ }
47
+ for (const dep of out)
48
+ if (via.has(dep.name))
49
+ dep.via = via.get(dep.name);
50
+ }
51
+ function collectV1(deps, out) {
52
+ for (const [name, entry] of Object.entries(deps)) {
53
+ if (entry.version && entry.resolved && PUBLIC_NPM.test(entry.resolved))
54
+ out.push({ ecosystem: 'npm', name, version: entry.version });
55
+ if (entry.dependencies)
56
+ collectV1(entry.dependencies, out);
57
+ }
58
+ }
59
+ export const parsePnpmLock = (text) => {
60
+ const out = [];
61
+ const lines = text.split(/\r?\n/);
62
+ const start = lines.findIndex((l) => /^packages:\s*$/.test(l));
63
+ if (start < 0)
64
+ throw new Error('no packages section');
65
+ for (let i = start + 1; i < lines.length; i++) {
66
+ const line = lines[i];
67
+ if (/^\S/.test(line))
68
+ break;
69
+ const key = line.match(/^ {2}'?\/?((?:@[^/]+\/)?[^@/\s']+)[@/](\d[^:(\s']*)[^:]*'?:/);
70
+ if (!key)
71
+ continue;
72
+ // The next line says where it came from. A tarball means not the registry.
73
+ if (/tarball:|type: git|directory:/.test(lines[i + 1] ?? ''))
74
+ continue;
75
+ out.push({ ecosystem: 'npm', name: key[1], version: key[2] });
76
+ }
77
+ return out;
78
+ };
79
+ export const parseYarnLock = (text) => {
80
+ const out = [];
81
+ for (const block of text.split(/\r?\n\r?\n/)) {
82
+ const head = block.split(/\r?\n/).find((l) => /^\S.*:\s*$/.test(l) && !l.startsWith('#') && !l.startsWith('__metadata'));
83
+ if (!head)
84
+ continue;
85
+ const name = head.replace(/^"/, '').match(/^(@?[^@]+)@/)?.[1];
86
+ const version = block.match(/^\s+version:?\s+"?([^"\s]+)"?/m)?.[1];
87
+ if (!name || !version)
88
+ continue;
89
+ const resolved = block.match(/^\s+resolved\s+"([^"]+)"/m)?.[1];
90
+ const berry = block.match(/^\s+resolution:\s+"[^"]*@(npm):/m);
91
+ if (resolved ? !PUBLIC_NPM.test(resolved) : !berry)
92
+ continue;
93
+ out.push({ ecosystem: 'npm', name, version });
94
+ }
95
+ return out;
96
+ };
97
+ export const parseRequirements = (text) => {
98
+ // A custom index means these names may be private, so none of them leave.
99
+ if (/^\s*(--index-url|-i\s|--extra-index-url|--find-links)/m.test(text))
100
+ throw new Error('uses a custom package index');
101
+ const out = [];
102
+ for (const raw of text.split(/\r?\n/)) {
103
+ const m = raw.split('#')[0].trim().match(/^([A-Za-z0-9][\w.-]*)(?:\[[^\]]*\])?\s*==\s*([\w.!+-]+)/);
104
+ if (m)
105
+ out.push({ ecosystem: 'PyPI', name: m[1], version: m[2], direct: true });
106
+ }
107
+ return out;
108
+ };
109
+ export const parseGoMod = (text, ownOwner) => {
110
+ const out = [];
111
+ const replaced = new Set([...text.matchAll(/^\s*(?:replace\s+)?(\S+)(?:\s+v\S+)?\s+=>/gm)].map((m) => m[1]));
112
+ const add = (mod, version, direct) => {
113
+ if (replaced.has(mod))
114
+ return;
115
+ // Modules of the same GitHub account are as likely private as not.
116
+ if (ownOwner && mod.toLowerCase().startsWith(`github.com/${ownOwner.toLowerCase()}/`))
117
+ return;
118
+ out.push({ ecosystem: 'Go', name: mod, version: version.replace(/^v/, ''), direct });
119
+ };
120
+ let inBlock = false;
121
+ for (const raw of text.split(/\r?\n/)) {
122
+ const line = raw.split('//')[0].trim();
123
+ if (/^require\s*\($/.test(line))
124
+ inBlock = true;
125
+ else if (line === ')')
126
+ inBlock = false;
127
+ else {
128
+ const m = inBlock ? line.match(/^(\S+)\s+(v\S+)$/) : line.match(/^require\s+(\S+)\s+(v\S+)$/);
129
+ if (m)
130
+ add(m[1], m[2], !/\/\/\s*indirect/.test(raw));
131
+ }
132
+ }
133
+ return out;
134
+ };
135
+ const PARSERS = {
136
+ 'package-lock.json': parsePackageLock,
137
+ 'pnpm-lock.yaml': parsePnpmLock,
138
+ 'yarn.lock': parseYarnLock,
139
+ 'requirements.txt': parseRequirements,
140
+ 'go.mod': parseGoMod,
141
+ };
142
+ /**
143
+ * The packages this folder's package.json asks for. Null when it cannot say: none
144
+ * there, or a workspace root, whose real lists are in each package's own file.
145
+ */
146
+ function directNames(dir) {
147
+ try {
148
+ const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
149
+ if (pkg.workspaces)
150
+ return null;
151
+ return new Set(Object.keys({ ...pkg.dependencies, ...pkg.devDependencies, ...pkg.optionalDependencies }));
152
+ }
153
+ catch {
154
+ return null;
155
+ }
156
+ }
157
+ /** Every lockfile in the project, monorepo folders included, within a depth limit. */
158
+ export function scanLockfiles(root, ownOwner) {
159
+ const seen = new Map();
160
+ const files = [];
161
+ const unchecked = [];
162
+ let dirs = 0;
163
+ const visit = (dir, depth) => {
164
+ if (++dirs > MAX_DIRS)
165
+ return;
166
+ let names;
167
+ try {
168
+ names = readdirSync(dir);
169
+ }
170
+ catch {
171
+ return;
172
+ }
173
+ for (const name of names) {
174
+ const path = join(dir, name);
175
+ const parser = PARSERS[name];
176
+ if (parser) {
177
+ const shown = relative(root, path).replace(/\\/g, '/');
178
+ try {
179
+ const own = directNames(dir);
180
+ for (const dep of parser(readFileSync(path, 'utf8'), ownOwner)) {
181
+ const direct = dep.direct ?? (dep.ecosystem === 'npm' && own ? own.has(dep.name) : null);
182
+ seen.set(`${dep.ecosystem}:${dep.name}@${dep.version}`, { ...dep, direct, lockfile: shown });
183
+ }
184
+ files.push(shown);
185
+ }
186
+ catch {
187
+ unchecked.push(shown);
188
+ }
189
+ }
190
+ else if (depth < MAX_DEPTH && !SKIP_DIRS.has(name) && !name.startsWith('.')) {
191
+ try {
192
+ if (statSync(path).isDirectory())
193
+ visit(path, depth + 1);
194
+ }
195
+ catch {
196
+ // Gone or unreadable: not a lockfile we can speak for.
197
+ }
198
+ }
199
+ }
200
+ };
201
+ visit(root, 0);
202
+ return { dependencies: [...seen.values()], files, unchecked };
203
+ }
@@ -0,0 +1,119 @@
1
+ export declare const PROJECT_TYPES: readonly ["frontend", "backend", "serverless", "monorepo"];
2
+ export type ProjectType = (typeof PROJECT_TYPES)[number];
3
+ export declare const FREQUENCIES: readonly ["push", "daily", "weekly", "off"];
4
+ export type Frequency = (typeof FREQUENCIES)[number];
5
+ export type ProjectSettings = {
6
+ name: string;
7
+ type: ProjectType;
8
+ frequency: Frequency;
9
+ audit: boolean;
10
+ due: boolean;
11
+ space: {
12
+ owner: string;
13
+ slug: string;
14
+ name: string;
15
+ } | null;
16
+ };
17
+ export type Finding = {
18
+ id: string;
19
+ package: string;
20
+ version: string;
21
+ severity: 'critical' | 'high' | 'moderate' | 'low' | 'unknown';
22
+ summary: string;
23
+ fixedIn: string | null;
24
+ url: string;
25
+ };
26
+ /** `owner/name` from the git remote, or the folder's name without one. */
27
+ export declare function detectRepo(root: string): string;
28
+ /** The token for this codebase: CI's secret first, then what `connect` saved for this folder, then for its repository. */
29
+ export declare function projectToken(root: string, env?: NodeJS.ProcessEnv): string | null;
30
+ /** A suggestion only. The person confirms it, and the dashboard can change it. */
31
+ export declare function detectType(root: string): ProjectType;
32
+ /** Where the backend lives in a monorepo, which is where the key-check package installs. */
33
+ export declare function detectBackendDir(root: string): string;
34
+ /** This project's settings, or null: unlinked, offline, or a revoked token. */
35
+ export declare function readSettings(token: string): Promise<ProjectSettings | null>;
36
+ export type RunReport = {
37
+ status: 'updated' | 'no_changes' | 'skipped' | 'failed';
38
+ pages?: number;
39
+ prUrl?: string;
40
+ commit?: string;
41
+ trigger?: string;
42
+ reason?: string;
43
+ };
44
+ /** Tells the dashboard what a run did. A failure here is not the run's failure. */
45
+ export declare function reportRun(token: string, run: RunReport): Promise<void>;
46
+ export type AuditAnswer = {
47
+ checked: boolean;
48
+ reason?: string;
49
+ findings: Finding[];
50
+ files: string[];
51
+ unchecked: string[];
52
+ packages: number;
53
+ };
54
+ export declare function runAudit(root: string, token: string): Promise<AuditAnswer>;
55
+ /** Critical and high by name with their fix; the rest as a count. */
56
+ export declare function printFindings(answer: AuditAnswer, log?: (line: string) => void): void;
57
+ export type AuditArgs = {
58
+ failOn?: string;
59
+ json: boolean;
60
+ };
61
+ /** `docugate audit`. Returns the exit code. */
62
+ export declare function auditCommand(root: string, args: AuditArgs, env?: NodeJS.ProcessEnv): Promise<number>;
63
+ export type ConnectArgs = {
64
+ /** The project to join: its slug, `owner/slug`, or name. */
65
+ project?: string;
66
+ frequency?: string;
67
+ type?: string;
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;
71
+ noAudit: boolean;
72
+ yes: boolean;
73
+ };
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
+ */
119
+ export declare function connectCommand(root: string, args: ConnectArgs): Promise<number>;