@readme/cli 0.2.0 → 0.3.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.
@@ -1,6 +1,5 @@
1
1
  import { execSync } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
- import { createRequire } from 'node:module';
4
3
  import path from 'node:path';
5
4
  import { fileURLToPath } from 'node:url';
6
5
 
@@ -19,8 +18,6 @@ import {
19
18
  import * as styles from './styles.js';
20
19
  import { getRandomTip } from './tips.js';
21
20
 
22
- const require = createRequire(import.meta.url);
23
-
24
21
  const isRunningInClaude = !!process.env.CLAUDECODE;
25
22
 
26
23
  const CATEGORY_LABELS = {
@@ -178,7 +175,7 @@ export function createHumanReporter() {
178
175
  if (isRunningInClaude && unfixed.length > 0) {
179
176
  const cliRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '../..');
180
177
 
181
- const gitFormatDir = path.dirname(require.resolve('@readmeio/git-format/package.json'));
178
+ const gitFormatDir = path.join(cliRoot, 'vendor/git-format');
182
179
  const claudeMdPath = path.join(gitFormatDir, 'CLAUDE.md');
183
180
  const toolsMdPath = path.join(cliRoot, 'vendor/TOOLS.md');
184
181
  const schemaPath = path.join(gitFormatDir, 'frontmatter.schema.json');
@@ -28,12 +28,19 @@ export function validate({ filePath, content, relativePath }) {
28
28
  return null;
29
29
  }
30
30
  if (trimmed.length === 0) {
31
- // index.md files that act as parent pages (have child .md siblings) can be empty.
31
+ // index.md files that act as parent pages (have child page siblings or nested page
32
+ // directories) can be empty. The backend accepts .md and .mdx in any case.
32
33
  if (path.basename(filePath) === 'index.md') {
33
- const siblings = fs.readdirSync(path.dirname(filePath));
34
- if (siblings.some(f => f.endsWith('.md') && f !== 'index.md')) {
35
- return null;
36
- }
34
+ const dir = path.dirname(filePath);
35
+ const isPage = f => /\.mdx?$/i.test(f);
36
+ const hasChildren = fs
37
+ .readdirSync(dir, { withFileTypes: true })
38
+ .some(
39
+ e =>
40
+ (!e.isDirectory() && isPage(e.name) && !/^index\.mdx?$/i.test(e.name)) ||
41
+ (e.isDirectory() && fs.readdirSync(path.join(dir, e.name)).some(isPage)),
42
+ );
43
+ if (hasChildren) return null;
37
44
  }
38
45
 
39
46
  return {
@@ -1,13 +1,10 @@
1
1
  import fs from 'node:fs';
2
- import { createRequire } from 'node:module';
3
- import path from 'node:path';
4
2
 
5
3
  import Ajv from 'ajv';
6
4
 
5
+ import frontmatterSchema from '../../vendor/git-format/frontmatter.schema.json' with { type: 'json' };
7
6
  import { parseFrontmatter } from '../utils/markdown.js';
8
7
 
9
- const require = createRequire(import.meta.url);
10
-
11
8
  export const name = 'frontmatter';
12
9
 
13
10
  function getNestedValue(obj, instancePath) {
@@ -77,18 +74,15 @@ const DIR_SCHEMA_INDEX = {
77
74
  custom_blocks: 4,
78
75
  };
79
76
 
80
- // Load the schema and compile a validator for each directory type.
81
- const schemaPath = require.resolve('@readmeio/git-format/frontmatter.schema.json');
82
- const fullSchema = JSON.parse(fs.readFileSync(schemaPath, 'utf-8'));
83
-
77
+ // Compile a validator for each directory type from the vendored schema.
84
78
  const ajv = new Ajv({ allErrors: true, strict: false, logger: false, verbose: true });
85
79
 
86
80
  const validators = {};
87
81
  const knownProperties = {};
88
82
  for (const [dir, index] of Object.entries(DIR_SCHEMA_INDEX)) {
89
- const subSchema = { defs: fullSchema.defs, ...fullSchema.oneOf[index] };
83
+ const subSchema = { defs: frontmatterSchema.defs, ...frontmatterSchema.oneOf[index] };
90
84
  validators[dir] = ajv.compile(subSchema);
91
- knownProperties[dir] = collectPropertyNames(subSchema, fullSchema.defs);
85
+ knownProperties[dir] = collectPropertyNames(subSchema, frontmatterSchema.defs);
92
86
  }
93
87
 
94
88
  export function validate({ content, filePath, relativePath, fix }) {
@@ -4,9 +4,14 @@ export const name = 'nesting';
4
4
 
5
5
  // Max folder nesting per top-level content dir. Anything deeper still syncs
6
6
  // but won't appear in the sidebar.
7
+ //
8
+ // For `docs` and `reference` the category is the first folder and each page
9
+ // level below it adds one, so this is the number of page levels the sidebar
10
+ // renders under a category. It mirrors our publicly documented sidebar levels.
11
+ // Keep in sync with src/dev/lib/docs.js.
7
12
  const MAX_DEPTH = {
8
- docs: 3,
9
- reference: 3,
13
+ docs: 5,
14
+ reference: 5,
10
15
  recipes: 0,
11
16
  custom_pages: 0,
12
17
  custom_blocks: 0,
@@ -23,8 +28,8 @@ export function validateAll(files) {
23
28
  if (max === undefined) continue;
24
29
 
25
30
  // Depth = number of folders between the top dir and the file.
26
- // docs/a/b/c/page.md → parts.length 5 → depth 3
27
- // docs/a/b/c/d/page.md → parts.length 6 → depth 4
31
+ // docs/a/b/c/d/e/page.md → parts.length 7 → depth 5
32
+ // docs/a/b/c/d/e/f/page.md → parts.length 8 → depth 6
28
33
  const depth = parts.length - 2;
29
34
  if (depth <= max) continue;
30
35
 
@@ -9,6 +9,7 @@ import {
9
9
  operationKey,
10
10
  findIgnoredInternalExtensions,
11
11
  } from '../commands/oas-sync.js';
12
+ import { PathEscapeError } from '../utils/containment.js';
12
13
  import { parseFrontmatter } from '../utils/markdown.js';
13
14
 
14
15
  export const name = 'oas-reference';
@@ -103,11 +104,24 @@ export function validateAll(files, gitRoot, { fix } = {}) {
103
104
  // reference files, which is far too much to do on the strength of an
104
105
  // unfixable warning (`x-readme.internal`, a page pointing at a missing spec).
105
106
  if (fix && results.some(r => r.fixable)) {
106
- const syncResults = syncOas(gitRoot);
107
- if (syncResults) {
108
- for (const r of results) {
109
- if (r.fixable) r.message += ' (fixed)';
107
+ try {
108
+ const syncResults = syncOas(gitRoot);
109
+ if (syncResults) {
110
+ for (const r of results) {
111
+ if (r.fixable) r.message += ' (fixed)';
112
+ }
110
113
  }
114
+ } catch (err) {
115
+ if (!(err instanceof PathEscapeError)) throw err;
116
+
117
+ // Don't mark the other fixes applied — sync stopped before it could.
118
+ results.push({
119
+ file: path.relative(gitRoot, err.target) || 'reference',
120
+ rule: name,
121
+ severity: 'error',
122
+ message: err.message,
123
+ fixable: false,
124
+ });
111
125
  }
112
126
  }
113
127
 
@@ -0,0 +1,352 @@
1
+ # ReadMe Git-Based Docs Format
2
+
3
+ This repo is the reference implementation of ReadMe's Git-based documentation format. It defines how documentation is structured when syncing with ReadMe via Git.
4
+
5
+ Any LLM modifying a repo that uses this format should reference this file and the examples in this repo.
6
+
7
+ ## Directory structure
8
+
9
+ ```
10
+ your-repo/
11
+ ├── frontmatter.schema.json # Frontmatter validation schema
12
+ ├── docs/ # Guides and documentation pages
13
+ │ ├── {Category Name}/ # Category folders (Title Case with spaces)
14
+ │ │ ├── _order.yaml # Page ordering within this category
15
+ │ │ ├── page-slug.md # Documentation pages (kebab-case filenames)
16
+ │ │ └── parent-page/ # Parent page with children
17
+ │ │ ├── index.md # The parent page content
18
+ │ │ └── child-page.md # Child page nested under parent
19
+ │ └── {Another Category}/
20
+ │ └── ...
21
+ ├── reference/ # API reference documentation
22
+ │ ├── oas.json # OpenAPI spec file(s)
23
+ │ └── {API Title}/ # Matches info.title from OAS
24
+ │ └── {Tag}/ # Matches OAS operation tags
25
+ │ ├── index.md # Optional category landing page
26
+ │ └── operationId.md # Endpoint page linked to OAS operation
27
+ ├── recipes/ # Code recipe/tutorial pages
28
+ │ ├── _order.yaml
29
+ │ └── recipe-slug.md
30
+ ├── custom_pages/ # Standalone pages (markdown or HTML)
31
+ │ └── page-slug.md
32
+ ├── custom_blocks/ # Reusable content snippets and components
33
+ │ ├── snippet.md # Markdown snippet
34
+ │ └── Component.mdx # React component (single-file)
35
+ └── reusable_content/ # (also valid top-level directory)
36
+ ```
37
+
38
+ **Valid top-level directories:** `docs/`, `reference/`, `custom_pages/`, `recipes/`, `custom_blocks/`, `reusable_content/`
39
+
40
+ ## Frontmatter
41
+
42
+ Every page uses YAML frontmatter. The full schema is in `frontmatter.schema.json`. Here's a summary by content type:
43
+
44
+ ### Doc pages (`docs/`)
45
+
46
+ ```yaml
47
+ ---
48
+ title: Page Title # Required
49
+ excerpt: One-sentence summary
50
+ hidden: true # Hides page from navigation
51
+ deprecated: true # Marks page as deprecated
52
+ icon: star # Icon identifier
53
+ metadata:
54
+ title: SEO Title
55
+ description: SEO description
56
+ keywords: [keyword1, keyword2]
57
+ image: https://example.com/image.png
58
+ robots: noindex # "index" or "noindex"
59
+ next:
60
+ description: What's next
61
+ pages:
62
+ - slug: page-slug
63
+ title: Display Title
64
+ type: basic # basic, endpoint, changelog, custom_page
65
+ - title: External Link
66
+ type: link
67
+ url: https://example.com
68
+ link:
69
+ url: https://example.com # Redirect page to this URL
70
+ new_tab: true
71
+ ---
72
+ ```
73
+
74
+ Only `title` is required. All other fields are optional — don't add fields you don't need.
75
+
76
+ ### API reference pages (`reference/`)
77
+
78
+ Same fields as doc pages, plus:
79
+
80
+ ```yaml
81
+ ---
82
+ title: Get a Pet
83
+ excerpt: Fetch a specific pet by ID
84
+ api:
85
+ file: oas.json # Path to OAS file (relative to reference/)
86
+ operationId: getPet # Matches operationId in the OAS spec
87
+ webhook: false # Set true for webhook operations
88
+ api_config: authentication # Special page type: "authentication", "getting-started", or "my-requests"
89
+ recipes: # Related recipe slugs
90
+ - publish-all-docs-in-category
91
+ ---
92
+ ```
93
+
94
+ Set `api` to `null` to detach a reference page from its API operation (makes it a plain page within `reference/`).
95
+
96
+ ### Recipe pages (`recipes/`)
97
+
98
+ ```yaml
99
+ ---
100
+ title: Publish all Docs in Category # Required
101
+ description: Update each doc as published. # Optional
102
+ hidden: true
103
+ recipe: # Required
104
+ color: '#018FF4' # Hex color, required
105
+ icon: circle-play # Icon string, required
106
+ ---
107
+ ```
108
+
109
+ ### Custom pages (`custom_pages/`)
110
+
111
+ ```yaml
112
+ ---
113
+ title: My Custom Page # Required
114
+ hidden: true
115
+ fullscreen: true # Renders page without sidebar/nav chrome
116
+ metadata:
117
+ description: ...
118
+ keywords: ...
119
+ title: ...
120
+ image: ...
121
+ ---
122
+ ```
123
+
124
+ Custom pages support markdown (`.md`) and HTML (`.html`). They cannot use `deprecated`.
125
+
126
+ ### Custom blocks (`custom_blocks/`)
127
+
128
+ Markdown snippets (`.md`):
129
+ ```yaml
130
+ ---
131
+ name: my-snippet # Required
132
+ ---
133
+ Content here...
134
+ ```
135
+
136
+ React components (`.mdx`) — export a component and include a usage example:
137
+ ```yaml
138
+ ---
139
+ name: MyComponent # Required
140
+ ---
141
+ ```
142
+
143
+ ## Ordering with `_order.yaml`
144
+
145
+ Every directory containing pages needs an `_order.yaml` to control display order.
146
+
147
+ **Category ordering** — lists folder names (Title Case with spaces):
148
+ ```yaml
149
+ # docs/_order.yaml (if it existed — currently categories display alphabetically without one)
150
+ - Documentation
151
+ - More Documentation
152
+ ```
153
+
154
+ **Page ordering within a category** — lists page slugs (kebab-case, no `.md`):
155
+ ```yaml
156
+ # docs/Documentation/_order.yaml
157
+ - hello-world
158
+ - parent-page
159
+ - another-page
160
+ ```
161
+
162
+ **Recipe ordering:**
163
+ ```yaml
164
+ # recipes/_order.yaml
165
+ - publish-all-docs-in-category
166
+ ```
167
+
168
+ ## Parent-child pages
169
+
170
+ Create a folder named after the parent page slug, with `index.md` as the parent and additional `.md` files as children:
171
+
172
+ ```
173
+ docs/Documentation/
174
+ └── parent-page/
175
+ ├── index.md # Parent page (title: "Parent Page")
176
+ └── child-page.md # Child page nested underneath
177
+ ```
178
+
179
+ ## Linking between pages
180
+
181
+ ### Published URL structure
182
+
183
+ On the published ReadMe site, pages have flat URL paths based on their content type:
184
+
185
+ | Content type | Published URL |
186
+ |---|---|
187
+ | Doc pages (`docs/`) | `/docs/{slug}` |
188
+ | API reference (`reference/`) | `/reference/{slug}` |
189
+ | Recipes (`recipes/`) | `/recipes/{slug}` |
190
+ | Custom pages (`custom_pages/`) | `/page/{slug}` |
191
+
192
+ The slug comes from the filename (without `.md`). Category folders do **not** appear in published URLs — `docs/Getting Started/quickstart.md` becomes `/docs/quickstart`. Child pages nest under their parent: `docs/Category/parent-page/child-page.md` becomes `/docs/parent-page/child-page`.
193
+
194
+ **Important:** Site URLs may include a prefix (e.g., `/lang-en/docs/slug`), so never use absolute paths when linking between pages. Use relative links instead.
195
+
196
+ ### Relative links (preferred)
197
+
198
+ Always use relative links based on the published URL structure above (not the file structure in this repo). Do not include `.md` extensions in links.
199
+
200
+ ```markdown
201
+ <!-- Link to a page in the same section -->
202
+ [Link text](./other-page)
203
+
204
+ <!-- Link from a doc page to a reference page -->
205
+ [Link text](../reference/some-endpoint)
206
+
207
+ <!-- Link from a child page to a sibling -->
208
+ [Link text](./sibling-page)
209
+
210
+ <!-- Link from a child page up to another doc -->
211
+ [Link text](../other-page)
212
+
213
+ <!-- HTML anchor tags also work with relative links -->
214
+ <a href="./other-page">Link text</a>
215
+ ```
216
+
217
+ ### `doc:` protocol links
218
+
219
+ ReadMe supports a special `doc:slug` protocol that resolves to doc pages regardless of URL prefix. This is convenient but only works in markdown links (not in `<a>` tags or component `href` attributes):
220
+
221
+ ```markdown
222
+ [Link text](doc:hello-world)
223
+ ```
224
+
225
+ **Use relative links as the default.** The `doc:` protocol is an acceptable alternative for markdown-only links to doc pages.
226
+
227
+ ## Markdown content conventions
228
+
229
+ - Write in active voice, address the reader as "you"
230
+ - Use sentence case for headings
231
+ - Category folder names: Title Case with spaces (e.g., `Getting Started`, `Core Features`)
232
+ - Page filenames: lowercase kebab-case (e.g., `quickstart.md`, `error-handling.md`)
233
+
234
+ ### MDX components
235
+
236
+ These built-in components are available in markdown content:
237
+
238
+ **Tabs** — for non-code tabbed content:
239
+ ```markdown
240
+ <Tabs>
241
+ <Tab title="First Tab">
242
+ Content for tab 1
243
+ </Tab>
244
+ <Tab title="Second Tab">
245
+ Content for tab 2
246
+ </Tab>
247
+ </Tabs>
248
+ ```
249
+
250
+ **Tabbed code blocks** — place fenced code blocks back-to-back with NO blank lines between them:
251
+
252
+ ````markdown
253
+ ```javascript
254
+ const pet = await sdk.getPet({ petId: 123 });
255
+ ```
256
+ ```python
257
+ pet = sdk.get_pet(pet_id=123)
258
+ ```
259
+ ````
260
+
261
+ **Accordion:**
262
+ ```markdown
263
+ <Accordion title="Click to expand" icon="info">
264
+ Hidden content here
265
+ </Accordion>
266
+ ```
267
+
268
+ **Cards:**
269
+ ```markdown
270
+ <Cards columns={3}>
271
+ <Card title="Getting Started" icon="rocket" href="./getting-started">
272
+ Quick intro to the platform
273
+ </Card>
274
+ <Card title="API Reference" icon="code" href="../reference/">
275
+ Explore the API
276
+ </Card>
277
+ </Cards>
278
+ ```
279
+
280
+ **Columns:**
281
+ ```markdown
282
+ <Columns>
283
+ <Column>
284
+ Left side content
285
+ </Column>
286
+ <Column>
287
+ Right side content
288
+ </Column>
289
+ </Columns>
290
+ ```
291
+
292
+ Only these components are valid. Convert any other components (e.g., `<Callout>`, `<Note>`, `<Info>`, `<Steps>`) to plain markdown or blockquotes.
293
+
294
+ ### Recipe line highlighting
295
+
296
+ Recipes can highlight specific code lines per language using HTML comments:
297
+
298
+ ```markdown
299
+ <!-- node@L0-L4,L6 -->
300
+ <!-- python@L2-L3,L9 -->
301
+ ```
302
+
303
+ These comments go after a markdown heading and before the explanatory text for that step. They reference the code blocks at the top of the recipe.
304
+
305
+ ## API reference structure
306
+
307
+ OAS (OpenAPI) spec files go in `reference/`. The directory structure maps to the API:
308
+
309
+ ```
310
+ reference/
311
+ ├── oas.json # The OpenAPI spec
312
+ └── Simple Pet API/ # Matches spec's info.title
313
+ ├── Pet/ # Matches a tag name
314
+ │ ├── index.md # Optional: landing page for this tag
315
+ │ └── getPet.md # Endpoint page (filename = operationId)
316
+ └── Pets/
317
+ ├── index.md
318
+ └── randomPet.md
319
+ ```
320
+
321
+ If you only need the auto-generated endpoint pages (no custom markdown), you can just have the OAS file with no subdirectories — ReadMe creates "virtual" categories from the spec's tags.
322
+
323
+ Endpoint pages link to their OAS operation via frontmatter:
324
+ ```yaml
325
+ api:
326
+ file: oas.json
327
+ operationId: getPet
328
+ ```
329
+
330
+ You can also add **conceptual guide pages** inside `reference/` (authentication, error handling, pagination, etc.) by creating pages without an `api` frontmatter field, or by setting `api: null`.
331
+
332
+ ## Validation rules
333
+
334
+ When attempting to validate the repo, these are the common issues to check for and how to fix them:
335
+
336
+ - **frontmatter** — Invalid or unknown frontmatter properties. Unknown properties should either be removed or prefixed with `x-` (e.g., `category` → `x-category`). Typos are flagged with the closest valid property name.
337
+ - **ordering** — Every file and subdirectory should be listed in its directory's `_order.yaml`. Slugs that YAML interprets as non-strings (numbers like `404`, booleans like `true`/`false`/`yes`/`no`) must be quoted (e.g., `- "404"`). Never remove quotes from existing entries.
338
+ - **numbering** — Files ending in `-1`, `-2`, etc. where no base name exists should be renamed (e.g., `setup-1.md` → `setup.md`). These suffixes are holdovers from an old bug.
339
+ - **duplicates** — Slugs must be unique across all `docs/` and `reference/` pages. Never rename files inside `ReadMeConfig/` — these have special internal names.
340
+ - **oas-reference** — Reference pages with `api.file` frontmatter must point to a valid OAS file. The `api.operationId` must exist in the spec.
341
+ - **oas-schema** — OAS files must be valid OpenAPI or Swagger specs. Error messages include line numbers and context.
342
+ - **components** — MDX component tags (`<Name>`) must reference a built-in or custom block. Custom block `.md` snippets should use PascalCase filenames (e.g., `MySnippet.md` for `<MySnippet />`).
343
+ - **recipes** — Must have code blocks at the top, then `# Heading` sections. Snippet references (`<!-- lang@lines -->`) must reference existing code blocks with valid line ranges.
344
+
345
+ ## Key rules when editing
346
+
347
+ 1. **Don't add frontmatter fields not in the schema.** Check `frontmatter.schema.json` if unsure.
348
+ 2. **Every directory with pages needs `_order.yaml`** to control ordering.
349
+ 3. **Category folders use Title Case with spaces.** Page files use kebab-case.
350
+ 4. **Don't modify auto-generated endpoint files** in `reference/` unless adding custom markdown to existing ones. You can add new conceptual pages alongside them.
351
+ 5. **Prefer more focused pages over fewer long ones.** Split distinct concepts into separate files.
352
+ 6. **Don't create changelog/release-notes content** — ReadMe handles that separately.