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
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

---

## [1.6] - 2026-04-09
## [1.6.1] - 2026-04-08

### Added

- `--version` now includes Python version, Python executable path, and OS/platform alongside the Atlas version string
- `session prune --older-than DAYS` bulk-deletes uncaptured sessions (created but never captured) older than the specified number of days; supports `--dry-run` to preview and `--force` to skip confirmation

### Changed

- Fixed the licensing issues in a few files to reflect the GPL 3.0 license properly

## [1.6] - 2026-04-08

### Added

Expand Down
18 changes: 3 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
# Platform Atlas

![Version](https://img.shields.io/badge/version-1.5-1B93D2?style=flat-square)
![Python](https://img.shields.io/badge/python-3.11%2B-3776AB?style=flat-square&logo=python&logoColor=white)
![License](https://img.shields.io/badge/license-Apache%202.0-99CA3C?style=flat-square)
![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20RHEL%208%2B%20%7C%20Rocky%208%2B-lightgrey?style=flat-square)
![License](https://img.shields.io/badge/license-%20%20GNU%20GPLv3%20-green?style=flat-square)

> Enterprise configuration auditing and compliance reporting for Itential Automation Platform

Expand Down Expand Up @@ -738,20 +736,10 @@ For issues, questions, or feature requests, please contact:

## License

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
This project is licensed under the GNU General Public License v3.0. See the [LICENSE](LICENSE) file for details.

---

**Version:** 1.5
**Version:** 1.6.1
**Author:** Cody Rester
**Last Updated:** April 2026
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[project]
name = "platform-atlas"
version = "1.6"
version = "1.6.1"
description = "Health Analysis for Platform"
license = "Apache-2.0"
license = "GPL-3.0-or-later"
authors = [{ name = "Cody Rester", email = "cody.rester@itential.com" }]
maintainers = [{ name = "Cody Rester", email = "cody.rester@itential.com" }]
readme = "README.md"
Expand Down
2 changes: 1 addition & 1 deletion src/platform_atlas/core/_version.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
"""Platform Atlas Version"""
__version__ = "1.6"
__version__ = "1.6.1"
55 changes: 53 additions & 2 deletions src/platform_atlas/core/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
- preflight: Run preflight connectivity checks
"""

import sys
import json
import argparse
import platform as _platform
from pathlib import Path
from rich_argparse import RichHelpFormatter

Expand All @@ -23,6 +25,28 @@

theme = ui.theme

# =================================================
# Version Action
# =================================================

class _VersionAction(argparse.Action):
"""Custom --version action that includes system info."""

def __init__(self, option_strings, dest=argparse.SUPPRESS, default=argparse.SUPPRESS, help=None):
super().__init__(option_strings=option_strings, dest=dest, default=default, nargs=0, help=help)

def __call__(self, parser, namespace, values, option_string=None):
py_version = sys.version.split()[0]
py_path = sys.executable
os_name = _platform.system()
os_release = _platform.release()
machine = _platform.machine()
print(f"platform-atlas {__version__}")
print(f"Python {py_version} ({py_path})")
print(f"{os_name} {os_release} ({machine})")
parser.exit()


# =================================================
# Custom Help Formatter
# =================================================
Expand Down Expand Up @@ -59,8 +83,8 @@ def create_parser() -> argparse.ArgumentParser:

parser.add_argument(
'--version',
action='version',
version=f'%(prog)s {__version__}'
action=_VersionAction,
help='Show version and system information'
)

parser.add_argument(
Expand Down Expand Up @@ -434,6 +458,33 @@ def _add_session_commands(subparsers):
help='Show what would change without writing anything'
)

# session prune
prune = session_subparsers.add_parser(
'prune',
help='Delete uncaptured sessions older than N days',
formatter_class=AtlasHelpFormatter,
description='Bulk-delete sessions that were created but never captured, '
'older than the specified number of days.'
)
prune.add_argument(
'--older-than',
dest='older_than',
type=int,
required=True,
metavar='DAYS',
help='Prune sessions created more than DAYS days ago (e.g. --older-than 90)'
)
prune.add_argument(
'--dry-run',
action='store_true',
help='Show what would be deleted without removing anything'
)
prune.add_argument(
'--force',
action='store_true',
help='Skip confirmation prompt'
)

# =================================================
# RULESET Command Group
# =================================================
Expand Down
124 changes: 124 additions & 0 deletions src/platform_atlas/core/handlers/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -1762,6 +1762,130 @@ def handle_session_repair(args: Namespace) -> int:
return 1


@registry.register("session", "prune", description="Delete uncaptured sessions older than N days")
def handle_session_prune(args: Namespace) -> int:
"""
Bulk-delete sessions that were created but never captured,
older than --older-than DAYS days.

The active session is always skipped even if it qualifies.
Use --dry-run to preview without deleting. Use --force to skip confirmation.
"""
from datetime import datetime, timezone, timedelta
from rich.table import Table
from rich import box

try:
manager = get_session_manager()
older_than: int = args.older_than
dry_run: bool = getattr(args, "dry_run", False)
force: bool = getattr(args, "force", False)

cutoff = datetime.now(tz=timezone.utc) - timedelta(days=older_than)
active_name = manager.get_active_session_name()

all_sessions = manager.list()
candidates = []
skipped_active = False

for session in all_sessions:
# Only sessions that were never captured
if session.metadata.capture_completed:
continue
# Age check against created_at
created = session.metadata.created_at
if created.tzinfo is None:
created = created.replace(tzinfo=timezone.utc)
if created >= cutoff:
continue
# Never touch the active session
if session.name == active_name:
skipped_active = True
continue
candidates.append(session)

if not candidates:
console.print(f"\n [{theme.text_dim}]No uncaptured sessions older than {older_than} days.[/{theme.text_dim}]\n")
if skipped_active:
console.print(
f" [{theme.warning}]Note: active session '{active_name}' was skipped "
f"(deactivate it first to include it).[/{theme.warning}]\n"
)
return 0

# Build preview table
table = Table(
title=f"{'[dim]Dry run — [/dim]' if dry_run else ''}Uncaptured sessions to prune ({len(candidates)})",
box=box.ROUNDED,
)
table.add_column("Name", style="cyan")
table.add_column("Environment", style=theme.accent)
table.add_column("Created", style="dim")
table.add_column("Age (days)", justify="right", style=theme.warning)

today = datetime.now(tz=timezone.utc)
for session in candidates:
created = session.metadata.created_at
if created.tzinfo is None:
created = created.replace(tzinfo=timezone.utc)
age_days = (today - created).days
env_display = session.metadata.environment or f"[{theme.text_ghost}]—[/{theme.text_ghost}]"
table.add_row(
session.name,
env_display,
created.strftime("%Y-%m-%d"),
str(age_days),
)

console.print()
console.print(table)

if skipped_active:
console.print(
f"\n [{theme.warning}]Note: active session '{active_name}' was skipped.[/{theme.warning}]"
)

if dry_run:
console.print(
f"\n [{theme.text_dim}]Dry run — nothing deleted. "
f"Remove --dry-run to prune these {len(candidates)} session(s).[/{theme.text_dim}]\n"
)
return 0

# Confirm unless --force
if not force:
console.print()
if not Confirm.ask(
f"Permanently delete {len(candidates)} session(s)?", default=False
):
console.print(f" [{theme.text_dim}]Cancelled[/{theme.text_dim}]")
return 0

# Delete
deleted = 0
failed = 0
for session in candidates:
try:
manager.delete(session.name, force=True)
console.print(f" [{theme.success}]✓[/{theme.success}] Deleted: {session.name}")
deleted += 1
except SessionError as e:
console.print(f" [red]✗[/red] {session.name}: {e.message}")
failed += 1

console.print()
console.print(
f" [{theme.success}]✓[/{theme.success}] {deleted} session(s) deleted"
+ (f", {failed} failed" if failed else "")
)
console.print()
return 0 if not failed else 1

except SessionError as e:
console.print(f"[red]✗[/red] {e.message}")
return 1


# =================================================
# Shared Helpers
# =================================================
Expand Down
Loading
Loading