From a4e0fc21b20705686a74292c1137cd73cb43e6c2 Mon Sep 17 00:00:00 2001 From: Justus Perillieux Date: Fri, 10 Jul 2026 10:43:30 +0200 Subject: [PATCH 1/9] docs: record repository housekeeping design --- ...26-07-10-repository-housekeeping-design.md | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-10-repository-housekeeping-design.md diff --git a/docs/superpowers/specs/2026-07-10-repository-housekeeping-design.md b/docs/superpowers/specs/2026-07-10-repository-housekeeping-design.md new file mode 100644 index 00000000..cae6fe54 --- /dev/null +++ b/docs/superpowers/specs/2026-07-10-repository-housekeeping-design.md @@ -0,0 +1,92 @@ +# Repository Housekeeping Design + +## Goal + +Make the repository easier to maintain by removing obsolete Chainguard configuration, documenting contribution expectations across the TypeScript, Python, and Go SDKs, and replacing the stale CODEOWNERS rules with ownership derived from the `lightning-sdk` commit history and refined by maintainer review. + +## Scope + +- Remove the two files under `.github/chainguard/` and leave no empty directory behind. +- Add a root `CONTRIBUTING.md` and link it from the root `README.md`. +- Replace `.github/CODEOWNERS` with the approved ownership rules below. +- Do not modify SDK implementation or test code. +- Preserve the unrelated untracked `.codex/` directory. + +## Contribution Guide + +The guide will describe the repository as three SDK implementations: + +- TypeScript under `js/` +- Python under `python/` +- Go under `go/` + +It will direct contributors to the language-specific development configuration and commands already present in each subtree instead of duplicating extensive setup instructions. + +The Python section will state these architectural requirements explicitly: + +1. Calls to generated API clients belong only in the `lightning_sdk.api` subpackage. Public modules use that layer instead of calling generated clients directly. +2. Public Python APIs must not require or optionally accept opaque resource IDs from users. They accept names or resource objects and resolve the corresponding IDs internally. +3. Tests should cover the public name-based behavior and keep generated-client interactions behind the API layer. + +## CODEOWNERS Ordering + +Within each rule, owners are ordered by priority: + +1. `@justusschock` +2. `@ethanwharris` +3. `@k223kim` +4. Other owners ordered according to their contribution history for that file or package + +The approved rules are: + +```text +* @justusschock @ethanwharris + +/.github/ @justusschock @ethanwharris @owbone + +/python/ @justusschock @ethanwharris @k223kim +/python/docs/ @justusschock @dhedey +/python/examples/ @justusschock @k223kim + +/python/lightning_sdk/api/ @justusschock @ethanwharris @k223kim +/python/lightning_sdk/cli/ @justusschock @ethanwharris @k223kim +/python/lightning_sdk/llm/ @k223kim @Danidapena +/python/lightning_sdk/pipeline/ @justusschock @tchaton +/python/lightning_sdk/sandbox/ @k223kim @rusenask + +/python/lightning_sdk/agents.py @k223kim @Danidapena +/python/lightning_sdk/base_studio.py @justusschock @ethanwharris @viveque +/python/lightning_sdk/deployment.py @justusschock @tchaton +/python/lightning_sdk/filesystem.py @justusschock @k223kim @owbone +/python/lightning_sdk/job.py @justusschock @ethanwharris +/python/lightning_sdk/k8s_cluster.py @ethanwharris @k223kim +/python/lightning_sdk/lit_container.py @ethanwharris @k223kim +/python/lightning_sdk/machine.py @justusschock @ethanwharris +/python/lightning_sdk/mmt.py @justusschock @ethanwharris +/python/lightning_sdk/models.py @justusschock @ethanwharris +/python/lightning_sdk/organization.py @justusschock @ethanwharris @k223kim +/python/lightning_sdk/owner.py @justusschock @ethanwharris @k223kim +/python/lightning_sdk/sandbox.py @justusschock @k223kim +/python/lightning_sdk/serve.py @k223kim @tchaton +/python/lightning_sdk/status.py @justusschock @ethanwharris @k223kim +/python/lightning_sdk/studio.py @justusschock @ethanwharris @k223kim +/python/lightning_sdk/teamspace.py @justusschock @ethanwharris @k223kim +/python/lightning_sdk/user.py @justusschock @ethanwharris @k223kim + +/js/ @k223kim @rusenask +/go/ @justusschock @owbone +``` + +`__init__.py`, `__version__.py`, `constants.py`, `exceptions.py`, `helpers.py`, tests, and `lightning_cloud` intentionally have no more-specific rule and inherit `/python/`. Every Python module therefore has at least three inherited owners or at least two explicit owners. + +`@viveque` currently needs write access to `Lightning-AI/sdk` before GitHub can enforce the `base_studio.py` rule; the repository owner will grant that access. + +## Verification + +- Confirm `.github/chainguard/` is absent and no repository file refers to the removed configuration. +- Validate that every CODEOWNERS path exists, except paths intentionally acting as subtree patterns. +- Confirm all named CODEOWNERS have write access after `@viveque` is upgraded. +- Check owner ordering and ensure every explicit module rule contains at least two owners. +- Check that the contribution guide describes all three language trees and contains both Python architectural requirements. +- Run Markdown formatting or lint checks available in the repository, plus `git diff --check`. +- Review the final diff to ensure only the approved housekeeping files changed. From 8a159e8fd533333cfa74c25e41dbe3a786a90fc5 Mon Sep 17 00:00:00 2001 From: Justus Perillieux Date: Fri, 10 Jul 2026 10:49:58 +0200 Subject: [PATCH 2/9] docs: include release guide in housekeeping design --- .../2026-07-10-repository-housekeeping-design.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/superpowers/specs/2026-07-10-repository-housekeeping-design.md b/docs/superpowers/specs/2026-07-10-repository-housekeeping-design.md index cae6fe54..52a9c98f 100644 --- a/docs/superpowers/specs/2026-07-10-repository-housekeeping-design.md +++ b/docs/superpowers/specs/2026-07-10-repository-housekeeping-design.md @@ -7,7 +7,7 @@ Make the repository easier to maintain by removing obsolete Chainguard configura ## Scope - Remove the two files under `.github/chainguard/` and leave no empty directory behind. -- Add a root `CONTRIBUTING.md` and link it from the root `README.md`. +- Add root `CONTRIBUTING.md` and `RELEASE.md` guides and link them from the root `README.md`. - Replace `.github/CODEOWNERS` with the approved ownership rules below. - Do not modify SDK implementation or test code. - Preserve the unrelated untracked `.codex/` directory. @@ -28,6 +28,16 @@ The Python section will state these architectural requirements explicitly: 2. Public Python APIs must not require or optionally accept opaque resource IDs from users. They accept names or resource objects and resolve the corresponding IDs internally. 3. Tests should cover the public name-based behavior and keep generated-client interactions behind the API layer. +## Release Guide + +The release guide will document the required sequence without duplicating the GitHub Actions implementation: + +1. Every release candidate must undergo automated internal integration testing before a release tag is created. +2. A tag and GitHub Release may be created only after that testing succeeds. +3. Publishing the GitHub Release triggers the existing workflows that publish the Python package to PyPI and the TypeScript package to npm automatically. + +The guide will link to `.github/workflows/release.yaml` and `.github/workflows/release-npm.yaml` as the source of truth for the publishing automation. + ## CODEOWNERS Ordering Within each rule, owners are ordered by priority: @@ -88,5 +98,6 @@ The approved rules are: - Confirm all named CODEOWNERS have write access after `@viveque` is upgraded. - Check owner ordering and ensure every explicit module rule contains at least two owners. - Check that the contribution guide describes all three language trees and contains both Python architectural requirements. +- Check that the release guide makes automated internal integration testing a pre-tag requirement and accurately describes GitHub Release-based PyPI and npm publishing. - Run Markdown formatting or lint checks available in the repository, plus `git diff --check`. - Review the final diff to ensure only the approved housekeeping files changed. From e958c79a704cf823568b30e4392e00c56de8b397 Mon Sep 17 00:00:00 2001 From: Justus Perillieux Date: Fri, 10 Jul 2026 11:03:46 +0200 Subject: [PATCH 3/9] docs: plan repository housekeeping --- .../2026-07-10-repository-housekeeping.md | 493 ++++++++++++++++++ ...26-07-10-repository-housekeeping-design.md | 17 +- 2 files changed, 502 insertions(+), 8 deletions(-) create mode 100644 docs/superpowers/plans/2026-07-10-repository-housekeeping.md diff --git a/docs/superpowers/plans/2026-07-10-repository-housekeeping.md b/docs/superpowers/plans/2026-07-10-repository-housekeeping.md new file mode 100644 index 00000000..e0324208 --- /dev/null +++ b/docs/superpowers/plans/2026-07-10-repository-housekeeping.md @@ -0,0 +1,493 @@ +# Repository Housekeeping Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Remove obsolete Chainguard configuration, establish history-derived ownership, and add contribution and release guides for the Python, TypeScript, and Go SDKs. + +**Architecture:** Keep the change documentation-only: `.github/CODEOWNERS` defines the reviewed ownership hierarchy, `CONTRIBUTING.md` defines repository and Python contribution boundaries, `RELEASE.md` defines the release gate and automated publishing behavior, and `README.md` routes contributors to both guides. Existing SDK implementation and tests remain unchanged. + +**Tech Stack:** GitHub CODEOWNERS, GitHub Actions, Markdown, Python, TypeScript/Node.js, Go + +## Global Constraints + +- Remove `.github/chainguard/` completely. +- Preserve the unrelated untracked `.codex/` directory. +- Do not modify SDK implementation or test code. +- Calls to generated Python API clients belong only in `python/lightning_sdk/api/`. +- Public Python APIs must not require or optionally accept resource IDs; accept names or resource objects and resolve IDs internally. +- Every Python module must resolve to at least two CODEOWNERS. +- Do not add specific rules for `__init__.py`, `__version__.py`, `constants.py`, `exceptions.py`, `helpers.py`, tests, or `lightning_cloud`. +- Within each rule, order `@justusschock` first, then `@ethanwharris`, then `@k223kim`; order all other owners by contribution history for that file or package. +- Before a release tag is created, the release candidate must undergo automated internal integration testing. +- The first release tag on a given day is `vYYYY.MM.DD`; additional same-day releases are `vYYYY.MM.DD.post0`, then `.post1`, and so on. +- Publishing a GitHub Release automatically publishes to PyPI and npm through the existing workflows. + +______________________________________________________________________ + +### Task 1: Remove Chainguard configuration and replace CODEOWNERS + +**Files:** + +- Delete: `.github/chainguard/grid-sdk-integration-read.sts.yaml` +- Delete: `.github/chainguard/grid-vendor-cloud.sts.yaml` +- Modify: `.github/CODEOWNERS` + +**Interfaces:** + +- Consumes: Contributor history from `../lightning-sdk` and the maintainer-reviewed priority order in the design specification. + +- Produces: Repository ownership rules inherited by all later documentation changes. + +- [ ] **Step 1: Capture the pre-change failure conditions** + +Run: + +```bash +test ! -d .github/chainguard +``` + +Expected: FAIL because `.github/chainguard/` still exists. + +Run: + +```bash +rg -n '^\* @justusschock @ethanwharris$' .github/CODEOWNERS +``` + +Expected: FAIL because the stale CODEOWNERS file does not contain the approved default rule. + +- [ ] **Step 2: Remove the obsolete Chainguard files** + +Delete both tracked files: + +```text +.github/chainguard/grid-sdk-integration-read.sts.yaml +.github/chainguard/grid-vendor-cloud.sts.yaml +``` + +The directory disappears once both files are deleted. + +- [ ] **Step 3: Replace `.github/CODEOWNERS` with the approved rules** + +Write exactly: + +```text +* @justusschock @ethanwharris + +/.github/ @justusschock @ethanwharris @owbone + +/python/ @justusschock @ethanwharris @k223kim +/python/docs/ @justusschock @dhedey +/python/examples/ @justusschock @k223kim + +/python/lightning_sdk/api/ @justusschock @ethanwharris @k223kim +/python/lightning_sdk/cli/ @justusschock @ethanwharris @k223kim +/python/lightning_sdk/llm/ @k223kim @Danidapena +/python/lightning_sdk/pipeline/ @justusschock @tchaton +/python/lightning_sdk/sandbox/ @k223kim @rusenask + +/python/lightning_sdk/agents.py @k223kim @Danidapena +/python/lightning_sdk/base_studio.py @justusschock @ethanwharris @viveque +/python/lightning_sdk/deployment.py @justusschock @tchaton +/python/lightning_sdk/filesystem.py @justusschock @k223kim @owbone +/python/lightning_sdk/job.py @justusschock @ethanwharris +/python/lightning_sdk/k8s_cluster.py @ethanwharris @k223kim +/python/lightning_sdk/lit_container.py @ethanwharris @k223kim +/python/lightning_sdk/machine.py @justusschock @ethanwharris +/python/lightning_sdk/mmt.py @justusschock @ethanwharris +/python/lightning_sdk/models.py @justusschock @ethanwharris +/python/lightning_sdk/organization.py @justusschock @ethanwharris @k223kim +/python/lightning_sdk/owner.py @justusschock @ethanwharris @k223kim +/python/lightning_sdk/sandbox.py @justusschock @k223kim +/python/lightning_sdk/serve.py @k223kim @tchaton +/python/lightning_sdk/status.py @justusschock @ethanwharris @k223kim +/python/lightning_sdk/studio.py @justusschock @ethanwharris @k223kim +/python/lightning_sdk/teamspace.py @justusschock @ethanwharris @k223kim +/python/lightning_sdk/user.py @justusschock @ethanwharris @k223kim + +/js/ @k223kim @rusenask +/go/ @justusschock @owbone +``` + +- [ ] **Step 4: Verify removal, rule coverage, and module-owner counts** + +Run: + +```bash +test ! -d .github/chainguard +! rg -n 'chainguard|grid-sdk-integration-read|grid-vendor-cloud' . --hidden -g '!.git/**' -g '!.codex/**' -g '!docs/superpowers/**' +``` + +Expected: PASS with no output. + +Run: + +```bash +while IFS= read -r pattern; do + [ "$pattern" = "*" ] && continue + path="${pattern#/}" + path="${path%/}" + test -e "$path" || { echo "Missing CODEOWNERS path: $pattern"; exit 1; } +done < <(awk 'NF && $1 !~ /^#/ {print $1}' .github/CODEOWNERS) +``` + +Expected: PASS with no output. + +Run: + +```bash +awk '$1 ~ /^\/python\/lightning_sdk\/.*\.py$/ && NF < 3 {print "Too few owners:", $0; failed=1} END {exit failed}' .github/CODEOWNERS +``` + +Expected: PASS with no output; each explicit module line has a path plus at least two owners. Modules without explicit rules inherit the three-owner `/python/` rule. + +- [ ] **Step 5: Verify GitHub permissions for every named owner** + +Run: + +```bash +for owner in justusschock ethanwharris k223kim owbone dhedey Danidapena tchaton rusenask viveque; do + gh api "repos/Lightning-AI/sdk/collaborators/$owner/permission" --jq '"\(.user.login)\t\(.permission)"' +done +``` + +Expected: every owner reports `write`, `maintain`, or `admin`. If `viveque` still reports `read`, stop and report that the approved `base_studio.py` ownership cannot yet be enforced; do not silently remove him. + +- [ ] **Step 6: Commit the ownership and cleanup change** + +```bash +git add .github/CODEOWNERS .github/chainguard/grid-sdk-integration-read.sts.yaml .github/chainguard/grid-vendor-cloud.sts.yaml +git diff --cached --check +git commit -m "chore: update code ownership and remove chainguard config" +``` + +Expected: one commit containing only `.github/CODEOWNERS` and deletion of the two Chainguard files. + +### Task 2: Add the contribution guide + +**Files:** + +- Create: `CONTRIBUTING.md` + +**Interfaces:** + +- Consumes: Existing package layouts and commands from `python/README.md`, `js/README.md`, `go/README.md`, and CI workflows. + +- Produces: Repository-wide contribution expectations and the public Python API boundary used during review. + +- [ ] **Step 1: Capture the missing-guide condition** + +Run: + +```bash +test -f CONTRIBUTING.md +``` + +Expected: FAIL because the repository has no contribution guide. + +- [ ] **Step 2: Create `CONTRIBUTING.md`** + +Write exactly: + +````markdown +# Contributing to Lightning SDK + +Thank you for contributing to the Lightning SDK. Keep changes focused on one +SDK or one shared concern, and include tests and documentation when behavior +changes. + +## Repository structure + +- `python/` contains the Python SDK, CLI, tests, examples, and documentation. +- `js/` contains the TypeScript SDK for Lightning AI sandboxes. +- `go/` contains the Go SDK and its generated internal API client. + +The package-specific READMEs contain additional setup and usage information: +[`python/README.md`](python/README.md), [`js/README.md`](js/README.md), and +[`go/README.md`](go/README.md). + +## Development checks + +Install the Python package from the repository root and run its tests from the +test directory: + +```bash +pip install -r python/tests/requirements.txt +pip install -e "./python[serve]" +cd python/tests +pytest . -vv -s +``` + +Build and test the TypeScript package: + +```bash +cd js +npm ci +npm run build +npm test +``` + +Test the Go package: + +```bash +cd go +go test -count=1 ./... +``` + +Run the checks relevant to the files you changed. Before opening a pull request, +run the repository hooks when available: + +```bash +pre-commit run --all-files +``` + +## Python architecture + +### Keep generated API calls in `lightning_sdk.api` + +Only modules in `python/lightning_sdk/api/` may call the generated API clients. +Public modules must use that API layer instead of importing or calling generated +clients from `python/lightning_sdk/lightning_cloud/` directly. This keeps +generated transport details out of the public SDK surface. + +### Accept names, not IDs + +Public Python APIs must not require or optionally accept opaque resource IDs +from users. Accept user-facing names or resource objects and resolve the +corresponding IDs internally through the API layer. IDs may be used internally +or exposed as returned resource state, but users must not need to supply them to +perform an operation. + +Tests should exercise the public name-based behavior and mock generated-client +interactions behind the API layer. + +## Pull requests + +- Keep changes scoped and avoid unrelated cleanup. +- Add or update focused tests for behavior changes. +- Update public documentation when an API or workflow changes. +- Confirm formatting, tests, and package builds relevant to the change pass. +```` + +- [ ] **Step 3: Verify the contribution requirements are explicit** + +Run: + +```bash +rg -n '^## Repository structure$|^## Python architecture$|Only modules in `python/lightning_sdk/api/`|must not require or optionally accept opaque resource IDs' CONTRIBUTING.md +``` + +Expected: four matches covering repository structure and both Python constraints. + +Run: + +```bash +pre-commit run mdformat --files CONTRIBUTING.md +git diff --check -- CONTRIBUTING.md +``` + +Expected: both commands pass. Re-read the formatted file if `mdformat` changes it. + +- [ ] **Step 4: Commit the contribution guide** + +```bash +git add CONTRIBUTING.md +git diff --cached --check +git commit -m "docs: add contribution guide" +``` + +Expected: one commit containing only `CONTRIBUTING.md`. + +### Task 3: Add the release guide and route readers from the README + +**Files:** + +- Create: `RELEASE.md` +- Modify: `README.md:272-302` + +**Interfaces:** + +- Consumes: `.github/workflows/release.yaml` for PyPI and `.github/workflows/release-npm.yaml` for npm. + +- Produces: A maintainer-facing pre-tag gate and discoverable links to both repository guides. + +- [ ] **Step 1: Capture the missing release documentation** + +Run: + +```bash +test -f RELEASE.md +``` + +Expected: FAIL because the repository has no release guide. + +Run: + +```bash +rg -n 'CONTRIBUTING\.md|RELEASE\.md' README.md +``` + +Expected: FAIL because the root README links neither guide. + +- [ ] **Step 2: Create `RELEASE.md`** + +Write exactly: + +```markdown +# Releasing Lightning SDK + +Lightning SDK releases are published automatically from GitHub Releases. + +## Before tagging + +Every release candidate must undergo automated internal integration testing +before a release tag is created. Do not create or push the tag until that +testing has completed successfully. + +## Tag format + +Tags use calendar versions with a `v` prefix: + +- The first release on a given day is `vYYYY.MM.DD`. +- The second release that day is `vYYYY.MM.DD.post0`. +- Further releases increment the suffix sequentially: `.post1`, `.post2`, and + so on. + +## Publish the release + +After automated internal integration testing succeeds: + +1. Create the release tag. +2. Create and publish a GitHub Release for that tag. +3. Monitor the GitHub Actions runs until both publishing workflows complete. + +Publishing the GitHub Release triggers the existing automation: + +- [`.github/workflows/release.yaml`](.github/workflows/release.yaml) builds the + Python distribution and publishes `lightning-sdk` to PyPI. +- [`.github/workflows/release-npm.yaml`](.github/workflows/release-npm.yaml) + builds the TypeScript distribution and publishes `@lightningai/sdk` to npm. + +The GitHub Release is the publishing mechanism; normal releases do not require +manual uploads to PyPI or npm. +``` + +- [ ] **Step 3: Link both guides from the root README** + +After the package-specific README paragraph in the `Development` section, add: + +```markdown + +See [`CONTRIBUTING.md`](CONTRIBUTING.md) for repository-wide contribution +requirements and development checks. +``` + +At the end of the `Community` section, add: + +```markdown + +Maintainers preparing a release should follow [`RELEASE.md`](RELEASE.md). +``` + +- [ ] **Step 4: Verify the release claims against the workflows** + +Run: + +```bash +rg -n 'release:|types: \[published\]|gh-action-pypi-publish' .github/workflows/release.yaml +rg -n 'release:|types: \[published\]|npm publish' .github/workflows/release-npm.yaml +``` + +Expected: each workflow listens for a published GitHub Release; the Python workflow uses `gh-action-pypi-publish`, and the npm workflow runs `npm publish`. + +Run: + +```bash +rg -n 'automated internal integration testing|vYYYY\.MM\.DD|\.post0|GitHub Release|PyPI|npm' RELEASE.md +rg -n 'CONTRIBUTING\.md|RELEASE\.md' README.md +``` + +Expected: the release guide contains the testing gate and both publishing targets, and the README links both guides. + +Run: + +```bash +pre-commit run mdformat --files RELEASE.md README.md +git diff --check -- RELEASE.md README.md +``` + +Expected: both commands pass. Re-read both files if `mdformat` changes them. + +- [ ] **Step 5: Commit the release guide and README links** + +```bash +git add RELEASE.md README.md +git diff --cached --check +git commit -m "docs: document automated releases" +``` + +Expected: one commit containing only `RELEASE.md` and `README.md`. + +### Task 4: Run final repository verification + +**Files:** + +- Verify: `.github/CODEOWNERS` +- Verify: `CONTRIBUTING.md` +- Verify: `RELEASE.md` +- Verify: `README.md` +- Verify deletion: `.github/chainguard/` + +**Interfaces:** + +- Consumes: The completed outputs from Tasks 1-3. + +- Produces: Evidence that the housekeeping change is internally consistent and contains no unrelated edits. + +- [ ] **Step 1: Run formatting and repository hygiene checks** + +Run: + +```bash +pre-commit run --all-files +git diff --check HEAD~3..HEAD +``` + +Expected: all hooks pass and Git reports no whitespace errors. + +- [ ] **Step 2: Re-run the focused acceptance checks** + +Run: + +```bash +test ! -d .github/chainguard +! rg -n 'chainguard|grid-sdk-integration-read|grid-vendor-cloud' . --hidden -g '!.git/**' -g '!.codex/**' -g '!docs/superpowers/**' +rg -n '^\* @justusschock @ethanwharris$' .github/CODEOWNERS +rg -n 'Only modules in `python/lightning_sdk/api/`|must not require or optionally accept opaque resource IDs' CONTRIBUTING.md +rg -n 'automated internal integration testing|vYYYY\.MM\.DD|\.post0|GitHub Release|PyPI|npm' RELEASE.md +rg -n 'CONTRIBUTING\.md|RELEASE\.md' README.md +``` + +Expected: all commands pass; searches print the approved ownership rule, both Python architecture requirements, the release gate, tag convention, publishing targets, and both README links. + +- [ ] **Step 3: Confirm the final commit range and worktree scope** + +Run: + +```bash +git status --short +git diff --stat a4e0fc21..HEAD +git diff --name-status a4e0fc21..HEAD +``` + +Expected: `.codex/` is the only untracked path. The implementation commits change only `.github/CODEOWNERS`, the two deleted `.github/chainguard/` files, `CONTRIBUTING.md`, `RELEASE.md`, and `README.md`, in addition to the already committed design and plan documents. + +- [ ] **Step 4: Record verification evidence** + +Update the Obsidian agent-memory state with: + +```text +Repository housekeeping implemented. Chainguard configuration removed; ordered CODEOWNERS installed; CONTRIBUTING.md documents TypeScript, Python, and Go structure plus Python API boundaries; RELEASE.md requires automated internal integration testing before tagging and documents GitHub Release-driven PyPI/npm publishing. Record pre-commit result, focused acceptance checks, commit hashes, and the remaining untracked .codex/ directory. +``` + +No additional repository commit is needed unless verification changes a tracked file. diff --git a/docs/superpowers/specs/2026-07-10-repository-housekeeping-design.md b/docs/superpowers/specs/2026-07-10-repository-housekeeping-design.md index 52a9c98f..66ba2e9f 100644 --- a/docs/superpowers/specs/2026-07-10-repository-housekeeping-design.md +++ b/docs/superpowers/specs/2026-07-10-repository-housekeeping-design.md @@ -25,16 +25,17 @@ It will direct contributors to the language-specific development configuration a The Python section will state these architectural requirements explicitly: 1. Calls to generated API clients belong only in the `lightning_sdk.api` subpackage. Public modules use that layer instead of calling generated clients directly. -2. Public Python APIs must not require or optionally accept opaque resource IDs from users. They accept names or resource objects and resolve the corresponding IDs internally. -3. Tests should cover the public name-based behavior and keep generated-client interactions behind the API layer. +1. Public Python APIs must not require or optionally accept opaque resource IDs from users. They accept names or resource objects and resolve the corresponding IDs internally. +1. Tests should cover the public name-based behavior and keep generated-client interactions behind the API layer. ## Release Guide The release guide will document the required sequence without duplicating the GitHub Actions implementation: 1. Every release candidate must undergo automated internal integration testing before a release tag is created. -2. A tag and GitHub Release may be created only after that testing succeeds. -3. Publishing the GitHub Release triggers the existing workflows that publish the Python package to PyPI and the TypeScript package to npm automatically. +1. The first release tag on a given day uses `vYYYY.MM.DD`. Additional releases that day use `.post0`, `.post1`, and subsequent sequential suffixes, for example `v2026.07.10.post0`. +1. A tag and GitHub Release may be created only after testing succeeds. +1. Publishing the GitHub Release triggers the existing workflows that publish the Python package to PyPI and the TypeScript package to npm automatically. The guide will link to `.github/workflows/release.yaml` and `.github/workflows/release-npm.yaml` as the source of truth for the publishing automation. @@ -43,9 +44,9 @@ The guide will link to `.github/workflows/release.yaml` and `.github/workflows/r Within each rule, owners are ordered by priority: 1. `@justusschock` -2. `@ethanwharris` -3. `@k223kim` -4. Other owners ordered according to their contribution history for that file or package +1. `@ethanwharris` +1. `@k223kim` +1. Other owners ordered according to their contribution history for that file or package The approved rules are: @@ -98,6 +99,6 @@ The approved rules are: - Confirm all named CODEOWNERS have write access after `@viveque` is upgraded. - Check owner ordering and ensure every explicit module rule contains at least two owners. - Check that the contribution guide describes all three language trees and contains both Python architectural requirements. -- Check that the release guide makes automated internal integration testing a pre-tag requirement and accurately describes GitHub Release-based PyPI and npm publishing. +- Check that the release guide makes automated internal integration testing a pre-tag requirement, documents the `vYYYY.MM.DD` and `.postN` tag convention, and accurately describes GitHub Release-based PyPI and npm publishing. - Run Markdown formatting or lint checks available in the repository, plus `git diff --check`. - Review the final diff to ensure only the approved housekeeping files changed. From 40e8d78c5aa2f93f9d411bcd3fab2ba0c115406b Mon Sep 17 00:00:00 2001 From: Justus Perillieux Date: Fri, 10 Jul 2026 11:06:54 +0200 Subject: [PATCH 4/9] chore: ignore local worktrees --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 90a93631..40f39aba 100644 --- a/.gitignore +++ b/.gitignore @@ -161,3 +161,4 @@ cython_debug/ .idea/ /uv.lock .vscode* +.worktrees/ From 16d04b5c0014d98baa55b583ea6bbe2c8bdb7a56 Mon Sep 17 00:00:00 2001 From: Justus Perillieux Date: Fri, 10 Jul 2026 12:43:09 +0200 Subject: [PATCH 5/9] docs: split SDK and CLI Sphinx roots --- .github/workflows/docs-build.yml | 20 ++- python/README.md | 3 +- python/docs/Makefile | 22 +-- python/docs/cli/Makefile | 14 ++ .../{source => cli}/_static/copybutton.js | 0 .../{source => cli}/_static/images/icon.svg | 0 .../_static/images/logo-large.svg | 0 .../_static/images/logo-small.svg | 0 .../{source => cli}/_static/images/logo.png | Bin .../{source => cli}/_static/images/logo.svg | 0 python/docs/{source => cli}/_static/main.css | 0 .../_templates/theme_variables.jinja | 0 python/docs/cli/api-key.md | 8 + python/docs/cli/base-studio.md | 8 + python/docs/{source => cli}/conf.py | 48 +----- python/docs/cli/config.md | 8 + python/docs/cli/container.md | 8 + python/docs/cli/cp.md | 8 + python/docs/cli/deployment.md | 8 + python/docs/cli/file.md | 8 + python/docs/cli/folder.md | 8 + python/docs/cli/index.md | 94 ++++++++++++ python/docs/cli/job.md | 8 + python/docs/cli/license.md | 8 + python/docs/cli/machine.md | 8 + python/docs/cli/make.bat | 35 +++++ python/docs/cli/mmt.md | 8 + python/docs/cli/model.md | 8 + python/docs/cli/sandbox.md | 8 + python/docs/cli/ssh.md | 8 + python/docs/cli/studio.md | 8 + python/docs/make.bat | 24 +-- python/docs/sdk/Makefile | 14 ++ python/docs/sdk/_static/copybutton.js | 78 ++++++++++ python/docs/sdk/_static/images/icon.svg | 9 ++ python/docs/sdk/_static/images/logo-large.svg | 9 ++ python/docs/sdk/_static/images/logo-small.svg | 9 ++ python/docs/sdk/_static/images/logo.png | Bin 0 -> 28893 bytes python/docs/sdk/_static/images/logo.svg | 12 ++ python/docs/sdk/_static/main.css | 1 + .../docs/sdk/_templates/theme_variables.jinja | 18 +++ python/docs/{source => sdk}/api.rst | 0 python/docs/{source => sdk}/api/agent.rst | 0 .../docs/{source => sdk}/api/deployment.rst | 0 .../docs/{source => sdk}/api/experiment.rst | 0 python/docs/{source => sdk}/api/job.rst | 0 .../docs/{source => sdk}/api/k8s-cluster.rst | 0 python/docs/{source => sdk}/api/machine.rst | 0 python/docs/{source => sdk}/api/mmt.rst | 0 python/docs/{source => sdk}/api/models.rst | 0 .../docs/{source => sdk}/api/organization.rst | 0 python/docs/{source => sdk}/api/owner.rst | 0 python/docs/{source => sdk}/api/status.rst | 0 python/docs/{source => sdk}/api/studio.rst | 0 python/docs/{source => sdk}/api/teamspace.rst | 0 python/docs/{source => sdk}/api/user.rst | 0 python/docs/sdk/conf.py | 139 ++++++++++++++++++ python/docs/{source => sdk}/examples.rst | 0 .../docs/{source => sdk}/examples/api-cli.rst | 0 .../{source => sdk}/examples/jobs-cli.rst | 0 python/docs/{source => sdk}/examples/jobs.rst | 0 .../{source => sdk}/examples/mmts-cli.rst | 0 python/docs/{source => sdk}/examples/mmts.rst | 0 .../examples/sandboxes-cli.rst | 0 .../{source => sdk}/examples/sandboxes.rst | 0 .../{source => sdk}/examples/studios-cli.rst | 0 .../docs/{source => sdk}/examples/studios.rst | 0 .../examples/teamspaces-cli.rst | 0 .../{source => sdk}/examples/teamspaces.rst | 0 python/docs/{source => sdk}/index.rst | 12 -- python/docs/{source => sdk}/install.rst | 0 python/docs/sdk/make.bat | 35 +++++ python/docs/{source => sdk}/quickstart.rst | 0 python/docs/source/cli.rst | 93 ------------ python/docs/source/cli/api-key.rst | 6 - python/docs/source/cli/base-studio.rst | 6 - python/docs/source/cli/config.rst | 6 - python/docs/source/cli/container.rst | 6 - python/docs/source/cli/cp.rst | 6 - python/docs/source/cli/deployment.rst | 6 - python/docs/source/cli/file.rst | 6 - python/docs/source/cli/folder.rst | 6 - python/docs/source/cli/job.rst | 6 - python/docs/source/cli/license.rst | 6 - python/docs/source/cli/machine.rst | 6 - python/docs/source/cli/mmt.rst | 6 - python/docs/source/cli/model.rst | 6 - python/docs/source/cli/sandbox.rst | 6 - python/docs/source/cli/ssh.rst | 6 - python/docs/source/cli/studio.rst | 6 - python/pyproject.toml | 2 +- 91 files changed, 633 insertions(+), 282 deletions(-) create mode 100644 python/docs/cli/Makefile rename python/docs/{source => cli}/_static/copybutton.js (100%) rename python/docs/{source => cli}/_static/images/icon.svg (100%) rename python/docs/{source => cli}/_static/images/logo-large.svg (100%) rename python/docs/{source => cli}/_static/images/logo-small.svg (100%) rename python/docs/{source => cli}/_static/images/logo.png (100%) rename python/docs/{source => cli}/_static/images/logo.svg (100%) rename python/docs/{source => cli}/_static/main.css (100%) rename python/docs/{source => cli}/_templates/theme_variables.jinja (100%) create mode 100644 python/docs/cli/api-key.md create mode 100644 python/docs/cli/base-studio.md rename python/docs/{source => cli}/conf.py (86%) create mode 100644 python/docs/cli/config.md create mode 100644 python/docs/cli/container.md create mode 100644 python/docs/cli/cp.md create mode 100644 python/docs/cli/deployment.md create mode 100644 python/docs/cli/file.md create mode 100644 python/docs/cli/folder.md create mode 100644 python/docs/cli/index.md create mode 100644 python/docs/cli/job.md create mode 100644 python/docs/cli/license.md create mode 100644 python/docs/cli/machine.md create mode 100644 python/docs/cli/make.bat create mode 100644 python/docs/cli/mmt.md create mode 100644 python/docs/cli/model.md create mode 100644 python/docs/cli/sandbox.md create mode 100644 python/docs/cli/ssh.md create mode 100644 python/docs/cli/studio.md create mode 100644 python/docs/sdk/Makefile create mode 100644 python/docs/sdk/_static/copybutton.js create mode 100644 python/docs/sdk/_static/images/icon.svg create mode 100644 python/docs/sdk/_static/images/logo-large.svg create mode 100644 python/docs/sdk/_static/images/logo-small.svg create mode 100644 python/docs/sdk/_static/images/logo.png create mode 100644 python/docs/sdk/_static/images/logo.svg create mode 100644 python/docs/sdk/_static/main.css create mode 100644 python/docs/sdk/_templates/theme_variables.jinja rename python/docs/{source => sdk}/api.rst (100%) rename python/docs/{source => sdk}/api/agent.rst (100%) rename python/docs/{source => sdk}/api/deployment.rst (100%) rename python/docs/{source => sdk}/api/experiment.rst (100%) rename python/docs/{source => sdk}/api/job.rst (100%) rename python/docs/{source => sdk}/api/k8s-cluster.rst (100%) rename python/docs/{source => sdk}/api/machine.rst (100%) rename python/docs/{source => sdk}/api/mmt.rst (100%) rename python/docs/{source => sdk}/api/models.rst (100%) rename python/docs/{source => sdk}/api/organization.rst (100%) rename python/docs/{source => sdk}/api/owner.rst (100%) rename python/docs/{source => sdk}/api/status.rst (100%) rename python/docs/{source => sdk}/api/studio.rst (100%) rename python/docs/{source => sdk}/api/teamspace.rst (100%) rename python/docs/{source => sdk}/api/user.rst (100%) create mode 100644 python/docs/sdk/conf.py rename python/docs/{source => sdk}/examples.rst (100%) rename python/docs/{source => sdk}/examples/api-cli.rst (100%) rename python/docs/{source => sdk}/examples/jobs-cli.rst (100%) rename python/docs/{source => sdk}/examples/jobs.rst (100%) rename python/docs/{source => sdk}/examples/mmts-cli.rst (100%) rename python/docs/{source => sdk}/examples/mmts.rst (100%) rename python/docs/{source => sdk}/examples/sandboxes-cli.rst (100%) rename python/docs/{source => sdk}/examples/sandboxes.rst (100%) rename python/docs/{source => sdk}/examples/studios-cli.rst (100%) rename python/docs/{source => sdk}/examples/studios.rst (100%) rename python/docs/{source => sdk}/examples/teamspaces-cli.rst (100%) rename python/docs/{source => sdk}/examples/teamspaces.rst (100%) rename python/docs/{source => sdk}/index.rst (88%) rename python/docs/{source => sdk}/install.rst (100%) create mode 100644 python/docs/sdk/make.bat rename python/docs/{source => sdk}/quickstart.rst (100%) delete mode 100644 python/docs/source/cli.rst delete mode 100644 python/docs/source/cli/api-key.rst delete mode 100644 python/docs/source/cli/base-studio.rst delete mode 100644 python/docs/source/cli/config.rst delete mode 100644 python/docs/source/cli/container.rst delete mode 100644 python/docs/source/cli/cp.rst delete mode 100644 python/docs/source/cli/deployment.rst delete mode 100644 python/docs/source/cli/file.rst delete mode 100644 python/docs/source/cli/folder.rst delete mode 100644 python/docs/source/cli/job.rst delete mode 100644 python/docs/source/cli/license.rst delete mode 100644 python/docs/source/cli/machine.rst delete mode 100644 python/docs/source/cli/mmt.rst delete mode 100644 python/docs/source/cli/model.rst delete mode 100644 python/docs/source/cli/sandbox.rst delete mode 100644 python/docs/source/cli/ssh.rst delete mode 100644 python/docs/source/cli/studio.rst diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index d1f0609f..3106a23e 100644 --- a/.github/workflows/docs-build.yml +++ b/.github/workflows/docs-build.yml @@ -9,6 +9,10 @@ on: jobs: docs-build: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + root: [sdk, cli] if: github.event.pull_request.draft == false timeout-minutes: 15 @@ -30,21 +34,25 @@ jobs: uv pip list - name: Build docs - working-directory: python/docs + working-directory: python/docs/${{ matrix.root }} run: | - export PATH="$PWD/../.venv/bin:$PATH" + export PATH="$PWD/../../.venv/bin:$PATH" make html SPHINXOPTS="-W --keep-going" - name: Upload docs artifact if: github.event_name == 'pull_request' uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: - name: docs-build - path: python/docs/build/html/ + name: docs-${{ matrix.root }} + path: python/docs/${{ matrix.root }}/build/html/ retention-days: 7 docs-linkcheck: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + root: [sdk, cli] if: github.event.pull_request.draft == false timeout-minutes: 15 @@ -65,9 +73,9 @@ jobs: uv pip install -e . - name: Check links - working-directory: python/docs + working-directory: python/docs/${{ matrix.root }} run: | - export PATH="$PWD/../.venv/bin:$PATH" + export PATH="$PWD/../../.venv/bin:$PATH" make linkcheck guardian-docs: diff --git a/python/README.md b/python/README.md index 1eea5542..6184ba44 100644 --- a/python/README.md +++ b/python/README.md @@ -197,7 +197,8 @@ pip install -e . Build docs from the repository root: ```bash -uv run --group docs sphinx-build -M html python/docs/source python/docs/build -W --keep-going +uv run --group docs sphinx-build -M html python/docs/sdk python/docs/sdk/build -W --keep-going +uv run --group docs sphinx-build -M html python/docs/cli python/docs/cli/build -W --keep-going ``` # License diff --git a/python/docs/Makefile b/python/docs/Makefile index 713e2915..c7222ae8 100644 --- a/python/docs/Makefile +++ b/python/docs/Makefile @@ -1,14 +1,18 @@ -# Minimal makefile for Sphinx documentation +# Build both independent Sphinx documentation roots. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -SOURCEDIR = source -BUILDDIR = build +SPHINXOPTS ?= +ROOTS = sdk cli + +.PHONY: help html linkcheck clean help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + @echo "Build targets: html, linkcheck, clean" + +html: + @for root in $(ROOTS); do $(MAKE) -C $$root html SPHINXOPTS="$(SPHINXOPTS)" || exit $$?; done -.PHONY: help Makefile +linkcheck: + @for root in $(ROOTS); do $(MAKE) -C $$root linkcheck SPHINXOPTS="$(SPHINXOPTS)" || exit $$?; done -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) +clean: + @for root in $(ROOTS); do $(MAKE) -C $$root clean || exit $$?; done diff --git a/python/docs/cli/Makefile b/python/docs/cli/Makefile new file mode 100644 index 00000000..91116bf4 --- /dev/null +++ b/python/docs/cli/Makefile @@ -0,0 +1,14 @@ +# Minimal makefile for Sphinx documentation + +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SOURCEDIR = . +BUILDDIR = build + +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/python/docs/source/_static/copybutton.js b/python/docs/cli/_static/copybutton.js similarity index 100% rename from python/docs/source/_static/copybutton.js rename to python/docs/cli/_static/copybutton.js diff --git a/python/docs/source/_static/images/icon.svg b/python/docs/cli/_static/images/icon.svg similarity index 100% rename from python/docs/source/_static/images/icon.svg rename to python/docs/cli/_static/images/icon.svg diff --git a/python/docs/source/_static/images/logo-large.svg b/python/docs/cli/_static/images/logo-large.svg similarity index 100% rename from python/docs/source/_static/images/logo-large.svg rename to python/docs/cli/_static/images/logo-large.svg diff --git a/python/docs/source/_static/images/logo-small.svg b/python/docs/cli/_static/images/logo-small.svg similarity index 100% rename from python/docs/source/_static/images/logo-small.svg rename to python/docs/cli/_static/images/logo-small.svg diff --git a/python/docs/source/_static/images/logo.png b/python/docs/cli/_static/images/logo.png similarity index 100% rename from python/docs/source/_static/images/logo.png rename to python/docs/cli/_static/images/logo.png diff --git a/python/docs/source/_static/images/logo.svg b/python/docs/cli/_static/images/logo.svg similarity index 100% rename from python/docs/source/_static/images/logo.svg rename to python/docs/cli/_static/images/logo.svg diff --git a/python/docs/source/_static/main.css b/python/docs/cli/_static/main.css similarity index 100% rename from python/docs/source/_static/main.css rename to python/docs/cli/_static/main.css diff --git a/python/docs/source/_templates/theme_variables.jinja b/python/docs/cli/_templates/theme_variables.jinja similarity index 100% rename from python/docs/source/_templates/theme_variables.jinja rename to python/docs/cli/_templates/theme_variables.jinja diff --git a/python/docs/cli/api-key.md b/python/docs/cli/api-key.md new file mode 100644 index 00000000..40574888 --- /dev/null +++ b/python/docs/cli/api-key.md @@ -0,0 +1,8 @@ +# API Key + +The command reference is generated from the installed Click command tree. + +```{click:tree} api_key +:root-label: lightning api-key --help +from lightning_sdk.cli.groups import api_key +``` diff --git a/python/docs/cli/base-studio.md b/python/docs/cli/base-studio.md new file mode 100644 index 00000000..f607b517 --- /dev/null +++ b/python/docs/cli/base-studio.md @@ -0,0 +1,8 @@ +# Base Studio + +The command reference is generated from the installed Click command tree. + +```{click:tree} base_studio +:root-label: lightning base-studio --help +from lightning_sdk.cli.groups import base_studio +``` diff --git a/python/docs/source/conf.py b/python/docs/cli/conf.py similarity index 86% rename from python/docs/source/conf.py rename to python/docs/cli/conf.py index 49ba2151..ca7645d3 100644 --- a/python/docs/source/conf.py +++ b/python/docs/cli/conf.py @@ -96,47 +96,6 @@ def _apply_cli_argument_help(command: click.Command, command_path: str) -> None: _apply_cli_argument_help(_main_cli, "lightning") -def _strip_click_bar(line: str) -> str: - return line[2:] if line.startswith("| ") else line - - -def _is_cli_example_heading(line: str) -> bool: - return _strip_click_bar(line).strip() in {"Example:", "Examples:"} - - -def _is_cli_example_line(line: str) -> bool: - line = _strip_click_bar(line) - return line == "" or line.startswith(" ") - - -def _format_cli_examples_as_code_blocks(app, ctx, lines: list[str]) -> None: # noqa: ANN001, ARG001 - formatted: list[str] = [] - index = 0 - - while index < len(lines): - line = lines[index] - if not _is_cli_example_heading(line): - formatted.append(line) - index += 1 - continue - - formatted.extend([_strip_click_bar(line).strip(), "", ".. code-block:: console", ""]) - index += 1 - - example_lines: list[str] = [] - while index < len(lines) and _is_cli_example_line(lines[index]): - example_lines.append(_strip_click_bar(lines[index])) - index += 1 - - while example_lines and example_lines[-1] == "": - example_lines.pop() - - formatted.extend(f" {example_line.lstrip()}" if example_line else "" for example_line in example_lines) - formatted.append("") - - lines[:] = formatted - - project = "Lightning SDK" copyright = "Lightning AI" # noqa: A001 author = "Lightning AI" @@ -155,13 +114,15 @@ def _format_cli_examples_as_code_blocks(app, ctx, lines: list[str]) -> None: # "sphinx.ext.autosectionlabel", "sphinx.ext.todo", "sphinx_autodoc_typehints", - "sphinx_click", + "click_extra.sphinx", "sphinx_copybutton", "sphinx_paramlinks", "sphinx_togglebutton", "myst_parser", ] +click_extra_enable_exec_directives = True + templates_path = ["_templates"] myst_heading_anchors = 3 @@ -254,8 +215,6 @@ def _can_resolve_host(hostname: str) -> bool: "tqdm", "backoff", ] -sphinx_click_mock_imports = autodoc_mock_imports - copybutton_prompt_text = ">>> " copybutton_prompt_text1 = "... " copybutton_only_copy_prompt_lines = True @@ -270,4 +229,3 @@ def _can_resolve_host(hostname: str) -> bool: def setup(app) -> None: # noqa: ANN001 app.add_css_file("main.css") - app.connect("sphinx-click-process-description", _format_cli_examples_as_code_blocks) diff --git a/python/docs/cli/config.md b/python/docs/cli/config.md new file mode 100644 index 00000000..7ef7bc66 --- /dev/null +++ b/python/docs/cli/config.md @@ -0,0 +1,8 @@ +# Config + +The command reference is generated from the installed Click command tree. + +```{click:tree} config +:root-label: lightning config --help +from lightning_sdk.cli.groups import config +``` diff --git a/python/docs/cli/container.md b/python/docs/cli/container.md new file mode 100644 index 00000000..3d81c61c --- /dev/null +++ b/python/docs/cli/container.md @@ -0,0 +1,8 @@ +# Container + +The command reference is generated from the installed Click command tree. + +```{click:tree} container +:root-label: lightning container --help +from lightning_sdk.cli.groups import container +``` diff --git a/python/docs/cli/cp.md b/python/docs/cli/cp.md new file mode 100644 index 00000000..d43eae37 --- /dev/null +++ b/python/docs/cli/cp.md @@ -0,0 +1,8 @@ +# Copy + +The command reference is generated from the installed Click command tree. + +```{click:tree} cp +:root-label: lightning cp --help +from lightning_sdk.cli.groups import cp +``` diff --git a/python/docs/cli/deployment.md b/python/docs/cli/deployment.md new file mode 100644 index 00000000..12679d52 --- /dev/null +++ b/python/docs/cli/deployment.md @@ -0,0 +1,8 @@ +# Deployment + +The command reference is generated from the installed Click command tree. + +```{click:tree} deployment +:root-label: lightning deployment --help +from lightning_sdk.cli.groups import deployment +``` diff --git a/python/docs/cli/file.md b/python/docs/cli/file.md new file mode 100644 index 00000000..a0062887 --- /dev/null +++ b/python/docs/cli/file.md @@ -0,0 +1,8 @@ +# File + +The command reference is generated from the installed Click command tree. + +```{click:tree} file +:root-label: lightning file --help +from lightning_sdk.cli.groups import file +``` diff --git a/python/docs/cli/folder.md b/python/docs/cli/folder.md new file mode 100644 index 00000000..5717c9d0 --- /dev/null +++ b/python/docs/cli/folder.md @@ -0,0 +1,8 @@ +# Folder + +The command reference is generated from the installed Click command tree. + +```{click:tree} folder +:root-label: lightning folder --help +from lightning_sdk.cli.groups import folder +``` diff --git a/python/docs/cli/index.md b/python/docs/cli/index.md new file mode 100644 index 00000000..bf4489df --- /dev/null +++ b/python/docs/cli/index.md @@ -0,0 +1,94 @@ +# Command Line Interface + +The Python package installs the `lightning` command. The `lightning-sdk` +console script is an alias for the same command group. + +Use the CLI when you want to manage Lightning AI resources from a terminal, +CI job, shell script, or other automation. The command reference below is +generated from the Click command tree, so command groups, options, arguments, +and examples stay aligned with the installed package. + +The live command tree below is expanded at documentation build time. Click +Extra supplies the headings and terminal fences, while the imported command +object remains the SDK's existing `rich-click` CLI. + +```{click:tree} main_cli +:root-label: lightning --help +:anchor-prefix: lightning +from lightning_sdk.cli.entrypoint import main_cli +``` + +## Install + +Install or upgrade the Python package: + +```{code-block} bash +pip install lightning-sdk -U +``` + +## Authenticate + +For interactive use, sign in with: + +```{code-block} bash +lightning login +``` + +For non-interactive environments, configure credentials through environment +variables instead: + +```{code-block} bash +export LIGHTNING_USER_ID=your-user-id +export LIGHTNING_API_KEY=your-api-key +``` + +## Usage + +Run a command group directly: + +```{code-block} bash +lightning [command] +``` + +Every command and subcommand exposes help from the same Click definitions used +by this reference: + +```{code-block} bash +lightning COMMAND --help +``` + +## Common Workflows + +- Develop interactively with {doc}`studio`. +- Submit and inspect training or batch work with {doc}`job` and {doc}`mmt`. +- Build and operate inference services with {doc}`deployment` and {doc}`model`. +- Move data and artifacts with {doc}`file`, {doc}`folder`, {doc}`container`, and {doc}`cp`. +- Configure accounts, organizations, teamspaces, cloud accounts, and SSH with + {doc}`config`, {doc}`api-key`, and {doc}`ssh`. +- Manage lower-level sandbox sessions with {doc}`sandbox`. + +## Command details + +The pages below keep focused URLs for each command group and its full option +reference. + +```{toctree} +:maxdepth: 1 + +config +job +mmt +machine +deployment +container +model +api-key +file +folder +ssh +studio +sandbox +base-studio +license +cp +``` diff --git a/python/docs/cli/job.md b/python/docs/cli/job.md new file mode 100644 index 00000000..7191def5 --- /dev/null +++ b/python/docs/cli/job.md @@ -0,0 +1,8 @@ +# Job + +The command reference is generated from the installed Click command tree. + +```{click:tree} job +:root-label: lightning job --help +from lightning_sdk.cli.groups import job +``` diff --git a/python/docs/cli/license.md b/python/docs/cli/license.md new file mode 100644 index 00000000..f024ef61 --- /dev/null +++ b/python/docs/cli/license.md @@ -0,0 +1,8 @@ +# License + +The command reference is generated from the installed Click command tree. + +```{click:tree} license +:root-label: lightning license --help +from lightning_sdk.cli.groups import license +``` diff --git a/python/docs/cli/machine.md b/python/docs/cli/machine.md new file mode 100644 index 00000000..02a42f16 --- /dev/null +++ b/python/docs/cli/machine.md @@ -0,0 +1,8 @@ +# Machine + +The command reference is generated from the installed Click command tree. + +```{click:tree} machine +:root-label: lightning machine --help +from lightning_sdk.cli.groups import machine +``` diff --git a/python/docs/cli/make.bat b/python/docs/cli/make.bat new file mode 100644 index 00000000..c1becfd1 --- /dev/null +++ b/python/docs/cli/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/python/docs/cli/mmt.md b/python/docs/cli/mmt.md new file mode 100644 index 00000000..c312a72f --- /dev/null +++ b/python/docs/cli/mmt.md @@ -0,0 +1,8 @@ +# Multi-Machine Training + +The command reference is generated from the installed Click command tree. + +```{click:tree} mmt +:root-label: lightning mmt --help +from lightning_sdk.cli.groups import mmt +``` diff --git a/python/docs/cli/model.md b/python/docs/cli/model.md new file mode 100644 index 00000000..41022471 --- /dev/null +++ b/python/docs/cli/model.md @@ -0,0 +1,8 @@ +# Model + +The command reference is generated from the installed Click command tree. + +```{click:tree} model +:root-label: lightning model --help +from lightning_sdk.cli.groups import model +``` diff --git a/python/docs/cli/sandbox.md b/python/docs/cli/sandbox.md new file mode 100644 index 00000000..ab979f2e --- /dev/null +++ b/python/docs/cli/sandbox.md @@ -0,0 +1,8 @@ +# Sandbox + +The command reference is generated from the installed Click command tree. + +```{click:tree} sandbox +:root-label: lightning sandbox --help +from lightning_sdk.cli.groups import sandbox +``` diff --git a/python/docs/cli/ssh.md b/python/docs/cli/ssh.md new file mode 100644 index 00000000..a56c1151 --- /dev/null +++ b/python/docs/cli/ssh.md @@ -0,0 +1,8 @@ +# SSH + +The command reference is generated from the installed Click command tree. + +```{click:tree} ssh +:root-label: lightning ssh --help +from lightning_sdk.cli.groups import ssh +``` diff --git a/python/docs/cli/studio.md b/python/docs/cli/studio.md new file mode 100644 index 00000000..449b83cf --- /dev/null +++ b/python/docs/cli/studio.md @@ -0,0 +1,8 @@ +# Studio + +The command reference is generated from the installed Click command tree. + +```{click:tree} studio +:root-label: lightning studio --help +from lightning_sdk.cli.groups import studio +``` diff --git a/python/docs/make.bat b/python/docs/make.bat index 4d9eb83d..b6f1cd11 100644 --- a/python/docs/make.bat +++ b/python/docs/make.bat @@ -2,34 +2,20 @@ pushd %~dp0 -REM Command file for Sphinx documentation +if "%1" == "" goto help if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) -set SOURCEDIR=source -set BUILDDIR=build - -if "%1" == "" goto help -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 +for %%R in (sdk cli) do ( + call %%R\make.bat %1 + if errorlevel 1 exit /b 1 ) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% goto end :help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +echo Build targets: html, linkcheck, clean :end popd diff --git a/python/docs/sdk/Makefile b/python/docs/sdk/Makefile new file mode 100644 index 00000000..91116bf4 --- /dev/null +++ b/python/docs/sdk/Makefile @@ -0,0 +1,14 @@ +# Minimal makefile for Sphinx documentation + +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SOURCEDIR = . +BUILDDIR = build + +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/python/docs/sdk/_static/copybutton.js b/python/docs/sdk/_static/copybutton.js new file mode 100644 index 00000000..aef241ab --- /dev/null +++ b/python/docs/sdk/_static/copybutton.js @@ -0,0 +1,78 @@ +/* Copied from the official Python docs: https://docs.python.org/3/_static/copybutton.js */ +$(document).ready(function () { + /* Add a [>>>] button on the top-right corner of code samples to hide + * the >>> and ... prompts and the output and thus make the code + * copyable. */ + var div = $( + ".highlight-python .highlight," + + ".highlight-python3 .highlight," + + ".highlight-pycon .highlight," + + ".highlight-default .highlight", + ); + var pre = div.find("pre"); + + // get the styles from the current theme + pre.parent().parent().css("position", "relative"); + var hide_text = "Hide the prompts and output"; + var show_text = "Show the prompts and output"; + var border_width = pre.css("border-top-width"); + var border_style = pre.css("border-top-style"); + var border_color = pre.css("border-top-color"); + var button_styles = { + cursor: "pointer", + position: "absolute", + top: "0", + right: "0", + "border-color": border_color, + "border-style": border_style, + "border-width": border_width, + color: border_color, + "text-size": "75%", + "font-family": "monospace", + "padding-left": "0.2em", + "padding-right": "0.2em", + "border-radius": "0 3px 0 0", + }; + + // create and add the button to all the code blocks that contain >>> + div.each(function (index) { + var jthis = $(this); + if (jthis.find(".gp").length > 0) { + var button = $('>>>'); + button.css(button_styles); + button.attr("title", hide_text); + button.data("hidden", "false"); + jthis.prepend(button); + } + // tracebacks (.gt) contain bare text elements that need to be + // wrapped in a span to work with .nextUntil() (see later) + jthis + .find("pre:has(.gt)") + .contents() + .filter(function () { + return this.nodeType == 3 && this.data.trim().length > 0; + }) + .wrap(""); + }); + + // define the behavior of the button when it's clicked + $(".copybutton").click(function (e) { + e.preventDefault(); + var button = $(this); + if (button.data("hidden") === "false") { + // hide the code output + button.parent().find(".go, .gp, .gt").hide(); + button.next("pre").find(".gt").nextUntil(".gp, .go").css("visibility", "hidden"); + button.css("text-decoration", "line-through"); + button.attr("title", show_text); + button.data("hidden", "true"); + } else { + // show the code output + button.parent().find(".go, .gp, .gt").show(); + button.next("pre").find(".gt").nextUntil(".gp, .go").css("visibility", "visible"); + button.css("text-decoration", "none"); + button.attr("title", hide_text); + button.data("hidden", "false"); + } + }); +}); diff --git a/python/docs/sdk/_static/images/icon.svg b/python/docs/sdk/_static/images/icon.svg new file mode 100644 index 00000000..e88fc190 --- /dev/null +++ b/python/docs/sdk/_static/images/icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/python/docs/sdk/_static/images/logo-large.svg b/python/docs/sdk/_static/images/logo-large.svg new file mode 100644 index 00000000..39531f95 --- /dev/null +++ b/python/docs/sdk/_static/images/logo-large.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/python/docs/sdk/_static/images/logo-small.svg b/python/docs/sdk/_static/images/logo-small.svg new file mode 100644 index 00000000..1f523a57 --- /dev/null +++ b/python/docs/sdk/_static/images/logo-small.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/python/docs/sdk/_static/images/logo.png b/python/docs/sdk/_static/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..392c965b243b8d58d064ba536d8c209f2355f53a GIT binary patch literal 28893 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 literal 0 HcmV?d00001 diff --git a/python/docs/sdk/_static/images/logo.svg b/python/docs/sdk/_static/images/logo.svg new file mode 100644 index 00000000..60efaa29 --- /dev/null +++ b/python/docs/sdk/_static/images/logo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/python/docs/sdk/_static/main.css b/python/docs/sdk/_static/main.css new file mode 100644 index 00000000..8739cdf7 --- /dev/null +++ b/python/docs/sdk/_static/main.css @@ -0,0 +1 @@ +/* Custom styles for lightning-sdk docs */ diff --git a/python/docs/sdk/_templates/theme_variables.jinja b/python/docs/sdk/_templates/theme_variables.jinja new file mode 100644 index 00000000..5d092539 --- /dev/null +++ b/python/docs/sdk/_templates/theme_variables.jinja @@ -0,0 +1,18 @@ +{%- set external_urls = { + 'github': 'https://github.com/Lightning-AI/lightning-Sandbox', + 'github_issues': 'https://github.com/Lightning-AI/lightning-Sandbox/issues', + 'contributing': 'https://github.com/Lightning-AI/lightning/blob/master/CONTRIBUTING.md', + 'governance': 'https://github.com/Lightning-AI/lightning/blob/master/governance.md', + 'docs': 'https://lightning-ai.github.io/lightning-Sandbox/', + 'twitter': 'https://twitter.com/LightningAI', + 'discuss': 'https://discord.com/invite/tfXFetEZxv', + 'tutorials': 'https://lightning.ai', + 'previous_pytorch_versions': 'https://lightning-ai.github.io/lightning-Sandbox/', + 'home': 'https://lightning-ai.github.io/lightning-Sandbox/', + 'get_started': 'https://lightning.ai', + 'features': 'https://lightning-ai.github.io/lightning-Sandbox/', + 'blog': 'https://www.Lightning.ai/blog', + 'resources': 'https://lightning.ai', + 'support': 'https://lightning-ai.github.io/lightning-Sandbox/', +} +-%} diff --git a/python/docs/source/api.rst b/python/docs/sdk/api.rst similarity index 100% rename from python/docs/source/api.rst rename to python/docs/sdk/api.rst diff --git a/python/docs/source/api/agent.rst b/python/docs/sdk/api/agent.rst similarity index 100% rename from python/docs/source/api/agent.rst rename to python/docs/sdk/api/agent.rst diff --git a/python/docs/source/api/deployment.rst b/python/docs/sdk/api/deployment.rst similarity index 100% rename from python/docs/source/api/deployment.rst rename to python/docs/sdk/api/deployment.rst diff --git a/python/docs/source/api/experiment.rst b/python/docs/sdk/api/experiment.rst similarity index 100% rename from python/docs/source/api/experiment.rst rename to python/docs/sdk/api/experiment.rst diff --git a/python/docs/source/api/job.rst b/python/docs/sdk/api/job.rst similarity index 100% rename from python/docs/source/api/job.rst rename to python/docs/sdk/api/job.rst diff --git a/python/docs/source/api/k8s-cluster.rst b/python/docs/sdk/api/k8s-cluster.rst similarity index 100% rename from python/docs/source/api/k8s-cluster.rst rename to python/docs/sdk/api/k8s-cluster.rst diff --git a/python/docs/source/api/machine.rst b/python/docs/sdk/api/machine.rst similarity index 100% rename from python/docs/source/api/machine.rst rename to python/docs/sdk/api/machine.rst diff --git a/python/docs/source/api/mmt.rst b/python/docs/sdk/api/mmt.rst similarity index 100% rename from python/docs/source/api/mmt.rst rename to python/docs/sdk/api/mmt.rst diff --git a/python/docs/source/api/models.rst b/python/docs/sdk/api/models.rst similarity index 100% rename from python/docs/source/api/models.rst rename to python/docs/sdk/api/models.rst diff --git a/python/docs/source/api/organization.rst b/python/docs/sdk/api/organization.rst similarity index 100% rename from python/docs/source/api/organization.rst rename to python/docs/sdk/api/organization.rst diff --git a/python/docs/source/api/owner.rst b/python/docs/sdk/api/owner.rst similarity index 100% rename from python/docs/source/api/owner.rst rename to python/docs/sdk/api/owner.rst diff --git a/python/docs/source/api/status.rst b/python/docs/sdk/api/status.rst similarity index 100% rename from python/docs/source/api/status.rst rename to python/docs/sdk/api/status.rst diff --git a/python/docs/source/api/studio.rst b/python/docs/sdk/api/studio.rst similarity index 100% rename from python/docs/source/api/studio.rst rename to python/docs/sdk/api/studio.rst diff --git a/python/docs/source/api/teamspace.rst b/python/docs/sdk/api/teamspace.rst similarity index 100% rename from python/docs/source/api/teamspace.rst rename to python/docs/sdk/api/teamspace.rst diff --git a/python/docs/source/api/user.rst b/python/docs/sdk/api/user.rst similarity index 100% rename from python/docs/source/api/user.rst rename to python/docs/sdk/api/user.rst diff --git a/python/docs/sdk/conf.py b/python/docs/sdk/conf.py new file mode 100644 index 00000000..b7dcd0a3 --- /dev/null +++ b/python/docs/sdk/conf.py @@ -0,0 +1,139 @@ +import os +import socket +import sys + +_PATH_HERE = os.path.abspath(os.path.dirname(__file__)) +_PATH_ROOT = os.path.realpath(os.path.join(_PATH_HERE, "..", "..")) +sys.path.insert(0, _PATH_ROOT) + +import lightning_sdk # noqa: E402 +project = "Lightning SDK" +copyright = "Lightning AI" # noqa: A001 +author = "Lightning AI" +version = lightning_sdk.__version__ +release = lightning_sdk.__version__ + +needs_sphinx = "8.0" + +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.doctest", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", + "sphinx.ext.viewcode", + "sphinx.ext.autosectionlabel", + "sphinx.ext.todo", + "sphinx_autodoc_typehints", + "sphinx_copybutton", + "sphinx_paramlinks", + "sphinx_togglebutton", + "myst_parser", +] + +templates_path = ["_templates"] + +myst_heading_anchors = 3 + +source_suffix = { + ".rst": "restructuredtext", + ".md": "markdown", +} + +master_doc = "index" +language = "en" +exclude_patterns = ["_build", "_templates"] +pygments_style = None + +html_theme = "pydata_sphinx_theme" +html_theme_options = { + "github_url": "https://github.com/Lightning-AI/lightning-sdk", + "show_toc_level": 2, +} + +html_static_path = ["_static"] +htmlhelp_basename = "lightning-sdk-doc" + + +def _can_resolve_host(hostname: str) -> bool: + try: + socket.getaddrinfo(hostname, 443) + except OSError: + return False + return True + + +if all(_can_resolve_host(host) for host in ("docs.python.org", "pytorch.org")): + intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "torch": ("https://pytorch.org/docs/stable/", None), + } +else: + intersphinx_mapping = {} + +nitpicky = True +nitpick_ignore = [ + ("py:class", "typing.Any"), + ("py:data", "typing.Any"), + ("py:data", "typing.Optional"), + ("py:data", "typing.Union"), + ("py:class", "pathlib.Path"), + ("py:class", "pathlib._local.Path"), + ("py:class", "enum.Enum"), + # base / internal types not worth documenting separately + ("py:class", "Auth"), + ("py:class", "lightning_sdk.api.deployment_api.Auth"), + ("py:class", "MachineDict"), +] +nitpick_ignore_regex = [ + # private / internal base classes + ("py:class", r".*\._\w+"), + # generated openapi types (full path) + ("py:class", r"lightning_sdk\.lightning_cloud\..*"), + # V1* and Externalv1* openapi short names + ("py:class", r"V1\w+"), + ("py:class", r"Externalv1\w+"), +] + +autosummary_generate = True +autodoc_member_order = "groupwise" +autoclass_content = "both" +autodoc_typehints = "description" +typehints_description_target = "documented_params" + +autodoc_default_options = { + "members": True, + "methods": True, + "special-members": "__call__", + "exclude-members": "_abc_impl", + "show-inheritance": True, +} + +autosectionlabel_prefix_document = True +autosectionlabel_maxdepth = 1 + +autodoc_mock_imports = [ + "lightning_cloud", + "requests", + "docker", + "fastapi", + "uvicorn", + "simple_term_menu", + "rich", + "tqdm", + "backoff", +] +copybutton_prompt_text = ">>> " +copybutton_prompt_text1 = "... " +copybutton_only_copy_prompt_lines = True + +linkcheck_anchors = False +linkcheck_timeout = 60 +linkcheck_retries = 3 +linkcheck_ignore = [ + r"https://lightning\.ai/.*", +] + + +def setup(app) -> None: # noqa: ANN001 + app.add_css_file("main.css") diff --git a/python/docs/source/examples.rst b/python/docs/sdk/examples.rst similarity index 100% rename from python/docs/source/examples.rst rename to python/docs/sdk/examples.rst diff --git a/python/docs/source/examples/api-cli.rst b/python/docs/sdk/examples/api-cli.rst similarity index 100% rename from python/docs/source/examples/api-cli.rst rename to python/docs/sdk/examples/api-cli.rst diff --git a/python/docs/source/examples/jobs-cli.rst b/python/docs/sdk/examples/jobs-cli.rst similarity index 100% rename from python/docs/source/examples/jobs-cli.rst rename to python/docs/sdk/examples/jobs-cli.rst diff --git a/python/docs/source/examples/jobs.rst b/python/docs/sdk/examples/jobs.rst similarity index 100% rename from python/docs/source/examples/jobs.rst rename to python/docs/sdk/examples/jobs.rst diff --git a/python/docs/source/examples/mmts-cli.rst b/python/docs/sdk/examples/mmts-cli.rst similarity index 100% rename from python/docs/source/examples/mmts-cli.rst rename to python/docs/sdk/examples/mmts-cli.rst diff --git a/python/docs/source/examples/mmts.rst b/python/docs/sdk/examples/mmts.rst similarity index 100% rename from python/docs/source/examples/mmts.rst rename to python/docs/sdk/examples/mmts.rst diff --git a/python/docs/source/examples/sandboxes-cli.rst b/python/docs/sdk/examples/sandboxes-cli.rst similarity index 100% rename from python/docs/source/examples/sandboxes-cli.rst rename to python/docs/sdk/examples/sandboxes-cli.rst diff --git a/python/docs/source/examples/sandboxes.rst b/python/docs/sdk/examples/sandboxes.rst similarity index 100% rename from python/docs/source/examples/sandboxes.rst rename to python/docs/sdk/examples/sandboxes.rst diff --git a/python/docs/source/examples/studios-cli.rst b/python/docs/sdk/examples/studios-cli.rst similarity index 100% rename from python/docs/source/examples/studios-cli.rst rename to python/docs/sdk/examples/studios-cli.rst diff --git a/python/docs/source/examples/studios.rst b/python/docs/sdk/examples/studios.rst similarity index 100% rename from python/docs/source/examples/studios.rst rename to python/docs/sdk/examples/studios.rst diff --git a/python/docs/source/examples/teamspaces-cli.rst b/python/docs/sdk/examples/teamspaces-cli.rst similarity index 100% rename from python/docs/source/examples/teamspaces-cli.rst rename to python/docs/sdk/examples/teamspaces-cli.rst diff --git a/python/docs/source/examples/teamspaces.rst b/python/docs/sdk/examples/teamspaces.rst similarity index 100% rename from python/docs/source/examples/teamspaces.rst rename to python/docs/sdk/examples/teamspaces.rst diff --git a/python/docs/source/index.rst b/python/docs/sdk/index.rst similarity index 88% rename from python/docs/source/index.rst rename to python/docs/sdk/index.rst index 5e78f425..234127f4 100644 --- a/python/docs/source/index.rst +++ b/python/docs/sdk/index.rst @@ -47,8 +47,6 @@ Documentation Map The :ref:`Examples ` provide tutorial walkthroughs for common CLI and SDK workflows. -The :ref:`CLI reference ` documents the command-line interface. - The :ref:`API reference ` documents every public class and function. .. _start-section: @@ -62,14 +60,4 @@ Start install quickstart examples - cli - -.. _api-reference-section: - -API Reference -============= - -.. toctree:: - :maxdepth: 1 - api diff --git a/python/docs/source/install.rst b/python/docs/sdk/install.rst similarity index 100% rename from python/docs/source/install.rst rename to python/docs/sdk/install.rst diff --git a/python/docs/sdk/make.bat b/python/docs/sdk/make.bat new file mode 100644 index 00000000..c1becfd1 --- /dev/null +++ b/python/docs/sdk/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/python/docs/source/quickstart.rst b/python/docs/sdk/quickstart.rst similarity index 100% rename from python/docs/source/quickstart.rst rename to python/docs/sdk/quickstart.rst diff --git a/python/docs/source/cli.rst b/python/docs/source/cli.rst deleted file mode 100644 index 55c2ae44..00000000 --- a/python/docs/source/cli.rst +++ /dev/null @@ -1,93 +0,0 @@ -Command Line Interface -====================== - -The Python package installs the ``lightning`` command. The ``lightning-sdk`` -console script is an alias for the same command group. - -Use the CLI when you want to manage Lightning AI resources from a terminal, -CI job, shell script, or other automation. The command reference below is -generated from the Click command tree, so command groups, options, arguments, -and examples stay aligned with the installed package. - -Install -------- - -Install or upgrade the Python package: - -.. code-block:: bash - - pip install lightning-sdk -U - -Authenticate ------------- - -For interactive use, sign in with: - -.. code-block:: bash - - lightning login - -For non-interactive environments, configure credentials through environment -variables instead: - -.. code-block:: bash - - export LIGHTNING_USER_ID=your-user-id - export LIGHTNING_API_KEY=your-api-key - -Usage ------ - -Run a command group directly: - -.. code-block:: bash - - lightning [command] - -Every command and subcommand exposes help from the same Click definitions used -by this reference: - -.. code-block:: bash - - lightning COMMAND --help - -Common Workflows ----------------- - -- Develop interactively with :doc:`cli/studio`. -- Submit and inspect training or batch work with :doc:`cli/job` and - :doc:`cli/mmt`. -- Build and operate inference services with :doc:`cli/deployment` and - :doc:`cli/model`. -- Move data and artifacts with :doc:`cli/file`, :doc:`cli/folder`, - :doc:`cli/container`, and :doc:`cli/cp`. -- Configure accounts, organizations, teamspaces, cloud accounts, and SSH with - :doc:`cli/config`, :doc:`cli/api-key`, and :doc:`cli/ssh`. -- Manage lower-level sandbox sessions with :doc:`cli/sandbox`. - -Command Groups --------------- - -.. toctree:: - :maxdepth: 1 - - cli/config - cli/job - cli/mmt - cli/machine - cli/deployment - cli/container - cli/model - cli/api-key - cli/file - cli/folder - cli/ssh - cli/studio - cli/sandbox - cli/base-studio - cli/license - cli/cp - -.. click:: lightning_sdk.cli.entrypoint:main_cli - :prog: lightning - :nested: short diff --git a/python/docs/source/cli/api-key.rst b/python/docs/source/cli/api-key.rst deleted file mode 100644 index 0ffcbc03..00000000 --- a/python/docs/source/cli/api-key.rst +++ /dev/null @@ -1,6 +0,0 @@ -API Key -======= - -.. click:: lightning_sdk.cli.groups:api_key - :prog: lightning api-key - :nested: full diff --git a/python/docs/source/cli/base-studio.rst b/python/docs/source/cli/base-studio.rst deleted file mode 100644 index f9cc7415..00000000 --- a/python/docs/source/cli/base-studio.rst +++ /dev/null @@ -1,6 +0,0 @@ -Base Studio -=========== - -.. click:: lightning_sdk.cli.groups:base_studio - :prog: lightning base-studio - :nested: full diff --git a/python/docs/source/cli/config.rst b/python/docs/source/cli/config.rst deleted file mode 100644 index e33ec115..00000000 --- a/python/docs/source/cli/config.rst +++ /dev/null @@ -1,6 +0,0 @@ -Config -====== - -.. click:: lightning_sdk.cli.groups:config - :prog: lightning config - :nested: full diff --git a/python/docs/source/cli/container.rst b/python/docs/source/cli/container.rst deleted file mode 100644 index 4c4a8143..00000000 --- a/python/docs/source/cli/container.rst +++ /dev/null @@ -1,6 +0,0 @@ -Container -========= - -.. click:: lightning_sdk.cli.groups:container - :prog: lightning container - :nested: full diff --git a/python/docs/source/cli/cp.rst b/python/docs/source/cli/cp.rst deleted file mode 100644 index 57598bb5..00000000 --- a/python/docs/source/cli/cp.rst +++ /dev/null @@ -1,6 +0,0 @@ -Copy -==== - -.. click:: lightning_sdk.cli.groups:cp - :prog: lightning cp - :nested: full diff --git a/python/docs/source/cli/deployment.rst b/python/docs/source/cli/deployment.rst deleted file mode 100644 index 1c6a1c65..00000000 --- a/python/docs/source/cli/deployment.rst +++ /dev/null @@ -1,6 +0,0 @@ -Deployment -========== - -.. click:: lightning_sdk.cli.groups:deployment - :prog: lightning deployment - :nested: full diff --git a/python/docs/source/cli/file.rst b/python/docs/source/cli/file.rst deleted file mode 100644 index 5cf5cfe8..00000000 --- a/python/docs/source/cli/file.rst +++ /dev/null @@ -1,6 +0,0 @@ -File -==== - -.. click:: lightning_sdk.cli.groups:file - :prog: lightning file - :nested: full diff --git a/python/docs/source/cli/folder.rst b/python/docs/source/cli/folder.rst deleted file mode 100644 index 35394def..00000000 --- a/python/docs/source/cli/folder.rst +++ /dev/null @@ -1,6 +0,0 @@ -Folder -====== - -.. click:: lightning_sdk.cli.groups:folder - :prog: lightning folder - :nested: full diff --git a/python/docs/source/cli/job.rst b/python/docs/source/cli/job.rst deleted file mode 100644 index 1741d463..00000000 --- a/python/docs/source/cli/job.rst +++ /dev/null @@ -1,6 +0,0 @@ -Job -=== - -.. click:: lightning_sdk.cli.groups:job - :prog: lightning job - :nested: full diff --git a/python/docs/source/cli/license.rst b/python/docs/source/cli/license.rst deleted file mode 100644 index 22ed5662..00000000 --- a/python/docs/source/cli/license.rst +++ /dev/null @@ -1,6 +0,0 @@ -License -======= - -.. click:: lightning_sdk.cli.groups:license - :prog: lightning license - :nested: full diff --git a/python/docs/source/cli/machine.rst b/python/docs/source/cli/machine.rst deleted file mode 100644 index c940cc95..00000000 --- a/python/docs/source/cli/machine.rst +++ /dev/null @@ -1,6 +0,0 @@ -Machine -======= - -.. click:: lightning_sdk.cli.groups:machine - :prog: lightning machine - :nested: full diff --git a/python/docs/source/cli/mmt.rst b/python/docs/source/cli/mmt.rst deleted file mode 100644 index 80819f7b..00000000 --- a/python/docs/source/cli/mmt.rst +++ /dev/null @@ -1,6 +0,0 @@ -Multi-Machine Training -====================== - -.. click:: lightning_sdk.cli.groups:mmt - :prog: lightning mmt - :nested: full diff --git a/python/docs/source/cli/model.rst b/python/docs/source/cli/model.rst deleted file mode 100644 index c5297b4d..00000000 --- a/python/docs/source/cli/model.rst +++ /dev/null @@ -1,6 +0,0 @@ -Model -===== - -.. click:: lightning_sdk.cli.groups:model - :prog: lightning model - :nested: full diff --git a/python/docs/source/cli/sandbox.rst b/python/docs/source/cli/sandbox.rst deleted file mode 100644 index 7069759d..00000000 --- a/python/docs/source/cli/sandbox.rst +++ /dev/null @@ -1,6 +0,0 @@ -Sandbox -======= - -.. click:: lightning_sdk.cli.groups:sandbox - :prog: lightning sandbox - :nested: full diff --git a/python/docs/source/cli/ssh.rst b/python/docs/source/cli/ssh.rst deleted file mode 100644 index 78b058c2..00000000 --- a/python/docs/source/cli/ssh.rst +++ /dev/null @@ -1,6 +0,0 @@ -SSH -=== - -.. click:: lightning_sdk.cli.groups:ssh - :prog: lightning ssh - :nested: full diff --git a/python/docs/source/cli/studio.rst b/python/docs/source/cli/studio.rst deleted file mode 100644 index d8b9d936..00000000 --- a/python/docs/source/cli/studio.rst +++ /dev/null @@ -1,6 +0,0 @@ -Studio -====== - -.. click:: lightning_sdk.cli.groups:studio - :prog: lightning studio - :nested: full diff --git a/python/pyproject.toml b/python/pyproject.toml index a51b62e3..36fea1cf 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -50,7 +50,7 @@ serve = ["litserve>=0.2.5"] [dependency-groups] docs = [ "sphinx>=8.0,<9.0", - "sphinx-click>=6.2,<7.0", + "click-extra[sphinx]>=8.3,<9.0", "myst-parser>=5.0.0", "docutils>=0.20", "sphinx-autodoc-typehints>=1.16", From d7846c282136440d671f534a08da04dba3170712 Mon Sep 17 00:00:00 2001 From: Justus Perillieux Date: Fri, 10 Jul 2026 16:02:08 +0200 Subject: [PATCH 6/9] docs: fix CLI help rendering --- python/docs/cli/conf.py | 8 +++++--- python/docs/cli/index.md | 3 +++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/python/docs/cli/conf.py b/python/docs/cli/conf.py index ca7645d3..a7d68c21 100644 --- a/python/docs/cli/conf.py +++ b/python/docs/cli/conf.py @@ -3,17 +3,19 @@ import sys import click -from click.formatting import HelpFormatter _PATH_HERE = os.path.abspath(os.path.dirname(__file__)) _PATH_ROOT = os.path.realpath(os.path.join(_PATH_HERE, "..", "..")) sys.path.insert(0, _PATH_ROOT) +# The legacy forwarding commands intentionally exit with a migration error when +# invoked. Keep them out of the generated reference while documenting the +# supported noun-first command tree. +os.environ["LIGHTNING_EXPERIMENTAL_CLI_ONLY"] = "1" + import lightning_sdk # noqa: E402 from lightning_sdk.cli.entrypoint import main_cli as _main_cli # noqa: E402 -_main_cli.context_class.formatter_class = HelpFormatter - _CLI_ARGUMENT_HELP = { ("lightning config set user", "user_name"): "User name to make active in the local Lightning CLI config.", ("lightning config set org", "org_name"): "Organization name to make active in the local Lightning CLI config.", diff --git a/python/docs/cli/index.md b/python/docs/cli/index.md index bf4489df..06cde4eb 100644 --- a/python/docs/cli/index.md +++ b/python/docs/cli/index.md @@ -12,6 +12,9 @@ The live command tree below is expanded at documentation build time. Click Extra supplies the headings and terminal fences, while the imported command object remains the SDK's existing `rich-click` CLI. +Legacy aliases that only emit a migration error are intentionally omitted; +their supported noun-first replacements are documented instead. + ```{click:tree} main_cli :root-label: lightning --help :anchor-prefix: lightning From 933944c147f2ed2c249031d7d59c9ed7b604d118 Mon Sep 17 00:00:00 2001 From: Justus Perillieux Date: Fri, 10 Jul 2026 23:26:04 +0200 Subject: [PATCH 7/9] docs: render CLI reference from Click metadata --- .gitignore | 1 + .../2026-07-10-repository-housekeeping.md | 493 ------------------ ...26-07-10-repository-housekeeping-design.md | 104 ---- python/docs/cli/_reference.py | 259 +++++++++ python/docs/cli/api-key.md | 8 - python/docs/cli/api-key.rst | 8 + python/docs/cli/base-studio.md | 8 - python/docs/cli/base-studio.rst | 8 + python/docs/cli/conf.py | 7 +- python/docs/cli/config.md | 8 - python/docs/cli/config.rst | 8 + python/docs/cli/container.md | 8 - python/docs/cli/container.rst | 8 + python/docs/cli/cp.md | 8 - python/docs/cli/cp.rst | 8 + python/docs/cli/deployment.md | 8 - python/docs/cli/deployment.rst | 8 + python/docs/cli/file.md | 8 - python/docs/cli/file.rst | 8 + python/docs/cli/folder.md | 8 - python/docs/cli/folder.rst | 8 + python/docs/cli/index.md | 97 ---- python/docs/cli/index.rst | 95 ++++ python/docs/cli/job.md | 8 - python/docs/cli/job.rst | 8 + python/docs/cli/license.md | 8 - python/docs/cli/license.rst | 8 + python/docs/cli/machine.md | 8 - python/docs/cli/machine.rst | 8 + python/docs/cli/mmt.md | 8 - python/docs/cli/mmt.rst | 8 + python/docs/cli/model.md | 8 - python/docs/cli/model.rst | 8 + python/docs/cli/sandbox.md | 8 - python/docs/cli/sandbox.rst | 8 + python/docs/cli/ssh.md | 8 - python/docs/cli/ssh.rst | 8 + python/docs/cli/studio.md | 8 - python/docs/cli/studio.rst | 8 + 39 files changed, 486 insertions(+), 826 deletions(-) delete mode 100644 docs/superpowers/plans/2026-07-10-repository-housekeeping.md delete mode 100644 docs/superpowers/specs/2026-07-10-repository-housekeeping-design.md create mode 100644 python/docs/cli/_reference.py delete mode 100644 python/docs/cli/api-key.md create mode 100644 python/docs/cli/api-key.rst delete mode 100644 python/docs/cli/base-studio.md create mode 100644 python/docs/cli/base-studio.rst delete mode 100644 python/docs/cli/config.md create mode 100644 python/docs/cli/config.rst delete mode 100644 python/docs/cli/container.md create mode 100644 python/docs/cli/container.rst delete mode 100644 python/docs/cli/cp.md create mode 100644 python/docs/cli/cp.rst delete mode 100644 python/docs/cli/deployment.md create mode 100644 python/docs/cli/deployment.rst delete mode 100644 python/docs/cli/file.md create mode 100644 python/docs/cli/file.rst delete mode 100644 python/docs/cli/folder.md create mode 100644 python/docs/cli/folder.rst delete mode 100644 python/docs/cli/index.md create mode 100644 python/docs/cli/index.rst delete mode 100644 python/docs/cli/job.md create mode 100644 python/docs/cli/job.rst delete mode 100644 python/docs/cli/license.md create mode 100644 python/docs/cli/license.rst delete mode 100644 python/docs/cli/machine.md create mode 100644 python/docs/cli/machine.rst delete mode 100644 python/docs/cli/mmt.md create mode 100644 python/docs/cli/mmt.rst delete mode 100644 python/docs/cli/model.md create mode 100644 python/docs/cli/model.rst delete mode 100644 python/docs/cli/sandbox.md create mode 100644 python/docs/cli/sandbox.rst delete mode 100644 python/docs/cli/ssh.md create mode 100644 python/docs/cli/ssh.rst delete mode 100644 python/docs/cli/studio.md create mode 100644 python/docs/cli/studio.rst diff --git a/.gitignore b/.gitignore index 40f39aba..b0639ec9 100644 --- a/.gitignore +++ b/.gitignore @@ -69,6 +69,7 @@ instance/ .scrapy # Sphinx documentation +/docs/ docs/_build/ python/docs/_build/ diff --git a/docs/superpowers/plans/2026-07-10-repository-housekeeping.md b/docs/superpowers/plans/2026-07-10-repository-housekeeping.md deleted file mode 100644 index e0324208..00000000 --- a/docs/superpowers/plans/2026-07-10-repository-housekeeping.md +++ /dev/null @@ -1,493 +0,0 @@ -# Repository Housekeeping Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Remove obsolete Chainguard configuration, establish history-derived ownership, and add contribution and release guides for the Python, TypeScript, and Go SDKs. - -**Architecture:** Keep the change documentation-only: `.github/CODEOWNERS` defines the reviewed ownership hierarchy, `CONTRIBUTING.md` defines repository and Python contribution boundaries, `RELEASE.md` defines the release gate and automated publishing behavior, and `README.md` routes contributors to both guides. Existing SDK implementation and tests remain unchanged. - -**Tech Stack:** GitHub CODEOWNERS, GitHub Actions, Markdown, Python, TypeScript/Node.js, Go - -## Global Constraints - -- Remove `.github/chainguard/` completely. -- Preserve the unrelated untracked `.codex/` directory. -- Do not modify SDK implementation or test code. -- Calls to generated Python API clients belong only in `python/lightning_sdk/api/`. -- Public Python APIs must not require or optionally accept resource IDs; accept names or resource objects and resolve IDs internally. -- Every Python module must resolve to at least two CODEOWNERS. -- Do not add specific rules for `__init__.py`, `__version__.py`, `constants.py`, `exceptions.py`, `helpers.py`, tests, or `lightning_cloud`. -- Within each rule, order `@justusschock` first, then `@ethanwharris`, then `@k223kim`; order all other owners by contribution history for that file or package. -- Before a release tag is created, the release candidate must undergo automated internal integration testing. -- The first release tag on a given day is `vYYYY.MM.DD`; additional same-day releases are `vYYYY.MM.DD.post0`, then `.post1`, and so on. -- Publishing a GitHub Release automatically publishes to PyPI and npm through the existing workflows. - -______________________________________________________________________ - -### Task 1: Remove Chainguard configuration and replace CODEOWNERS - -**Files:** - -- Delete: `.github/chainguard/grid-sdk-integration-read.sts.yaml` -- Delete: `.github/chainguard/grid-vendor-cloud.sts.yaml` -- Modify: `.github/CODEOWNERS` - -**Interfaces:** - -- Consumes: Contributor history from `../lightning-sdk` and the maintainer-reviewed priority order in the design specification. - -- Produces: Repository ownership rules inherited by all later documentation changes. - -- [ ] **Step 1: Capture the pre-change failure conditions** - -Run: - -```bash -test ! -d .github/chainguard -``` - -Expected: FAIL because `.github/chainguard/` still exists. - -Run: - -```bash -rg -n '^\* @justusschock @ethanwharris$' .github/CODEOWNERS -``` - -Expected: FAIL because the stale CODEOWNERS file does not contain the approved default rule. - -- [ ] **Step 2: Remove the obsolete Chainguard files** - -Delete both tracked files: - -```text -.github/chainguard/grid-sdk-integration-read.sts.yaml -.github/chainguard/grid-vendor-cloud.sts.yaml -``` - -The directory disappears once both files are deleted. - -- [ ] **Step 3: Replace `.github/CODEOWNERS` with the approved rules** - -Write exactly: - -```text -* @justusschock @ethanwharris - -/.github/ @justusschock @ethanwharris @owbone - -/python/ @justusschock @ethanwharris @k223kim -/python/docs/ @justusschock @dhedey -/python/examples/ @justusschock @k223kim - -/python/lightning_sdk/api/ @justusschock @ethanwharris @k223kim -/python/lightning_sdk/cli/ @justusschock @ethanwharris @k223kim -/python/lightning_sdk/llm/ @k223kim @Danidapena -/python/lightning_sdk/pipeline/ @justusschock @tchaton -/python/lightning_sdk/sandbox/ @k223kim @rusenask - -/python/lightning_sdk/agents.py @k223kim @Danidapena -/python/lightning_sdk/base_studio.py @justusschock @ethanwharris @viveque -/python/lightning_sdk/deployment.py @justusschock @tchaton -/python/lightning_sdk/filesystem.py @justusschock @k223kim @owbone -/python/lightning_sdk/job.py @justusschock @ethanwharris -/python/lightning_sdk/k8s_cluster.py @ethanwharris @k223kim -/python/lightning_sdk/lit_container.py @ethanwharris @k223kim -/python/lightning_sdk/machine.py @justusschock @ethanwharris -/python/lightning_sdk/mmt.py @justusschock @ethanwharris -/python/lightning_sdk/models.py @justusschock @ethanwharris -/python/lightning_sdk/organization.py @justusschock @ethanwharris @k223kim -/python/lightning_sdk/owner.py @justusschock @ethanwharris @k223kim -/python/lightning_sdk/sandbox.py @justusschock @k223kim -/python/lightning_sdk/serve.py @k223kim @tchaton -/python/lightning_sdk/status.py @justusschock @ethanwharris @k223kim -/python/lightning_sdk/studio.py @justusschock @ethanwharris @k223kim -/python/lightning_sdk/teamspace.py @justusschock @ethanwharris @k223kim -/python/lightning_sdk/user.py @justusschock @ethanwharris @k223kim - -/js/ @k223kim @rusenask -/go/ @justusschock @owbone -``` - -- [ ] **Step 4: Verify removal, rule coverage, and module-owner counts** - -Run: - -```bash -test ! -d .github/chainguard -! rg -n 'chainguard|grid-sdk-integration-read|grid-vendor-cloud' . --hidden -g '!.git/**' -g '!.codex/**' -g '!docs/superpowers/**' -``` - -Expected: PASS with no output. - -Run: - -```bash -while IFS= read -r pattern; do - [ "$pattern" = "*" ] && continue - path="${pattern#/}" - path="${path%/}" - test -e "$path" || { echo "Missing CODEOWNERS path: $pattern"; exit 1; } -done < <(awk 'NF && $1 !~ /^#/ {print $1}' .github/CODEOWNERS) -``` - -Expected: PASS with no output. - -Run: - -```bash -awk '$1 ~ /^\/python\/lightning_sdk\/.*\.py$/ && NF < 3 {print "Too few owners:", $0; failed=1} END {exit failed}' .github/CODEOWNERS -``` - -Expected: PASS with no output; each explicit module line has a path plus at least two owners. Modules without explicit rules inherit the three-owner `/python/` rule. - -- [ ] **Step 5: Verify GitHub permissions for every named owner** - -Run: - -```bash -for owner in justusschock ethanwharris k223kim owbone dhedey Danidapena tchaton rusenask viveque; do - gh api "repos/Lightning-AI/sdk/collaborators/$owner/permission" --jq '"\(.user.login)\t\(.permission)"' -done -``` - -Expected: every owner reports `write`, `maintain`, or `admin`. If `viveque` still reports `read`, stop and report that the approved `base_studio.py` ownership cannot yet be enforced; do not silently remove him. - -- [ ] **Step 6: Commit the ownership and cleanup change** - -```bash -git add .github/CODEOWNERS .github/chainguard/grid-sdk-integration-read.sts.yaml .github/chainguard/grid-vendor-cloud.sts.yaml -git diff --cached --check -git commit -m "chore: update code ownership and remove chainguard config" -``` - -Expected: one commit containing only `.github/CODEOWNERS` and deletion of the two Chainguard files. - -### Task 2: Add the contribution guide - -**Files:** - -- Create: `CONTRIBUTING.md` - -**Interfaces:** - -- Consumes: Existing package layouts and commands from `python/README.md`, `js/README.md`, `go/README.md`, and CI workflows. - -- Produces: Repository-wide contribution expectations and the public Python API boundary used during review. - -- [ ] **Step 1: Capture the missing-guide condition** - -Run: - -```bash -test -f CONTRIBUTING.md -``` - -Expected: FAIL because the repository has no contribution guide. - -- [ ] **Step 2: Create `CONTRIBUTING.md`** - -Write exactly: - -````markdown -# Contributing to Lightning SDK - -Thank you for contributing to the Lightning SDK. Keep changes focused on one -SDK or one shared concern, and include tests and documentation when behavior -changes. - -## Repository structure - -- `python/` contains the Python SDK, CLI, tests, examples, and documentation. -- `js/` contains the TypeScript SDK for Lightning AI sandboxes. -- `go/` contains the Go SDK and its generated internal API client. - -The package-specific READMEs contain additional setup and usage information: -[`python/README.md`](python/README.md), [`js/README.md`](js/README.md), and -[`go/README.md`](go/README.md). - -## Development checks - -Install the Python package from the repository root and run its tests from the -test directory: - -```bash -pip install -r python/tests/requirements.txt -pip install -e "./python[serve]" -cd python/tests -pytest . -vv -s -``` - -Build and test the TypeScript package: - -```bash -cd js -npm ci -npm run build -npm test -``` - -Test the Go package: - -```bash -cd go -go test -count=1 ./... -``` - -Run the checks relevant to the files you changed. Before opening a pull request, -run the repository hooks when available: - -```bash -pre-commit run --all-files -``` - -## Python architecture - -### Keep generated API calls in `lightning_sdk.api` - -Only modules in `python/lightning_sdk/api/` may call the generated API clients. -Public modules must use that API layer instead of importing or calling generated -clients from `python/lightning_sdk/lightning_cloud/` directly. This keeps -generated transport details out of the public SDK surface. - -### Accept names, not IDs - -Public Python APIs must not require or optionally accept opaque resource IDs -from users. Accept user-facing names or resource objects and resolve the -corresponding IDs internally through the API layer. IDs may be used internally -or exposed as returned resource state, but users must not need to supply them to -perform an operation. - -Tests should exercise the public name-based behavior and mock generated-client -interactions behind the API layer. - -## Pull requests - -- Keep changes scoped and avoid unrelated cleanup. -- Add or update focused tests for behavior changes. -- Update public documentation when an API or workflow changes. -- Confirm formatting, tests, and package builds relevant to the change pass. -```` - -- [ ] **Step 3: Verify the contribution requirements are explicit** - -Run: - -```bash -rg -n '^## Repository structure$|^## Python architecture$|Only modules in `python/lightning_sdk/api/`|must not require or optionally accept opaque resource IDs' CONTRIBUTING.md -``` - -Expected: four matches covering repository structure and both Python constraints. - -Run: - -```bash -pre-commit run mdformat --files CONTRIBUTING.md -git diff --check -- CONTRIBUTING.md -``` - -Expected: both commands pass. Re-read the formatted file if `mdformat` changes it. - -- [ ] **Step 4: Commit the contribution guide** - -```bash -git add CONTRIBUTING.md -git diff --cached --check -git commit -m "docs: add contribution guide" -``` - -Expected: one commit containing only `CONTRIBUTING.md`. - -### Task 3: Add the release guide and route readers from the README - -**Files:** - -- Create: `RELEASE.md` -- Modify: `README.md:272-302` - -**Interfaces:** - -- Consumes: `.github/workflows/release.yaml` for PyPI and `.github/workflows/release-npm.yaml` for npm. - -- Produces: A maintainer-facing pre-tag gate and discoverable links to both repository guides. - -- [ ] **Step 1: Capture the missing release documentation** - -Run: - -```bash -test -f RELEASE.md -``` - -Expected: FAIL because the repository has no release guide. - -Run: - -```bash -rg -n 'CONTRIBUTING\.md|RELEASE\.md' README.md -``` - -Expected: FAIL because the root README links neither guide. - -- [ ] **Step 2: Create `RELEASE.md`** - -Write exactly: - -```markdown -# Releasing Lightning SDK - -Lightning SDK releases are published automatically from GitHub Releases. - -## Before tagging - -Every release candidate must undergo automated internal integration testing -before a release tag is created. Do not create or push the tag until that -testing has completed successfully. - -## Tag format - -Tags use calendar versions with a `v` prefix: - -- The first release on a given day is `vYYYY.MM.DD`. -- The second release that day is `vYYYY.MM.DD.post0`. -- Further releases increment the suffix sequentially: `.post1`, `.post2`, and - so on. - -## Publish the release - -After automated internal integration testing succeeds: - -1. Create the release tag. -2. Create and publish a GitHub Release for that tag. -3. Monitor the GitHub Actions runs until both publishing workflows complete. - -Publishing the GitHub Release triggers the existing automation: - -- [`.github/workflows/release.yaml`](.github/workflows/release.yaml) builds the - Python distribution and publishes `lightning-sdk` to PyPI. -- [`.github/workflows/release-npm.yaml`](.github/workflows/release-npm.yaml) - builds the TypeScript distribution and publishes `@lightningai/sdk` to npm. - -The GitHub Release is the publishing mechanism; normal releases do not require -manual uploads to PyPI or npm. -``` - -- [ ] **Step 3: Link both guides from the root README** - -After the package-specific README paragraph in the `Development` section, add: - -```markdown - -See [`CONTRIBUTING.md`](CONTRIBUTING.md) for repository-wide contribution -requirements and development checks. -``` - -At the end of the `Community` section, add: - -```markdown - -Maintainers preparing a release should follow [`RELEASE.md`](RELEASE.md). -``` - -- [ ] **Step 4: Verify the release claims against the workflows** - -Run: - -```bash -rg -n 'release:|types: \[published\]|gh-action-pypi-publish' .github/workflows/release.yaml -rg -n 'release:|types: \[published\]|npm publish' .github/workflows/release-npm.yaml -``` - -Expected: each workflow listens for a published GitHub Release; the Python workflow uses `gh-action-pypi-publish`, and the npm workflow runs `npm publish`. - -Run: - -```bash -rg -n 'automated internal integration testing|vYYYY\.MM\.DD|\.post0|GitHub Release|PyPI|npm' RELEASE.md -rg -n 'CONTRIBUTING\.md|RELEASE\.md' README.md -``` - -Expected: the release guide contains the testing gate and both publishing targets, and the README links both guides. - -Run: - -```bash -pre-commit run mdformat --files RELEASE.md README.md -git diff --check -- RELEASE.md README.md -``` - -Expected: both commands pass. Re-read both files if `mdformat` changes them. - -- [ ] **Step 5: Commit the release guide and README links** - -```bash -git add RELEASE.md README.md -git diff --cached --check -git commit -m "docs: document automated releases" -``` - -Expected: one commit containing only `RELEASE.md` and `README.md`. - -### Task 4: Run final repository verification - -**Files:** - -- Verify: `.github/CODEOWNERS` -- Verify: `CONTRIBUTING.md` -- Verify: `RELEASE.md` -- Verify: `README.md` -- Verify deletion: `.github/chainguard/` - -**Interfaces:** - -- Consumes: The completed outputs from Tasks 1-3. - -- Produces: Evidence that the housekeeping change is internally consistent and contains no unrelated edits. - -- [ ] **Step 1: Run formatting and repository hygiene checks** - -Run: - -```bash -pre-commit run --all-files -git diff --check HEAD~3..HEAD -``` - -Expected: all hooks pass and Git reports no whitespace errors. - -- [ ] **Step 2: Re-run the focused acceptance checks** - -Run: - -```bash -test ! -d .github/chainguard -! rg -n 'chainguard|grid-sdk-integration-read|grid-vendor-cloud' . --hidden -g '!.git/**' -g '!.codex/**' -g '!docs/superpowers/**' -rg -n '^\* @justusschock @ethanwharris$' .github/CODEOWNERS -rg -n 'Only modules in `python/lightning_sdk/api/`|must not require or optionally accept opaque resource IDs' CONTRIBUTING.md -rg -n 'automated internal integration testing|vYYYY\.MM\.DD|\.post0|GitHub Release|PyPI|npm' RELEASE.md -rg -n 'CONTRIBUTING\.md|RELEASE\.md' README.md -``` - -Expected: all commands pass; searches print the approved ownership rule, both Python architecture requirements, the release gate, tag convention, publishing targets, and both README links. - -- [ ] **Step 3: Confirm the final commit range and worktree scope** - -Run: - -```bash -git status --short -git diff --stat a4e0fc21..HEAD -git diff --name-status a4e0fc21..HEAD -``` - -Expected: `.codex/` is the only untracked path. The implementation commits change only `.github/CODEOWNERS`, the two deleted `.github/chainguard/` files, `CONTRIBUTING.md`, `RELEASE.md`, and `README.md`, in addition to the already committed design and plan documents. - -- [ ] **Step 4: Record verification evidence** - -Update the Obsidian agent-memory state with: - -```text -Repository housekeeping implemented. Chainguard configuration removed; ordered CODEOWNERS installed; CONTRIBUTING.md documents TypeScript, Python, and Go structure plus Python API boundaries; RELEASE.md requires automated internal integration testing before tagging and documents GitHub Release-driven PyPI/npm publishing. Record pre-commit result, focused acceptance checks, commit hashes, and the remaining untracked .codex/ directory. -``` - -No additional repository commit is needed unless verification changes a tracked file. diff --git a/docs/superpowers/specs/2026-07-10-repository-housekeeping-design.md b/docs/superpowers/specs/2026-07-10-repository-housekeeping-design.md deleted file mode 100644 index 66ba2e9f..00000000 --- a/docs/superpowers/specs/2026-07-10-repository-housekeeping-design.md +++ /dev/null @@ -1,104 +0,0 @@ -# Repository Housekeeping Design - -## Goal - -Make the repository easier to maintain by removing obsolete Chainguard configuration, documenting contribution expectations across the TypeScript, Python, and Go SDKs, and replacing the stale CODEOWNERS rules with ownership derived from the `lightning-sdk` commit history and refined by maintainer review. - -## Scope - -- Remove the two files under `.github/chainguard/` and leave no empty directory behind. -- Add root `CONTRIBUTING.md` and `RELEASE.md` guides and link them from the root `README.md`. -- Replace `.github/CODEOWNERS` with the approved ownership rules below. -- Do not modify SDK implementation or test code. -- Preserve the unrelated untracked `.codex/` directory. - -## Contribution Guide - -The guide will describe the repository as three SDK implementations: - -- TypeScript under `js/` -- Python under `python/` -- Go under `go/` - -It will direct contributors to the language-specific development configuration and commands already present in each subtree instead of duplicating extensive setup instructions. - -The Python section will state these architectural requirements explicitly: - -1. Calls to generated API clients belong only in the `lightning_sdk.api` subpackage. Public modules use that layer instead of calling generated clients directly. -1. Public Python APIs must not require or optionally accept opaque resource IDs from users. They accept names or resource objects and resolve the corresponding IDs internally. -1. Tests should cover the public name-based behavior and keep generated-client interactions behind the API layer. - -## Release Guide - -The release guide will document the required sequence without duplicating the GitHub Actions implementation: - -1. Every release candidate must undergo automated internal integration testing before a release tag is created. -1. The first release tag on a given day uses `vYYYY.MM.DD`. Additional releases that day use `.post0`, `.post1`, and subsequent sequential suffixes, for example `v2026.07.10.post0`. -1. A tag and GitHub Release may be created only after testing succeeds. -1. Publishing the GitHub Release triggers the existing workflows that publish the Python package to PyPI and the TypeScript package to npm automatically. - -The guide will link to `.github/workflows/release.yaml` and `.github/workflows/release-npm.yaml` as the source of truth for the publishing automation. - -## CODEOWNERS Ordering - -Within each rule, owners are ordered by priority: - -1. `@justusschock` -1. `@ethanwharris` -1. `@k223kim` -1. Other owners ordered according to their contribution history for that file or package - -The approved rules are: - -```text -* @justusschock @ethanwharris - -/.github/ @justusschock @ethanwharris @owbone - -/python/ @justusschock @ethanwharris @k223kim -/python/docs/ @justusschock @dhedey -/python/examples/ @justusschock @k223kim - -/python/lightning_sdk/api/ @justusschock @ethanwharris @k223kim -/python/lightning_sdk/cli/ @justusschock @ethanwharris @k223kim -/python/lightning_sdk/llm/ @k223kim @Danidapena -/python/lightning_sdk/pipeline/ @justusschock @tchaton -/python/lightning_sdk/sandbox/ @k223kim @rusenask - -/python/lightning_sdk/agents.py @k223kim @Danidapena -/python/lightning_sdk/base_studio.py @justusschock @ethanwharris @viveque -/python/lightning_sdk/deployment.py @justusschock @tchaton -/python/lightning_sdk/filesystem.py @justusschock @k223kim @owbone -/python/lightning_sdk/job.py @justusschock @ethanwharris -/python/lightning_sdk/k8s_cluster.py @ethanwharris @k223kim -/python/lightning_sdk/lit_container.py @ethanwharris @k223kim -/python/lightning_sdk/machine.py @justusschock @ethanwharris -/python/lightning_sdk/mmt.py @justusschock @ethanwharris -/python/lightning_sdk/models.py @justusschock @ethanwharris -/python/lightning_sdk/organization.py @justusschock @ethanwharris @k223kim -/python/lightning_sdk/owner.py @justusschock @ethanwharris @k223kim -/python/lightning_sdk/sandbox.py @justusschock @k223kim -/python/lightning_sdk/serve.py @k223kim @tchaton -/python/lightning_sdk/status.py @justusschock @ethanwharris @k223kim -/python/lightning_sdk/studio.py @justusschock @ethanwharris @k223kim -/python/lightning_sdk/teamspace.py @justusschock @ethanwharris @k223kim -/python/lightning_sdk/user.py @justusschock @ethanwharris @k223kim - -/js/ @k223kim @rusenask -/go/ @justusschock @owbone -``` - -`__init__.py`, `__version__.py`, `constants.py`, `exceptions.py`, `helpers.py`, tests, and `lightning_cloud` intentionally have no more-specific rule and inherit `/python/`. Every Python module therefore has at least three inherited owners or at least two explicit owners. - -`@viveque` currently needs write access to `Lightning-AI/sdk` before GitHub can enforce the `base_studio.py` rule; the repository owner will grant that access. - -## Verification - -- Confirm `.github/chainguard/` is absent and no repository file refers to the removed configuration. -- Validate that every CODEOWNERS path exists, except paths intentionally acting as subtree patterns. -- Confirm all named CODEOWNERS have write access after `@viveque` is upgraded. -- Check owner ordering and ensure every explicit module rule contains at least two owners. -- Check that the contribution guide describes all three language trees and contains both Python architectural requirements. -- Check that the release guide makes automated internal integration testing a pre-tag requirement, documents the `vYYYY.MM.DD` and `.postN` tag convention, and accurately describes GitHub Release-based PyPI and npm publishing. -- Run Markdown formatting or lint checks available in the repository, plus `git diff --check`. -- Review the final diff to ensure only the approved housekeeping files changed. diff --git a/python/docs/cli/_reference.py b/python/docs/cli/_reference.py new file mode 100644 index 00000000..5314f809 --- /dev/null +++ b/python/docs/cli/_reference.py @@ -0,0 +1,259 @@ +"""Structured Click command reference rendering for the CLI documentation.""" + +from __future__ import annotations + +import inspect +from collections.abc import Iterable + +import click +from click_extra.sphinx.click import TreeDirective +from docutils import nodes +from docutils.statemachine import StringList + + +def _clean_text(value: str | None) -> str: + """Return a compact description for a command or parameter.""" + if not value: + return "No description provided." + return " ".join(inspect.cleandoc(value).split()) + + +def _display_default(value: object) -> str: + if value is None: + return "—" + if isinstance(value, bool): + return str(value).lower() + return repr(value) + + +def _literal(value: object, rst: bool) -> str: + marker = "``" if rst else "`" + return f"{marker}{value}{marker}" + + +def _display_type(param: click.Parameter, context: click.Context) -> str: + """Get the Click metavar without depending on a Click minor version.""" + for getter in ( + lambda: param.make_metavar(context), + lambda: param.make_metavar(), + lambda: param.type.get_metavar(param, context), + lambda: param.type.name, + ): + try: + value = getter() + except (AttributeError, TypeError): + continue + if value: + return str(value) + return "—" + + +def _split_help(value: str | None) -> tuple[list[str], list[str]]: + """Separate command prose from its documented examples.""" + text = inspect.cleandoc(value or "").strip() + if not text: + return [], [] + + lines = text.splitlines() + example_index = next( + ( + index + for index, line in enumerate(lines) + if line.strip().lower() in {"example:", "examples:"} + ), + None, + ) + if example_index is None: + return lines, [] + + description = lines[:example_index] + while description and not description[-1].strip(): + description.pop() + + examples = [ + line[2:] if line.startswith(" ") else line + for line in lines[example_index + 1 :] + ] + while examples and not examples[-1].strip(): + examples.pop() + return description, examples + + +def _parameter_rows( + command: click.Command, + context: click.Context, + rst: bool, +) -> tuple[list[list[str]], list[list[str]]]: + arguments: list[list[str]] = [] + options: list[list[str]] = [] + params: Iterable[click.Parameter] = command.params + + # Click adds --help dynamically, so include it alongside declared options. + help_option = command.get_help_option(context) + if help_option is not None and not any( + isinstance(param, click.Option) and "--help" in param.opts + for param in params + ): + params = [*params, help_option] + + for param in params: + description = _clean_text(getattr(param, "help", None)) + type_name = _display_type(param, context) + if isinstance(param, click.Argument): + arguments.append( + [ + _literal(param.name.upper(), rst), + _literal(type_name, rst), + "yes" if param.required else "no", + description, + ], + ) + continue + + if isinstance(param, click.Option): + names = [*param.opts, *param.secondary_opts] + options.append( + [ + ", ".join(_literal(name, rst) for name in names), + _literal(type_name, rst) if not param.is_flag else "—", + "yes" if param.required else "no", + _literal(_display_default(param.default), rst) + if param.default is not None + else "—", + description, + ], + ) + + return arguments, options + + +def _append_table( + directive: ReferenceDirective, + parent: nodes.Element, + headers: list[str], + rows: list[list[str]], + source_file: str, +) -> None: + table = nodes.table(classes=["colwidths-auto"]) + tgroup = nodes.tgroup(cols=len(headers)) + table += tgroup + for _ in headers: + tgroup += nodes.colspec(colwidth=1) + + def add_row(container: nodes.Element, values: list[str]) -> None: + row = nodes.row() + for value in values: + entry = nodes.entry() + directive.state.nested_parse(StringList([value], source_file), 0, entry) + row += entry + container += row + + thead = nodes.thead() + add_row(thead, headers) + tgroup += thead + tbody = nodes.tbody() + for row in rows: + add_row(tbody, row) + tgroup += tbody + parent += table + + +class ReferenceDirective(TreeDirective): + """Render Click metadata as a structured API-style reference.""" + + def run(self) -> list[nodes.Node]: + if self.content: + self.runner.execute_source(self) + + cli_expr = self.arguments[0].strip() + try: + cli = eval(cli_expr, self.runner.namespace) # noqa: S307 + except Exception as exc: + raise RuntimeError( + f"lightning-reference: failed to evaluate {cli_expr!r}: {exc}", + ) from exc + if not isinstance(cli, click.Command): + raise TypeError( + f"lightning-reference: {cli_expr!r} did not yield a click.Command " + f"(got {type(cli).__name__}).", + ) + + max_depth = self.options.get("max-depth", 10) + root_label = self.options.get("root-label") or cli.name or cli_expr + anchor_prefix = self.options.get("anchor-prefix") or self._slug(root_label) + root_parts = root_label.split() + entries = self._walk(cli, max_depth) + source_file, _ = self.get_source_info() + root_section = nodes.section(ids=[anchor_prefix]) + command_sections: dict[tuple[str, ...], nodes.section] = {(): root_section} + + for path, command in [([], cli), *entries]: + anchor = "-".join([anchor_prefix, *(self._slug(part) for part in path)]) + label = " ".join([root_label, *path]) + full_path = [*root_parts, *path] + context = click.Context(command, info_name=" ".join(full_path)) + if path: + command_section = nodes.section(ids=[anchor]) + command_sections[tuple(path[:-1])] += command_section + command_sections[tuple(path)] = command_section + else: + command_section = root_section + command_section += nodes.title(text=label) + + description, examples = _split_help(command.help) + if description: + self.state.nested_parse(StringList(description, source_file), 0, command_section) + + usage = " ".join(command.collect_usage_pieces(context)) + usage = " ".join([*full_path, usage]).strip() + if usage: + usage_node = nodes.paragraph() + usage_node += nodes.strong(text="Usage:") + usage_node += nodes.Text(" ") + usage_node += nodes.literal(text=usage) + command_section += usage_node + + if isinstance(command, click.Group) and command.commands: + subcommands = nodes.section(ids=[f"{anchor}-subcommands"]) + subcommands += nodes.title(text="Subcommands") + rows = [ + [_literal(name, True), _clean_text(subcommand.get_short_help_str())] + for name, subcommand in sorted(command.commands.items()) + ] + _append_table(self, subcommands, ["Command", "Description"], rows, source_file) + command_section += subcommands + + arguments, options = _parameter_rows(command, context, True) + if arguments: + arguments_section = nodes.section(ids=[f"{anchor}-arguments"]) + arguments_section += nodes.title(text="Arguments") + _append_table( + self, + arguments_section, + ["Name", "Type", "Required", "Description"], + arguments, + source_file, + ) + command_section += arguments_section + if options: + options_section = nodes.section(ids=[f"{anchor}-options"]) + options_section += nodes.title(text="Options") + _append_table( + self, + options_section, + ["Option", "Type", "Required", "Default", "Description"], + options, + source_file, + ) + command_section += options_section + if examples: + examples_section = nodes.section(ids=[f"{anchor}-examples"]) + examples_section += nodes.title(text="Examples") + examples_section += nodes.literal_block( + "\n".join(examples), + "\n".join(examples), + language="console", + ) + command_section += examples_section + + return [root_section] diff --git a/python/docs/cli/api-key.md b/python/docs/cli/api-key.md deleted file mode 100644 index 40574888..00000000 --- a/python/docs/cli/api-key.md +++ /dev/null @@ -1,8 +0,0 @@ -# API Key - -The command reference is generated from the installed Click command tree. - -```{click:tree} api_key -:root-label: lightning api-key --help -from lightning_sdk.cli.groups import api_key -``` diff --git a/python/docs/cli/api-key.rst b/python/docs/cli/api-key.rst new file mode 100644 index 00000000..ce5874b4 --- /dev/null +++ b/python/docs/cli/api-key.rst @@ -0,0 +1,8 @@ +API Key ++++++++ + +.. lightning-reference:: api_key + :root-label: lightning api-key + :anchor-prefix: api-key + + from lightning_sdk.cli.groups import api_key diff --git a/python/docs/cli/base-studio.md b/python/docs/cli/base-studio.md deleted file mode 100644 index f607b517..00000000 --- a/python/docs/cli/base-studio.md +++ /dev/null @@ -1,8 +0,0 @@ -# Base Studio - -The command reference is generated from the installed Click command tree. - -```{click:tree} base_studio -:root-label: lightning base-studio --help -from lightning_sdk.cli.groups import base_studio -``` diff --git a/python/docs/cli/base-studio.rst b/python/docs/cli/base-studio.rst new file mode 100644 index 00000000..b1846b0a --- /dev/null +++ b/python/docs/cli/base-studio.rst @@ -0,0 +1,8 @@ +Base Studio ++++++++++++ + +.. lightning-reference:: base_studio + :root-label: lightning base-studio + :anchor-prefix: base-studio + + from lightning_sdk.cli.groups import base_studio diff --git a/python/docs/cli/conf.py b/python/docs/cli/conf.py index a7d68c21..178e871c 100644 --- a/python/docs/cli/conf.py +++ b/python/docs/cli/conf.py @@ -6,6 +6,7 @@ _PATH_HERE = os.path.abspath(os.path.dirname(__file__)) _PATH_ROOT = os.path.realpath(os.path.join(_PATH_HERE, "..", "..")) +sys.path.insert(0, _PATH_HERE) sys.path.insert(0, _PATH_ROOT) # The legacy forwarding commands intentionally exit with a migration error when @@ -15,6 +16,7 @@ import lightning_sdk # noqa: E402 from lightning_sdk.cli.entrypoint import main_cli as _main_cli # noqa: E402 +from _reference import ReferenceDirective # noqa: E402 _CLI_ARGUMENT_HELP = { ("lightning config set user", "user_name"): "User name to make active in the local Lightning CLI config.", @@ -120,18 +122,14 @@ def _apply_cli_argument_help(command: click.Command, command_path: str) -> None: "sphinx_copybutton", "sphinx_paramlinks", "sphinx_togglebutton", - "myst_parser", ] click_extra_enable_exec_directives = True templates_path = ["_templates"] -myst_heading_anchors = 3 - source_suffix = { ".rst": "restructuredtext", - ".md": "markdown", } master_doc = "index" @@ -231,3 +229,4 @@ def _can_resolve_host(hostname: str) -> bool: def setup(app) -> None: # noqa: ANN001 app.add_css_file("main.css") + app.add_directive("lightning-reference", ReferenceDirective) diff --git a/python/docs/cli/config.md b/python/docs/cli/config.md deleted file mode 100644 index 7ef7bc66..00000000 --- a/python/docs/cli/config.md +++ /dev/null @@ -1,8 +0,0 @@ -# Config - -The command reference is generated from the installed Click command tree. - -```{click:tree} config -:root-label: lightning config --help -from lightning_sdk.cli.groups import config -``` diff --git a/python/docs/cli/config.rst b/python/docs/cli/config.rst new file mode 100644 index 00000000..c801b8af --- /dev/null +++ b/python/docs/cli/config.rst @@ -0,0 +1,8 @@ +Config +++++++ + +.. lightning-reference:: config + :root-label: lightning config + :anchor-prefix: config + + from lightning_sdk.cli.groups import config diff --git a/python/docs/cli/container.md b/python/docs/cli/container.md deleted file mode 100644 index 3d81c61c..00000000 --- a/python/docs/cli/container.md +++ /dev/null @@ -1,8 +0,0 @@ -# Container - -The command reference is generated from the installed Click command tree. - -```{click:tree} container -:root-label: lightning container --help -from lightning_sdk.cli.groups import container -``` diff --git a/python/docs/cli/container.rst b/python/docs/cli/container.rst new file mode 100644 index 00000000..c93c1c61 --- /dev/null +++ b/python/docs/cli/container.rst @@ -0,0 +1,8 @@ +Container ++++++++++ + +.. lightning-reference:: container + :root-label: lightning container + :anchor-prefix: container + + from lightning_sdk.cli.groups import container diff --git a/python/docs/cli/cp.md b/python/docs/cli/cp.md deleted file mode 100644 index d43eae37..00000000 --- a/python/docs/cli/cp.md +++ /dev/null @@ -1,8 +0,0 @@ -# Copy - -The command reference is generated from the installed Click command tree. - -```{click:tree} cp -:root-label: lightning cp --help -from lightning_sdk.cli.groups import cp -``` diff --git a/python/docs/cli/cp.rst b/python/docs/cli/cp.rst new file mode 100644 index 00000000..4c47fd57 --- /dev/null +++ b/python/docs/cli/cp.rst @@ -0,0 +1,8 @@ +Copy +++++ + +.. lightning-reference:: cp + :root-label: lightning cp + :anchor-prefix: cp + + from lightning_sdk.cli.groups import cp diff --git a/python/docs/cli/deployment.md b/python/docs/cli/deployment.md deleted file mode 100644 index 12679d52..00000000 --- a/python/docs/cli/deployment.md +++ /dev/null @@ -1,8 +0,0 @@ -# Deployment - -The command reference is generated from the installed Click command tree. - -```{click:tree} deployment -:root-label: lightning deployment --help -from lightning_sdk.cli.groups import deployment -``` diff --git a/python/docs/cli/deployment.rst b/python/docs/cli/deployment.rst new file mode 100644 index 00000000..cc29bf15 --- /dev/null +++ b/python/docs/cli/deployment.rst @@ -0,0 +1,8 @@ +Deployment +++++++++++ + +.. lightning-reference:: deployment + :root-label: lightning deployment + :anchor-prefix: deployment + + from lightning_sdk.cli.groups import deployment diff --git a/python/docs/cli/file.md b/python/docs/cli/file.md deleted file mode 100644 index a0062887..00000000 --- a/python/docs/cli/file.md +++ /dev/null @@ -1,8 +0,0 @@ -# File - -The command reference is generated from the installed Click command tree. - -```{click:tree} file -:root-label: lightning file --help -from lightning_sdk.cli.groups import file -``` diff --git a/python/docs/cli/file.rst b/python/docs/cli/file.rst new file mode 100644 index 00000000..b9cb5e78 --- /dev/null +++ b/python/docs/cli/file.rst @@ -0,0 +1,8 @@ +File +++++ + +.. lightning-reference:: file + :root-label: lightning file + :anchor-prefix: file + + from lightning_sdk.cli.groups import file diff --git a/python/docs/cli/folder.md b/python/docs/cli/folder.md deleted file mode 100644 index 5717c9d0..00000000 --- a/python/docs/cli/folder.md +++ /dev/null @@ -1,8 +0,0 @@ -# Folder - -The command reference is generated from the installed Click command tree. - -```{click:tree} folder -:root-label: lightning folder --help -from lightning_sdk.cli.groups import folder -``` diff --git a/python/docs/cli/folder.rst b/python/docs/cli/folder.rst new file mode 100644 index 00000000..4c317f2a --- /dev/null +++ b/python/docs/cli/folder.rst @@ -0,0 +1,8 @@ +Folder +++++++ + +.. lightning-reference:: folder + :root-label: lightning folder + :anchor-prefix: folder + + from lightning_sdk.cli.groups import folder diff --git a/python/docs/cli/index.md b/python/docs/cli/index.md deleted file mode 100644 index 06cde4eb..00000000 --- a/python/docs/cli/index.md +++ /dev/null @@ -1,97 +0,0 @@ -# Command Line Interface - -The Python package installs the `lightning` command. The `lightning-sdk` -console script is an alias for the same command group. - -Use the CLI when you want to manage Lightning AI resources from a terminal, -CI job, shell script, or other automation. The command reference below is -generated from the Click command tree, so command groups, options, arguments, -and examples stay aligned with the installed package. - -The live command tree below is expanded at documentation build time. Click -Extra supplies the headings and terminal fences, while the imported command -object remains the SDK's existing `rich-click` CLI. - -Legacy aliases that only emit a migration error are intentionally omitted; -their supported noun-first replacements are documented instead. - -```{click:tree} main_cli -:root-label: lightning --help -:anchor-prefix: lightning -from lightning_sdk.cli.entrypoint import main_cli -``` - -## Install - -Install or upgrade the Python package: - -```{code-block} bash -pip install lightning-sdk -U -``` - -## Authenticate - -For interactive use, sign in with: - -```{code-block} bash -lightning login -``` - -For non-interactive environments, configure credentials through environment -variables instead: - -```{code-block} bash -export LIGHTNING_USER_ID=your-user-id -export LIGHTNING_API_KEY=your-api-key -``` - -## Usage - -Run a command group directly: - -```{code-block} bash -lightning [command] -``` - -Every command and subcommand exposes help from the same Click definitions used -by this reference: - -```{code-block} bash -lightning COMMAND --help -``` - -## Common Workflows - -- Develop interactively with {doc}`studio`. -- Submit and inspect training or batch work with {doc}`job` and {doc}`mmt`. -- Build and operate inference services with {doc}`deployment` and {doc}`model`. -- Move data and artifacts with {doc}`file`, {doc}`folder`, {doc}`container`, and {doc}`cp`. -- Configure accounts, organizations, teamspaces, cloud accounts, and SSH with - {doc}`config`, {doc}`api-key`, and {doc}`ssh`. -- Manage lower-level sandbox sessions with {doc}`sandbox`. - -## Command details - -The pages below keep focused URLs for each command group and its full option -reference. - -```{toctree} -:maxdepth: 1 - -config -job -mmt -machine -deployment -container -model -api-key -file -folder -ssh -studio -sandbox -base-studio -license -cp -``` diff --git a/python/docs/cli/index.rst b/python/docs/cli/index.rst new file mode 100644 index 00000000..ef9a1618 --- /dev/null +++ b/python/docs/cli/index.rst @@ -0,0 +1,95 @@ +Command Line Interface +++++++++++++++++++++++ + +The Python package installs the ``lightning`` command. The ``lightning-sdk`` +console script is an alias for the same command group. + +Use the CLI when you want to manage Lightning AI resources from a terminal, +CI job, shell script, or other automation. + +Legacy aliases that only emit a migration error are intentionally omitted; +their supported noun-first replacements are documented instead. + +.. lightning-reference:: main_cli + :root-label: lightning + :anchor-prefix: lightning + + from lightning_sdk.cli.entrypoint import main_cli + +Install +------- + +Install or upgrade the Python package: + +.. code-block:: bash + + pip install lightning-sdk -U + +Authenticate +------------ + +For interactive use, sign in with: + +.. code-block:: bash + + lightning login + +For non-interactive environments, configure credentials through environment +variables instead: + +.. code-block:: bash + + export LIGHTNING_USER_ID=your-user-id + export LIGHTNING_API_KEY=your-api-key + +Usage +----- + +Run a command group directly: + +.. code-block:: bash + + lightning [command] + +Every command and subcommand exposes help directly: + +.. code-block:: bash + + lightning COMMAND --help + +Common Workflows +---------------- + +* Develop interactively with :doc:`studio`. +* Submit and inspect training or batch work with :doc:`job` and :doc:`mmt`. +* Build and operate inference services with :doc:`deployment` and :doc:`model`. +* Move data and artifacts with :doc:`file`, :doc:`folder`, :doc:`container`, and :doc:`cp`. +* Configure accounts, organizations, teamspaces, cloud accounts, and SSH with + :doc:`config`, :doc:`api-key`, and :doc:`ssh`. +* Manage lower-level sandbox sessions with :doc:`sandbox`. + +Command details +--------------- + +The pages below keep focused URLs for each command group and its full option +reference. + +.. toctree:: + :maxdepth: 1 + + config + job + mmt + machine + deployment + container + model + api-key + file + folder + ssh + studio + sandbox + base-studio + license + cp diff --git a/python/docs/cli/job.md b/python/docs/cli/job.md deleted file mode 100644 index 7191def5..00000000 --- a/python/docs/cli/job.md +++ /dev/null @@ -1,8 +0,0 @@ -# Job - -The command reference is generated from the installed Click command tree. - -```{click:tree} job -:root-label: lightning job --help -from lightning_sdk.cli.groups import job -``` diff --git a/python/docs/cli/job.rst b/python/docs/cli/job.rst new file mode 100644 index 00000000..292fc270 --- /dev/null +++ b/python/docs/cli/job.rst @@ -0,0 +1,8 @@ +Job ++++ + +.. lightning-reference:: job + :root-label: lightning job + :anchor-prefix: job + + from lightning_sdk.cli.groups import job diff --git a/python/docs/cli/license.md b/python/docs/cli/license.md deleted file mode 100644 index f024ef61..00000000 --- a/python/docs/cli/license.md +++ /dev/null @@ -1,8 +0,0 @@ -# License - -The command reference is generated from the installed Click command tree. - -```{click:tree} license -:root-label: lightning license --help -from lightning_sdk.cli.groups import license -``` diff --git a/python/docs/cli/license.rst b/python/docs/cli/license.rst new file mode 100644 index 00000000..9ff67763 --- /dev/null +++ b/python/docs/cli/license.rst @@ -0,0 +1,8 @@ +License ++++++++ + +.. lightning-reference:: license + :root-label: lightning license + :anchor-prefix: license + + from lightning_sdk.cli.groups import license diff --git a/python/docs/cli/machine.md b/python/docs/cli/machine.md deleted file mode 100644 index 02a42f16..00000000 --- a/python/docs/cli/machine.md +++ /dev/null @@ -1,8 +0,0 @@ -# Machine - -The command reference is generated from the installed Click command tree. - -```{click:tree} machine -:root-label: lightning machine --help -from lightning_sdk.cli.groups import machine -``` diff --git a/python/docs/cli/machine.rst b/python/docs/cli/machine.rst new file mode 100644 index 00000000..d02e6d0c --- /dev/null +++ b/python/docs/cli/machine.rst @@ -0,0 +1,8 @@ +Machine ++++++++ + +.. lightning-reference:: machine + :root-label: lightning machine + :anchor-prefix: machine + + from lightning_sdk.cli.groups import machine diff --git a/python/docs/cli/mmt.md b/python/docs/cli/mmt.md deleted file mode 100644 index c312a72f..00000000 --- a/python/docs/cli/mmt.md +++ /dev/null @@ -1,8 +0,0 @@ -# Multi-Machine Training - -The command reference is generated from the installed Click command tree. - -```{click:tree} mmt -:root-label: lightning mmt --help -from lightning_sdk.cli.groups import mmt -``` diff --git a/python/docs/cli/mmt.rst b/python/docs/cli/mmt.rst new file mode 100644 index 00000000..b994fb61 --- /dev/null +++ b/python/docs/cli/mmt.rst @@ -0,0 +1,8 @@ +Multi-Machine Training +++++++++++++++++++++++ + +.. lightning-reference:: mmt + :root-label: lightning mmt + :anchor-prefix: mmt + + from lightning_sdk.cli.groups import mmt diff --git a/python/docs/cli/model.md b/python/docs/cli/model.md deleted file mode 100644 index 41022471..00000000 --- a/python/docs/cli/model.md +++ /dev/null @@ -1,8 +0,0 @@ -# Model - -The command reference is generated from the installed Click command tree. - -```{click:tree} model -:root-label: lightning model --help -from lightning_sdk.cli.groups import model -``` diff --git a/python/docs/cli/model.rst b/python/docs/cli/model.rst new file mode 100644 index 00000000..5dafe706 --- /dev/null +++ b/python/docs/cli/model.rst @@ -0,0 +1,8 @@ +Model ++++++ + +.. lightning-reference:: model + :root-label: lightning model + :anchor-prefix: model + + from lightning_sdk.cli.groups import model diff --git a/python/docs/cli/sandbox.md b/python/docs/cli/sandbox.md deleted file mode 100644 index ab979f2e..00000000 --- a/python/docs/cli/sandbox.md +++ /dev/null @@ -1,8 +0,0 @@ -# Sandbox - -The command reference is generated from the installed Click command tree. - -```{click:tree} sandbox -:root-label: lightning sandbox --help -from lightning_sdk.cli.groups import sandbox -``` diff --git a/python/docs/cli/sandbox.rst b/python/docs/cli/sandbox.rst new file mode 100644 index 00000000..f8e711d2 --- /dev/null +++ b/python/docs/cli/sandbox.rst @@ -0,0 +1,8 @@ +Sandbox ++++++++ + +.. lightning-reference:: sandbox + :root-label: lightning sandbox + :anchor-prefix: sandbox + + from lightning_sdk.cli.groups import sandbox diff --git a/python/docs/cli/ssh.md b/python/docs/cli/ssh.md deleted file mode 100644 index a56c1151..00000000 --- a/python/docs/cli/ssh.md +++ /dev/null @@ -1,8 +0,0 @@ -# SSH - -The command reference is generated from the installed Click command tree. - -```{click:tree} ssh -:root-label: lightning ssh --help -from lightning_sdk.cli.groups import ssh -``` diff --git a/python/docs/cli/ssh.rst b/python/docs/cli/ssh.rst new file mode 100644 index 00000000..fdd0ab7e --- /dev/null +++ b/python/docs/cli/ssh.rst @@ -0,0 +1,8 @@ +SSH ++++ + +.. lightning-reference:: ssh + :root-label: lightning ssh + :anchor-prefix: ssh + + from lightning_sdk.cli.groups import ssh diff --git a/python/docs/cli/studio.md b/python/docs/cli/studio.md deleted file mode 100644 index 449b83cf..00000000 --- a/python/docs/cli/studio.md +++ /dev/null @@ -1,8 +0,0 @@ -# Studio - -The command reference is generated from the installed Click command tree. - -```{click:tree} studio -:root-label: lightning studio --help -from lightning_sdk.cli.groups import studio -``` diff --git a/python/docs/cli/studio.rst b/python/docs/cli/studio.rst new file mode 100644 index 00000000..43ae54cc --- /dev/null +++ b/python/docs/cli/studio.rst @@ -0,0 +1,8 @@ +Studio +++++++ + +.. lightning-reference:: studio + :root-label: lightning studio + :anchor-prefix: studio + + from lightning_sdk.cli.groups import studio From 765ab47a623bf1c70ffedbc9fda6e10e0127e725 Mon Sep 17 00:00:00 2001 From: Justus Perillieux Date: Fri, 10 Jul 2026 23:58:47 +0200 Subject: [PATCH 8/9] docs: remove CLI migration commentary --- python/docs/cli/conf.py | 3 --- python/docs/cli/index.rst | 3 --- 2 files changed, 6 deletions(-) diff --git a/python/docs/cli/conf.py b/python/docs/cli/conf.py index 178e871c..ff834e1d 100644 --- a/python/docs/cli/conf.py +++ b/python/docs/cli/conf.py @@ -9,9 +9,6 @@ sys.path.insert(0, _PATH_HERE) sys.path.insert(0, _PATH_ROOT) -# The legacy forwarding commands intentionally exit with a migration error when -# invoked. Keep them out of the generated reference while documenting the -# supported noun-first command tree. os.environ["LIGHTNING_EXPERIMENTAL_CLI_ONLY"] = "1" import lightning_sdk # noqa: E402 diff --git a/python/docs/cli/index.rst b/python/docs/cli/index.rst index ef9a1618..6b864f4d 100644 --- a/python/docs/cli/index.rst +++ b/python/docs/cli/index.rst @@ -7,9 +7,6 @@ console script is an alias for the same command group. Use the CLI when you want to manage Lightning AI resources from a terminal, CI job, shell script, or other automation. -Legacy aliases that only emit a migration error are intentionally omitted; -their supported noun-first replacements are documented instead. - .. lightning-reference:: main_cli :root-label: lightning :anchor-prefix: lightning From 2f9fea965e9376cbb8d47f1c0712ba3ca58d5fc0 Mon Sep 17 00:00:00 2001 From: David Edey Date: Wed, 15 Jul 2026 14:30:11 +0000 Subject: [PATCH 9/9] docs: restructure and declutter the CLI reference rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Drop the root command heading — the page title already names the command. The root's prose renders in a div.cli-reference container; child sections are hoisted to document level so command headings sit at h2/h3 and the page toc lists them. * Rename "Subcommands" to "Commands", matching the usage line's COMMAND placeholder, the table's own header, and Click's help output. * Link each name in a Commands table to its section on the page, and stop truncating the short-help descriptions at 45 chars. * Render the usage synopsis as a console literal block (highlighting + copy button) instead of an inline literal. * Declutter parameter tables: drop the repeated --help rows, show an argument's Click type instead of its name-echoing metavar, and show "—" instead of `false` for a flag's implicit default. Co-Authored-By: Claude Fable 5 --- python/docs/cli/_reference.py | 109 ++++++++++++++++++++-------------- 1 file changed, 64 insertions(+), 45 deletions(-) diff --git a/python/docs/cli/_reference.py b/python/docs/cli/_reference.py index 5314f809..2f0ff1a7 100644 --- a/python/docs/cli/_reference.py +++ b/python/docs/cli/_reference.py @@ -3,7 +3,6 @@ from __future__ import annotations import inspect -from collections.abc import Iterable import click from click_extra.sphinx.click import TreeDirective @@ -56,11 +55,7 @@ def _split_help(value: str | None) -> tuple[list[str], list[str]]: lines = text.splitlines() example_index = next( - ( - index - for index, line in enumerate(lines) - if line.strip().lower() in {"example:", "examples:"} - ), + (index for index, line in enumerate(lines) if line.strip().lower() in {"example:", "examples:"}), None, ) if example_index is None: @@ -70,10 +65,7 @@ def _split_help(value: str | None) -> tuple[list[str], list[str]]: while description and not description[-1].strip(): description.pop() - examples = [ - line[2:] if line.startswith(" ") else line - for line in lines[example_index + 1 :] - ] + examples = [line[2:] if line.startswith(" ") else line for line in lines[example_index + 1 :]] while examples and not examples[-1].strip(): examples.pop() return description, examples @@ -86,24 +78,20 @@ def _parameter_rows( ) -> tuple[list[list[str]], list[list[str]]]: arguments: list[list[str]] = [] options: list[list[str]] = [] - params: Iterable[click.Parameter] = command.params - # Click adds --help dynamically, so include it alongside declared options. - help_option = command.get_help_option(context) - if help_option is not None and not any( - isinstance(param, click.Option) and "--help" in param.opts - for param in params - ): - params = [*params, help_option] - - for param in params: + for param in command.params: + # Every command answers --help identically, so a row for it on each + # Options table is repeated noise — omit it throughout. + if isinstance(param, click.Option) and "--help" in param.opts: + continue description = _clean_text(getattr(param, "help", None)) - type_name = _display_type(param, context) if isinstance(param, click.Argument): + # An argument's metavar just repeats its name, so show the Click + # type (text, path, integer, ...) instead. arguments.append( [ _literal(param.name.upper(), rst), - _literal(type_name, rst), + _literal(param.type.name or "text", rst), "yes" if param.required else "no", description, ], @@ -115,11 +103,12 @@ def _parameter_rows( options.append( [ ", ".join(_literal(name, rst) for name in names), - _literal(type_name, rst) if not param.is_flag else "—", + _literal(_display_type(param, context), rst) if not param.is_flag else "—", "yes" if param.required else "no", - _literal(_display_default(param.default), rst) - if param.default is not None - else "—", + # A flag's implicit false default carries no information. + "—" + if param.default is None or (param.is_flag and param.default is False) + else _literal(_display_default(param.default), rst), description, ], ) @@ -127,6 +116,15 @@ def _parameter_rows( return arguments, options +def _command_link(name: str, anchor: str) -> nodes.paragraph: + """A table cell linking a command's name to its section on this page.""" + paragraph = nodes.paragraph() + link = nodes.reference(refuri=f"#{anchor}") + link += nodes.literal(text=name) + paragraph += link + return paragraph + + def _append_table( directive: ReferenceDirective, parent: nodes.Element, @@ -140,11 +138,14 @@ def _append_table( for _ in headers: tgroup += nodes.colspec(colwidth=1) - def add_row(container: nodes.Element, values: list[str]) -> None: + def add_row(container: nodes.Element, values: list[str | nodes.Node]) -> None: row = nodes.row() for value in values: entry = nodes.entry() - directive.state.nested_parse(StringList([value], source_file), 0, entry) + if isinstance(value, nodes.Node): + entry += value + else: + directive.state.nested_parse(StringList([value], source_file), 0, entry) row += entry container += row @@ -167,15 +168,14 @@ def run(self) -> list[nodes.Node]: cli_expr = self.arguments[0].strip() try: - cli = eval(cli_expr, self.runner.namespace) # noqa: S307 + cli = eval(cli_expr, self.runner.namespace) except Exception as exc: raise RuntimeError( f"lightning-reference: failed to evaluate {cli_expr!r}: {exc}", ) from exc if not isinstance(cli, click.Command): raise TypeError( - f"lightning-reference: {cli_expr!r} did not yield a click.Command " - f"(got {type(cli).__name__}).", + f"lightning-reference: {cli_expr!r} did not yield a click.Command (got {type(cli).__name__}).", ) max_depth = self.options.get("max-depth", 10) @@ -184,8 +184,13 @@ def run(self) -> list[nodes.Node]: root_parts = root_label.split() entries = self._walk(cli, max_depth) source_file, _ = self.get_source_info() - root_section = nodes.section(ids=[anchor_prefix]) - command_sections: dict[tuple[str, ...], nodes.section] = {(): root_section} + # A container (not a section): the page's own title already names the + # root command, so the root gets no heading of its own — and a title-less + # section would leak its first paragraph into the page toc. The class lets + # downstream stylesheets target CLI reference pages without content-sniffing. + # Child sections are hoisted out on return (see below). + root_section = nodes.container(ids=[anchor_prefix], classes=["cli-reference"]) + command_sections: dict[tuple[str, ...], nodes.Element] = {(): root_section} for path, command in [([], cli), *entries]: anchor = "-".join([anchor_prefix, *(self._slug(part) for part in path)]) @@ -196,9 +201,9 @@ def run(self) -> list[nodes.Node]: command_section = nodes.section(ids=[anchor]) command_sections[tuple(path[:-1])] += command_section command_sections[tuple(path)] = command_section + command_section += nodes.title(text=label) else: command_section = root_section - command_section += nodes.title(text=label) description, examples = _split_help(command.help) if description: @@ -207,21 +212,27 @@ def run(self) -> list[nodes.Node]: usage = " ".join(command.collect_usage_pieces(context)) usage = " ".join([*full_path, usage]).strip() if usage: - usage_node = nodes.paragraph() - usage_node += nodes.strong(text="Usage:") - usage_node += nodes.Text(" ") - usage_node += nodes.literal(text=usage) - command_section += usage_node + # A literal block (rather than an inline literal) so themes give + # the synopsis code-block treatment: highlighting + a copy button. + label = nodes.paragraph() + label += nodes.strong(text="Usage:") + command_section += label + command_section += nodes.literal_block(usage, usage, language="console") if isinstance(command, click.Group) and command.commands: - subcommands = nodes.section(ids=[f"{anchor}-subcommands"]) - subcommands += nodes.title(text="Subcommands") + # "Commands" matches Click's own vocabulary: the usage line says + # `COMMAND [ARGS]...` and Click's help prints a "Commands" section. + commands_section = nodes.section(ids=[f"{anchor}-commands"]) + commands_section += nodes.title(text="Commands") rows = [ - [_literal(name, True), _clean_text(subcommand.get_short_help_str())] + [ + _command_link(name, f"{anchor}-{self._slug(name)}"), + _clean_text(subcommand.get_short_help_str(limit=200)), + ] for name, subcommand in sorted(command.commands.items()) ] - _append_table(self, subcommands, ["Command", "Description"], rows, source_file) - command_section += subcommands + _append_table(self, commands_section, ["Command", "Description"], rows, source_file) + command_section += commands_section arguments, options = _parameter_rows(command, context, True) if arguments: @@ -256,4 +267,12 @@ def run(self) -> list[nodes.Node]: ) command_section += examples_section - return [root_section] + # Hoist the root's child sections (Commands, each subcommand, ...) to + # document level: Sphinx's page toc only descends through sections, so + # anything left inside the container would vanish from "On this page". + result: list[nodes.Node] = [root_section] + for child in list(root_section.children): + if isinstance(child, nodes.section): + root_section.remove(child) + result.append(child) + return result