@@ -7,6 +7,281 @@ const parseFrontMatter = require('front-matter')
77const checkNav = require('./check-nav.js')
88const { DOC_EXT, ...transform } = require('./index.js')
9910+// Helper to check if a directory exists
11+const dirExists = async (path) => {
12+try {
13+const stat = await fs.stat(path)
14+return stat.isDirectory()
15+} catch {
16+return false
17+}
18+}
19+20+// Helper to read docs from a section directory
21+const readSectionDocs = async (contentPath, section, orderedUrls) => {
22+const sectionPath = join(contentPath, section)
23+if (!await dirExists(sectionPath)) {
24+return []
25+}
26+27+const files = await fs.readdir(sectionPath)
28+const docFiles = files.filter(f => f.endsWith(DOC_EXT))
29+30+// If no doc files exist, return empty array
31+/* istanbul ignore if - defensive check for empty directories */
32+if (docFiles.length === 0) {
33+return []
34+}
35+36+// Parse each doc file to get title and description from frontmatter
37+const docs = await Promise.all(
38+docFiles.map(async (file) => {
39+const content = await fs.readFile(join(sectionPath, file), 'utf-8')
40+const { attributes } = parseFrontMatter(content)
41+const name = basename(file, DOC_EXT)
42+43+return {
44+title: attributes.title,
45+url: `/${section}/${name}`,
46+description: attributes.description,
47+ name,
48+}
49+})
50+)
51+52+// Preserve order from orderedUrls, append any new files at the end sorted alphabetically
53+const orderedDocs = []
54+const docsByUrl = new Map(docs.map(d => [d.url, d]))
55+56+// First, add docs in the order they appear in orderedUrls
57+for (const url of orderedUrls) {
58+const doc = docsByUrl.get(url)
59+if (doc) {
60+orderedDocs.push(doc)
61+docsByUrl.delete(url)
62+}
63+}
64+65+return orderedDocs.map(({ name, ...rest }) => rest)
66+}
67+68+// Generate nav.yml from the filesystem
69+const generateNav = async (contentPath, navPath) => {
70+const docsCommandsPath = join(contentPath, 'commands')
71+72+// Read all command files
73+const commandFiles = await dirExists(docsCommandsPath) ? await fs.readdir(docsCommandsPath) : []
74+const commandDocs = commandFiles.filter(f => f.endsWith(DOC_EXT))
75+76+// Parse each command file to get title and description
77+const allCommands = await Promise.all(
78+commandDocs.map(async (file) => {
79+const content = await fs.readFile(join(docsCommandsPath, file), 'utf-8')
80+const { attributes } = parseFrontMatter(content)
81+const name = basename(file, DOC_EXT)
82+const title = (attributes.title || name).replace(/^npm-/, 'npm ')
83+84+return {
85+ title,
86+url: `/commands/${name}`,
87+description: attributes.description || '',
88+ name,
89+}
90+})
91+)
92+93+// Sort commands: npm first, then alphabetically, npx last
94+const npm = allCommands.find(c => c.name === 'npm')
95+const npx = allCommands.find(c => c.name === 'npx')
96+const others = allCommands
97+.filter(c => c.name !== 'npm' && c.name !== 'npx')
98+.sort((a, b) => a.name.localeCompare(b.name))
99+100+// Remove the name field
101+const commands = [npm, ...others, npx].filter(Boolean).map(({ name, ...rest }) => rest)
102+103+// Hardcoded order for configuring-npm section (only urls - title/description come from frontmatter)
104+const configuringNpmOrder = [
105+'/configuring-npm/install',
106+'/configuring-npm/folders',
107+'/configuring-npm/npmrc',
108+'/configuring-npm/npm-shrinkwrap-json',
109+'/configuring-npm/package-json',
110+'/configuring-npm/package-lock-json',
111+]
112+113+// Hardcoded order for using-npm section (only urls - title/description come from frontmatter)
114+const usingNpmOrder = [
115+'/using-npm/registry',
116+'/using-npm/package-spec',
117+'/using-npm/config',
118+'/using-npm/logging',
119+'/using-npm/scope',
120+'/using-npm/scripts',
121+'/using-npm/workspaces',
122+'/using-npm/orgs',
123+'/using-npm/dependency-selectors',
124+'/using-npm/developers',
125+'/using-npm/removal',
126+]
127+128+// Read actual docs from configuring-npm and using-npm directories
129+const configuringNpmDocs = await readSectionDocs(contentPath, 'configuring-npm', configuringNpmOrder)
130+const usingNpmDocs = await readSectionDocs(contentPath, 'using-npm', usingNpmOrder)
131+132+// Build the navigation structure - only include sections with content
133+const navData = []
134+135+if (commands.length > 0) {
136+navData.push({
137+title: 'CLI Commands',
138+shortName: 'Commands',
139+url: '/commands',
140+children: commands,
141+})
142+}
143+144+if (configuringNpmDocs.length > 0) {
145+navData.push({
146+title: 'Configuring npm',
147+shortName: 'Configuring',
148+url: '/configuring-npm',
149+children: configuringNpmDocs,
150+})
151+}
152+153+if (usingNpmDocs.length > 0) {
154+navData.push({
155+title: 'Using npm',
156+shortName: 'Using',
157+url: '/using-npm',
158+children: usingNpmDocs,
159+})
160+}
161+162+const prefix = `# This is the navigation for the documentation pages; it is not used
163+# directly within the CLI documentation. Instead, it will be used
164+# for the https://docs.npmjs.com/ site.
165+`
166+await fs.writeFile(navPath, `${prefix}\n${yaml.stringify(navData, { indent: 2, indentSeq: false })}`, 'utf-8')
167+}
168+169+// Auto-generate doc templates for commands without docs
170+const autoGenerateMissingDocs = async (contentPath, navPath, commandsPath = null) => {
171+commandsPath = commandsPath || join(__dirname, '../../lib/commands')
172+const docsCommandsPath = join(contentPath, 'commands')
173+174+// Get all commands from commandsPath directory
175+let commands
176+try {
177+const cmdListPath = join(commandsPath, '..', 'utils', 'cmd-list.js')
178+const cmdList = require(cmdListPath)
179+commands = cmdList.commands
180+} catch {
181+// Fall back to reading command files from commandsPath
182+const cmdFiles = await fs.readdir(commandsPath)
183+commands = cmdFiles
184+.filter(f => f.endsWith('.js'))
185+.map(f => basename(f, '.js'))
186+}
187+188+// Get existing doc files
189+const existingDocs = await fs.readdir(docsCommandsPath)
190+const documentedCommands = existingDocs
191+.filter(f => f.startsWith('npm-') && f.endsWith(DOC_EXT))
192+.map(f => f.replace('npm-', '').replace(DOC_EXT, ''))
193+194+// Find commands without docs
195+const missingDocs = commands.filter(cmd => !documentedCommands.includes(cmd))
196+197+// Generate docs for missing commands
198+const newEntries = []
199+for (const cmd of missingDocs) {
200+const Command = require(join(commandsPath, `${cmd}.js`))
201+const description = Command.description || `The ${cmd} command`
202+const docPath = join(docsCommandsPath, `npm-${cmd}${DOC_EXT}`)
203+204+const template = `---
205+title: npm-${cmd}
206+section: 1
207+description: ${description}
208+---
209+210+### Synopsis
211+212+<!-- AUTOGENERATED USAGE DESCRIPTIONS -->
213+214+### Description
215+216+${description}
217+218+### Configuration
219+220+<!-- AUTOGENERATED CONFIG DESCRIPTIONS -->
221+222+### See Also
223+224+* [npm help config](/commands/npm-config)
225+`
226+227+await fs.writeFile(docPath, template, 'utf-8')
228+229+// Track new entry for nav update
230+newEntries.push({
231+title: `npm ${cmd}`,
232+url: `/commands/npm-${cmd}`,
233+ description,
234+})
235+}
236+237+// Update nav.yml if there are new entries
238+if (newEntries.length > 0) {
239+const navContent = await fs.readFile(navPath, 'utf-8')
240+const navData = yaml.parse(navContent)
241+242+// Find CLI Commands section
243+let commandsSection = navData.find(s => s.title === 'CLI Commands')
244+if (!commandsSection) {
245+// Create CLI Commands section if it doesn't exist
246+commandsSection = {
247+title: 'CLI Commands',
248+shortName: 'Commands',
249+url: '/commands',
250+children: [],
251+}
252+navData.unshift(commandsSection)
253+}
254+255+if (!commandsSection.children) {
256+commandsSection.children = []
257+}
258+259+// Add new entries that don't already exist
260+for (const entry of newEntries) {
261+const exists = commandsSection.children.some(c => c.url === entry.url)
262+if (!exists) {
263+commandsSection.children.push(entry)
264+}
265+}
266+267+// Sort children: npm first, then alphabetically, npx last
268+const npm = commandsSection.children.find(c => c.title === 'npm')
269+const npx = commandsSection.children.find(c => c.title === 'npx')
270+const others = commandsSection.children
271+.filter(c => c.title !== 'npm' && c.title !== 'npx')
272+.sort((a, b) => a.title.localeCompare(b.title))
273+274+commandsSection.children = [npm, ...others, npx].filter(Boolean)
275+276+// Write updated nav
277+const prefix = `# This is the navigation for the documentation pages; it is not used
278+# directly within the CLI documentation. Instead, it will be used
279+# for the https://docs.npmjs.com/ site.
280+`
281+await fs.writeFile(navPath, `${prefix}\n${yaml.stringify(navData, { indent: 2, indentSeq: false })}`, 'utf-8')
282+}
283+}
284+10285const mkDirs = async (paths) => {
11286const uniqDirs = [...new Set(paths.map((p) => dirname(p)))]
12287return Promise.all(uniqDirs.map((d) => fs.mkdir(d, { recursive: true })))
@@ -28,7 +303,21 @@ const pAll = async (obj) => {
28303}, {})
29304}
3030531-const run = async ({ content, template, nav, man, html, md }) => {
306+const run = async (opts) => {
307+const {
308+ content, template, nav, man, html, md,
309+ skipAutoGenerate, skipGenerateNav, commandLoader,
310+} = opts
311+// Auto-generate docs for commands without documentation
312+if (!skipAutoGenerate) {
313+await autoGenerateMissingDocs(content, nav)
314+}
315+316+// Generate nav.yml from filesystem
317+if (!skipGenerateNav) {
318+await generateNav(content, nav)
319+}
320+32321await rmAll(man, html, md)
33322const [contentPaths, navFile, options] = await Promise.all([
34323readDocs(content),
@@ -73,6 +362,7 @@ const run = async ({ content, template, nav, man, html, md }) => {
73362}) => {
74363const applyTransforms = makeTransforms({
75364path: childPath,
365+ commandLoader,
76366data: {
77367 ...data,
78368github_repo: 'npm/cli',
@@ -86,7 +376,7 @@ const run = async ({ content, template, nav, man, html, md }) => {
86376const transformedSrc = applyTransforms(body, [
87377transform.version,
88378 ...(fullName.startsWith('commands/')
89- ? [transform.usage, transform.params]
379+ ? [transform.usage, transform.definitions]
90380 : []),
91381 ...(fullName === 'using-npm/config'
92382 ? [transform.shorthands, transform.config]
@@ -145,3 +435,5 @@ const run = async ({ content, template, nav, man, html, md }) => {
145435}
146436147437module.exports = run
438+module.exports.generateNav = generateNav
439+module.exports.autoGenerateMissingDocs = autoGenerateMissingDocs