docs(man): move the manual-page entry to the canonical CLI page

alfred-openspec on #1784: docs-lab/README.md makes docs-lab/ canonical and the
old docs/ tree legacy, and 'man openspec' was documented only in docs/cli.md.

The entry now lives in docs-lab/reference/cli.md, listed under Utilities, and
docs/cli.md is back to its state on main.

Two things changed in the move rather than being copied across:

- CodeRabbit's finding on the fallback command was valid. The only fallback
  called 'pnpm root -g', which a contributor who installed with another manager
  may not have. The command is now derived from the installed CLI itself, so it
  works whichever manager installed it, with the pnpm form kept as the fallback
  for an older readlink with no -f. Verified against the packed tarball, whose
  layout really is bin/../dist/man/openspec.1.
- The exit-code and environment-variable parity tests read docs/cli.md, so this
  PR was adding a hard test dependency on the tree that is being retired, and
  it forced the '130' row into the legacy table to keep the test green. The
  parity read backwards anyway: the legacy table omits 130, which the CLI
  really does exit with, and which docs-lab records per command. The exit-code
  test now pins the three codes directly, and the environment test holds a
  property no prose table can: every documented variable is one src/ actually
  reads. Verified it bites by adding an invented variable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Clay Good
2026-09-07 11:50:04 -05:00
co-authored by Claude Opus 5
parent f68487cdce
commit f993469dcd
3 changed files with 48 additions and 45 deletions
+21
View File
@@ -52,6 +52,7 @@ Your agent runs most of these during the workflow.
|---|---|
| [`openspec feedback`](#openspec-feedback) | Submit feedback about OpenSpec. |
| [`openspec completion`](#openspec-completion) | Install or generate shell completions. |
| [`man openspec`](#man-openspec) | Read the full command reference offline. |
**Deprecated**
@@ -2182,6 +2183,26 @@ Removes the script and the marked config block. It asks before touching your con
- `0`: script generated, installed, or removed. A cancelled uninstall also exits 0.
- `1`: shell not supported or not detected, or an install or uninstall step failed.
## man openspec
The full command reference, offline.
```bash
man openspec
```
The page lists every command, its arguments, and its flags, plus exit codes, environment variables, the files OpenSpec reads, and examples. It is generated from the CLI itself at build time, so it always matches `openspec --help` for the version you have installed.
`npm install -g` links the page into your man path, so `man openspec` works with no extra step. Other package managers ship the file without linking it. Point `man` at the copy next to the installed CLI:
```bash
man "$(dirname "$(readlink -f "$(command -v openspec)")")/../dist/man/openspec.1"
```
That resolves the `openspec` on your `PATH` back to the package it came from, so it works whichever manager installed it. If your `readlink` has no `-f` (older macOS), ask the manager for its global package directory instead, for example `man "$(pnpm root -g)/@fission-ai/openspec/dist/man/openspec.1"`.
Windows has no `man`. Use `openspec --help` there.
## openspec change
Deprecated noun form of `show`, `list`, and `validate`. Every run warns and points to the verb-first commands, then runs anyway:
-26
View File
@@ -1307,31 +1307,6 @@ run a command in an interactive terminal, and never again — it also stays quie
if you already have completions installed. Set `OPENSPEC_NO_COMPLETIONS=1` to
suppress that tip entirely.
### `man openspec`
A global install ships a manual page, so the full command reference is available
offline:
```bash
man openspec
```
The page lists every command, its arguments, and its flags, plus exit codes,
environment variables, the files OpenSpec reads, and examples. It is generated
from the CLI itself at build time, so it always matches `openspec --help` for
the version you have installed.
`man openspec` works out of the box after `npm install -g`, which links the page
into your man path. Other package managers ship the file but don't link it, so
point `man` at the copy in their global package directory — with pnpm, for
example:
```bash
man "$(pnpm root -g)/@fission-ai/openspec/dist/man/openspec.1"
```
Windows has no `man` — use `openspec --help` there.
---
## Exit Codes
@@ -1340,7 +1315,6 @@ Windows has no `man` — use `openspec --help` there.
|------|---------|
| `0` | Success |
| `1` | Error (validation failure, missing files, etc.) |
| `130` | Cancelled at a prompt |
---
+27 -19
View File
@@ -3,6 +3,7 @@ import { Command } from 'commander';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import fg from 'fast-glob';
import {
ENVIRONMENT,
@@ -201,19 +202,6 @@ describe('the manual rendered from the real CLI', () => {
describe('the sections a command tree cannot supply', () => {
const page = render(program);
const docs = fs.readFileSync(path.join(repoRoot, 'docs', 'cli.md'), 'utf-8');
/**
* Every term in the first column of a docs table, so a row that names two
* (`EDITOR` or `VISUAL`) contributes both.
*/
function tableTerms(heading: string): string[] {
const section = docs.split(`## ${heading}`)[1] ?? '';
const table = section.split('\n---')[0];
return [...table.matchAll(/^\|([^|]+)\|/gm)].flatMap((row) =>
[...row[1].matchAll(/`([^`]+)`/g)].map((term) => term[1])
);
}
it('places its sections in the order a manual is read in', () => {
const sections = [...page.matchAll(/^\.SH (.+)$/gm)].map((match) => match[1]);
@@ -232,14 +220,34 @@ describe('the sections a command tree cannot supply', () => {
]);
});
it('documents the same exit codes as the CLI reference', () => {
expect(EXIT_STATUS.map(([code]) => code)).toEqual(tableTerms('Exit Codes'));
it('names an environment variable the CLI actually reads', () => {
// These lists are not pinned to a prose page. Parity against `docs/cli.md`
// would tie the manual to the tree `docs-lab/README.md` retires, and it
// read backwards: the legacy table omits 130, which the CLI really does
// exit with. The canonical home for the variables is
// `docs-lab/reference/configuration/environment-variables.md`, still a
// skeleton; re-anchor here once it is written. Until then this holds the
// one property a prose table cannot: every documented variable is one the
// code reads, so the manual cannot advertise a variable that does nothing.
const sources = fg
.sync('src/**/*.ts', { cwd: repoRoot, absolute: true })
.flatMap((file) => [
...fs
.readFileSync(file, 'utf-8')
.matchAll(/process\.env(?:\.([A-Za-z_][A-Za-z0-9_]*)|\[['"]([A-Za-z_][A-Za-z0-9_]*)['"]\])/g),
])
.map((match) => match[1] ?? match[2]);
const readByCode = new Set(sources);
const documented = ENVIRONMENT.flatMap(([term]) => term.split(', '));
expect(documented.filter((name) => !readByCode.has(name))).toEqual([]);
});
it('documents the same environment variables as the CLI reference', () => {
const documented = ENVIRONMENT.flatMap(([term]) => term.split(', '));
expect(documented).toEqual(tableTerms('Environment Variables'));
it('documents the exit codes the CLI can produce', () => {
// 0 and 1 are universal; 130 is the Ctrl-C code the prompts exit with, and
// `docs-lab/reference/cli.md` records it per command (openspec init,
// openspec config profile, openspec workset open).
expect(EXIT_STATUS.map(([code]) => code)).toEqual(['0', '1', '130']);
});
it('renders every entry into the page', () => {