base-cli 0.1.0__tar.gz → 0.3.0__tar.gz

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.
Files changed (61) hide show
  1. {base_cli-0.1.0/lib/python/base_cli.egg-info → base_cli-0.3.0}/PKG-INFO +166 -140
  2. {base_cli-0.1.0 → base_cli-0.3.0}/README.md +161 -138
  3. base_cli-0.3.0/VERSION +1 -0
  4. {base_cli-0.1.0 → base_cli-0.3.0}/lib/python/base_cli/__init__.py +32 -10
  5. base_cli-0.3.0/lib/python/base_cli/_dependencies.py +15 -0
  6. base_cli-0.3.0/lib/python/base_cli/_private_files.py +51 -0
  7. base_cli-0.3.0/lib/python/base_cli/_runtime.py +131 -0
  8. {base_cli-0.1.0 → base_cli-0.3.0}/lib/python/base_cli/app.py +74 -87
  9. base_cli-0.3.0/lib/python/base_cli/command_filters.py +70 -0
  10. {base_cli-0.1.0 → base_cli-0.3.0}/lib/python/base_cli/command_protocol.py +60 -61
  11. base_cli-0.3.0/lib/python/base_cli/config.py +28 -0
  12. {base_cli-0.1.0 → base_cli-0.3.0}/lib/python/base_cli/context.py +7 -8
  13. {base_cli-0.1.0 → base_cli-0.3.0}/lib/python/base_cli/exit_codes.py +1 -1
  14. {base_cli-0.1.0 → base_cli-0.3.0}/lib/python/base_cli/history.py +63 -144
  15. {base_cli-0.1.0 → base_cli-0.3.0}/lib/python/base_cli/logging.py +22 -13
  16. {base_cli-0.1.0 → base_cli-0.3.0}/lib/python/base_cli/output.py +6 -10
  17. base_cli-0.3.0/lib/python/base_cli/paths.py +90 -0
  18. base_cli-0.3.0/lib/python/base_cli/profile.py +147 -0
  19. {base_cli-0.1.0 → base_cli-0.3.0}/lib/python/base_cli/testing.py +21 -25
  20. {base_cli-0.1.0 → base_cli-0.3.0/lib/python/base_cli.egg-info}/PKG-INFO +166 -140
  21. {base_cli-0.1.0 → base_cli-0.3.0}/lib/python/base_cli.egg-info/SOURCES.txt +8 -6
  22. {base_cli-0.1.0 → base_cli-0.3.0}/pyproject.toml +4 -1
  23. {base_cli-0.1.0 → base_cli-0.3.0}/tests/test_app_log_retention.py +84 -19
  24. {base_cli-0.1.0 → base_cli-0.3.0}/tests/test_app_run.py +45 -11
  25. {base_cli-0.1.0 → base_cli-0.3.0}/tests/test_app_runtime_boundary.py +32 -15
  26. {base_cli-0.1.0 → base_cli-0.3.0}/tests/test_app_runtime_errors.py +18 -16
  27. base_cli-0.3.0/tests/test_command_filters.py +36 -0
  28. base_cli-0.3.0/tests/test_command_protocol.py +202 -0
  29. {base_cli-0.1.0 → base_cli-0.3.0}/tests/test_context_workspace.py +25 -14
  30. base_cli-0.3.0/tests/test_generic_core.py +80 -0
  31. base_cli-0.3.0/tests/test_history.py +61 -0
  32. {base_cli-0.1.0 → base_cli-0.3.0}/tests/test_logging.py +26 -3
  33. {base_cli-0.1.0 → base_cli-0.3.0}/tests/test_output.py +8 -0
  34. base_cli-0.3.0/tests/test_paths.py +86 -0
  35. base_cli-0.3.0/tests/test_profile.py +119 -0
  36. {base_cli-0.1.0 → base_cli-0.3.0}/tests/test_public_api.py +29 -13
  37. {base_cli-0.1.0 → base_cli-0.3.0}/tests/test_testing.py +83 -17
  38. base_cli-0.1.0/VERSION +0 -1
  39. base_cli-0.1.0/lib/python/base_cli/_runtime.py +0 -89
  40. base_cli-0.1.0/lib/python/base_cli/command_filters.py +0 -37
  41. base_cli-0.1.0/lib/python/base_cli/config.py +0 -270
  42. base_cli-0.1.0/lib/python/base_cli/ide_schema.py +0 -74
  43. base_cli-0.1.0/lib/python/base_cli/paths.py +0 -136
  44. base_cli-0.1.0/tests/test_base_cli.py +0 -1015
  45. base_cli-0.1.0/tests/test_command_protocol.py +0 -147
  46. base_cli-0.1.0/tests/test_history.py +0 -398
  47. base_cli-0.1.0/tests/test_ide_schema.py +0 -48
  48. base_cli-0.1.0/tests/test_user_config_github.py +0 -95
  49. base_cli-0.1.0/tests/test_user_config_workspace.py +0 -24
  50. {base_cli-0.1.0 → base_cli-0.3.0}/LICENSE +0 -0
  51. {base_cli-0.1.0 → base_cli-0.3.0}/lib/python/base_cli/inspection.py +0 -0
  52. {base_cli-0.1.0 → base_cli-0.3.0}/lib/python/base_cli/py.typed +0 -0
  53. {base_cli-0.1.0 → base_cli-0.3.0}/lib/python/base_cli/redaction.py +0 -0
  54. {base_cli-0.1.0 → base_cli-0.3.0}/lib/python/base_cli.egg-info/dependency_links.txt +0 -0
  55. {base_cli-0.1.0 → base_cli-0.3.0}/lib/python/base_cli.egg-info/requires.txt +0 -0
  56. {base_cli-0.1.0 → base_cli-0.3.0}/lib/python/base_cli.egg-info/top_level.txt +0 -0
  57. {base_cli-0.1.0 → base_cli-0.3.0}/setup.cfg +0 -0
  58. {base_cli-0.1.0 → base_cli-0.3.0}/tests/test_app_dry_run.py +0 -0
  59. {base_cli-0.1.0 → base_cli-0.3.0}/tests/test_app_quiet.py +0 -0
  60. {base_cli-0.1.0 → base_cli-0.3.0}/tests/test_app_subcommands.py +0 -0
  61. {base_cli-0.1.0 → base_cli-0.3.0}/tests/test_inspection.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.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
3
+ Version: 0.3.0
4
+ Summary: A small, consumer-neutral Python CLI framework
5
5
  Author: Base Foundry
6
6
  License: Apache-2.0
7
7
  Project-URL: Homepage, https://github.com/basefoundry/base-cli
@@ -10,6 +10,9 @@ Project-URL: Issues, https://github.com/basefoundry/base-cli/issues
10
10
  Classifier: Development Status :: 3 - Alpha
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Operating System :: MacOS
14
+ Classifier: Operating System :: Microsoft :: Windows
15
+ Classifier: Operating System :: POSIX :: Linux
13
16
  Classifier: Programming Language :: Python :: 3
14
17
  Classifier: Programming Language :: Python :: 3 :: Only
15
18
  Classifier: Programming Language :: Python :: 3.10
@@ -44,18 +47,18 @@ documented in [`docs/releasing.md`](docs/releasing.md). The package exposes
44
47
  The package is distributed under the Apache License 2.0. Base itself remains
45
48
  licensed separately under AGPL-3.0-or-later.
46
49
 
47
- `base_cli` is Base's small Python framework for writing command-line tools that
48
- feel consistent across Base and Base-supported projects.
50
+ `base_cli` is a small Python framework for writing command-line tools with
51
+ a consistent lifecycle. It is designed to be embedded by applications rather
52
+ than to define an application's project model. Base is one consumer of the
53
+ library, not part of its generic contract.
49
54
 
50
55
  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:
56
+ execution, while `base_cli` provides reusable lifecycle behavior:
53
57
 
54
58
  - standard command options such as `--debug`, `--quiet`, `--environment`,
55
59
  `--config`, `--keep-temp`, and `--log-file`
56
60
  - 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
61
+ - optional project discovery and configuration policies supplied by the consumer
59
62
  - per-run temp directories, persistent cache directories, and cleanup hooks
60
63
  - sensitive argument redaction in debug invocation logs
61
64
  - a command context object shared by command code and helper functions
@@ -63,7 +66,7 @@ should get by default:
63
66
 
64
67
  ## Design Goals
65
68
 
66
- Base CLI tools should be easy to write, but not magical. A command should be
69
+ CLI tools should be easy to write, but not magical. A command should be
67
70
  explicitly registered, receive an explicit `Context`, and use standard Python
68
71
  functions instead of import-time side effects.
69
72
 
@@ -73,22 +76,59 @@ The package follows these rules:
73
76
  decorating a function.
74
77
  - **Logs go to stderr**: user-facing program output can stay on stdout, while
75
78
  logs remain redirectable and skippable.
76
- - **Every run has a context**: logs, paths, config, environment, manifest, and
77
- cleanup are available through one object.
79
+ - **Every run has a context**: logs, paths, configuration, environment, and
80
+ cleanup are available through one object. Project metadata is available when
81
+ the selected consumer profile supplies it.
78
82
  - **No import-time filesystem writes**: state directories are created only when
79
83
  a command runs.
80
- - **Base-aware, Click-compatible**: command authors keep using familiar Click
81
- concepts such as options and arguments.
84
+ - **Consumer-neutral, Click-compatible**: command authors keep using familiar
85
+ Click concepts such as options and arguments.
86
+
87
+ ## Consumer Profiles
88
+
89
+ `App` accepts a `CliProfile` that supplies the policies which vary
90
+ between applications: project discovery, configuration, runtime placement, and
91
+ optional history persistence.
92
+
93
+ Standalone consumers use the generic profile by default. It can also be passed
94
+ explicitly when making the policy boundary visible:
95
+
96
+ ```python
97
+ app = base_cli.App(
98
+ name="hello",
99
+ version="0.1.0",
100
+ profile=base_cli.CliProfile.generic(),
101
+ )
102
+ ```
103
+
104
+ The generic profile has no manifest filename convention, no product-owned
105
+ configuration directory, and no implicit history writer. Applications can
106
+ provide those policies through callbacks or build their own profile. The
107
+ consumer-owned adapters should supply any product-specific policies. See
108
+ [`docs/consumer-profiles.md`](docs/consumer-profiles.md) for the boundary and
109
+ migration guidance.
82
110
 
83
111
  ## Public API
84
112
 
85
113
  The supported facade is `import base_cli`. It exports the command lifecycle
86
114
  (`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
115
+ and the structured command protocol helpers. Consumer-owned user configuration
116
+ is passed through `Context.user_config`; the library does not impose a schema.
117
+ The corresponding modules are also available as
89
118
  `base_cli.command_filters`, `base_cli.command_protocol`, and
90
119
  `base_cli.history`.
91
120
 
121
+ The command protocol owns only generic framing, field validation, and schema
122
+ registration. It ships with no application record types and uses
123
+ `COMMAND_PROTOCOL_V1` by default. A consumer can register its own schemas and
124
+ pass a compatibility `protocol_header` when it must interoperate with an
125
+ existing peer protocol.
126
+
127
+ Command filters use consumer-neutral name normalization by default. Consumers
128
+ with legacy command names can pass a `normalizer` callback to
129
+ `normalize_command_filter`, `normalize_command_filters`, and
130
+ `command_matches` to define compatibility aliases or prefixes.
131
+
92
132
  Low-level implementation helpers are intentionally not included in the
93
133
  module `__all__` surfaces. Downstream code should use the documented facade or
94
134
  the explicitly supported symbols from those modules.
@@ -101,7 +141,11 @@ from __future__ import annotations
101
141
  import base_cli
102
142
 
103
143
 
104
- app = base_cli.App(name="hello", version="0.1.0")
144
+ app = base_cli.App(
145
+ name="hello",
146
+ version="0.1.0",
147
+ profile=base_cli.CliProfile.generic(),
148
+ )
105
149
 
106
150
 
107
151
  @app.command()
@@ -116,7 +160,7 @@ if __name__ == "__main__":
116
160
  ```
117
161
 
118
162
  Running this command directly as a Python package automatically adds the
119
- standard Base options:
163
+ standard options:
120
164
 
121
165
  ```bash
122
166
  hello --name Ada
@@ -129,19 +173,16 @@ hello --log-file /tmp/hello.log --name Ada
129
173
 
130
174
  Long options with values use space-separated syntax. `base_cli.run_app()` rejects
131
175
  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.
176
+ These options belong to the application-level lifecycle. A consumer may expose
177
+ them through its own launcher or compose them with a higher-level command
178
+ wrapper.
138
179
 
139
180
  ## Command Registration
140
181
 
141
182
  Use `App` when you want a named command:
142
183
 
143
184
  ```python
144
- app = base_cli.App(name="base-projects", version="0.1.0")
185
+ app = base_cli.App(name="workspace-tools", version="0.1.0")
145
186
  ```
146
187
 
147
188
  Register the command function explicitly:
@@ -153,7 +194,7 @@ def main(ctx: base_cli.Context) -> None:
153
194
  ```
154
195
 
155
196
  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
197
+ options and arguments are passed after the standard lifecycle options have been
157
198
  removed from Click's keyword arguments.
158
199
 
159
200
  For small scripts, the module-level decorators are available:
@@ -164,10 +205,10 @@ def main(ctx: base_cli.Context) -> None:
164
205
  ...
165
206
  ```
166
207
 
167
- In Base itself, prefer an explicit `App` so command names and versions are
168
- obvious at the top of the module.
208
+ Prefer an explicit `App` when command names, versions, or consumer
209
+ policies should be visible at the top of the module.
169
210
 
170
- Use `@app.subcommand()` when one CLI needs multiple verbs while keeping Base's
211
+ Use `@app.subcommand()` when one CLI needs multiple verbs while keeping the
171
212
  standard context, logging, redaction, and cleanup lifecycle for each invocation:
172
213
 
173
214
  ```python
@@ -193,7 +234,7 @@ def sync_project(ctx: base_cli.Context, dry_run: bool) -> None:
193
234
 
194
235
  Subcommands use the same `base_cli.option()` and `base_cli.argument()` metadata
195
236
  as single commands. `App(help=...)` appears in the command group's `--help`
196
- output. For subcommand apps, prefer standard Base options before the subcommand
237
+ output. For subcommand apps, prefer standard options before the subcommand
197
238
  name, for example `workspace-tools --debug status demo`. The post-subcommand
198
239
  form, such as `workspace-tools status --debug demo`, remains accepted for
199
240
  compatibility. Use either `@app.command()` for a single-command CLI or
@@ -222,11 +263,11 @@ def main(ctx: base_cli.Context, token: str) -> None:
222
263
  ```
223
264
 
224
265
  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.
266
+ redacted in debug logs. The lifecycle rejects equals-form option values before
267
+ Click parses them.
227
268
 
228
269
  Use `dry_run=True` when a nonstandard option should drive `ctx.dry_run` and
229
- Base's default durable-write suppression:
270
+ the lifecycle's default durable-write suppression:
230
271
 
231
272
  ```python
232
273
  @base_cli.option("--preview", is_flag=True, dry_run=True)
@@ -258,7 +299,7 @@ are consumed before the command function is called.
258
299
 
259
300
  ## Exit Codes
260
301
 
261
- Use `base_cli.ExitCode` when command code or tests need to name Base's standard
302
+ Use `base_cli.ExitCode` when command code or tests need to name standard
262
303
  command result meanings:
263
304
 
264
305
  - `ExitCode.SUCCESS` (`0`): the command completed successfully.
@@ -272,28 +313,30 @@ constants when it makes intent clearer:
272
313
 
273
314
  ```python
274
315
  if ctx.project_root is None:
275
- ctx.log.error("run this command from a Base project")
316
+ ctx.log.error("run this command from a project recognized by the consumer")
276
317
  return base_cli.ExitCode.USAGE_ERROR
277
318
  ```
278
319
 
279
320
  ## Context
280
321
 
281
322
  `Context` is the object command code should pass around instead of rediscovering
282
- Base paths or global settings.
323
+ runtime paths or global settings.
283
324
 
284
325
  Important fields include:
285
326
 
286
327
  - `ctx.cli_name`: normalized CLI name used for state paths and logger names.
287
328
  - `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.
329
+ - `ctx.application_home`: optional application home supplied by the profile.
330
+ - `ctx.project_root`: project root returned by the profile, when any.
331
+ - `ctx.workspace_root`: optional workspace root supplied by user configuration.
332
+ - `ctx.manifest_path`: project metadata path returned by the profile, when any.
333
+ - `ctx.history_scope`: history scope supplied by the profile or its
334
+ compatibility adapter.
335
+ - `ctx.history_parent_run_id`: optional parent invocation ID supplied by
336
+ the consumer.
337
+ - `ctx.runtime_owner`: consumer-defined runtime owner; the generic
338
+ profile uses `default`.
339
+ - `ctx.owner_root`: application namespace root under the configured cache root.
297
340
  - `ctx.run_root`: this invocation's run bundle.
298
341
  - `ctx.state_dir`: owner root (compatibility alias).
299
342
  - `ctx.log_dir`: run-bundle log directory.
@@ -302,13 +345,14 @@ Important fields include:
302
345
  - `ctx.log_file`: the run's shared `logs/primary.log`, or `None` when persistent
303
346
  logging is disabled.
304
347
  - `ctx.config`: merged configuration dictionary.
305
- - `ctx.user_config`: typed user configuration from `~/.base.d/config.yaml`.
348
+ - `ctx.user_config`: opaque consumer-owned user configuration returned by the
349
+ profile, or `None` for the generic default.
306
350
  - `ctx.environment`: active environment, defaulting to `dev`.
307
351
  - `ctx.debug`: whether debug logging is enabled for the stderr stream.
308
352
  - `ctx.quiet`: whether INFO logs are suppressed on the stderr stream.
309
353
  - `ctx.dry_run`: whether the command is running in a no-durable-write mode.
310
354
  - `ctx.keep_temp`: whether `ctx.temp_dir` should survive cleanup.
311
- - `ctx.log`: standard Python logger configured by Base.
355
+ - `ctx.log`: standard Python logger configured by `base_cli`.
312
356
 
313
357
  Helpers can retrieve the active context without threading it through every call:
314
358
 
@@ -335,37 +379,39 @@ def helper() -> None:
335
379
  `--quiet` suppresses INFO output on the user-facing stream but still shows
336
380
  warnings and errors. `--debug` and `--quiet` cannot be used together. Persistent
337
381
  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.
382
+ from stderr. User-facing logs use colors automatically on interactive terminals;
383
+ persistent log files remain plain text. Set `NO_COLOR=1` or
384
+ `BASE_CLI_COLOR=0` to disable colors. A consumer wrapper may add its own color
385
+ option and map it to the environment variable.
386
+
387
+ Click also provides shell completion. For an app named `hello`, request a
388
+ completion script with `_HELLO_COMPLETE=bash_source hello`, replacing `bash`
389
+ with `zsh` or `fish` as needed. `base_cli` leaves installation to the caller so
390
+ shell startup files remain under user control.
341
391
 
342
392
  Advanced tests and CI wrappers can call `base_cli.configure_logger(...,
343
393
  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.
394
+ formatter. Leave those arguments as `None` to keep the default stderr stream
395
+ and formatter. Log timestamps use the host's local timezone and include its
396
+ numeric offset by default. A consumer can set `LOG_UTC=1` to use UTC and
397
+ include an explicit `UTC` marker.
349
398
 
350
399
  This setting affects log presentation only. Run metadata, history records, and
351
400
  run IDs retain their canonical UTC representation.
352
401
 
353
402
  Commands that inspect runtime artifacts can use `base_cli.App(log_to_file=False)`
354
403
  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.
404
+ default `logs/`, `cache/`, or `tmp/<run-id>/` directories. An explicit
405
+ `--log-file <path>` still enables file logging for that invocation.
359
406
 
360
407
  Commands running with `ctx.dry_run` also skip default `logs/`, `cache/`, and
361
408
  `tmp/<run-id>/` creation. Passing `--log-file <path>` still writes to that
362
409
  explicit file so tests and diagnostics can inspect dry-run logs when needed.
363
410
 
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.
411
+ The generic profile does not write command history. A profile may provide a
412
+ history writer to persist redacted command metadata, timing, exit status,
413
+ project context, and a pointer to the raw log file. History writes should be
414
+ best-effort and should not fail the user command when an index cannot be updated.
369
415
 
370
416
  High-frequency tools can set `base_cli.App(max_log_files=<count>)` to keep at
371
417
  most that many default persistent log files across the owner's run bundles.
@@ -374,10 +420,10 @@ resolved, and the current run's log file is never pruned. The policy is skipped
374
420
  for `ctx.dry_run`,
375
421
  `log_to_file=False`, and explicit `--log-file` paths so no-durable-write modes
376
422
  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.
423
+ small guardrail for busy local tools; an application can provide broader
424
+ maintenance commands for caches, logs, and retained temp files.
379
425
 
380
- Logs use the same general shape as Base Bash logs:
426
+ Logs use a stable, human-readable shape:
381
427
 
382
428
  ```text
383
429
  2026-05-26 12:34:56 INFO path/to/file.py:42 message
@@ -404,65 +450,52 @@ the real command output.
404
450
 
405
451
  ## Config Precedence
406
452
 
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:
453
+ The generic profile has no implicit configuration files. It loads the file
454
+ passed through `--config`, when present, and otherwise starts with an empty
455
+ configuration dictionary. Standard command-line options are applied by the
456
+ lifecycle after the profile's configuration is loaded; for example,
457
+ `--environment prod` overrides `environment: dev` from an explicit
458
+ configuration file.
416
459
 
417
- - `BASE_CLI_ENVIRONMENT`
418
- - `BASE_CLI_LOG_LEVEL`
419
- - `BASE_CLI_KEEP_TEMP`
420
460
 
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.
461
+ `ctx.config` exposes the dictionary returned by the profile. `ctx.user_config`
462
+ exposes the opaque user-configuration value returned by the profile. Consumers
463
+ that need user files, project files, environment variables, or a merge
464
+ precedence must implement those policies in `CliProfile.load_config` and
465
+ `CliProfile.load_user_config`; `base_cli` does not define the value's fields.
439
466
 
440
467
  ## Project Discovery
441
468
 
442
- When a command runs, `base_cli` walks upward from the current working directory
443
- looking for `base_manifest.yaml`.
469
+ The generic profile does not discover projects or assume a manifest filename.
470
+ Its `ctx.project_root` and `ctx.manifest_path` fields are `None` unless the
471
+ consumer supplies a `discover_project` policy. A profile can discover projects
472
+ from a manifest, workspace, repository metadata, or any other application-owned
473
+ source and return a `ProjectInfo` value.
444
474
 
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.
475
+ Commands that require a project should validate the profile-provided value
476
+ explicitly and return a clear usage error or actionable message.
453
477
 
454
478
  ## Runtime Directories
455
479
 
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.
480
+ The generic profile uses the configured cache root and an application namespace
481
+ to create per-run logs, caches, and temporary directories. Pass
482
+ `cache_root` to `CliProfile.generic()` for deterministic placement in tests or
483
+ applications; otherwise the platform cache directory is used. Linux and WSL2
484
+ follow `XDG_CACHE_HOME` or `~/.cache`, macOS uses `~/Library/Caches`, and
485
+ Windows uses `%LOCALAPPDATA%` (falling back to `~/AppData/Local`). Set
486
+ `BASE_CLI_CACHE_DIR` to override the default on any platform. The generic
487
+ profile does not prescribe a product-wide cache name or cleanup command.
488
+
489
+ Each invocation is a run bundle containing a private `run.json`, `logs/`, and
490
+ `tmp/`, while persistent component caches live in the bundle's cache directory.
491
+ On POSIX, base-cli enforces owner-only `0600`/`0700` modes. On Windows, the
492
+ default user-local cache root relies on inherited user-profile ACLs; consumers
493
+ using a custom cache root must provide the appropriate ACL themselves.
494
+
495
+ See [Platform support](docs/platform-support.md) for the supported Linux,
496
+ WSL2, macOS, and native Windows boundaries. Native Windows support covers the
497
+ generic `base-cli` framework; it does not imply native Windows support for
498
+ Base or `basectl`.
466
499
 
467
500
  Use `ctx.on_cleanup()` for cleanup work that should happen even when helper code
468
501
  does not own the main command wrapper:
@@ -497,39 +530,32 @@ def test_command(tmp_path: Path) -> None:
497
530
  ["--name", "Ada"],
498
531
  home=tmp_path,
499
532
  cwd=project,
500
- manifest={"project": {"name": "demo"}, "artifacts": []},
501
533
  )
502
534
 
503
535
  assert result.exit_code == 0
504
536
  assert "hello Ada" in result.stdout
505
537
  ```
506
538
 
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.
539
+ The helper wraps Click's `CliRunner`, sets `HOME` plus the relevant
540
+ `USERPROFILE`, `LOCALAPPDATA`, and `XDG_CACHE_HOME` values when requested, and
541
+ supplies `cwd` to the invocation for the duration of the test. Calls that use
542
+ `cwd` are serialized and the caller's cwd is restored afterward, but this
543
+ remains process-global: do not use it concurrently with code that changes cwd
544
+ outside `invoke()` or from threads spawned by the invoked command. A
545
+ generic profile should receive project fixtures through its
546
+ `discover_project` callback. The helper does not create or interpret any
547
+ product-specific manifest fixture.
514
548
 
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.
549
+ When `home` is supplied, `invoke()` provides an isolated default cache
550
+ environment for tests. Pass `env={"BASE_CLI_CACHE_DIR": str(path)}` when a test
551
+ needs an explicit cache location.
519
552
 
520
553
  ## When To Use `base_cli`
521
554
 
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`.
555
+ Use `base_cli` for Python commands that need a predictable command
556
+ lifecycle: standard options, logging, redaction, runtime state, cleanup, and
557
+ test helpers. Standalone consumers should use `CliProfile.generic()` or
558
+ provide an explicit profile with their own project and configuration policies.
533
559
 
534
560
  It is a good fit for:
535
561
 
@@ -539,5 +565,5 @@ It is a good fit for:
539
565
  - CLIs that need predictable logs, temp directories, and config precedence
540
566
 
541
567
  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,
568
+ frameworks. It is the reusable layer around command lifecycle, context, logging,
543
569
  configuration, and state.