base-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- base_cli/__init__.py +86 -0
- base_cli/_runtime.py +89 -0
- base_cli/app.py +483 -0
- base_cli/command_filters.py +37 -0
- base_cli/command_protocol.py +263 -0
- base_cli/config.py +270 -0
- base_cli/context.py +103 -0
- base_cli/exit_codes.py +9 -0
- base_cli/history.py +426 -0
- base_cli/ide_schema.py +74 -0
- base_cli/inspection.py +45 -0
- base_cli/logging.py +182 -0
- base_cli/output.py +202 -0
- base_cli/paths.py +136 -0
- base_cli/py.typed +0 -0
- base_cli/redaction.py +50 -0
- base_cli/testing.py +59 -0
- base_cli-0.1.0.dist-info/LICENSE +183 -0
- base_cli-0.1.0.dist-info/METADATA +543 -0
- base_cli-0.1.0.dist-info/RECORD +22 -0
- base_cli-0.1.0.dist-info/WHEEL +5 -0
- base_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: base-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A small, consistent Python CLI framework for Base and Base-supported projects
|
|
5
|
+
Author: Base Foundry
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/basefoundry/base-cli
|
|
8
|
+
Project-URL: Repository, https://github.com/basefoundry/base-cli
|
|
9
|
+
Project-URL: Issues, https://github.com/basefoundry/base-cli/issues
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: click>=8.1
|
|
25
|
+
Requires-Dist: PyYAML>=6.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
29
|
+
|
|
30
|
+
# `base-cli`
|
|
31
|
+
|
|
32
|
+
`base-cli` is the PyPI distribution; import it in Python as `base_cli`.
|
|
33
|
+
|
|
34
|
+
Install it with:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
python -m pip install base-cli
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Release builds, TestPyPI rehearsals, and protected PyPI publication are
|
|
41
|
+
documented in [`docs/releasing.md`](docs/releasing.md). The package exposes
|
|
42
|
+
`base_cli.__version__`, which matches the distribution version.
|
|
43
|
+
|
|
44
|
+
The package is distributed under the Apache License 2.0. Base itself remains
|
|
45
|
+
licensed separately under AGPL-3.0-or-later.
|
|
46
|
+
|
|
47
|
+
`base_cli` is Base's small Python framework for writing command-line tools that
|
|
48
|
+
feel consistent across Base and Base-supported projects.
|
|
49
|
+
|
|
50
|
+
It is intentionally thin. Click still owns argument parsing and command
|
|
51
|
+
execution, while `base_cli` adds the Base-specific behavior every project CLI
|
|
52
|
+
should get by default:
|
|
53
|
+
|
|
54
|
+
- standard command options such as `--debug`, `--quiet`, `--environment`,
|
|
55
|
+
`--config`, `--keep-temp`, and `--log-file`
|
|
56
|
+
- structured logging to stderr and, by default, to a persistent per-run log file
|
|
57
|
+
- Base project discovery through `base_manifest.yaml`
|
|
58
|
+
- config loading with predictable precedence
|
|
59
|
+
- per-run temp directories, persistent cache directories, and cleanup hooks
|
|
60
|
+
- sensitive argument redaction in debug invocation logs
|
|
61
|
+
- a command context object shared by command code and helper functions
|
|
62
|
+
- test helpers built on Click's `CliRunner`
|
|
63
|
+
|
|
64
|
+
## Design Goals
|
|
65
|
+
|
|
66
|
+
Base CLI tools should be easy to write, but not magical. A command should be
|
|
67
|
+
explicitly registered, receive an explicit `Context`, and use standard Python
|
|
68
|
+
functions instead of import-time side effects.
|
|
69
|
+
|
|
70
|
+
The package follows these rules:
|
|
71
|
+
|
|
72
|
+
- **Decorator-driven setup**: commands opt in by creating an `App` and
|
|
73
|
+
decorating a function.
|
|
74
|
+
- **Logs go to stderr**: user-facing program output can stay on stdout, while
|
|
75
|
+
logs remain redirectable and skippable.
|
|
76
|
+
- **Every run has a context**: logs, paths, config, environment, manifest, and
|
|
77
|
+
cleanup are available through one object.
|
|
78
|
+
- **No import-time filesystem writes**: state directories are created only when
|
|
79
|
+
a command runs.
|
|
80
|
+
- **Base-aware, Click-compatible**: command authors keep using familiar Click
|
|
81
|
+
concepts such as options and arguments.
|
|
82
|
+
|
|
83
|
+
## Public API
|
|
84
|
+
|
|
85
|
+
The supported facade is `import base_cli`. It exports the command lifecycle
|
|
86
|
+
(`App`, `Context`, `run_app`, decorators, and logging helpers), command filters,
|
|
87
|
+
the structured command protocol helpers, and the user configuration types used
|
|
88
|
+
by `Context.user_config`. The corresponding modules are also available as
|
|
89
|
+
`base_cli.command_filters`, `base_cli.command_protocol`, and
|
|
90
|
+
`base_cli.history`.
|
|
91
|
+
|
|
92
|
+
Low-level implementation helpers are intentionally not included in the
|
|
93
|
+
module `__all__` surfaces. Downstream code should use the documented facade or
|
|
94
|
+
the explicitly supported symbols from those modules.
|
|
95
|
+
|
|
96
|
+
## Minimal Command
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from __future__ import annotations
|
|
100
|
+
|
|
101
|
+
import base_cli
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
app = base_cli.App(name="hello", version="0.1.0")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@app.command()
|
|
108
|
+
@base_cli.option("--name", required=True)
|
|
109
|
+
def main(ctx: base_cli.Context, name: str) -> None:
|
|
110
|
+
ctx.log.info("starting hello")
|
|
111
|
+
print(f"hello {name}")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
if __name__ == "__main__":
|
|
115
|
+
raise SystemExit(base_cli.run_app(app))
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Running this command directly as a Python package automatically adds the
|
|
119
|
+
standard Base options:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
hello --name Ada
|
|
123
|
+
hello --debug --name Ada
|
|
124
|
+
hello --quiet --name Ada
|
|
125
|
+
hello --environment prod --name Ada
|
|
126
|
+
hello --keep-temp --name Ada
|
|
127
|
+
hello --log-file /tmp/hello.log --name Ada
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Long options with values use space-separated syntax. `base_cli.run_app()` rejects
|
|
131
|
+
equals-form values such as `--name=Ada` before Click parses arguments.
|
|
132
|
+
These are direct package options. Public `basectl` launchers expose `-v` for
|
|
133
|
+
command-level debug logs and command-specific flags from
|
|
134
|
+
`basectl <command> --help`; they do not expose `--debug`, `--quiet`,
|
|
135
|
+
`--log-file`, `--config`, or `--environment` as public `basectl` options.
|
|
136
|
+
The wrapper-level `basectl --keep-temp <command>` option preserves the
|
|
137
|
+
complete temporary tree for that run.
|
|
138
|
+
|
|
139
|
+
## Command Registration
|
|
140
|
+
|
|
141
|
+
Use `App` when you want a named command:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
app = base_cli.App(name="base-projects", version="0.1.0")
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Register the command function explicitly:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
@app.command()
|
|
151
|
+
def main(ctx: base_cli.Context) -> None:
|
|
152
|
+
...
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The command function always receives `ctx` as its first argument. User-defined
|
|
156
|
+
options and arguments are passed after the Base standard options have been
|
|
157
|
+
removed from Click's keyword arguments.
|
|
158
|
+
|
|
159
|
+
For small scripts, the module-level decorators are available:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
@base_cli.command()
|
|
163
|
+
def main(ctx: base_cli.Context) -> None:
|
|
164
|
+
...
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
In Base itself, prefer an explicit `App` so command names and versions are
|
|
168
|
+
obvious at the top of the module.
|
|
169
|
+
|
|
170
|
+
Use `@app.subcommand()` when one CLI needs multiple verbs while keeping Base's
|
|
171
|
+
standard context, logging, redaction, and cleanup lifecycle for each invocation:
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
app = base_cli.App(
|
|
175
|
+
name="workspace-tools",
|
|
176
|
+
version="0.1.0",
|
|
177
|
+
help="Inspect and sync workspace projects.",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@app.subcommand()
|
|
182
|
+
@base_cli.argument("project")
|
|
183
|
+
def status(ctx: base_cli.Context, project: str) -> None:
|
|
184
|
+
ctx.log.info("checking %s", project)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@app.subcommand("sync")
|
|
188
|
+
@base_cli.option("--dry-run", is_flag=True)
|
|
189
|
+
def sync_project(ctx: base_cli.Context, dry_run: bool) -> None:
|
|
190
|
+
if ctx.dry_run:
|
|
191
|
+
ctx.log.info("previewing sync")
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Subcommands use the same `base_cli.option()` and `base_cli.argument()` metadata
|
|
195
|
+
as single commands. `App(help=...)` appears in the command group's `--help`
|
|
196
|
+
output. For subcommand apps, prefer standard Base options before the subcommand
|
|
197
|
+
name, for example `workspace-tools --debug status demo`. The post-subcommand
|
|
198
|
+
form, such as `workspace-tools status --debug demo`, remains accepted for
|
|
199
|
+
compatibility. Use either `@app.command()` for a single-command CLI or
|
|
200
|
+
`@app.subcommand()` for a command group; do not mix the two registration styles
|
|
201
|
+
on one `App`.
|
|
202
|
+
|
|
203
|
+
## Options And Arguments
|
|
204
|
+
|
|
205
|
+
`base_cli.option` and `base_cli.argument` mirror Click's decorators:
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
@app.command()
|
|
209
|
+
@base_cli.argument("project")
|
|
210
|
+
@base_cli.option("--workspace", type=str)
|
|
211
|
+
def main(ctx: base_cli.Context, project: str, workspace: str | None) -> None:
|
|
212
|
+
...
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Use `sensitive=True` for options whose values should not appear in invocation
|
|
216
|
+
logs:
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
@base_cli.option("--token", sensitive=True, required=True)
|
|
220
|
+
def main(ctx: base_cli.Context, token: str) -> None:
|
|
221
|
+
...
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Both `--token secret` and an externally supplied `--token=secret` token are
|
|
225
|
+
redacted in debug logs, even though Base command invocation rejects equals-form
|
|
226
|
+
option values before Click parses them.
|
|
227
|
+
|
|
228
|
+
Use `dry_run=True` when a nonstandard option should drive `ctx.dry_run` and
|
|
229
|
+
Base's default durable-write suppression:
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
@base_cli.option("--preview", is_flag=True, dry_run=True)
|
|
233
|
+
def main(ctx: base_cli.Context, preview: bool) -> None:
|
|
234
|
+
if ctx.dry_run:
|
|
235
|
+
ctx.log.info("previewing changes")
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The conventional `dry_run` parameter is recognized automatically, so commands
|
|
239
|
+
using `@base_cli.option("--dry-run", is_flag=True)` do not need the marker.
|
|
240
|
+
Only one option on a command may be marked `dry_run=True`; duplicate dry-run
|
|
241
|
+
markers fail during command registration so authors do not accidentally ship an
|
|
242
|
+
option that is ignored by `ctx.dry_run`.
|
|
243
|
+
|
|
244
|
+
## Standard Options
|
|
245
|
+
|
|
246
|
+
Every `base_cli.App` command gets these options:
|
|
247
|
+
|
|
248
|
+
- `--debug`: enable DEBUG logging on the user-facing stderr stream.
|
|
249
|
+
- `--quiet`, `-q`: suppress INFO logs on the user-facing stderr stream.
|
|
250
|
+
- `--environment <name>`: set `ctx.environment` for the run.
|
|
251
|
+
- `--config <path>`: merge an additional YAML config file.
|
|
252
|
+
- `--keep-temp`: preserve the run's temp directory after command completion.
|
|
253
|
+
- `--log-file <path>`: write the persistent log to a specific file.
|
|
254
|
+
- `--version`: shown when the `App` was created with a version.
|
|
255
|
+
|
|
256
|
+
The command receives only its own application-specific options. Standard options
|
|
257
|
+
are consumed before the command function is called.
|
|
258
|
+
|
|
259
|
+
## Exit Codes
|
|
260
|
+
|
|
261
|
+
Use `base_cli.ExitCode` when command code or tests need to name Base's standard
|
|
262
|
+
command result meanings:
|
|
263
|
+
|
|
264
|
+
- `ExitCode.SUCCESS` (`0`): the command completed successfully.
|
|
265
|
+
- `ExitCode.FAILURE` (`1`): the command was valid, but an operational problem
|
|
266
|
+
prevented successful completion.
|
|
267
|
+
- `ExitCode.USAGE_ERROR` (`2`): the command could not proceed because user
|
|
268
|
+
input, configuration, or environment setup was invalid or incomplete.
|
|
269
|
+
|
|
270
|
+
Existing commands can keep returning integers. New code should prefer the named
|
|
271
|
+
constants when it makes intent clearer:
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
if ctx.project_root is None:
|
|
275
|
+
ctx.log.error("run this command from a Base project")
|
|
276
|
+
return base_cli.ExitCode.USAGE_ERROR
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Context
|
|
280
|
+
|
|
281
|
+
`Context` is the object command code should pass around instead of rediscovering
|
|
282
|
+
Base paths or global settings.
|
|
283
|
+
|
|
284
|
+
Important fields include:
|
|
285
|
+
|
|
286
|
+
- `ctx.cli_name`: normalized CLI name used for state paths and logger names.
|
|
287
|
+
- `ctx.run_id`: timestamp plus short random suffix for this invocation.
|
|
288
|
+
- `ctx.base_home`: resolved `BASE_HOME`, when available.
|
|
289
|
+
- `ctx.project_root`: directory containing the nearest `base_manifest.yaml`.
|
|
290
|
+
- `ctx.workspace_root`: configured workspace root from `~/.base.d/config.yaml`.
|
|
291
|
+
- `ctx.manifest_path`: nearest discovered Base manifest.
|
|
292
|
+
- `ctx.history_scope`: compatibility scope marker; delegated children are not
|
|
293
|
+
written as separate history events.
|
|
294
|
+
- `ctx.history_parent_run_id`: shared parent `basectl` invocation ID, when delegated.
|
|
295
|
+
- `ctx.runtime_owner`: `base` or `project`.
|
|
296
|
+
- `ctx.owner_root`: owner namespace root under the Base cache root.
|
|
297
|
+
- `ctx.run_root`: this invocation's run bundle.
|
|
298
|
+
- `ctx.state_dir`: owner root (compatibility alias).
|
|
299
|
+
- `ctx.log_dir`: run-bundle log directory.
|
|
300
|
+
- `ctx.cache_dir`: persistent component cache directory.
|
|
301
|
+
- `ctx.temp_dir`: per-run temp directory inside the bundle.
|
|
302
|
+
- `ctx.log_file`: the run's shared `logs/primary.log`, or `None` when persistent
|
|
303
|
+
logging is disabled.
|
|
304
|
+
- `ctx.config`: merged configuration dictionary.
|
|
305
|
+
- `ctx.user_config`: typed user configuration from `~/.base.d/config.yaml`.
|
|
306
|
+
- `ctx.environment`: active environment, defaulting to `dev`.
|
|
307
|
+
- `ctx.debug`: whether debug logging is enabled for the stderr stream.
|
|
308
|
+
- `ctx.quiet`: whether INFO logs are suppressed on the stderr stream.
|
|
309
|
+
- `ctx.dry_run`: whether the command is running in a no-durable-write mode.
|
|
310
|
+
- `ctx.keep_temp`: whether `ctx.temp_dir` should survive cleanup.
|
|
311
|
+
- `ctx.log`: standard Python logger configured by Base.
|
|
312
|
+
|
|
313
|
+
Helpers can retrieve the active context without threading it through every call:
|
|
314
|
+
|
|
315
|
+
```python
|
|
316
|
+
from base_cli import get_current_context
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def helper() -> None:
|
|
320
|
+
ctx = get_current_context()
|
|
321
|
+
ctx.log.debug("helper is running")
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
`get_current_context()` is valid only while a `base_cli.App` command is running.
|
|
325
|
+
|
|
326
|
+
## Logging
|
|
327
|
+
|
|
328
|
+
`base_cli` configures two handlers:
|
|
329
|
+
|
|
330
|
+
- a user-facing stderr handler at INFO by default, DEBUG with `--debug`, or
|
|
331
|
+
WARNING with `--quiet` / `-q`
|
|
332
|
+
- a persistent file handler that records DEBUG logs when persistent logging is
|
|
333
|
+
enabled
|
|
334
|
+
|
|
335
|
+
`--quiet` suppresses INFO output on the user-facing stream but still shows
|
|
336
|
+
warnings and errors. `--debug` and `--quiet` cannot be used together. Persistent
|
|
337
|
+
log files still receive DEBUG-level detail, including INFO messages suppressed
|
|
338
|
+
from stderr. When `basectl --color` is used on a terminal, the user-facing
|
|
339
|
+
Python logs use the same level colors as Bash logs; persistent log files remain
|
|
340
|
+
plain text. `NO_COLOR` disables colors.
|
|
341
|
+
|
|
342
|
+
Advanced tests and CI wrappers can call `base_cli.configure_logger(...,
|
|
343
|
+
stream=..., formatter=...)` to capture user-facing logs or apply a custom
|
|
344
|
+
formatter without replacing Base's logger setup. Leave those arguments as
|
|
345
|
+
`None` to keep the default stderr stream and `BaseCliFormatter`. Base CLI log
|
|
346
|
+
timestamps use the host's local timezone and include its numeric offset by
|
|
347
|
+
default. When the wrapper sets `LOG_UTC=1` (for example via
|
|
348
|
+
`basectl --utc-wrapper`), they use UTC and include an explicit `UTC` marker.
|
|
349
|
+
|
|
350
|
+
This setting affects log presentation only. Run metadata, history records, and
|
|
351
|
+
run IDs retain their canonical UTC representation.
|
|
352
|
+
|
|
353
|
+
Commands that inspect runtime artifacts can use `base_cli.App(log_to_file=False)`
|
|
354
|
+
to keep the standard context, `--debug`, and `--quiet` behavior without creating
|
|
355
|
+
default `logs/`, `cache/`, or `tmp/<run-id>/` directories. `base_logs` uses this
|
|
356
|
+
mode so `basectl logs` does not appear in its own output; `base_history` does
|
|
357
|
+
the same for `basectl history`. An explicit `--log-file <path>` still enables
|
|
358
|
+
file logging for that invocation.
|
|
359
|
+
|
|
360
|
+
Commands running with `ctx.dry_run` also skip default `logs/`, `cache/`, and
|
|
361
|
+
`tmp/<run-id>/` creation. Passing `--log-file <path>` still writes to that
|
|
362
|
+
explicit file so tests and diagnostics can inspect dry-run logs when needed.
|
|
363
|
+
|
|
364
|
+
For Python-backed commands with persistent logs, `base_cli.App` also writes a
|
|
365
|
+
best-effort final history record to `<base-cache-root>/base/history/runs.jsonl`.
|
|
366
|
+
History records contain redacted command metadata, timing, exit status, project
|
|
367
|
+
context when known, and a pointer to the raw log file. History writes are local
|
|
368
|
+
only and do not fail the user command when the index cannot be updated.
|
|
369
|
+
|
|
370
|
+
High-frequency tools can set `base_cli.App(max_log_files=<count>)` to keep at
|
|
371
|
+
most that many default persistent log files across the owner's run bundles.
|
|
372
|
+
Retention runs during startup after the current run's default log file is
|
|
373
|
+
resolved, and the current run's log file is never pruned. The policy is skipped
|
|
374
|
+
for `ctx.dry_run`,
|
|
375
|
+
`log_to_file=False`, and explicit `--log-file` paths so no-durable-write modes
|
|
376
|
+
and caller-selected log locations stay under caller control. Use this as a
|
|
377
|
+
small guardrail for busy local tools; `basectl clean` remains the broader
|
|
378
|
+
maintenance command for caches, logs, and retained temp files.
|
|
379
|
+
|
|
380
|
+
Logs use the same general shape as Base Bash logs:
|
|
381
|
+
|
|
382
|
+
```text
|
|
383
|
+
2026-05-26 12:34:56 INFO path/to/file.py:42 message
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Use either `ctx.log` directly:
|
|
387
|
+
|
|
388
|
+
```python
|
|
389
|
+
ctx.log.info("processed %s items", count)
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
or the convenience functions:
|
|
393
|
+
|
|
394
|
+
```python
|
|
395
|
+
base_cli.log_debug("cache_dir=%s", ctx.cache_dir)
|
|
396
|
+
base_cli.log_info("done")
|
|
397
|
+
base_cli.log_warning("using fallback")
|
|
398
|
+
base_cli.log_error("failed")
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Program output should still use stdout when another command might consume it.
|
|
402
|
+
Logs should stay on stderr so users can redirect or ignore logs without losing
|
|
403
|
+
the real command output.
|
|
404
|
+
|
|
405
|
+
## Config Precedence
|
|
406
|
+
|
|
407
|
+
Configuration is loaded from YAML files and environment variables in this order:
|
|
408
|
+
|
|
409
|
+
1. user config: `~/.base.d/config.yaml`
|
|
410
|
+
2. project config: `<project>/.base/config.yaml`
|
|
411
|
+
3. explicit config from `--config`
|
|
412
|
+
4. environment variables
|
|
413
|
+
5. direct command-line standard options
|
|
414
|
+
|
|
415
|
+
Environment variables currently recognized by the config layer:
|
|
416
|
+
|
|
417
|
+
- `BASE_CLI_ENVIRONMENT`
|
|
418
|
+
- `BASE_CLI_LOG_LEVEL`
|
|
419
|
+
- `BASE_CLI_KEEP_TEMP`
|
|
420
|
+
|
|
421
|
+
`LOG_DEBUG=1` or `LOG_DEBUG=true` is also accepted as an internal compatibility
|
|
422
|
+
fallback for wrapper/debug paths when `BASE_CLI_LOG_LEVEL` is unset. Prefer
|
|
423
|
+
`BASE_CLI_LOG_LEVEL=debug` for user-facing Python CLI debug logging.
|
|
424
|
+
|
|
425
|
+
Command-line standard options are applied after config is loaded. For example,
|
|
426
|
+
`--environment prod` overrides `environment: dev` from config.
|
|
427
|
+
|
|
428
|
+
`ctx.config` exposes the merged raw configuration after user, project,
|
|
429
|
+
explicit, and environment layers are applied. `ctx.user_config` exposes only the
|
|
430
|
+
typed machine-local user config, including `workspace.root`,
|
|
431
|
+
`workspace.manifest`, and IDE
|
|
432
|
+
preferences, so command code does not need to re-read `~/.base.d/config.yaml`
|
|
433
|
+
for those structured values.
|
|
434
|
+
|
|
435
|
+
The user config file is machine-local by default. Base owns the semantics of
|
|
436
|
+
`~/.base.d/config.yaml`, while users own backup and sync choices such as iCloud,
|
|
437
|
+
chezmoi, dotfiles repositories, Time Machine, or manual copy. See
|
|
438
|
+
`docs/local-config.md` for the product-level boundary.
|
|
439
|
+
|
|
440
|
+
## Project Discovery
|
|
441
|
+
|
|
442
|
+
When a command runs, `base_cli` walks upward from the current working directory
|
|
443
|
+
looking for `base_manifest.yaml`.
|
|
444
|
+
|
|
445
|
+
If found:
|
|
446
|
+
|
|
447
|
+
- `ctx.manifest_path` points to the manifest
|
|
448
|
+
- `ctx.project_root` points to the manifest's parent directory
|
|
449
|
+
|
|
450
|
+
If no manifest is found, both fields are `None`. Commands that require a Base
|
|
451
|
+
project should validate this explicitly and return a clear usage error or
|
|
452
|
+
actionable message.
|
|
453
|
+
|
|
454
|
+
## Runtime Directories
|
|
455
|
+
|
|
456
|
+
Runtime state is rooted at `~/Library/Caches/base` on macOS and `~/.cache/base`
|
|
457
|
+
elsewhere. `BASE_CACHE_DIR` overrides the root. See
|
|
458
|
+
[`docs/cache-ownership-and-layout.md`](../../../docs/cache-ownership-and-layout.md)
|
|
459
|
+
for the owner-aware layout. Base control-plane commands use `base/`; a
|
|
460
|
+
Base-compliant project's own commands use `projects/<project>/<checkout-id>/`.
|
|
461
|
+
Each invocation is a run bundle containing private (`0600`) `run.json`,
|
|
462
|
+
`logs/`, and `tmp/`,
|
|
463
|
+
while persistent component caches live in the owner's `cache/components/`.
|
|
464
|
+
`basectl clean --older-than <age>` removes old bundles and component caches;
|
|
465
|
+
`--keep-last <count>` retains the newest completed bundles per owner.
|
|
466
|
+
|
|
467
|
+
Use `ctx.on_cleanup()` for cleanup work that should happen even when helper code
|
|
468
|
+
does not own the main command wrapper:
|
|
469
|
+
|
|
470
|
+
```python
|
|
471
|
+
def close_connection() -> None:
|
|
472
|
+
connection.close()
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
ctx.on_cleanup(close_connection)
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
Cleanup hooks run before temp directory removal. Hook failures are logged as
|
|
479
|
+
warnings and do not prevent later hooks from running.
|
|
480
|
+
|
|
481
|
+
## Testing
|
|
482
|
+
|
|
483
|
+
Use `base_cli.testing.invoke` for unit tests:
|
|
484
|
+
|
|
485
|
+
```python
|
|
486
|
+
from pathlib import Path
|
|
487
|
+
|
|
488
|
+
from base_cli.testing import invoke
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def test_command(tmp_path: Path) -> None:
|
|
492
|
+
project = tmp_path / "project"
|
|
493
|
+
project.mkdir()
|
|
494
|
+
|
|
495
|
+
result = invoke(
|
|
496
|
+
app,
|
|
497
|
+
["--name", "Ada"],
|
|
498
|
+
home=tmp_path,
|
|
499
|
+
cwd=project,
|
|
500
|
+
manifest={"project": {"name": "demo"}, "artifacts": []},
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
assert result.exit_code == 0
|
|
504
|
+
assert "hello Ada" in result.stdout
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
The helper wraps Click's `CliRunner`, sets `HOME` when requested, supplies
|
|
508
|
+
`cwd` to Base's context discovery without mutating process-global cwd, and keeps
|
|
509
|
+
stderr separate on Click versions that support it. Use `cwd` for commands whose
|
|
510
|
+
behavior depends on project discovery, including tests that intentionally run
|
|
511
|
+
outside a Base project. Pass
|
|
512
|
+
`manifest={...}` with `cwd` to write a temporary `base_manifest.yaml` before
|
|
513
|
+
the command runs.
|
|
514
|
+
|
|
515
|
+
When `home` is supplied, `invoke()` also defaults `BASE_CACHE_DIR` to
|
|
516
|
+
`<home>/.cache/base` so helper-based tests do not inherit a developer's real
|
|
517
|
+
cache root. Pass `env={"BASE_CACHE_DIR": str(path)}` when a test needs an
|
|
518
|
+
explicit cache location.
|
|
519
|
+
|
|
520
|
+
## When To Use `base_cli`
|
|
521
|
+
|
|
522
|
+
Use `base_cli` for Python commands that are part of Base or a Base-supported
|
|
523
|
+
project and need standard Base behavior.
|
|
524
|
+
|
|
525
|
+
Base public command engines under `cli/python/base_*/engine.py` should
|
|
526
|
+
instantiate `base_cli.App` so standard options, logging, redaction, runtime
|
|
527
|
+
state, and local command history stay consistent. If a future public Python
|
|
528
|
+
engine intentionally bypasses this lifecycle, document the reason in code and
|
|
529
|
+
in this guide, then add it as an explicit lifecycle-audit exemption. Shell-only
|
|
530
|
+
helpers that avoid Python startup, such as `basectl config path`, do not create
|
|
531
|
+
Python logs or history records; once a `basectl` path enters a Python command
|
|
532
|
+
package, it should participate in `base_cli.App`.
|
|
533
|
+
|
|
534
|
+
It is a good fit for:
|
|
535
|
+
|
|
536
|
+
- project discovery commands
|
|
537
|
+
- setup and artifact management commands
|
|
538
|
+
- developer workflow commands
|
|
539
|
+
- CLIs that need predictable logs, temp directories, and config precedence
|
|
540
|
+
|
|
541
|
+
It is not meant to replace Click, Typer, argparse, or rich terminal UI
|
|
542
|
+
frameworks. It is the Base layer around command lifecycle, context, logging,
|
|
543
|
+
configuration, and state.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
base_cli/__init__.py,sha256=QJY2CEfaVDF50rNdCzB2LjO5o9O8j46JhJGjmV6SSEI,2465
|
|
2
|
+
base_cli/_runtime.py,sha256=VhxeIfl8GMIEOIBvu2k7ZIm4XG4TdMm00xCLwDHuJBc,2677
|
|
3
|
+
base_cli/app.py,sha256=0gELgIMJsX7Px5nIRd_kophLUEy2IwTl31cTU4io2P0,20363
|
|
4
|
+
base_cli/command_filters.py,sha256=BnOfEThw5LvEp06lv2PZXLHFWDposGy1qeOA9tqvKBo,1285
|
|
5
|
+
base_cli/command_protocol.py,sha256=GG7bggxoDqF9VA2rfK8uShhIFI2vdg2Ii7gk5ZVp7-w,9929
|
|
6
|
+
base_cli/config.py,sha256=fTz_QH_s_R-DWOv_NksoJln2EoeyN7fsd-cq-R50VlM,9525
|
|
7
|
+
base_cli/context.py,sha256=Mb_xiemnUiZXTCaczaFkN-0MFzPCPTLC-th4_N565NM,3561
|
|
8
|
+
base_cli/exit_codes.py,sha256=eVHoCtFbZJ10KfbOSIQfHl8HZGiWFYyFdg58EblArIU,159
|
|
9
|
+
base_cli/history.py,sha256=_7FKNZZ9I0sRCDtpxsG1igVw_-ryZZzGcvDcoZc66gw,13513
|
|
10
|
+
base_cli/ide_schema.py,sha256=jJH2ZVbBASinXWhedHrQ8P-eF_XNXeso1oJJ9kXKZKI,2192
|
|
11
|
+
base_cli/inspection.py,sha256=I1LMvspXelTdqEXuFlf9-62OHJ5qYU_aPPiK82JV-j8,1191
|
|
12
|
+
base_cli/logging.py,sha256=kwICbRyXwqzlaWMvNd5T03vpvSvzbO2Q56Fq3m1lSVs,5728
|
|
13
|
+
base_cli/output.py,sha256=bJPrrbfUzdbvSjxG51RW0yG1reifkG63ISFoC9a4Eow,6838
|
|
14
|
+
base_cli/paths.py,sha256=nZm6OMQdSgRVS0i33cAmyu1TW-x9ywT12plmgKg24kA,3978
|
|
15
|
+
base_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
base_cli/redaction.py,sha256=qoASJ6KccGTABA0Ao7hYAGMUDJJuqhFgtQ9T7Hyztz0,1591
|
|
17
|
+
base_cli/testing.py,sha256=NmV6PYYa34iqQLlvfVzu5ZIEp4oXoGBmoC3vcf2u0fw,1923
|
|
18
|
+
base_cli-0.1.0.dist-info/LICENSE,sha256=ZzMAmMf8LNaj3S2uxM7wg7fenYafjBwfHeTeOyBTikY,10328
|
|
19
|
+
base_cli-0.1.0.dist-info/METADATA,sha256=LMqiWfR9GLsafhLzgymcndKInzmiCtXWgaRnFaDtUDw,20714
|
|
20
|
+
base_cli-0.1.0.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
|
|
21
|
+
base_cli-0.1.0.dist-info/top_level.txt,sha256=IvAvZRcOvOpPR7McHaJ9xdAgOjoQgkddkQMnR5vs-O8,9
|
|
22
|
+
base_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
base_cli
|