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
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1113,13 +1113,15 @@ basectl history --command check --status error
basectl history --format json
basectl history --report
basectl history --report --format json
basectl history --include-internal
```

`basectl history` reads the local history index at
`<base-cache-root>/history/runs.jsonl`. History records are best-effort
metadata for Python-backed Base commands with persistent logs; they point to raw
logs instead of replacing them, and malformed history lines are ignored while
listing recent runs. `--report` prints a privacy-conscious local activity
`<base-cache-root>/history/runs.jsonl`. The default view shows one primary row
per public `basectl` command; delegated Python and resolver steps remain linked
as internal records and can be shown with `--include-internal`. History records
point to raw logs instead of replacing them, and malformed history lines are
ignored while listing recent runs. `--report` prints a privacy-conscious local activity
summary with recent commands, failure counts, common failing command families,
and log file locations. Reports do not include raw log contents, compact home
paths to `~`, and redact secret-looking arguments and URL credentials. The
Expand Down
114 changes: 84 additions & 30 deletions cli/bash/commands/basectl/basectl.sh
Original file line number Diff line number Diff line change
Expand Up @@ -495,9 +495,50 @@ basectl_default_activate_project() {
printf '%s\n' base
}

basectl_history_recordable_command() {
case "$1" in
""|help|history|logs|version)
return 1
;;
*)
return 0
;;
esac
}

basectl_history_record() {
local command="$1"
local exit_code="$2"
local wrapper="$BASE_HOME/bin/base-wrapper"
local args=()
shift 2

basectl_history_recordable_command "$command" || return 0
[[ -x "$wrapper" ]] || return 0

args+=(--command "$command")
args+=(--run-id "${BASE_CLI_HISTORY_PARENT_RUN_ID:-}")
args+=(--exit-code "$exit_code")
if [[ -n "${BASE_CLI_HISTORY_STARTED_AT:-}" ]]; then
args+=(--started-at "$BASE_CLI_HISTORY_STARTED_AT")
fi
if [[ -n "${BASE_CLI_HISTORY_PROJECT:-}" ]]; then
args+=(--project "$BASE_CLI_HISTORY_PROJECT")
fi
if [[ -n "${BASE_CLI_HISTORY_PROJECT_ROOT:-}" ]]; then
args+=(--project-root "$BASE_CLI_HISTORY_PROJECT_ROOT")
fi
if [[ -n "${BASE_CLI_HISTORY_MANIFEST:-}" ]]; then
args+=(--manifest "$BASE_CLI_HISTORY_MANIFEST")
fi

"$wrapper" --project base base_history.record "${args[@]}" -- basectl "$command" "$@" >/dev/null 2>&1 || true
}


basectl_main() {
local base_debug=0 command=""
local base_debug=0 command="" command_status
local history_args=()
local opt

if [[ "${1:-}" == "help" ]]; then
Expand Down Expand Up @@ -568,49 +609,58 @@ basectl_main() {

command="${1:-}"
[[ -n "$command" ]] && shift
history_args=("$@")

basectl_reject_equals_option_values "$@" || return $?
basectl_reject_private_standard_options "$@" || return $?

basectl_get_base_home || return 1
if [[ -z "${BASE_CLI_HISTORY_PARENT_RUN_ID:-}" ]]; then
export BASE_CLI_HISTORY_PARENT_RUN_ID="basectl-${BASHPID:-$$}-${RANDOM}"
fi
export BASE_CLI_HISTORY_SCOPE=internal
BASE_CLI_HISTORY_STARTED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || true)"
export BASE_CLI_HISTORY_STARTED_AT
((base_debug)) && basectl_enable_debug_logging
log_debug "Running basectl command '${command:-<none>}' with args: $*"

case "$command" in
activate) basectl_do_activate "$@" ;;
check) basectl_do_check "$@" ;;
test) basectl_do_test "$@" ;;
export-context) basectl_do_export_context "$@" ;;
devcontainer) basectl_do_devcontainer "$@" ;;
devenv-report) basectl_do_devenv_report "$@" ;;
build) basectl_do_build "$@" ;;
demo) basectl_do_demo "$@" ;;
run) basectl_do_run "$@" ;;
repo) basectl_do_repo "$@" ;;
ci) basectl_do_ci "$@" ;;
release) basectl_do_release "$@" ;;
prompt) basectl_do_prompt "$@" ;;
docs) basectl_do_docs "$@" ;;
clean) basectl_do_clean "$@" ;;
logs) basectl_do_logs "$@" ;;
history) basectl_do_history "$@" ;;
config) basectl_do_config "$@" ;;
trust) basectl_do_trust "$@" ;;
doctor) basectl_do_doctor "$@" ;;
gh) basectl_do_gh "$@" ;;
onboard) basectl_do_onboard "$@" ;;
setup) basectl_do_setup "$@" ;;
help) basectl_show_help ;;
projects) basectl_do_projects "$@" ;;
workspace) basectl_do_workspace "$@" ;;
update) basectl_do_update "$@" ;;
update-profile) basectl_do_update_profile "$@" ;;
version) basectl_do_version "$@" ;;
activate) basectl_do_activate "$@"; command_status=$? ;;
check) basectl_do_check "$@"; command_status=$? ;;
test) basectl_do_test "$@"; command_status=$? ;;
export-context) basectl_do_export_context "$@"; command_status=$? ;;
devcontainer) basectl_do_devcontainer "$@"; command_status=$? ;;
devenv-report) basectl_do_devenv_report "$@"; command_status=$? ;;
build) basectl_do_build "$@"; command_status=$? ;;
demo) basectl_do_demo "$@"; command_status=$? ;;
run) basectl_do_run "$@"; command_status=$? ;;
repo) basectl_do_repo "$@"; command_status=$? ;;
ci) basectl_do_ci "$@"; command_status=$? ;;
release) basectl_do_release "$@"; command_status=$? ;;
prompt) basectl_do_prompt "$@"; command_status=$? ;;
docs) basectl_do_docs "$@"; command_status=$? ;;
clean) basectl_do_clean "$@"; command_status=$? ;;
logs) basectl_do_logs "$@"; command_status=$? ;;
history) basectl_do_history "$@"; command_status=$? ;;
config) basectl_do_config "$@"; command_status=$? ;;
trust) basectl_do_trust "$@"; command_status=$? ;;
doctor) basectl_do_doctor "$@"; command_status=$? ;;
gh) basectl_do_gh "$@"; command_status=$? ;;
onboard) basectl_do_onboard "$@"; command_status=$? ;;
setup) basectl_do_setup "$@"; command_status=$? ;;
help) basectl_show_help; command_status=$? ;;
projects) basectl_do_projects "$@"; command_status=$? ;;
workspace) basectl_do_workspace "$@"; command_status=$? ;;
update) basectl_do_update "$@"; command_status=$? ;;
update-profile) basectl_do_update_profile "$@"; command_status=$? ;;
version) basectl_do_version "$@"; command_status=$? ;;
"")
if basectl_should_start_shell; then
BASE_ACTIVATE_PRESERVE_CWD=1 basectl_do_activate "$(basectl_default_activate_project)"
command_status=$?
else
basectl_show_help
command_status=$?
fi
;;
*)
Expand All @@ -619,8 +669,12 @@ basectl_main() {
else
basectl_usage_error "Unrecognized command: $command"
fi
command_status=$?
;;
esac

basectl_history_record "$command" "$command_status" "${history_args[@]}"
return "$command_status"
}

main() {
Expand Down
1 change: 1 addition & 0 deletions cli/bash/commands/basectl/subcommands/activate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ base_activate_subcommand_main() {
route_venv_dir="${BASE_COMMAND_PROTOCOL_FIELDS[project_venv_dir]}"
uses_uv_manager="${BASE_COMMAND_PROTOCOL_FIELDS[uses_uv_manager]}"
trust_required="${BASE_COMMAND_PROTOCOL_FIELDS[manifest_command_trust_required]}"
base_project_set_history_context "$resolved_name" "$project_root" "$manifest_path"

[[ -n "$resolved_name" && -n "$project_root" && -n "$manifest_path" ]] || {
fatal_error "Unable to resolve project '$project'."
Expand Down
2 changes: 2 additions & 0 deletions cli/bash/commands/basectl/subcommands/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ base_build_run_target_record() {
local command_runner="${BASE_COMMAND_PROTOCOL_FIELDS[runner]}"
local command_to_run display_command

base_project_set_history_context "$resolved_name" "$project_root" "$manifest_path"

command_to_run="$(base_command_with_runner "$command_runner" "$build_command" "${extra_args[@]}")" || return $?
display_command="$(base_display_command_with_runner "$command_runner" "$build_command" "${extra_args[@]}")" || return $?

Expand Down
1 change: 1 addition & 0 deletions cli/bash/commands/basectl/subcommands/demo.sh
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ base_demo_subcommand_main() {
route_venv_dir="${BASE_COMMAND_PROTOCOL_FIELDS[project_venv_dir]}"
uses_uv_manager="${BASE_COMMAND_PROTOCOL_FIELDS[uses_uv_manager]}"
trust_required="${BASE_COMMAND_PROTOCOL_FIELDS[manifest_command_trust_required]}"
base_project_set_history_context "$resolved_name" "$project_root" "$manifest_path"
demo_script="${BASE_COMMAND_PROTOCOL_FIELDS[demo_script]}"
command_runner="${BASE_COMMAND_PROTOCOL_FIELDS[runner]}"

Expand Down
1 change: 1 addition & 0 deletions cli/bash/commands/basectl/subcommands/export_context.sh
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ base_export_context_subcommand_main() {
resolved_name="${BASE_COMMAND_PROTOCOL_FIELDS[project_name]}"
project_root="${BASE_COMMAND_PROTOCOL_FIELDS[project_root]}"
manifest_path="${BASE_COMMAND_PROTOCOL_FIELDS[manifest_path]}"
base_project_set_history_context "$resolved_name" "$project_root" "$manifest_path"

[[ -n "$resolved_name" && -n "$project_root" && -n "$manifest_path" ]] || {
fatal_error "Unable to resolve project for export-context."
Expand Down
5 changes: 5 additions & 0 deletions cli/bash/commands/basectl/subcommands/history.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Options:
--format <text|markdown|json>
Output format. Defaults to text, or Markdown with --report.
--report Print a privacy-conscious Markdown or JSON activity report.
--include-internal Include delegated internal steps in the output.
-v Enable DEBUG logging for this subcommand.
-h, --help Show this help text.

Expand All @@ -42,6 +43,10 @@ base_history_subcommand_main() {
args+=("$1")
shift
;;
--include-internal)
args+=("$1")
shift
;;
--project|--command|--status|--limit|--format)
[[ -n "${2:-}" ]] || {
base_history_subcommand_usage >&2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ base_project_venv_dir() {
printf '%s\n' "$HOME/.base.d/$project/.venv"
}

base_project_set_history_context() {
export BASE_CLI_HISTORY_PROJECT="$1"
export BASE_CLI_HISTORY_PROJECT_ROOT="$2"
export BASE_CLI_HISTORY_MANIFEST="$3"
}

base_project_venv_uses_project_local_default() {
local project="$1"
local project_root="${2:-}"
Expand Down
1 change: 1 addition & 0 deletions cli/bash/commands/basectl/subcommands/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ base_run_subcommand_main() {
route_venv_dir="${BASE_COMMAND_PROTOCOL_FIELDS[project_venv_dir]}"
uses_uv_manager="${BASE_COMMAND_PROTOCOL_FIELDS[uses_uv_manager]}"
trust_required="${BASE_COMMAND_PROTOCOL_FIELDS[manifest_command_trust_required]}"
base_project_set_history_context "$resolved_name" "$project_root" "$manifest_path"
run_command="${BASE_COMMAND_PROTOCOL_FIELDS[command]}"
command_runner="${BASE_COMMAND_PROTOCOL_FIELDS[runner]}"

Expand Down
3 changes: 3 additions & 0 deletions cli/bash/commands/basectl/subcommands/setup_common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,9 @@ setup_run_project_artifact_layer() {
project="${BASE_COMMAND_PROTOCOL_FIELDS[project_name]}"
resolved_root="${BASE_COMMAND_PROTOCOL_FIELDS[project_root]}"
manifest_path="${BASE_COMMAND_PROTOCOL_FIELDS[manifest_path]}"
export BASE_CLI_HISTORY_PROJECT="$project"
export BASE_CLI_HISTORY_PROJECT_ROOT="$resolved_root"
export BASE_CLI_HISTORY_MANIFEST="$manifest_path"
project_venv_dir="${BASE_COMMAND_PROTOCOL_FIELDS[project_venv_dir]}"
project_uses_uv_manager="${BASE_COMMAND_PROTOCOL_FIELDS[uses_uv_manager]}"
project_requires_python="${BASE_COMMAND_PROTOCOL_FIELDS[requires_project_python]}"
Expand Down
1 change: 1 addition & 0 deletions cli/bash/commands/basectl/subcommands/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ base_test_subcommand_main() {
route_venv_dir="${BASE_COMMAND_PROTOCOL_FIELDS[project_venv_dir]}"
uses_uv_manager="${BASE_COMMAND_PROTOCOL_FIELDS[uses_uv_manager]}"
trust_required="${BASE_COMMAND_PROTOCOL_FIELDS[manifest_command_trust_required]}"
base_project_set_history_context "$resolved_name" "$project_root" "$manifest_path"
test_command="${BASE_COMMAND_PROTOCOL_FIELDS[command]}"
command_runner="${BASE_COMMAND_PROTOCOL_FIELDS[runner]}"

Expand Down
9 changes: 8 additions & 1 deletion cli/bash/commands/basectl/subcommands/trust.sh
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ base_trust_usage_error() {
base_trust_subcommand_main() {
local trust_command="${1:-}"
local wrapper="$BASE_HOME/bin/base-wrapper"
local args=()
local args=() project_name=""

case "$trust_command" in
""|-h|--help|help)
Expand Down Expand Up @@ -121,6 +121,13 @@ base_trust_subcommand_main() {
esac
done

if [[ "$trust_command" != status || ${#args[@]} -gt 1 ]]; then
project_name="${args[1]:-}"
fi
if [[ -n "$project_name" && "$project_name" != -* ]]; then
export BASE_CLI_HISTORY_PROJECT="$project_name"
fi

[[ -x "$wrapper" ]] || fatal_error "Base Python wrapper '$wrapper' is missing or is not executable."
BASE_TRUST_ACTIVE_PROJECT="${BASE_PROJECT:-}" \
BASE_TRUST_ACTIVE_PROJECT_MANIFEST="${BASE_PROJECT_MANIFEST:-}" \
Expand Down
2 changes: 1 addition & 1 deletion cli/bash/commands/basectl/tests/completions.bats
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ EOF
[[ "$output" == *"logs_options=--command --limit --path --tail --open --lines"* ]]
[[ "$output" == *"logs_last_options=--command --lines --format"* ]]
[[ "$output" == *"logs_commands=last"* ]]
[[ "$output" == *"history_options=--project --command --status --limit --format --report"* ]]
[[ "$output" == *"history_options=--project --command --status --limit --format --report --include-internal"* ]]
[[ "$output" == *"trust_commands=status allow revoke"* ]]
[[ "$output" == *"trust_projects=base demo"* ]]
[[ "$output" == *"trust_status_options=--workspace --format"* ]]
Expand Down
5 changes: 3 additions & 2 deletions cli/bash/commands/basectl/tests/history.bats
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ exit 1
EOF
chmod +x "$python_bin"

run_basectl history --project demo --command check --status error --limit 3 --format json
run_basectl history --project demo --command check --status error --limit 3 --format json --include-internal

[ "$status" -eq 0 ]
[[ "$output" == *"BASE_PROJECT=base"* ]]
[[ "$output" == *"ARGS=--project demo --command check --status error --limit 3 --format json"* ]]
[[ "$output" == *"ARGS=--project demo --command check --status error --limit 3 --format json --include-internal"* ]]
}

@test "basectl history forwards report mode to the Python history layer" {
Expand Down Expand Up @@ -76,6 +76,7 @@ EOF
[[ "$output" == *"basectl history [options]"* ]]
[[ "$output" == *"--project <name>"* ]]
[[ "$output" == *"--report"* ]]
[[ "$output" == *"--include-internal"* ]]
[[ "$output" == *"--format <text|markdown|json>"* ]]
}

Expand Down
3 changes: 3 additions & 0 deletions cli/bash/commands/basectl/tests/projects.bats
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ EOF
mkdir -p "$(dirname "$python_bin")" "$workspace/base"
cat > "$python_bin" <<'EOF'
#!/usr/bin/env bash
if [[ "${1:-}" == "-m" && "${2:-}" == "base_history.record" ]]; then
exit 0
fi
printf '%s\n' "$*" > "${BASE_TEST_PROJECTS_LIST_STATE:?}"
if [[ "${1:-}" == "-m" && "${2:-}" == "base_projects" && "${3:-}" == "list" ]]; then
printf '[{"name":"base","path":"%s"}]\n' "${BASE_TEST_WORKSPACE:?}/base"
Expand Down
10 changes: 10 additions & 0 deletions cli/python/base_history/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

import base_cli
from base_cli.history import HISTORY_PATH
from base_cli.history import HISTORY_SCOPE_INTERNAL
from base_cli.history import HISTORY_SCOPE_PRIMARY
from base_cli.history import optional_int
from base_cli.history import optional_string
from base_cli.history import parse_finished_history_record_line
Expand All @@ -33,6 +35,7 @@ class HistoryRecord:
ended_at: str
sort_time: datetime
log_path: str | None
scope: str

@property
def log_exists(self) -> bool:
Expand All @@ -54,6 +57,7 @@ class HistoryOptions:
limit: int
output_format: str
report: bool
include_internal: bool


@dataclass(frozen=True)
Expand Down Expand Up @@ -84,6 +88,7 @@ def main(argv: list[str] | None = None) -> int:
@base_cli.option("--limit", default="10", help="Maximum history records to list.")
@base_cli.option("--format", "output_format", default="text", help="Output format: text, markdown, or json.")
@base_cli.option("--report", is_flag=True, help="Print a privacy-conscious local activity report.")
@base_cli.option("--include-internal", is_flag=True, help="Include delegated internal steps in the output.")
# pylint: disable=too-many-arguments,too-many-positional-arguments
def run(
ctx: base_cli.Context,
Expand All @@ -93,6 +98,7 @@ def run(
limit: str,
output_format: str,
report: bool,
include_internal: bool,
) -> int:
try:
options = HistoryOptions(
Expand All @@ -102,6 +108,7 @@ def run(
limit=parse_positive_int("--limit", limit),
output_format=normalize_report_format(output_format) if report else normalize_format(output_format),
report=report,
include_internal=include_internal,
)
except ValueError as exc:
ctx.log.error(str(exc))
Expand Down Expand Up @@ -202,6 +209,7 @@ def parse_history_line(line: str) -> HistoryRecord | None:
ended_at=ended_at,
sort_time=parse_timestamp(ended_at),
log_path=optional_string(payload.get("log_path")),
scope=optional_string(payload.get("scope")) or HISTORY_SCOPE_PRIMARY,
)


Expand All @@ -216,6 +224,8 @@ def parse_timestamp(value: str) -> datetime:

def filter_history(records: list[HistoryRecord], options: HistoryOptions) -> list[HistoryRecord]:
filtered = records
if not options.include_internal:
filtered = [record for record in filtered if record.scope != HISTORY_SCOPE_INTERNAL]
if options.project:
filtered = [record for record in filtered if record.project == options.project]
if options.command:
Expand Down
Loading
Loading