kctl-op 0.5.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.
- kctl_op-0.5.0/.gitignore +33 -0
- kctl_op-0.5.0/PKG-INFO +17 -0
- kctl_op-0.5.0/README.md +74 -0
- kctl_op-0.5.0/pyproject.toml +45 -0
- kctl_op-0.5.0/skills/1password-admin/SKILL.md +141 -0
- kctl_op-0.5.0/src/kctl_op/__init__.py +3 -0
- kctl_op-0.5.0/src/kctl_op/__main__.py +3 -0
- kctl_op-0.5.0/src/kctl_op/cli.py +196 -0
- kctl_op-0.5.0/src/kctl_op/commands/__init__.py +0 -0
- kctl_op-0.5.0/src/kctl_op/commands/backup.py +178 -0
- kctl_op-0.5.0/src/kctl_op/commands/config_cmd.py +374 -0
- kctl_op-0.5.0/src/kctl_op/commands/diff_cmd.py +124 -0
- kctl_op-0.5.0/src/kctl_op/commands/discover.py +69 -0
- kctl_op-0.5.0/src/kctl_op/commands/doctor_cmd.py +75 -0
- kctl_op-0.5.0/src/kctl_op/commands/health.py +175 -0
- kctl_op-0.5.0/src/kctl_op/commands/projects.py +197 -0
- kctl_op-0.5.0/src/kctl_op/commands/skill_cmd.py +76 -0
- kctl_op-0.5.0/src/kctl_op/commands/status.py +95 -0
- kctl_op-0.5.0/src/kctl_op/commands/sync_cmd.py +153 -0
- kctl_op-0.5.0/src/kctl_op/commands/vault.py +122 -0
- kctl_op-0.5.0/src/kctl_op/core/__init__.py +0 -0
- kctl_op-0.5.0/src/kctl_op/core/callbacks.py +44 -0
- kctl_op-0.5.0/src/kctl_op/core/client.py +390 -0
- kctl_op-0.5.0/src/kctl_op/core/config.py +147 -0
- kctl_op-0.5.0/src/kctl_op/core/diff.py +224 -0
- kctl_op-0.5.0/src/kctl_op/core/discovery.py +171 -0
- kctl_op-0.5.0/src/kctl_op/core/exceptions.py +47 -0
- kctl_op-0.5.0/src/kctl_op/core/op_client.py +250 -0
- kctl_op-0.5.0/src/kctl_op/core/op_config.py +140 -0
- kctl_op-0.5.0/src/kctl_op/core/output.py +8 -0
- kctl_op-0.5.0/src/kctl_op/core/parser.py +101 -0
- kctl_op-0.5.0/src/kctl_op/core/sync.py +258 -0
- kctl_op-0.5.0/tests/__init__.py +0 -0
- kctl_op-0.5.0/tests/conftest.py +95 -0
- kctl_op-0.5.0/tests/test_backup.py +152 -0
- kctl_op-0.5.0/tests/test_config.py +133 -0
- kctl_op-0.5.0/tests/test_diff.py +336 -0
- kctl_op-0.5.0/tests/test_discovery.py +272 -0
- kctl_op-0.5.0/tests/test_parser.py +198 -0
- kctl_op-0.5.0/tests/test_smoke.py +21 -0
- kctl_op-0.5.0/tests/test_vault.py +134 -0
kctl_op-0.5.0/.gitignore
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
*.egg
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
.eggs/
|
|
9
|
+
|
|
10
|
+
# Virtual environments
|
|
11
|
+
.venv/
|
|
12
|
+
venv/
|
|
13
|
+
|
|
14
|
+
# IDE
|
|
15
|
+
.idea/
|
|
16
|
+
.vscode/
|
|
17
|
+
*.swp
|
|
18
|
+
*.swo
|
|
19
|
+
|
|
20
|
+
# Testing
|
|
21
|
+
.pytest_cache/
|
|
22
|
+
.coverage
|
|
23
|
+
htmlcov/
|
|
24
|
+
.mypy_cache/
|
|
25
|
+
.ruff_cache/
|
|
26
|
+
|
|
27
|
+
# OS
|
|
28
|
+
.DS_Store
|
|
29
|
+
Thumbs.db
|
|
30
|
+
|
|
31
|
+
# Environment
|
|
32
|
+
.env
|
|
33
|
+
.env.local
|
kctl_op-0.5.0/PKG-INFO
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kctl-op
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Kodemeio 1Password CLI — secret management and .env sync
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: kctl-lib>=0.5.0
|
|
7
|
+
Requires-Dist: pydantic>=2.10.0
|
|
8
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
9
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
10
|
+
Requires-Dist: rich>=13.9.0
|
|
11
|
+
Requires-Dist: typer>=0.15.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: mypy>=1.14.0; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest-httpx>=0.35.0; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest>=8.3.0; extra == 'dev'
|
|
16
|
+
Requires-Dist: ruff>=0.9.0; extra == 'dev'
|
|
17
|
+
Requires-Dist: types-pyyaml>=6.0.0; extra == 'dev'
|
kctl_op-0.5.0/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# kctl-op
|
|
2
|
+
|
|
3
|
+
Kodemeio 1Password CLI — secret management and .env sync across all projects.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv tool install .
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
kctl-op config init
|
|
15
|
+
kctl-op health
|
|
16
|
+
kctl-op vault list
|
|
17
|
+
kctl-op sync pull
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Command Groups
|
|
21
|
+
|
|
22
|
+
| Group | Description |
|
|
23
|
+
|-------|-------------|
|
|
24
|
+
| `config` | Manage CLI configuration and profiles |
|
|
25
|
+
| `health` | Health checks and diagnostics |
|
|
26
|
+
| `vault` | Vault management operations |
|
|
27
|
+
| `projects` | Project discovery and status |
|
|
28
|
+
| `push` | Push .env files to 1Password |
|
|
29
|
+
| `pull` | Pull secrets from 1Password to .env |
|
|
30
|
+
| `diff` | Show differences between local and 1Password |
|
|
31
|
+
| `discover` | Discover .env files in project directories |
|
|
32
|
+
| `backup` | Backup management for .env files |
|
|
33
|
+
| `status` | Check sync status |
|
|
34
|
+
|
|
35
|
+
Top-level commands: `list` (list all vault items)
|
|
36
|
+
|
|
37
|
+
## Configuration
|
|
38
|
+
|
|
39
|
+
Config lives in `~/.config/kodemeio/config.yaml` under the `op` service key.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Initialize a profile
|
|
43
|
+
kctl-op config init
|
|
44
|
+
|
|
45
|
+
# Add a named profile
|
|
46
|
+
kctl-op config add prod --vault MyVault --token $OP_SERVICE_ACCOUNT_TOKEN
|
|
47
|
+
|
|
48
|
+
# Switch active profile
|
|
49
|
+
kctl-op config use prod
|
|
50
|
+
|
|
51
|
+
# Show current profile (token masked)
|
|
52
|
+
kctl-op config show
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Global Options
|
|
56
|
+
|
|
57
|
+
| Option | Short | Description |
|
|
58
|
+
|--------|-------|-------------|
|
|
59
|
+
| `--json` | | Output as JSON |
|
|
60
|
+
| `--quiet` | `-q` | Suppress non-essential output |
|
|
61
|
+
| `--format` | `-f` | Output format: pretty, json, csv, yaml |
|
|
62
|
+
| `--no-header` | | Omit header row in CSV output |
|
|
63
|
+
| `--profile` | `-p` | Config profile to use |
|
|
64
|
+
| `--vault` | | Override vault name |
|
|
65
|
+
| `--token` | | Override service account token |
|
|
66
|
+
| `--version` | `-V` | Show version and exit |
|
|
67
|
+
|
|
68
|
+
## Development
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
uv run pytest tests/ -v
|
|
72
|
+
uv run ruff check src/
|
|
73
|
+
uv run mypy src/
|
|
74
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "kctl-op"
|
|
7
|
+
version = "0.5.0"
|
|
8
|
+
description = "Kodemeio 1Password CLI — secret management and .env sync"
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"kctl-lib>=0.5.0",
|
|
12
|
+
"typer>=0.15.0",
|
|
13
|
+
"rich>=13.9.0",
|
|
14
|
+
"pydantic>=2.10.0",
|
|
15
|
+
"pyyaml>=6.0.2",
|
|
16
|
+
"python-dotenv>=1.0.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
dev = [
|
|
21
|
+
"pytest>=8.3.0",
|
|
22
|
+
"pytest-httpx>=0.35.0",
|
|
23
|
+
"ruff>=0.9.0",
|
|
24
|
+
"mypy>=1.14.0",
|
|
25
|
+
"types-PyYAML>=6.0.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
kctl-op = "kctl_op.cli:_run"
|
|
30
|
+
|
|
31
|
+
[tool.uv.sources]
|
|
32
|
+
kctl-lib = { workspace = true }
|
|
33
|
+
|
|
34
|
+
[project.entry-points."kctl_op.plugins"]
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.wheel]
|
|
37
|
+
packages = ["src/kctl_op"]
|
|
38
|
+
|
|
39
|
+
[tool.ruff]
|
|
40
|
+
target-version = "py312"
|
|
41
|
+
line-length = 120
|
|
42
|
+
|
|
43
|
+
[tool.mypy]
|
|
44
|
+
python_version = "3.12"
|
|
45
|
+
strict = true
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: 1password-admin
|
|
3
|
+
description: >
|
|
4
|
+
1Password secret management via kctl-op CLI (12 groups, ~27 commands).
|
|
5
|
+
MUST use for ANY kctl-op operation.
|
|
6
|
+
Triggers on: "backup", "check", "clean", "config", "current", "dashboard", "diff", "discover", "envs", "generate", "health", "info", "init", "items", "kctl-op", "list", "migrate", "profile", "profiles", "projects", "pull", "push", "remove", "restore", "skill", "status", "test", "vault".
|
|
7
|
+
Auto-generated: 2026-04-05
|
|
8
|
+
registry_hash: d1005426c491
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# 1password-admin — kctl-op CLI Reference
|
|
12
|
+
|
|
13
|
+
> Auto-generated from `kctl-op` command registry. Do not edit manually.
|
|
14
|
+
> To regenerate: `kctl-op skill generate`
|
|
15
|
+
> To add custom content: edit `SKILL.extra.md` in the same directory.
|
|
16
|
+
|
|
17
|
+
## Overview
|
|
18
|
+
|
|
19
|
+
**CLI:** `kctl-op`
|
|
20
|
+
**Command groups:** 12
|
|
21
|
+
**Total commands:** ~27
|
|
22
|
+
**Install:** `cd cli && uv tool install --editable .`
|
|
23
|
+
|
|
24
|
+
## Global Options
|
|
25
|
+
|
|
26
|
+
| Flag | Description |
|
|
27
|
+
|------|-------------|
|
|
28
|
+
| `--json` | JSON output |
|
|
29
|
+
| `--quiet`, `-q` | Suppress info messages |
|
|
30
|
+
| `--format`, `-f` | Output format: pretty/json/csv/yaml |
|
|
31
|
+
| `--no-header` | Omit CSV header row |
|
|
32
|
+
| `--profile`, `-p` | Config profile name |
|
|
33
|
+
| `--version`, `-V` | Show version |
|
|
34
|
+
|
|
35
|
+
## Command Reference
|
|
36
|
+
|
|
37
|
+
### `kctl-op backup`
|
|
38
|
+
|
|
39
|
+
Backup management for .env files.
|
|
40
|
+
|
|
41
|
+
| Command | Description |
|
|
42
|
+
|---------|-------------|
|
|
43
|
+
| `backup clean [--keep] [--force]` | Clean old backups, keeping the N most recent per file. |
|
|
44
|
+
| `backup list [--project]` | List available backups. |
|
|
45
|
+
| `backup restore <project> <environment> [--timestamp] [--force]` | Restore a .env file from backup. |
|
|
46
|
+
|
|
47
|
+
### `kctl-op config`
|
|
48
|
+
|
|
49
|
+
Manage CLI configuration and profiles.
|
|
50
|
+
|
|
51
|
+
| Command | Description |
|
|
52
|
+
|---------|-------------|
|
|
53
|
+
| `config add <name> [--vault] [--token] [--scan_root]` | Add a new profile. |
|
|
54
|
+
| `config current` | Show current active profile. |
|
|
55
|
+
| `config init [--vault] [--token] [--name] [--scan_root]` | Initialize CLI configuration (interactive if no flags given). |
|
|
56
|
+
| `config migrate` | Migrate legacy configuration to service-scoped format. |
|
|
57
|
+
| `config profiles` | List all profiles. |
|
|
58
|
+
| `config remove <name> [--force]` | Remove a profile's 1Password configuration. |
|
|
59
|
+
| `config set <key> <value>` | Set a configuration value in the current profile. |
|
|
60
|
+
| `config show` | Show full configuration (tokens masked). |
|
|
61
|
+
| `config test` | Test current profile's 1Password connection. |
|
|
62
|
+
| `config use <name>` | Set the default profile. |
|
|
63
|
+
|
|
64
|
+
### `kctl-op diff`
|
|
65
|
+
|
|
66
|
+
Show differences between local and 1Password.
|
|
67
|
+
|
|
68
|
+
### `kctl-op discover`
|
|
69
|
+
|
|
70
|
+
Discover .env files.
|
|
71
|
+
|
|
72
|
+
### `kctl-op health`
|
|
73
|
+
|
|
74
|
+
Health checks and diagnostics.
|
|
75
|
+
|
|
76
|
+
| Command | Description |
|
|
77
|
+
|---------|-------------|
|
|
78
|
+
| `health dashboard` | Overview: projects, files, sync status, last sync times. |
|
|
79
|
+
|
|
80
|
+
### `kctl-op list`
|
|
81
|
+
|
|
82
|
+
List all items in the 1Password vault.
|
|
83
|
+
|
|
84
|
+
### `kctl-op projects`
|
|
85
|
+
|
|
86
|
+
Project discovery and status.
|
|
87
|
+
|
|
88
|
+
| Command | Description |
|
|
89
|
+
|---------|-------------|
|
|
90
|
+
| `projects envs <project>` | List all .env files for a project. |
|
|
91
|
+
| `projects list` | List all projects found across scan roots. |
|
|
92
|
+
| `projects status <project>` | Show sync status for all environments in a project. |
|
|
93
|
+
|
|
94
|
+
### `kctl-op pull`
|
|
95
|
+
|
|
96
|
+
Pull .env files from 1Password.
|
|
97
|
+
|
|
98
|
+
### `kctl-op push`
|
|
99
|
+
|
|
100
|
+
Push .env files to 1Password.
|
|
101
|
+
|
|
102
|
+
### `kctl-op skill`
|
|
103
|
+
|
|
104
|
+
Claude Code skill management.
|
|
105
|
+
|
|
106
|
+
| Command | Description |
|
|
107
|
+
|---------|-------------|
|
|
108
|
+
| `skill generate [--output] [--install] [--check]` | Auto-generate SKILL.md from CLI command registry. |
|
|
109
|
+
|
|
110
|
+
**Examples:**
|
|
111
|
+
```bash
|
|
112
|
+
kctl-op skill generate
|
|
113
|
+
kctl-op skill generate --install
|
|
114
|
+
kctl-op skill generate --check
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### `kctl-op status`
|
|
118
|
+
|
|
119
|
+
Check sync status.
|
|
120
|
+
|
|
121
|
+
### `kctl-op vault`
|
|
122
|
+
|
|
123
|
+
Vault management operations.
|
|
124
|
+
|
|
125
|
+
| Command | Description |
|
|
126
|
+
|---------|-------------|
|
|
127
|
+
| `vault create [--name] [--description]` | Create a new 1Password vault. |
|
|
128
|
+
| `vault info` | Show vault details. |
|
|
129
|
+
| `vault items` | List all items in the vault with metadata. |
|
|
130
|
+
|
|
131
|
+
## Configuration
|
|
132
|
+
|
|
133
|
+
Shared config: `~/.config/kodemeio/config.yaml`
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
kctl-op config init # Interactive setup
|
|
137
|
+
kctl-op config show # Show current config
|
|
138
|
+
kctl-op config profiles # List profiles
|
|
139
|
+
kctl-op config current # Show active profile
|
|
140
|
+
kctl-op config validate # Verify config
|
|
141
|
+
```
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Main Typer application for kctl-op."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from kctl_op import __version__
|
|
12
|
+
from kctl_op.commands.backup import app as backup_app
|
|
13
|
+
|
|
14
|
+
# Command group imports
|
|
15
|
+
from kctl_op.commands.config_cmd import app as config_app
|
|
16
|
+
from kctl_op.commands.diff_cmd import app as diff_app
|
|
17
|
+
from kctl_op.commands.discover import app as discover_app
|
|
18
|
+
from kctl_op.commands.health import app as health_app
|
|
19
|
+
from kctl_op.commands.projects import app as projects_app
|
|
20
|
+
from kctl_op.commands.status import app as status_app
|
|
21
|
+
from kctl_op.commands.sync_cmd import pull_app, push_app
|
|
22
|
+
from kctl_op.commands.vault import app as vault_app
|
|
23
|
+
from kctl_op.core.callbacks import AppContext
|
|
24
|
+
from kctl_lib import handle_cli_error
|
|
25
|
+
from kctl_op.commands.skill_cmd import app as skill_app
|
|
26
|
+
from kctl_op.commands.doctor_cmd import app as doctor_app
|
|
27
|
+
from kctl_op.core.exceptions import (
|
|
28
|
+
KctlError,
|
|
29
|
+
)
|
|
30
|
+
from kctl_lib.self_update import notify_if_outdated
|
|
31
|
+
from kctl_lib.tui import add_tui_command
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _version_callback(value: bool) -> None:
|
|
35
|
+
if value:
|
|
36
|
+
print(f"kctl-op {__version__}")
|
|
37
|
+
raise typer.Exit()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
app = typer.Typer(
|
|
41
|
+
name="kctl-op",
|
|
42
|
+
help="Kodemeio 1Password CLI - manage secrets across all projects via 1Password.",
|
|
43
|
+
no_args_is_help=True,
|
|
44
|
+
rich_markup_mode="rich",
|
|
45
|
+
pretty_exceptions_enable=False,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@app.callback()
|
|
50
|
+
def main(
|
|
51
|
+
ctx: typer.Context,
|
|
52
|
+
json_output: Annotated[bool, typer.Option("--json", help="Output in JSON format.")] = False,
|
|
53
|
+
quiet: Annotated[bool, typer.Option("--quiet", "-q", help="Suppress non-essential output.")] = False,
|
|
54
|
+
profile: Annotated[str | None, typer.Option("--profile", "-p", help="Config profile to use.")] = None,
|
|
55
|
+
format: Annotated[str, typer.Option("--format", "-f", help="Output format: pretty, json, csv, yaml.")] = "pretty",
|
|
56
|
+
no_header: Annotated[bool, typer.Option("--no-header", help="Omit header row in CSV output.")] = False,
|
|
57
|
+
vault: Annotated[str | None, typer.Option("--vault", help="Override vault name.")] = None,
|
|
58
|
+
token: Annotated[str | None, typer.Option("--token", help="Override service account token.")] = None,
|
|
59
|
+
version: Annotated[
|
|
60
|
+
bool,
|
|
61
|
+
typer.Option(
|
|
62
|
+
"--version",
|
|
63
|
+
"-V",
|
|
64
|
+
callback=_version_callback,
|
|
65
|
+
is_eager=True,
|
|
66
|
+
help="Show version and exit.",
|
|
67
|
+
),
|
|
68
|
+
] = False,
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Global options applied to all commands."""
|
|
71
|
+
ctx.ensure_object(dict)
|
|
72
|
+
ctx.obj = AppContext(
|
|
73
|
+
json_mode=json_output,
|
|
74
|
+
quiet=quiet,
|
|
75
|
+
profile=profile,
|
|
76
|
+
format=format,
|
|
77
|
+
no_header=no_header,
|
|
78
|
+
vault_override=vault,
|
|
79
|
+
token_override=token,
|
|
80
|
+
)
|
|
81
|
+
notify_if_outdated(ctx.obj.output, "kctl-op", __version__)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# Register command groups
|
|
85
|
+
app.add_typer(config_app, name="config")
|
|
86
|
+
app.add_typer(health_app, name="health")
|
|
87
|
+
app.add_typer(discover_app, name="discover")
|
|
88
|
+
app.add_typer(status_app, name="status")
|
|
89
|
+
app.add_typer(push_app, name="push")
|
|
90
|
+
app.add_typer(pull_app, name="pull")
|
|
91
|
+
app.add_typer(diff_app, name="diff")
|
|
92
|
+
app.add_typer(vault_app, name="vault")
|
|
93
|
+
app.add_typer(projects_app, name="projects")
|
|
94
|
+
app.add_typer(backup_app, name="backup")
|
|
95
|
+
app.add_typer(skill_app, name="skill", hidden=True)
|
|
96
|
+
app.add_typer(doctor_app, name="doctor")
|
|
97
|
+
add_tui_command(app, service_key="op", version=__version__)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# Top-level convenience commands
|
|
101
|
+
@app.command(name="list")
|
|
102
|
+
def list_items(ctx: typer.Context) -> None:
|
|
103
|
+
"""List all items in the 1Password vault."""
|
|
104
|
+
actx: AppContext = ctx.obj
|
|
105
|
+
out = actx.output
|
|
106
|
+
client = actx.client
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
items = client.list_items()
|
|
110
|
+
except Exception as e:
|
|
111
|
+
out.error(f"Cannot list items: {e}")
|
|
112
|
+
raise typer.Exit(code=1) from e
|
|
113
|
+
|
|
114
|
+
if not items:
|
|
115
|
+
out.info("No items in vault.")
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
rows = []
|
|
119
|
+
json_data = []
|
|
120
|
+
for item in items:
|
|
121
|
+
title = item.get("title", "untitled")
|
|
122
|
+
tags = ", ".join(item.get("tags", []))
|
|
123
|
+
updated = item.get("updated_at", "unknown")
|
|
124
|
+
rows.append([title, tags, updated])
|
|
125
|
+
json_data.append(
|
|
126
|
+
{
|
|
127
|
+
"title": title,
|
|
128
|
+
"id": item.get("id", ""),
|
|
129
|
+
"tags": item.get("tags", []),
|
|
130
|
+
"updated_at": updated,
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
out.table(
|
|
135
|
+
title=f"Vault Items ({len(items)})",
|
|
136
|
+
columns=[
|
|
137
|
+
("Title", "green"),
|
|
138
|
+
("Tags", "cyan"),
|
|
139
|
+
("Updated", "dim"),
|
|
140
|
+
],
|
|
141
|
+
rows=rows,
|
|
142
|
+
data_for_json=json_data,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@app.command("self-update")
|
|
147
|
+
def self_update_cmd(ctx: typer.Context) -> None:
|
|
148
|
+
"""Check for updates and upgrade kctl-op."""
|
|
149
|
+
actx = ctx.obj
|
|
150
|
+
out = actx.output
|
|
151
|
+
|
|
152
|
+
from kctl_lib.self_update import check_update
|
|
153
|
+
from kctl_lib.self_update import update as do_update
|
|
154
|
+
|
|
155
|
+
latest = check_update("kctl-op", __version__)
|
|
156
|
+
if latest:
|
|
157
|
+
out.info(f"Updating to {latest}...")
|
|
158
|
+
do_update("kctl-op")
|
|
159
|
+
out.success(f"Updated to {latest}")
|
|
160
|
+
else:
|
|
161
|
+
out.success("Already up to date")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@app.command()
|
|
165
|
+
def completions(
|
|
166
|
+
shell: Annotated[str, typer.Argument(help="Shell type: zsh, bash, fish")] = "zsh",
|
|
167
|
+
install: Annotated[bool, typer.Option("--install", help="Install completions")] = False,
|
|
168
|
+
) -> None:
|
|
169
|
+
"""Generate or install shell completions."""
|
|
170
|
+
from kctl_lib.completions import get_completion_script, install_completions
|
|
171
|
+
|
|
172
|
+
if install:
|
|
173
|
+
path = install_completions("kctl-op", shell)
|
|
174
|
+
if path:
|
|
175
|
+
typer.echo(f"Completions installed to {path}")
|
|
176
|
+
else:
|
|
177
|
+
typer.echo(f"Could not install completions for {shell}", err=True)
|
|
178
|
+
raise typer.Exit(code=1)
|
|
179
|
+
else:
|
|
180
|
+
script = get_completion_script("kctl-op", shell)
|
|
181
|
+
typer.echo(script)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _run() -> None:
|
|
185
|
+
"""Entry point with error handling."""
|
|
186
|
+
try:
|
|
187
|
+
app()
|
|
188
|
+
except KctlError as e:
|
|
189
|
+
handle_cli_error(e)
|
|
190
|
+
except KeyboardInterrupt:
|
|
191
|
+
print("\nAborted.", file=sys.stderr)
|
|
192
|
+
sys.exit(130)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
if __name__ == "__main__":
|
|
196
|
+
_run()
|
|
File without changes
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Backup management commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from kctl_op.core.callbacks import AppContext
|
|
11
|
+
|
|
12
|
+
BACKUP_DIR = Path.home() / ".kodemeio-op" / "backups"
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(help="Backup management for .env files.")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command(name="list")
|
|
18
|
+
def list_backups(
|
|
19
|
+
ctx: typer.Context,
|
|
20
|
+
project: Annotated[str | None, typer.Argument(help="Filter by project name.")] = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
"""List available backups."""
|
|
23
|
+
actx: AppContext = ctx.obj
|
|
24
|
+
out = actx.output
|
|
25
|
+
|
|
26
|
+
if not BACKUP_DIR.exists():
|
|
27
|
+
out.info("No backups found.")
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
rows = []
|
|
31
|
+
json_data = []
|
|
32
|
+
|
|
33
|
+
for project_dir in sorted(BACKUP_DIR.iterdir()):
|
|
34
|
+
if not project_dir.is_dir():
|
|
35
|
+
continue
|
|
36
|
+
if project and project_dir.name != project:
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
for env_dir in sorted(project_dir.iterdir()):
|
|
40
|
+
if not env_dir.is_dir():
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
backups = sorted(env_dir.glob("*.bak"), reverse=True)
|
|
44
|
+
for bak in backups:
|
|
45
|
+
size = bak.stat().st_size
|
|
46
|
+
rows.append(
|
|
47
|
+
[
|
|
48
|
+
project_dir.name,
|
|
49
|
+
env_dir.name,
|
|
50
|
+
bak.name,
|
|
51
|
+
f"{size:,} bytes",
|
|
52
|
+
]
|
|
53
|
+
)
|
|
54
|
+
json_data.append(
|
|
55
|
+
{
|
|
56
|
+
"project": project_dir.name,
|
|
57
|
+
"environment": env_dir.name,
|
|
58
|
+
"filename": bak.name,
|
|
59
|
+
"size": size,
|
|
60
|
+
"path": str(bak),
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if not rows:
|
|
65
|
+
out.info("No backups found." + (f" (project: {project})" if project else ""))
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
out.table(
|
|
69
|
+
title=f"Backups ({len(rows)})",
|
|
70
|
+
columns=[
|
|
71
|
+
("Project", "green"),
|
|
72
|
+
("Environment", "cyan"),
|
|
73
|
+
("Filename", ""),
|
|
74
|
+
("Size", "dim"),
|
|
75
|
+
],
|
|
76
|
+
rows=rows,
|
|
77
|
+
data_for_json=json_data,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@app.command()
|
|
82
|
+
def restore(
|
|
83
|
+
ctx: typer.Context,
|
|
84
|
+
project: Annotated[str, typer.Argument(help="Project name.")],
|
|
85
|
+
environment: Annotated[str, typer.Argument(help="Environment name.")],
|
|
86
|
+
timestamp: Annotated[
|
|
87
|
+
str | None, typer.Argument(help="Backup timestamp (from filename). Latest if omitted.")
|
|
88
|
+
] = None,
|
|
89
|
+
force: Annotated[bool, typer.Option("--force", help="Skip confirmation.")] = False,
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Restore a .env file from backup."""
|
|
92
|
+
actx: AppContext = ctx.obj
|
|
93
|
+
out = actx.output
|
|
94
|
+
|
|
95
|
+
env_backup_dir = BACKUP_DIR / project / environment
|
|
96
|
+
if not env_backup_dir.exists():
|
|
97
|
+
out.error(f"No backups found for {project}/{environment}")
|
|
98
|
+
raise typer.Exit(code=1)
|
|
99
|
+
|
|
100
|
+
backups = sorted(env_backup_dir.glob("*.bak"), reverse=True)
|
|
101
|
+
if not backups:
|
|
102
|
+
out.error(f"No backup files in {env_backup_dir}")
|
|
103
|
+
raise typer.Exit(code=1)
|
|
104
|
+
|
|
105
|
+
if timestamp:
|
|
106
|
+
selected = [b for b in backups if timestamp in b.name]
|
|
107
|
+
if not selected:
|
|
108
|
+
out.error(f"No backup matching timestamp '{timestamp}'")
|
|
109
|
+
raise typer.Exit(code=1)
|
|
110
|
+
backup_file = selected[0]
|
|
111
|
+
else:
|
|
112
|
+
backup_file = backups[0]
|
|
113
|
+
|
|
114
|
+
out.info(f"Restoring from: {backup_file.name}")
|
|
115
|
+
|
|
116
|
+
# Find the target .env file
|
|
117
|
+
cfg = actx.service_config
|
|
118
|
+
client = actx.client
|
|
119
|
+
files = client.discover_files(
|
|
120
|
+
cfg.scan_roots,
|
|
121
|
+
cfg.exclude_patterns,
|
|
122
|
+
cfg.env_mappings,
|
|
123
|
+
cfg.custom_env_pattern,
|
|
124
|
+
)
|
|
125
|
+
matches = [f for f in files if f.project == project and f.environment == environment]
|
|
126
|
+
|
|
127
|
+
if not matches:
|
|
128
|
+
out.error(f"Cannot find target .env file for {project}/{environment}")
|
|
129
|
+
raise typer.Exit(code=1)
|
|
130
|
+
|
|
131
|
+
target = matches[0].path
|
|
132
|
+
|
|
133
|
+
if not force and not typer.confirm(f"Restore {backup_file.name} to {target}?"):
|
|
134
|
+
raise typer.Exit()
|
|
135
|
+
|
|
136
|
+
# Read backup and write to target
|
|
137
|
+
content = backup_file.read_text()
|
|
138
|
+
target.write_text(content)
|
|
139
|
+
out.success(f"Restored {backup_file.name} -> {target}")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@app.command()
|
|
143
|
+
def clean(
|
|
144
|
+
ctx: typer.Context,
|
|
145
|
+
keep: Annotated[int, typer.Option("--keep", "-k", help="Number of backups to keep per file.")] = 10,
|
|
146
|
+
force: Annotated[bool, typer.Option("--force", help="Skip confirmation.")] = False,
|
|
147
|
+
) -> None:
|
|
148
|
+
"""Clean old backups, keeping the N most recent per file."""
|
|
149
|
+
actx: AppContext = ctx.obj
|
|
150
|
+
out = actx.output
|
|
151
|
+
|
|
152
|
+
if not BACKUP_DIR.exists():
|
|
153
|
+
out.info("No backups to clean.")
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
to_delete: list[Path] = []
|
|
157
|
+
for project_dir in BACKUP_DIR.iterdir():
|
|
158
|
+
if not project_dir.is_dir():
|
|
159
|
+
continue
|
|
160
|
+
for env_dir in project_dir.iterdir():
|
|
161
|
+
if not env_dir.is_dir():
|
|
162
|
+
continue
|
|
163
|
+
backups = sorted(env_dir.glob("*.bak"), reverse=True)
|
|
164
|
+
if len(backups) > keep:
|
|
165
|
+
to_delete.extend(backups[keep:])
|
|
166
|
+
|
|
167
|
+
if not to_delete:
|
|
168
|
+
out.info(f"Nothing to clean (keeping {keep} per file).")
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
out.info(f"Will delete {len(to_delete)} old backup(s).")
|
|
172
|
+
|
|
173
|
+
if not force and not typer.confirm("Proceed?"):
|
|
174
|
+
raise typer.Exit()
|
|
175
|
+
|
|
176
|
+
for f in to_delete:
|
|
177
|
+
f.unlink()
|
|
178
|
+
out.success(f"Deleted {len(to_delete)} old backup(s).")
|