Skip to content

Commit 3fd641a

Browse files
committed
added AGENTS.md & DOCS
1 parent 72f0f50 commit 3fd641a

3 files changed

Lines changed: 137 additions & 0 deletions

File tree

‎.gitattributes‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
.gitattributes export-ignore
22
.github/ export-ignore
33
.gitignore export-ignore
4+
AGENTS.md export-ignore
45
dresscode.neon export-ignore
56
phpstan*.neon export-ignore
7+
docs/ export-ignore
68
tests/ export-ignore
79

810
* text=auto

‎AGENTS.md‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# To My Agents!
2+
3+
It is my fervent wish that this file guide every AI coding agent working with code in this repository.
4+
5+
## Documentation
6+
7+
Any distilled, agent-facing documentation for this package - how it works
8+
internally and the rationale behind key design decisions - lives in `docs/`.
9+
Consult it before non-trivial changes; it is the source of truth from which the
10+
public manual is distilled.
11+
12+
Two small classes, mostly clear from signatures; the value is a few traps - the
13+
help-text-as-schema parser, the `parse()` sentinel, and the two terminal checks.
14+
Read `docs/internals.md` before editing them.
15+
16+
## Project Overview
17+
18+
**Nette Command Line** is a tiny, zero-dependency library with two utilities:
19+
`Parser` (argument/option parsing, including help-text-driven definitions) and
20+
`Console` (terminal color output with capability detection).
21+
22+
- **PHP Version**: 8.2 - 8.6
23+
- **Package**: `nette/command-line`
24+
25+
## Essential Commands
26+
27+
```bash
28+
# Run all tests
29+
vendor/bin/tester tests -s # or: composer tester
30+
vendor/bin/tester tests/Parser.fluent.phpt -s
31+
32+
# Static analysis (PHPStan level 8)
33+
composer phpstan
34+
```
35+
36+
## Conventions
37+
38+
- Every file starts with `declare(strict_types=1);`; **tabs**; everything typed;
39+
DressCode.
40+
- Constants are modern PascalCase (`Parser::Optional`) with deprecated UPPERCASE
41+
aliases kept for BC.
42+
- Tests are Nette Tester `.phpt` under `tests/` (require `bootstrap.php`); use
43+
`test()` / `Assert::same` / `Assert::exception`, no comment before `test()`.
44+
45+
## Working in this repo
46+
47+
- **Help text *is* the schema.** `addFromHelp()` parses formatted help with two
48+
regexes: the option name is the **last** flag on a line, the alias the first; a
49+
`<file>`/`[type]` spec sets required/optional, `...` marks repeatable, `<a|b|c>` an
50+
enum. The `$defaults` array merges over the parsed result (it supplies `RealPath`,
51+
`Normalizer`, etc.); `RealPath` desugars into a `Normalizer`.
52+
- **`parse()` uses an `OptionPresent = true` sentinel.** A bare `--flag` yields the
53+
literal `true`; a value is taken from the next token only if it doesn't start with
54+
`-`. So an optional-value option used bare parses as **`true`, not its fallback** -
55+
the **fallback applies only when the option is absent entirely**. A missing required
56+
*positional argument* throws; a missing required *option* becomes `null`.
57+
- **`parseOnly()` is deliberately dumb** - it parses only the named options, never
58+
validates, never throws (so `--help`/`--version` work despite a missing required
59+
argument). Don't add validation to it.
60+
- **`Console::detectColors()` and `detectTerminal()` are separate on purpose.** Gate
61+
*color* on `detectColors` (honors `NO_COLOR`/`FORCE_COLOR`), but gate
62+
*interactive-only* features (progress bars, prompts) on `detectTerminal` (pure TTY)
63+
- a user may disable color yet still be on a real terminal.
64+
- User-facing how-to (fluent `addSwitch`/`addOption`/`addArgument`, the help-text
65+
format, color codes) is manual material and lives in the public web docs, not here.

‎docs/internals.md‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# CommandLine internals
2+
3+
Two small classes; most of it is clear from signatures. This is the thin
4+
"conventions & traps" layer — the few facts that are expensive to rediscover.
5+
6+
## Help text is the schema (`addFromHelp`)
7+
8+
The clever, non-obvious part: `addFromHelp()` **parses formatted help text into
9+
option definitions** with two regexes. The outer one extracts each option line and
10+
its `(default: …)`; the inner one parses the option syntax itself, and its result
11+
determines everything:
12+
13+
- `--long` / `-s, --long` → the option name is the **last** matched flag (`--long`),
14+
the alias the first (`-s`) when they differ;
15+
- a value spec `<file>` (required), `[type]` (optional) or a bare word (`--width N`,
16+
required) → whether the option takes a value; a trailing `...` → **repeatable**;
17+
`<a|b|c>` → an **enum** (split on `|`);
18+
- a `(default: …)` (or a `[…]` value spec) → the option is optional with that
19+
fallback.
20+
21+
The whole matched flag string must be fully consumed by the inner regex or it throws
22+
`Unable to parse '…'`. The `$defaults` array passed alongside **merges over** the
23+
parsed values (it supplies what help text can't express: `RealPath`, `Normalizer`,
24+
`Enum`, `Repeatable`). `RealPath` is desugared into a `Normalizer` that resolves the
25+
path (composing with any user normalizer). The `ValueType` (`None`/`Optional`/
26+
`Required`) is then derived: no value spec → `None`; optional or has a default →
27+
`Optional`; else `Required`.
28+
29+
## `parse()` and the `OptionPresent` sentinel
30+
31+
`parse()` walks the args with a **positional cursor** (`current()`/`next()` over the
32+
positional options) alongside named-option lookup. A token starting with `-` is an
33+
option, anything else is a positional argument — so **options and arguments may
34+
interleave freely** (an argument can precede an option).
35+
36+
The trap is the sentinel `OptionPresent = true`. A `--flag` with no `=value` yields
37+
this literal `true` as its "value". Then:
38+
39+
- if the option takes a value and the sentinel is still present, the **next token is
40+
consumed as the value only if it does not start with `-`**; otherwise a `Required`
41+
option throws and an `Optional` one keeps the `true`.
42+
- so an optional-value option used bare (`--format`) parses as **`true`**, not its
43+
fallback. The **fallback is used only when the option is absent entirely** (the
44+
final fill-in pass). `enum` validation skips the `true` sentinel — but a
45+
`normalizer` does **not**: it receives the raw `true`. And fallbacks are assigned
46+
as-is, never normalized.
47+
48+
The fill-in pass at the end assigns fallbacks to unused non-`Required` options,
49+
throws for a missing required **positional** argument (a missing required *option*
50+
just becomes `null`), and casts a repeatable value to an array. A repeatable option
51+
accumulates into a list; a non-repeatable one keeps the **last** occurrence.
52+
53+
## `parseOnly()` is a deliberately dumb early-exit path
54+
55+
`parseOnly(['--help', '--version'])` exists so info options work even when a required
56+
argument is missing. It parses **only** the named options, **ignores everything
57+
else, never validates, never throws**, and returns `null` for anything unused (but
58+
respects aliases). Do not add validation to it — that would defeat the "`--help`
59+
before the missing-argument error" use case.
60+
61+
## Console: two distinct terminal checks
62+
63+
`detectColors()` and `detectTerminal()` are **separate on purpose**. `detectTerminal`
64+
is the pure CLI+TTY check (`stream_isatty(STDOUT)`); `detectColors` builds on it and
65+
adds `NO_COLOR` (disables) and `FORCE_COLOR` (overrides the TTY check). The rule:
66+
gate **color** on `detectColors`, but gate **interactive-only features** (progress
67+
bars, line-rewriting, prompts) on `detectTerminal` — a user may set `NO_COLOR` yet
68+
still be on a real terminal that should get a progress indicator. `color($color)`
69+
with the string omitted emits the escape code with **no reset**, for building a
70+
sequence manually (it is the null *string*, not a null color, that skips the reset).

0 commit comments

Comments
 (0)