docugate 0.2.8 → 0.3.0

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.
Files changed (82) hide show
  1. package/README.md +200 -57
  2. package/dist/api.d.ts +44 -0
  3. package/dist/api.js +11 -5
  4. package/dist/args.d.ts +29 -0
  5. package/dist/args.js +121 -0
  6. package/dist/auth.d.ts +21 -0
  7. package/dist/banner.d.ts +13 -0
  8. package/dist/banner.js +62 -0
  9. package/dist/check.d.ts +22 -0
  10. package/dist/check.js +4 -3
  11. package/dist/cli.d.ts +2 -0
  12. package/dist/cli.js +214 -165
  13. package/dist/commands.d.ts +35 -0
  14. package/dist/commands.js +293 -0
  15. package/dist/config.d.ts +22 -0
  16. package/dist/config.js +43 -3
  17. package/dist/developers.d.ts +35 -0
  18. package/dist/developers.js +439 -0
  19. package/dist/init.d.ts +130 -0
  20. package/dist/init.js +102 -36
  21. package/dist/keys/core.d.ts +108 -0
  22. package/dist/keys/core.js +142 -0
  23. package/dist/keys/express.d.ts +21 -0
  24. package/dist/keys/express.js +27 -0
  25. package/dist/keys/fastify.d.ts +21 -0
  26. package/dist/keys/fastify.js +21 -0
  27. package/dist/keys/format.d.ts +11 -0
  28. package/dist/keys/format.js +46 -0
  29. package/dist/keys/hono.d.ts +19 -0
  30. package/dist/keys/hono.js +21 -0
  31. package/dist/keys/index.d.ts +13 -0
  32. package/dist/keys/index.js +12 -0
  33. package/dist/logo-pixels.d.ts +4 -0
  34. package/dist/logo-pixels.js +35 -0
  35. package/dist/logo.d.ts +19 -0
  36. package/dist/logo.js +68 -0
  37. package/dist/menu.d.ts +16 -0
  38. package/dist/menu.js +157 -0
  39. package/dist/openapi.d.ts +38 -0
  40. package/dist/openapi.js +2 -1
  41. package/dist/rules.d.ts +69 -0
  42. package/dist/rules.js +2 -1
  43. package/dist/select.d.ts +75 -0
  44. package/dist/select.js +215 -0
  45. package/dist/session.d.ts +46 -0
  46. package/dist/session.js +54 -0
  47. package/dist/spinner.d.ts +12 -0
  48. package/dist/spinner.js +12 -12
  49. package/dist/status.d.ts +46 -0
  50. package/dist/status.js +166 -0
  51. package/dist/sync/ai.d.ts +44 -0
  52. package/dist/sync/ai.js +147 -0
  53. package/dist/sync/apply.d.ts +25 -0
  54. package/dist/sync/apply.js +104 -0
  55. package/dist/sync/changes.d.ts +58 -0
  56. package/dist/sync/changes.js +167 -0
  57. package/dist/sync/command.d.ts +12 -0
  58. package/dist/sync/command.js +236 -0
  59. package/dist/sync/guard.d.ts +15 -0
  60. package/dist/sync/guard.js +21 -0
  61. package/dist/sync/hook.d.ts +10 -0
  62. package/dist/sync/hook.js +62 -0
  63. package/dist/sync/impact.d.ts +23 -0
  64. package/dist/sync/impact.js +93 -0
  65. package/dist/sync/pr.d.ts +37 -0
  66. package/dist/sync/pr.js +106 -0
  67. package/dist/sync/run.d.ts +50 -0
  68. package/dist/sync/run.js +124 -0
  69. package/dist/sync/state.d.ts +8 -0
  70. package/dist/sync/state.js +22 -0
  71. package/dist/sync/workflow.d.ts +20 -0
  72. package/dist/sync/workflow.js +84 -0
  73. package/dist/text.d.ts +11 -0
  74. package/dist/text.js +16 -0
  75. package/dist/tour-init.d.ts +61 -0
  76. package/dist/tour-install.d.ts +38 -0
  77. package/dist/tour-install.js +58 -0
  78. package/dist/tour-server.d.ts +21 -0
  79. package/dist/tour.d.ts +60 -0
  80. package/dist/ui.d.ts +29 -0
  81. package/dist/ui.js +63 -0
  82. package/package.json +67 -47
@@ -0,0 +1,35 @@
1
+ export type Flag = {
2
+ name: string;
3
+ /** Present when the flag takes a value; the text is the placeholder shown. */
4
+ value?: string;
5
+ description: string;
6
+ };
7
+ export type Group = 'Docs sync' | 'Setup' | 'Checks' | 'Walkthrough' | 'Account' | 'Help';
8
+ export type Command = {
9
+ /** One to three words: `check`, `sync init`, `sync hook install`. */
10
+ name: string;
11
+ group: Group;
12
+ summary: string;
13
+ description?: string;
14
+ /** A trailing positional argument, like `<file>`. */
15
+ positional?: string;
16
+ flags: Flag[];
17
+ examples: string[];
18
+ /** Whether the command works with the DocuGate account: not at all, if you have one, or only with one. */
19
+ account: 'none' | 'optional' | 'required';
20
+ };
21
+ export declare const COMMANDS: Command[];
22
+ export declare const GLOBAL_FLAGS: Flag[];
23
+ export declare const GROUP_ORDER: Group[];
24
+ export declare const findCommand: (name: string) => Command | undefined;
25
+ /** Every flag that takes a value, anywhere: needed to read the line before the command is known. */
26
+ export declare const VALUE_FLAG_NAMES: Set<string>;
27
+ /** The flags a command accepts, with the ones every command accepts. */
28
+ export declare const flagsOf: (c: Command) => Flag[];
29
+ /** The page `docugate --help` prints. */
30
+ export declare function renderHelp(site: string): string;
31
+ /** The page `docugate help <command>` and `docugate <command> --help` print. */
32
+ export declare function renderCommandHelp(c: Command, command?: string): string;
33
+ /** What `docugate tour` and `docugate sync hook` list when given no sub-command. */
34
+ export declare function renderGroupHelp(prefix: string, command?: string): string;
35
+ export declare function renderCompletion(shell: string): string | null;
@@ -0,0 +1,293 @@
1
+ import { bold, dim, gold } from './ui.js';
2
+ const YES = { name: 'yes', description: 'Accept every default without asking' };
3
+ export const COMMANDS = [
4
+ {
5
+ name: 'sync',
6
+ group: 'Docs sync',
7
+ summary: 'Update the docs from what changed in the code',
8
+ description: 'Reads the git changes since the last sync (or a time you give), finds the docs pages they affect, asks an AI model for the edits, and writes them. ' +
9
+ 'With --pr it opens a pull request instead, so nothing changes without a review. It uses your own AI key and never sends anything through DocuGate.',
10
+ flags: [
11
+ { name: 'since', value: 'when', description: 'Look back to a commit, branch, or a time like 7d (default: the last sync)' },
12
+ { name: 'dry-run', description: 'Only say which pages are affected: no AI, nothing written' },
13
+ { name: 'pr', description: 'Push the edits and open a pull request' },
14
+ { name: 'max-files', value: 'n', description: 'Most pages one run may change (default: 10)' },
15
+ { name: 'model', value: 'name', description: 'Model to use (default: sync.model in docugate.json)' },
16
+ { name: 'quiet', description: 'Print one line only when pages are out of date (for the git hook)' },
17
+ YES,
18
+ ],
19
+ examples: ['docugate sync --dry-run', 'docugate sync --since 7d', 'docugate sync --pr --yes'],
20
+ account: 'none',
21
+ },
22
+ {
23
+ name: 'sync init',
24
+ group: 'Docs sync',
25
+ summary: 'Add the GitHub Action that runs sync on a schedule or on every push',
26
+ description: 'How often comes from "freshness" in docugate.json: push, daily, weekly or off.',
27
+ flags: [
28
+ { name: 'freshness', value: 'when', description: 'push, daily, weekly or off (default: weekly)' },
29
+ { name: 'force', description: 'Rewrite the workflow if it already exists' },
30
+ ],
31
+ examples: ['docugate sync init', 'docugate sync init --freshness daily'],
32
+ account: 'none',
33
+ },
34
+ {
35
+ name: 'sync hook install',
36
+ group: 'Docs sync',
37
+ summary: 'Tell me after each commit which pages the code left behind',
38
+ description: 'A git hook that runs `sync --dry-run` after every commit. It sends nothing and changes nothing.',
39
+ flags: [],
40
+ examples: ['docugate sync hook install'],
41
+ account: 'none',
42
+ },
43
+ {
44
+ name: 'sync hook remove',
45
+ group: 'Docs sync',
46
+ summary: 'Remove the post-commit hook',
47
+ flags: [],
48
+ examples: ['docugate sync hook remove'],
49
+ account: 'none',
50
+ },
51
+ {
52
+ name: 'status',
53
+ group: 'Docs sync',
54
+ summary: 'Where things stand: account, docs, sync and tour, and what to do next',
55
+ flags: [{ name: 'json', description: 'Print it as JSON, for scripts' }],
56
+ examples: ['docugate status', 'docugate status --json'],
57
+ account: 'optional',
58
+ },
59
+ {
60
+ name: 'init',
61
+ group: 'Setup',
62
+ summary: 'Set up this repository for DocuGate (interactive)',
63
+ flags: [
64
+ { name: 'dir', value: 'folder', description: 'Docs folder (default: docs)' },
65
+ { name: 'title', value: 'text', description: "Space title (default: this folder's name)" },
66
+ { name: 'role', value: 'role', description: 'Repository role: frontend, backend, or both' },
67
+ { name: 'no-serve', description: 'Do not start the tour server at the end' },
68
+ YES,
69
+ ],
70
+ examples: ['docugate init', 'docugate init --dir documentation --yes'],
71
+ account: 'optional',
72
+ },
73
+ {
74
+ name: 'developers',
75
+ group: 'Setup',
76
+ summary: 'Set up a developer space: API reference, guides and keys (interactive)',
77
+ description: 'Finds the OpenAPI file, writes the first developer pages where there are none, and creates or links a public space for them. On Pro it also claims a subdomain, sets how the API console sends requests, switches on API keys and prints the verify secret with the lines your backend needs.',
78
+ flags: [{ name: 'dir', value: 'folder', description: 'Folder for the developer docs (default: docs/developers)' }, YES],
79
+ examples: ['docugate developers', 'docugate developers --dir docs/api'],
80
+ account: 'required',
81
+ },
82
+ {
83
+ name: 'check',
84
+ group: 'Checks',
85
+ summary: 'Check the docs the way DocuGate will read them',
86
+ flags: [
87
+ { name: 'dir', value: 'folder', description: "Check this folder instead of docugate.json's docsDir" },
88
+ { name: 'strict', description: 'Treat warnings as errors' },
89
+ ],
90
+ examples: ['docugate check', 'docugate check --strict'],
91
+ account: 'none',
92
+ },
93
+ {
94
+ name: 'openapi',
95
+ group: 'Checks',
96
+ summary: "Prepare an OpenAPI spec for DocuGate's API reference",
97
+ flags: [
98
+ { name: 'from', value: 'file|url', description: 'The spec to read (or api.generate.from in docugate.json)' },
99
+ { name: 'out', value: 'file', description: 'Where to write it (default: openapi.json)' },
100
+ { name: 'include', value: 'glob', description: 'Keep only paths that match; repeatable' },
101
+ { name: 'exclude', value: 'glob', description: 'Drop paths that match; repeatable' },
102
+ { name: 'redact', value: 'name', description: 'Remove a property from every schema and example; repeatable' },
103
+ { name: 'check', description: 'Fail if the file is out of date, instead of writing it' },
104
+ ],
105
+ examples: ['docugate openapi --from http://localhost:3000/openapi.json'],
106
+ account: 'none',
107
+ },
108
+ {
109
+ name: 'tour init',
110
+ group: 'Walkthrough',
111
+ summary: 'Write the first tour of this app with IBM Bob',
112
+ flags: [
113
+ { name: 'max-cost', value: 'n', description: 'Most Bobcoins the run may spend (default: 3)' },
114
+ { name: 'team-id', value: 'id', description: 'Bob team to run under, for API keys that need one' },
115
+ ],
116
+ examples: ['docugate tour init'],
117
+ account: 'none',
118
+ },
119
+ {
120
+ name: 'tour install',
121
+ group: 'Walkthrough',
122
+ summary: 'Add the pill loader to your app',
123
+ flags: [
124
+ { name: 'port', value: 'n', description: 'Port the pill server listens on (default: 4178)' },
125
+ { name: 'remove', description: 'Remove the pill snippet instead of adding it' },
126
+ ],
127
+ examples: ['docugate tour install', 'docugate tour install --remove'],
128
+ account: 'none',
129
+ },
130
+ {
131
+ name: 'tour serve',
132
+ group: 'Walkthrough',
133
+ summary: 'Start a local server that serves tour data for the pill',
134
+ flags: [
135
+ { name: 'port', value: 'n', description: 'Port to listen on (default: 4178)' },
136
+ { name: 'background', description: 'Start it detached and return at once (used by `npm run dev`)' },
137
+ ],
138
+ examples: ['docugate tour serve'],
139
+ account: 'none',
140
+ },
141
+ {
142
+ name: 'tour export',
143
+ group: 'Walkthrough',
144
+ summary: 'Write every tour screen to a single JSON file',
145
+ positional: '<file>',
146
+ flags: [],
147
+ examples: ['docugate tour export tours.json'],
148
+ account: 'none',
149
+ },
150
+ {
151
+ name: 'login',
152
+ group: 'Account',
153
+ summary: 'Sign in to DocuGate with GitHub',
154
+ flags: [],
155
+ examples: ['docugate login'],
156
+ account: 'none',
157
+ },
158
+ {
159
+ name: 'logout',
160
+ group: 'Account',
161
+ summary: 'Sign out and delete saved credentials',
162
+ flags: [],
163
+ examples: ['docugate logout'],
164
+ account: 'none',
165
+ },
166
+ {
167
+ name: 'whoami',
168
+ group: 'Account',
169
+ summary: 'Print the currently signed-in user',
170
+ flags: [],
171
+ examples: ['docugate whoami'],
172
+ account: 'none',
173
+ },
174
+ {
175
+ name: 'help',
176
+ group: 'Help',
177
+ summary: 'Show help for everything, or for one command',
178
+ positional: '[command]',
179
+ flags: [],
180
+ examples: ['docugate help sync', 'docugate sync --help'],
181
+ account: 'none',
182
+ },
183
+ {
184
+ name: 'completion',
185
+ group: 'Help',
186
+ summary: 'Print a shell completion script (bash, zsh or powershell)',
187
+ positional: '<shell>',
188
+ flags: [],
189
+ examples: ['docugate completion powershell | Out-String | Invoke-Expression'],
190
+ account: 'none',
191
+ },
192
+ ];
193
+ export const GLOBAL_FLAGS = [
194
+ { name: 'help', description: 'Show help (also -h)' },
195
+ { name: 'version', description: 'Print the version (also -v)' },
196
+ { name: 'no-input', description: 'Never ask a question; fail with the fix instead (always so in CI)' },
197
+ ];
198
+ export const GROUP_ORDER = ['Docs sync', 'Setup', 'Checks', 'Walkthrough', 'Account', 'Help'];
199
+ export const findCommand = (name) => COMMANDS.find((c) => c.name === name);
200
+ /** Every flag that takes a value, anywhere: needed to read the line before the command is known. */
201
+ export const VALUE_FLAG_NAMES = new Set(COMMANDS.flatMap((c) => c.flags.filter((f) => f.value).map((f) => f.name)));
202
+ /** The flags a command accepts, with the ones every command accepts. */
203
+ export const flagsOf = (c) => [...c.flags, ...GLOBAL_FLAGS];
204
+ // ── Rendering ───────────────────────────────────────────────────────────
205
+ const pad = (text, width) => text + ' '.repeat(Math.max(2, width - text.length));
206
+ /** The page `docugate --help` prints. */
207
+ export function renderHelp(site) {
208
+ const lines = [`docugate: keep your docs in step with your code`, '', 'Usage', ' docugate <command> [options]', ' docugate (in a terminal: an interactive menu)', ''];
209
+ const width = Math.max(...COMMANDS.map((c) => c.name.length)) + 2;
210
+ for (const group of GROUP_ORDER) {
211
+ lines.push(group);
212
+ for (const c of COMMANDS.filter((x) => x.group === group))
213
+ lines.push(` ${pad(c.name + (c.positional ? ` ${c.positional}` : ''), width + 8)}${c.summary}`);
214
+ lines.push('');
215
+ }
216
+ lines.push('Run `docugate help <command>` for a command\'s options and examples.', 'Run it from the root of your repository.', site, '');
217
+ return lines.join('\n');
218
+ }
219
+ /** The page `docugate help <command>` and `docugate <command> --help` print. */
220
+ export function renderCommandHelp(c, command = 'docugate') {
221
+ const lines = [`${bold(`${command} ${c.name}`)} ${dim(c.summary)}`, ''];
222
+ if (c.description)
223
+ lines.push(c.description, '');
224
+ lines.push('Usage', ` ${command} ${c.name}${c.positional ? ` ${c.positional}` : ''}${c.flags.length ? ' [options]' : ''}`, '');
225
+ const all = flagsOf(c);
226
+ const width = Math.max(...all.map((f) => `--${f.name}${f.value ? ` <${f.value}>` : ''}`.length)) + 2;
227
+ lines.push('Options');
228
+ for (const f of all)
229
+ lines.push(` ${pad(`--${f.name}${f.value ? ` <${f.value}>` : ''}`, width)}${f.description}`);
230
+ if (c.examples.length) {
231
+ lines.push('', 'Examples');
232
+ for (const e of c.examples)
233
+ lines.push(` ${gold(e.replace(/^docugate/, command))}`);
234
+ }
235
+ lines.push('');
236
+ return lines.join('\n');
237
+ }
238
+ /** What `docugate tour` and `docugate sync hook` list when given no sub-command. */
239
+ export function renderGroupHelp(prefix, command = 'docugate') {
240
+ const subs = COMMANDS.filter((c) => c.name.startsWith(`${prefix} `));
241
+ const width = Math.max(...subs.map((c) => c.name.length)) + 2;
242
+ return [`${bold(`${command} ${prefix}`)}`, '', ...subs.map((c) => ` ${pad(c.name, width)}${c.summary}`), ''].join('\n');
243
+ }
244
+ // ── Completions ─────────────────────────────────────────────────────────
245
+ export function renderCompletion(shell) {
246
+ const names = COMMANDS.map((c) => c.name);
247
+ const words = [...new Set(names.map((n) => n.split(' ')[0]))];
248
+ const flagsFor = (c) => flagsOf(c).map((f) => `--${f.name}`);
249
+ if (shell === 'bash') {
250
+ // For each first word: the sub-commands and flags that can follow it.
251
+ const cases = words.map((w) => {
252
+ const group = COMMANDS.filter((c) => c.name.split(' ')[0] === w);
253
+ const subs = [...new Set(group.filter((c) => c.name.includes(' ')).map((c) => c.name.split(' ')[1]))];
254
+ const flags = [...new Set(group.flatMap(flagsFor))];
255
+ return ` ${w}) COMPREPLY=($(compgen -W "${[...subs, ...flags].join(' ')}" -- "$cur"));;`;
256
+ });
257
+ return [
258
+ '_docugate() {',
259
+ ' local cur="${COMP_WORDS[COMP_CWORD]}"',
260
+ ` if [ "$COMP_CWORD" -eq 1 ]; then COMPREPLY=($(compgen -W "${words.join(' ')}" -- "$cur")); return; fi`,
261
+ ' case "${COMP_WORDS[1]}" in',
262
+ ...cases,
263
+ ' esac',
264
+ '}',
265
+ 'complete -F _docugate docugate',
266
+ '',
267
+ ].join('\n');
268
+ }
269
+ if (shell === 'zsh') {
270
+ return [
271
+ '#compdef docugate',
272
+ '_docugate() {',
273
+ ' local -a cmds',
274
+ ` cmds=(${words.map((w) => `'${w}:${(findCommand(w)?.summary ?? `${w} commands`).replace(/'/g, '')}'`).join(' ')})`,
275
+ ' if (( CURRENT == 2 )); then _describe command cmds; else _files; fi',
276
+ '}',
277
+ '_docugate "$@"',
278
+ '',
279
+ ].join('\n');
280
+ }
281
+ if (shell === 'powershell' || shell === 'pwsh') {
282
+ return [
283
+ "Register-ArgumentCompleter -Native -CommandName docugate -ScriptBlock {",
284
+ ' param($wordToComplete, $commandAst, $cursorPosition)',
285
+ ` $words = @(${words.map((w) => `'${w}'`).join(', ')})`,
286
+ ' $tokens = $commandAst.CommandElements | Select-Object -Skip 1 | ForEach-Object { $_.ToString() }',
287
+ ' if ($tokens.Count -le 1) { $words | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", $_) } }',
288
+ '}',
289
+ '',
290
+ ].join('\n');
291
+ }
292
+ return null;
293
+ }
@@ -0,0 +1,22 @@
1
+ import type { DocugateConfig } from './rules.js';
2
+ export type Issue = {
3
+ file?: string;
4
+ message: string;
5
+ };
6
+ export type LoadedConfig = {
7
+ /** Whether docugate.json exists at all. It is optional. */
8
+ found: boolean;
9
+ config: DocugateConfig;
10
+ docsDir: string;
11
+ errors: Issue[];
12
+ warnings: Issue[];
13
+ };
14
+ /**
15
+ * Reads docugate.json from the repository root, where DocuGate looks for it.
16
+ *
17
+ * The server treats an unreadable file as if it were not there and falls back
18
+ * to `docs/`, silently. That is the right thing for a reader and the wrong
19
+ * thing for the person who wrote the file, so here it is an error.
20
+ */
21
+ export declare function loadConfig(root: string): LoadedConfig;
22
+ export declare const CONFIG_FILE = "docugate.json";
package/dist/config.js CHANGED
@@ -1,7 +1,9 @@
1
- import { existsSync, readFileSync } from 'node:fs';
1
+ import { existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { KNOWN_KEYS } from './rules.js';
3
+ import { FRESHNESS_VALUES, KNOWN_KEYS } from './rules.js';
4
+ import { readText } from './text.js';
4
5
  const FILE = 'docugate.json';
6
+ const SYNC_KEYS = new Set(['provider', 'model', 'include', 'exclude', 'map', 'maxFiles', 'digest']);
5
7
  /**
6
8
  * Reads docugate.json from the repository root, where DocuGate looks for it.
7
9
  *
@@ -18,7 +20,7 @@ export function loadConfig(root) {
18
20
  }
19
21
  let parsed;
20
22
  try {
21
- parsed = JSON.parse(readFileSync(path, 'utf8'));
23
+ parsed = JSON.parse(readText(path));
22
24
  }
23
25
  catch (error) {
24
26
  errors.push({
@@ -47,7 +49,45 @@ export function loadConfig(root) {
47
49
  (!Array.isArray(config.sidebar) || config.sidebar.some((s) => typeof s !== 'string'))) {
48
50
  errors.push({ file: FILE, message: '"sidebar" must be a list of file and folder names.' });
49
51
  }
52
+ if (config.freshness !== undefined && !FRESHNESS_VALUES.includes(config.freshness)) {
53
+ errors.push({ file: FILE, message: `"freshness" must be one of ${FRESHNESS_VALUES.join(', ')}.` });
54
+ }
55
+ if (config.sync !== undefined)
56
+ errors.push(...validateSync(config.sync));
50
57
  const docsDir = typeof config.docsDir === 'string' ? config.docsDir.replace(/^\/|\/$/g, '') || 'docs' : 'docs';
51
58
  return { found: true, config: config, docsDir, errors, warnings };
52
59
  }
60
+ const isStringList = (v) => Array.isArray(v) && v.every((s) => typeof s === 'string');
61
+ function validateSync(sync) {
62
+ const bad = (message) => ({ file: FILE, message });
63
+ if (!sync || typeof sync !== 'object' || Array.isArray(sync))
64
+ return [bad('"sync" must be an object.')];
65
+ const s = sync;
66
+ const errors = [];
67
+ for (const key of Object.keys(s)) {
68
+ if (!SYNC_KEYS.has(key))
69
+ errors.push(bad(`"sync" has an unknown key "${key}".`));
70
+ }
71
+ if (s.provider !== undefined && s.provider !== 'anthropic' && s.provider !== 'openai') {
72
+ errors.push(bad('"sync.provider" must be "anthropic" or "openai".'));
73
+ }
74
+ if (s.model !== undefined && typeof s.model !== 'string')
75
+ errors.push(bad('"sync.model" must be a string.'));
76
+ for (const key of ['include', 'exclude']) {
77
+ if (s[key] !== undefined && !isStringList(s[key]))
78
+ errors.push(bad(`"sync.${key}" must be a list of path globs.`));
79
+ }
80
+ if (s.map !== undefined) {
81
+ const map = s.map;
82
+ const ok = map && typeof map === 'object' && !Array.isArray(map) && Object.values(map).every((v) => isStringList(v));
83
+ if (!ok)
84
+ errors.push(bad('"sync.map" must map a page to a list of code globs, like {"docs/api.md": ["server/**"]}.'));
85
+ }
86
+ if (s.maxFiles !== undefined && !(Number.isInteger(s.maxFiles) && s.maxFiles > 0)) {
87
+ errors.push(bad('"sync.maxFiles" must be a whole number above zero.'));
88
+ }
89
+ if (s.digest !== undefined && typeof s.digest !== 'boolean')
90
+ errors.push(bad('"sync.digest" must be true or false.'));
91
+ return errors;
92
+ }
53
93
  export const CONFIG_FILE = FILE;
@@ -0,0 +1,35 @@
1
+ import type { IO } from './init.js';
2
+ /**
3
+ * OpenAPI files in a repository, nearest the root first. Shallow on purpose: a
4
+ * spec lives near the top, and a walk through a large monorepo should not make
5
+ * the setup feel stuck.
6
+ */
7
+ export declare function findSpecs(root: string, maxDepth?: number): string[];
8
+ export type SpecSummary = {
9
+ title: string | null;
10
+ operations: number;
11
+ tags: string[];
12
+ swagger2: boolean;
13
+ };
14
+ /** What a spec holds, enough to say so in a line. Null when it cannot be read. */
15
+ export declare function readSpec(path: string): SpecSummary | null;
16
+ /** Where the server will find a spec without being told: openapi.json/.yaml/.yml at the root or in the docs folder. */
17
+ export declare function specIsFoundByDefault(spec: string, docsDir: string): boolean;
18
+ export type Framework = 'express' | 'hono' | 'fastify';
19
+ /** The web framework a Node backend uses, from its package.json. */
20
+ export declare function detectFramework(root: string): Framework | null;
21
+ /** The lines that make a backend check keys, for the framework it uses. */
22
+ export declare function middlewareSnippet(framework: Framework | null): string[];
23
+ /** A scope name the server accepts, from an OpenAPI tag: `Shipments` → `shipments:read`. */
24
+ export declare function scopeFromTag(tag: string): string | null;
25
+ /** The first pages of a developer space. Only the ones missing are written. */
26
+ export declare function starterPages(title: string, spec: SpecSummary | null): {
27
+ file: string;
28
+ body: string;
29
+ }[];
30
+ export type DevelopersOptions = {
31
+ io?: IO;
32
+ yes?: boolean;
33
+ dir?: string;
34
+ };
35
+ export declare function developersFlow(root: string, options?: DevelopersOptions): Promise<void>;