Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions cli/bash/commands/basectl/basectl.sh
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ Wrapper options:
--debug-wrapper Enable DEBUG logging before the Base runtime is loaded.
--verbose-wrapper Enable verbose runtime argument handling before dispatch.
--utc-wrapper Print wrapper/runtime log timestamps in UTC.
--keep-temp Preserve temporary run files after the command completes.
--color Preserve color-aware wrapper argument handling.

Notes:
Expand All @@ -131,12 +132,14 @@ Notes:
Base rejects `--option=value` syntax before command delegation. Arguments
after `--` belong to the delegated project command.
- Use `-v` for command-level debug logs. Python package standard options such
as `--debug`, `--quiet`, `--log-file`, `--config`, `--environment`, and
`--keep-temp` are not public `basectl` options.
as `--debug`, `--quiet`, `--log-file`, `--config`, and `--environment` are
not public `basectl` options.
- Invoking `basectl` with no command starts a Base runtime shell for the
nearest project manifest above the current directory, preserving that
directory. If no manifest is found, it falls back to project `base`. In
non-interactive shells it prints this help text.
- Use `--keep-temp` before the command name to preserve the complete run
temporary tree; temporary files are removed by default.
- Use `--debug-wrapper` when debugging startup before command dispatch or
Base runtime initialization.
EOF
Expand Down Expand Up @@ -643,6 +646,9 @@ basectl_finalize_run_bundle() {
"${BASE_CLI_HISTORY_STARTED_AT:-}" \
"$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || true)" >"$tmp_file" && mv -f "$tmp_file" "$run_root/run.json"
chmod 600 "$run_root/run.json" "${BASE_CLI_PRIMARY_LOG:-$run_root/logs/primary.log}" || return 1
if [[ "${BASE_CLI_KEEP_TEMP:-}" != true ]]; then
rm -rf -- "$run_root/tmp"
fi
}

basectl_history_record() {
Expand Down Expand Up @@ -682,7 +688,12 @@ basectl_history_record() {


basectl_main() {
local base_debug=0 command="" command_status run_bundle_enabled=1
local base_debug=0 keep_temp=0 command="" command_status run_bundle_enabled=1

while [[ "${1:-}" == --keep-temp ]]; do
keep_temp=1
shift
done
local history_args=() history_scope="${BASE_CLI_HISTORY_SCOPE:-primary}"
local opt

Expand Down Expand Up @@ -752,6 +763,11 @@ basectl_main() {
done
shift $((OPTIND - 1))

while [[ "${1:-}" == --keep-temp ]]; do
keep_temp=1
shift
done

command="${1:-}"
[[ -n "$command" ]] && shift
history_args=("$@")
Expand All @@ -762,6 +778,9 @@ basectl_main() {
basectl_history_recordable_command "$command" || run_bundle_enabled=0

basectl_get_base_home || return 1
if ((keep_temp)); then
export BASE_CLI_KEEP_TEMP=true
fi
if ((run_bundle_enabled)); then
basectl_initialize_run_bundle "$command" "$@" || return 1
fi
Expand Down
1 change: 1 addition & 0 deletions cli/bash/commands/basectl/tests/help.bats
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ load ./basectl_helpers.bash
[[ "$output" == *"--debug-wrapper"* ]]
[[ "$output" == *"--verbose-wrapper"* ]]
[[ "$output" == *"--utc-wrapper"* ]]
[[ "$output" == *"--keep-temp"* ]]
[[ "$output" == *"--color"* ]]
}

Expand Down
54 changes: 54 additions & 0 deletions cli/bash/commands/basectl/tests/runtime-dispatch.bats
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,60 @@ load ./basectl_helpers.bash
}


@test "basectl removes the complete temp tree by default" {
run env \
BASE_HOME="$BASE_REPO_ROOT" \
BASE_TEST_TMPDIR="$TEST_TMPDIR" \
bash -c '
source "$BASE_HOME/cli/bash/commands/basectl/basectl.sh"
BASE_CACHE_DIR="$BASE_TEST_TMPDIR/cache" basectl_initialize_run_bundle setup base-demo || exit $?
mkdir -p "$BASE_CLI_RUN_ROOT/tmp/base_setup"
printf "temporary\n" >"$BASE_CLI_RUN_ROOT/tmp/base_setup/file.txt"
run_root="$BASE_CLI_RUN_ROOT"
basectl_finalize_run_bundle 0 || exit $?
printf "run_root=%s\n" "$run_root"
[[ ! -e "$run_root/tmp" ]]
'

[ "$status" -eq 0 ]
}


@test "basectl preserves the complete temp tree when explicitly requested" {
run env \
BASE_HOME="$BASE_REPO_ROOT" \
BASE_TEST_TMPDIR="$TEST_TMPDIR" \
BASE_CLI_KEEP_TEMP=true \
bash -c '
source "$BASE_HOME/cli/bash/commands/basectl/basectl.sh"
BASE_CACHE_DIR="$BASE_TEST_TMPDIR/cache" basectl_initialize_run_bundle setup base-demo || exit $?
mkdir -p "$BASE_CLI_RUN_ROOT/tmp/base_setup"
printf "temporary\n" >"$BASE_CLI_RUN_ROOT/tmp/base_setup/file.txt"
run_root="$BASE_CLI_RUN_ROOT"
basectl_finalize_run_bundle 0 || exit $?
[[ -f "$run_root/tmp/base_setup/file.txt" ]]
'

[ "$status" -eq 0 ]
}


@test "basectl exposes keep-temp as a wrapper option" {
run env \
BASE_HOME="$BASE_REPO_ROOT" \
bash -c '
source "$BASE_HOME/cli/bash/commands/basectl/basectl.sh"
log_debug() { :; }
basectl_get_base_home() { return 0; }
basectl_do_version() { printf "keep=%s\n" "${BASE_CLI_KEEP_TEMP:-}"; }
basectl_main --keep-temp version
'

[ "$status" -eq 0 ]
[ "$output" = "keep=true" ]
}


@test "basectl prints help when no command is given in a non-interactive shell" {
run_basectl

Expand Down
9 changes: 5 additions & 4 deletions docs/base-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ Directory lifecycle:

| Directory | Created | Removed |
|---|---|---|
| `run.json`, `logs/`, `tmp/` | before command execution | run-bundle cleanup |
| `run.json`, `logs/`, `tmp/` | before command execution | run-bundle cleanup; `tmp/` is removed unless `--keep-temp` is requested |
| `cache/components/` | when a component needs it | explicit CLI cleanup |

Commands running with `ctx.dry_run` skip default `logs/`, `cache/`, and
Expand Down Expand Up @@ -352,8 +352,9 @@ arguments.
These are direct Python package options. Public `basectl` launchers expose
`-v` for command-level debug logs and command-specific flags from
`basectl <command> --help`; they do not expose `--debug`, `--quiet`,
`--log-file`, `--config`, `--environment`, or `--keep-temp` as public
`basectl` options.
`--log-file`, `--config`, or `--environment` as public `basectl` options.
The wrapper-level `basectl --keep-temp <command>` option explicitly preserves
the complete temporary tree for that run.

## Interrupt And Cleanup

Expand All @@ -362,7 +363,7 @@ register them on import. Cleanup should:

1. call user cleanup hooks
2. flush and close log handlers
3. remove `ctx.temp_dir` unless `keep_temp` is true
3. remove `ctx.temp_dir` and empty temporary parents unless `keep_temp` is true

CLI authors can register hooks:

Expand Down
8 changes: 5 additions & 3 deletions docs/cache-ownership-and-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,11 @@ project run through the history index and `run.json` metadata.

### `tmp/`

Temporary files scoped to one run and component. Successful runs remove these
directories automatically unless retention is requested. Failed or interrupted
runs retain them for diagnosis until cleanup removes the completed bundle.
Temporary files scoped to one run and component. Base removes the complete
`tmp/` tree, including empty component parents, after every run by default.
Pass the explicit `--keep-temp` wrapper option to preserve the complete tree
for diagnosis. Run metadata, history, and `primary.log` are retained
independently of this choice.

## Timestamps and permissions

Expand Down
5 changes: 3 additions & 2 deletions docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ syntax.

`basectl` exposes `-v` as the public command-level debug switch. Direct
`base_cli` package standard options such as `--debug`, `--quiet`, `--log-file`,
`--config`, `--environment`, and `--keep-temp` are private to Python package
execution and are rejected by `basectl`.
`--config` and `--environment` are private to Python package execution and are
rejected by `basectl`. Use `basectl --keep-temp <command>` when temporary run
files must be preserved for diagnosis; they are removed by default.

## Stability Tiers

Expand Down
5 changes: 3 additions & 2 deletions lib/python/base_cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,9 @@ equals-form values such as `--name=Ada` before Click parses arguments.
These are direct package options. Public `basectl` launchers expose `-v` for
command-level debug logs and command-specific flags from
`basectl <command> --help`; they do not expose `--debug`, `--quiet`,
`--log-file`, `--config`, `--environment`, or `--keep-temp` as public
`basectl` options.
`--log-file`, `--config`, or `--environment` as public `basectl` options.
The wrapper-level `basectl --keep-temp <command>` option preserves the
complete temporary tree for that run.

## Command Registration

Expand Down
5 changes: 5 additions & 0 deletions lib/python/base_cli/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ def cleanup(self) -> None:
if not self.keep_temp and self.temp_dir.exists():
try:
shutil.rmtree(self.temp_dir)
for parent in (self.temp_dir.parent, self.temp_dir.parent.parent):
try:
parent.rmdir()
except OSError:
break
except OSError as exc:
self.log.warning("Temp directory cleanup failed for '%s': %s", self.temp_dir, exc)
for handler in list(self.log.handlers):
Expand Down
3 changes: 3 additions & 0 deletions lib/python/base_cli/tests/test_base_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,8 @@ def main(ctx: base_cli.Context, name: str) -> None:
self.assertEqual(result.exit_code, 0, result.output)
self.assertEqual(seen["name"], "Ada")
self.assertFalse(seen["temp_dir"].exists())
self.assertFalse(seen["temp_dir"].parent.exists())
self.assertFalse(seen["temp_dir"].parent.parent.exists())
self.assertTrue(seen["cache_dir"].is_dir())
log_dir = home / ".cache" / "base" / "base" / "runs" / next(
path.name for path in (home / ".cache" / "base" / "base" / "runs").iterdir()
Expand Down Expand Up @@ -945,6 +947,7 @@ def main(ctx: base_cli.Context, token: str) -> None:
self.assertEqual(seen["token"], "super-secret")
self.assertTrue(seen["debug"])
self.assertTrue(seen["temp_dir"].exists())
self.assertTrue(seen["temp_dir"].parent.exists())
self.assertEqual(seen["log_file"], log_file)
self.assertTrue(
seen["temp_dir"].is_relative_to(home / ".cache" / "base" / "base" / "runs")
Expand Down
Loading