diff --git a/.changeset/docs-flatten-dlp-cli-nav.md b/.changeset/docs-flatten-dlp-cli-nav.md new file mode 100644 index 0000000..f27d766 --- /dev/null +++ b/.changeset/docs-flatten-dlp-cli-nav.md @@ -0,0 +1,5 @@ +--- +"@cdot65/prisma-airs-cli": patch +--- + +The CLI Reference sidebar at https://cdot65.github.io/prisma-airs-cli/cli/ now lists `runtime dlp dictionaries`, `runtime dlp filtering-profiles`, `runtime dlp patterns`, and `runtime dlp profiles` as flat siblings under Runtime instead of collapsing them under a separate "Dlp" group. Achieved via a generated `docs/cli/SUMMARY.md` consumed by mkdocs `literate-nav`. No URLs or command structure changed. diff --git a/docs/cli/SUMMARY.md b/docs/cli/SUMMARY.md new file mode 100644 index 0000000..58220b0 --- /dev/null +++ b/docs/cli/SUMMARY.md @@ -0,0 +1,40 @@ +* [CLI Reference](index.md) +* Runtime + * [airs runtime api-keys](runtime/api-keys.md) + * [airs runtime bulk-scan](runtime/bulk-scan.md) + * [airs runtime customer-apps](runtime/customer-apps.md) + * [airs runtime deployment-profiles](runtime/deployment-profiles.md) + * [airs runtime dlp dictionaries](runtime/dlp/dictionaries.md) + * [airs runtime dlp filtering-profiles](runtime/dlp/filtering-profiles.md) + * [airs runtime dlp patterns](runtime/dlp/patterns.md) + * [airs runtime dlp profiles](runtime/dlp/profiles.md) + * [airs runtime dlp-gen](runtime/dlp-gen.md) + * [airs runtime dlp-profiles](runtime/dlp-profiles.md) + * [airs runtime profiles](runtime/profiles.md) + * [airs runtime resume-poll](runtime/resume-poll.md) + * [airs runtime scan](runtime/scan.md) + * [airs runtime scan-logs](runtime/scan-logs.md) + * [airs runtime topics](runtime/topics.md) +* Redteam + * [airs redteam abort](redteam/abort.md) + * [airs redteam categories](redteam/categories.md) + * [airs redteam devices](redteam/devices.md) + * [airs redteam eula](redteam/eula.md) + * [airs redteam instances](redteam/instances.md) + * [airs redteam list](redteam/list.md) + * [airs redteam prompt-sets](redteam/prompt-sets.md) + * [airs redteam prompts](redteam/prompts.md) + * [airs redteam properties](redteam/properties.md) + * [airs redteam registry-credentials](redteam/registry-credentials.md) + * [airs redteam report](redteam/report.md) + * [airs redteam scan](redteam/scan.md) + * [airs redteam status](redteam/status.md) + * [airs redteam targets](redteam/targets.md) +* Model-security + * [airs model-security groups](model-security/groups.md) + * [airs model-security install](model-security/install.md) + * [airs model-security labels](model-security/labels.md) + * [airs model-security pypi-auth](model-security/pypi-auth.md) + * [airs model-security rule-instances](model-security/rule-instances.md) + * [airs model-security rules](model-security/rules.md) + * [airs model-security scans](model-security/scans.md) diff --git a/scripts/cli-docs/render.ts b/scripts/cli-docs/render.ts index 235e791..31289c2 100644 --- a/scripts/cli-docs/render.ts +++ b/scripts/cli-docs/render.ts @@ -80,3 +80,24 @@ export function renderIndex(pages: PageNode[]): string { '', ].join('\n'); } + +export function renderSummary(pages: PageNode[]): string { + const groups = new Map(); + for (const p of pages) { + const top = p.slug.split('/')[0]; + const arr = groups.get(top) ?? []; + arr.push(p); + groups.set(top, arr); + } + const out: string[] = ['* [CLI Reference](index.md)']; + for (const top of groups.keys()) { + const label = top.charAt(0).toUpperCase() + top.slice(1); + out.push(`* ${label}`); + const sorted = (groups.get(top) ?? []).slice().sort((a, b) => a.title.localeCompare(b.title)); + for (const p of sorted) { + out.push(` * [airs ${p.title}](${p.slug}.md)`); + } + } + out.push(''); + return out.join('\n'); +} diff --git a/scripts/gen-cli-docs.ts b/scripts/gen-cli-docs.ts index d9ba150..218b569 100644 --- a/scripts/gen-cli-docs.ts +++ b/scripts/gen-cli-docs.ts @@ -2,7 +2,7 @@ import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { buildProgram } from '../src/cli/program.js'; import { loadExamples } from './cli-docs/examples.js'; -import { renderGroupPage, renderIndex } from './cli-docs/render.js'; +import { renderGroupPage, renderIndex, renderSummary } from './cli-docs/render.js'; import { collectPages, walkProgram } from './cli-docs/walk.js'; const OUT_DIR = 'docs/cli'; @@ -26,6 +26,10 @@ export function generate(outDir = OUT_DIR, examplesDir = EXAMPLES_DIR): string[] const indexFile = join(outDir, 'index.md'); writeFileSync(indexFile, renderIndex(pages)); written.push(indexFile); + + const summaryFile = join(outDir, 'SUMMARY.md'); + writeFileSync(summaryFile, renderSummary(pages)); + written.push(summaryFile); return written; } diff --git a/tests/unit/scripts/render.spec.ts b/tests/unit/scripts/render.spec.ts index 18ab340..c57d0cc 100644 --- a/tests/unit/scripts/render.spec.ts +++ b/tests/unit/scripts/render.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; import type { CommandNode, ExampleMap, PageNode } from '../../../scripts/cli-docs/model.js'; -import { renderGroupPage, renderIndex } from '../../../scripts/cli-docs/render.js'; +import { renderGroupPage, renderIndex, renderSummary } from '../../../scripts/cli-docs/render.js'; const scan: CommandNode = { path: 'runtime scan', @@ -60,3 +60,108 @@ describe('renderIndex', () => { expect(idx).toContain('runtime/scan.md'); }); }); + +describe('renderSummary', () => { + const pages: PageNode[] = [ + { + slug: 'runtime/api-keys', + title: 'runtime api-keys', + groupPath: 'runtime api-keys', + commands: [scan], + }, + { + slug: 'runtime/dlp/dictionaries', + title: 'runtime dlp dictionaries', + groupPath: 'runtime dlp dictionaries', + commands: [scan], + }, + { + slug: 'runtime/dlp/filtering-profiles', + title: 'runtime dlp filtering-profiles', + groupPath: 'runtime dlp filtering-profiles', + commands: [scan], + }, + { + slug: 'runtime/dlp/patterns', + title: 'runtime dlp patterns', + groupPath: 'runtime dlp patterns', + commands: [scan], + }, + { + slug: 'runtime/dlp/profiles', + title: 'runtime dlp profiles', + groupPath: 'runtime dlp profiles', + commands: [scan], + }, + { + slug: 'runtime/dlp-gen', + title: 'runtime dlp-gen', + groupPath: 'runtime dlp-gen', + commands: [scan], + }, + { + slug: 'runtime/dlp-profiles', + title: 'runtime dlp-profiles', + groupPath: 'runtime dlp-profiles', + commands: [scan], + }, + { slug: 'runtime/scan', title: 'runtime scan', groupPath: 'runtime scan', commands: [scan] }, + { slug: 'redteam/scan', title: 'redteam scan', groupPath: 'redteam scan', commands: [scan] }, + { + slug: 'model-security/groups', + title: 'model-security groups', + groupPath: 'model-security groups', + commands: [scan], + }, + ]; + + it('groups pages by top-level command with DLP entries flat under Runtime', () => { + const out = renderSummary(pages); + const lines = out.split('\n'); + const runtimeIdx = lines.findIndex((l) => l === '* Runtime'); + const redteamIdx = lines.findIndex((l) => l === '* Redteam'); + expect(runtimeIdx).toBeGreaterThanOrEqual(0); + expect(redteamIdx).toBeGreaterThan(runtimeIdx); + const runtimeBlock = lines + .slice(runtimeIdx + 1, redteamIdx) + .filter((l) => l.startsWith(' *')); + // DLP entries listed as flat siblings — single-level indent, not nested under another group + expect(runtimeBlock).toContain( + ' * [airs runtime dlp dictionaries](runtime/dlp/dictionaries.md)', + ); + expect(runtimeBlock).toContain( + ' * [airs runtime dlp filtering-profiles](runtime/dlp/filtering-profiles.md)', + ); + expect(runtimeBlock).toContain(' * [airs runtime dlp patterns](runtime/dlp/patterns.md)'); + expect(runtimeBlock).toContain(' * [airs runtime dlp profiles](runtime/dlp/profiles.md)'); + // No nested "Dlp" group label + expect(out).not.toMatch(/^\s*\*\s+Dlp\s*$/m); + }); + + it('preserves existing flat pages (dlp-gen, dlp-profiles) without duplication', () => { + const out = renderSummary(pages); + expect(out.match(/runtime\/dlp-gen\.md/g)?.length).toBe(1); + expect(out.match(/runtime\/dlp-profiles\.md/g)?.length).toBe(1); + }); + + it('sorts entries within a group by title alphabetically', () => { + const out = renderSummary(pages); + const lines = out.split('\n'); + const runtimeLinks = lines + .filter((l) => l.startsWith(' * [airs runtime')) + .map((l) => l.match(/\[(airs runtime[^\]]+)\]/)?.[1] ?? ''); + const sorted = [...runtimeLinks].sort(); + expect(runtimeLinks).toEqual(sorted); + }); + + it('emits index.md as the section landing', () => { + expect(renderSummary(pages)).toContain('* [CLI Reference](index.md)'); + }); + + it('handles all three top-level groups', () => { + const out = renderSummary(pages); + expect(out).toContain('* Runtime'); + expect(out).toContain('* Redteam'); + expect(out).toContain('* Model-security'); + }); +});