Skip to content

Commit 7161d5b

Browse files
authored
Scaffold read-only sourceosctl CLI
Adds a conservative read-only/dry-run sourceosctl CLI scaffold with fixtures, tests, validation, and documentation. Closes #1.
1 parent abdf2d3 commit 7161d5b

19 files changed

Lines changed: 627 additions & 17 deletions

‎.gitignore‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Python bytecode
2+
__pycache__/
3+
*.py[cod]
4+
*.pyo
5+
6+
# Distribution / packaging
7+
*.egg-info/
8+
dist/
9+
build/
10+
11+
# Temporary files
12+
*.tmp
13+
.DS_Store

‎Makefile‎

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,12 @@
1-
.PHONY: validate
1+
.PHONY: validate test
22

3-
validate:
3+
validate: test
44
@test -f README.md
55
@test -f AGENTS.md
66
@test -f .github/copilot-instructions.md
77
@test -f docs/DEVTOOLS_SCOPE.md
88
@test -f repo.maturity.yaml
9-
@python3 - <<'PY'
10-
import pathlib
11-
for path in [
12-
'README.md',
13-
'AGENTS.md',
14-
'.github/copilot-instructions.md',
15-
'docs/DEVTOOLS_SCOPE.md',
16-
'repo.maturity.yaml',
17-
]:
18-
text = pathlib.Path(path).read_text()
19-
if not text.strip():
20-
raise SystemExit(f'{path} is empty')
21-
print('OK: sourceos-devtools validation')
22-
PY
9+
@python3 scripts/validate_scaffold.py
10+
11+
test:
12+
@python3 -m unittest discover -s tests -v

‎README.md‎

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,45 @@ It should not contain:
3434
- SourceOS image build state;
3535
- secrets, tokens, credentials, private keys, or device-specific enrollment secrets.
3636

37+
## sourceosctl CLI
38+
39+
`sourceosctl` is the read-only/dry-run CLI surface for SourceOS developer and AI operator workflows.
40+
41+
### Usage
42+
43+
```text
44+
sourceosctl [--version] <command> [<subcommand>] [options]
45+
```
46+
47+
### Commands
48+
49+
| Command | Description |
50+
| --- | --- |
51+
| `sourceosctl doctor` | Run environment health checks (read-only) |
52+
| `sourceosctl profiles list` | List available SourceOS profiles (read-only) |
53+
| `sourceosctl nlboot evidence inspect <path>` | Inspect a NLBoot evidence JSON file (read-only) |
54+
| `sourceosctl release inspect <path>` | Inspect a release artifact JSON file (read-only) |
55+
| `sourceosctl fingerprint collect --dry-run` | Print environment fingerprint fields (dry-run only) |
56+
| `sourceosctl ai labs list` | List available AI labs (read-only) |
57+
| `sourceosctl agents sandbox plan --dry-run` | Print agent sandbox plan (dry-run only) |
58+
59+
### Running from the repo
60+
61+
```bash
62+
python3 bin/sourceosctl --help
63+
python3 bin/sourceosctl doctor
64+
python3 bin/sourceosctl profiles list
65+
python3 bin/sourceosctl nlboot evidence inspect fixtures/sample_nlboot_evidence.json
66+
python3 bin/sourceosctl release inspect fixtures/sample_release.json
67+
python3 bin/sourceosctl fingerprint collect --dry-run
68+
python3 bin/sourceosctl ai labs list
69+
python3 bin/sourceosctl agents sandbox plan --dry-run
70+
```
71+
72+
### Design constraints
73+
74+
All commands in the current surface are **read-only or dry-run**. No mutating command is implemented. Commands that would mutate host state are explicitly rejected at runtime.
75+
3776
## First milestone
3877

3978
M1 is repo maturity and install surface definition:
@@ -62,4 +101,9 @@ M1 is repo maturity and install surface definition:
62101
make validate
63102
```
64103

65-
The initial validation target checks repository metadata and JSON/YAML syntax where present. Implementation-specific validation should be added with each tool surface.
104+
The validation target runs the unit test suite and checks repository metadata. All 21 tests must pass.
105+
106+
```bash
107+
make test # run tests only
108+
```
109+

‎bin/sourceosctl‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/usr/bin/env python3
2+
"""sourceosctl entry-point script."""
3+
import sys
4+
import os
5+
6+
# Allow running directly from the repo root without installing.
7+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
8+
9+
from sourceosctl.cli import main
10+
11+
sys.exit(main())
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"schemaVersion": "nlboot-evidence.v1",
3+
"kind": "NLBootEvidence",
4+
"timestamp": "2025-01-01T00:00:00Z",
5+
"target": {
6+
"host": "example-host",
7+
"arch": "x86_64",
8+
"uefi": true
9+
},
10+
"boot": {
11+
"loader": "systemd-boot",
12+
"kernel": "6.6.0-sourceos",
13+
"cmdline": "root=/dev/sda1 ro quiet"
14+
},
15+
"status": "verified",
16+
"signature": "stub-signature-not-real"
17+
}

‎fixtures/sample_release.json‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"schemaVersion": "sourceos-release.v1",
3+
"name": "sourceos-devtools",
4+
"version": "0.1.0",
5+
"channel": "stable",
6+
"artifacts": [
7+
"sourceosctl-0.1.0-linux-x86_64.tar.gz",
8+
"sourceosctl-0.1.0-darwin-arm64.tar.gz"
9+
],
10+
"metadata": {
11+
"gitRef": "refs/heads/main",
12+
"builtAt": "2025-01-01T00:00:00Z"
13+
}
14+
}

‎scripts/validate_scaffold.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""Validation script for sourceos-devtools repository scaffold."""
2+
3+
import pathlib
4+
import sys
5+
6+
REQUIRED = [
7+
"README.md",
8+
"AGENTS.md",
9+
".github/copilot-instructions.md",
10+
"docs/DEVTOOLS_SCOPE.md",
11+
"repo.maturity.yaml",
12+
]
13+
14+
for path in REQUIRED:
15+
p = pathlib.Path(path)
16+
if not p.exists():
17+
raise SystemExit(f"MISSING: {path}")
18+
if not p.read_text().strip():
19+
raise SystemExit(f"EMPTY: {path}")
20+
21+
print("OK: sourceos-devtools validation")

‎sourceosctl/__init__.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""sourceosctl - SourceOS Developer and AI Operator CLI."""
2+
3+
__version__ = "0.1.0"

‎sourceosctl/cli.py‎

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"""sourceosctl CLI entry point."""
2+
3+
import argparse
4+
import sys
5+
6+
from sourceosctl import __version__
7+
from sourceosctl.commands import (
8+
doctor,
9+
profiles,
10+
nlboot,
11+
release,
12+
fingerprint,
13+
ai,
14+
agents,
15+
)
16+
17+
18+
def build_parser() -> argparse.ArgumentParser:
19+
"""Build and return the argument parser."""
20+
parser = argparse.ArgumentParser(
21+
prog="sourceosctl",
22+
description="SourceOS developer and AI operator CLI (read-only / dry-run surface)",
23+
)
24+
parser.add_argument(
25+
"--version", action="version", version=f"sourceosctl {__version__}"
26+
)
27+
28+
sub = parser.add_subparsers(dest="command", metavar="<command>")
29+
sub.required = True
30+
31+
# --- doctor ---
32+
doctor_p = sub.add_parser("doctor", help="Run environment health checks")
33+
doctor_p.set_defaults(func=doctor.run)
34+
35+
# --- profiles ---
36+
profiles_p = sub.add_parser("profiles", help="Profile management")
37+
profiles_sub = profiles_p.add_subparsers(dest="profiles_command", metavar="<subcommand>")
38+
profiles_sub.required = True
39+
profiles_list_p = profiles_sub.add_parser("list", help="List available profiles")
40+
profiles_list_p.set_defaults(func=profiles.list_profiles)
41+
42+
# --- nlboot ---
43+
nlboot_p = sub.add_parser("nlboot", help="NLBoot operator helpers")
44+
nlboot_sub = nlboot_p.add_subparsers(dest="nlboot_command", metavar="<subcommand>")
45+
nlboot_sub.required = True
46+
nlboot_evidence_p = nlboot_sub.add_parser("evidence", help="NLBoot evidence helpers")
47+
nlboot_evidence_sub = nlboot_evidence_p.add_subparsers(
48+
dest="nlboot_evidence_command", metavar="<subcommand>"
49+
)
50+
nlboot_evidence_sub.required = True
51+
nlboot_inspect_p = nlboot_evidence_sub.add_parser(
52+
"inspect", help="Inspect a NLBoot evidence file"
53+
)
54+
nlboot_inspect_p.add_argument("path", help="Path to NLBoot evidence JSON file")
55+
nlboot_inspect_p.set_defaults(func=nlboot.inspect_evidence)
56+
57+
# --- release ---
58+
release_p = sub.add_parser("release", help="Release artifact inspection")
59+
release_sub = release_p.add_subparsers(dest="release_command", metavar="<subcommand>")
60+
release_sub.required = True
61+
release_inspect_p = release_sub.add_parser("inspect", help="Inspect a release artifact")
62+
release_inspect_p.add_argument("path", help="Path to release artifact JSON file")
63+
release_inspect_p.set_defaults(func=release.inspect)
64+
65+
# --- fingerprint ---
66+
fingerprint_p = sub.add_parser("fingerprint", help="Environment fingerprint utilities")
67+
fingerprint_sub = fingerprint_p.add_subparsers(
68+
dest="fingerprint_command", metavar="<subcommand>"
69+
)
70+
fingerprint_sub.required = True
71+
fingerprint_collect_p = fingerprint_sub.add_parser(
72+
"collect", help="Collect environment fingerprint (dry-run only)"
73+
)
74+
fingerprint_collect_p.add_argument(
75+
"--dry-run",
76+
action="store_true",
77+
default=True,
78+
dest="dry_run",
79+
help="Print what would be collected without writing to disk (default: True)",
80+
)
81+
fingerprint_collect_p.set_defaults(func=fingerprint.collect)
82+
83+
# --- ai ---
84+
ai_p = sub.add_parser("ai", help="AI operator utilities")
85+
ai_sub = ai_p.add_subparsers(dest="ai_command", metavar="<subcommand>")
86+
ai_sub.required = True
87+
ai_labs_p = ai_sub.add_parser("labs", help="AI lab helpers")
88+
ai_labs_sub = ai_labs_p.add_subparsers(dest="ai_labs_command", metavar="<subcommand>")
89+
ai_labs_sub.required = True
90+
ai_labs_list_p = ai_labs_sub.add_parser("list", help="List available AI labs")
91+
ai_labs_list_p.set_defaults(func=ai.list_labs)
92+
93+
# --- agents ---
94+
agents_p = sub.add_parser("agents", help="Agent sandbox helpers")
95+
agents_sub = agents_p.add_subparsers(dest="agents_command", metavar="<subcommand>")
96+
agents_sub.required = True
97+
agents_sandbox_p = agents_sub.add_parser("sandbox", help="Agent sandbox management")
98+
agents_sandbox_sub = agents_sandbox_p.add_subparsers(
99+
dest="agents_sandbox_command", metavar="<subcommand>"
100+
)
101+
agents_sandbox_sub.required = True
102+
agents_sandbox_plan_p = agents_sandbox_sub.add_parser(
103+
"plan", help="Plan agent sandbox (dry-run only)"
104+
)
105+
agents_sandbox_plan_p.add_argument(
106+
"--dry-run",
107+
action="store_true",
108+
default=True,
109+
dest="dry_run",
110+
help="Print plan without executing (default: True)",
111+
)
112+
agents_sandbox_plan_p.set_defaults(func=agents.sandbox_plan)
113+
114+
return parser
115+
116+
117+
def main(argv=None) -> int:
118+
"""Main entry point. Returns exit code."""
119+
parser = build_parser()
120+
args = parser.parse_args(argv)
121+
return args.func(args) or 0
122+
123+
124+
if __name__ == "__main__":
125+
sys.exit(main())

‎sourceosctl/commands/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""sourceosctl command modules."""

0 commit comments

Comments
 (0)