agentix-cli 0.1.1__tar.gz → 0.2.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.
- agentix_cli-0.2.0/.github/workflows/ci.yml +56 -0
- {agentix_cli-0.1.1 → agentix_cli-0.2.0}/PKG-INFO +12 -2
- {agentix_cli-0.1.1 → agentix_cli-0.2.0}/pyproject.toml +23 -4
- {agentix_cli-0.1.1 → agentix_cli-0.2.0}/src/agentix/__init__.py +1 -1
- agentix_cli-0.2.0/src/agentix/cli.py +76 -0
- agentix_cli-0.2.0/src/agentix/config/__init__.py +1 -0
- agentix_cli-0.2.0/src/agentix/config/commands.py +180 -0
- agentix_cli-0.2.0/src/agentix/config/manager.py +109 -0
- agentix_cli-0.2.0/src/agentix/config/models.py +105 -0
- agentix_cli-0.2.0/src/agentix/confluence/__init__.py +1 -0
- agentix_cli-0.2.0/src/agentix/confluence/client.py +224 -0
- agentix_cli-0.2.0/src/agentix/confluence/commands.py +260 -0
- agentix_cli-0.2.0/src/agentix/confluence/models.py +78 -0
- agentix_cli-0.2.0/src/agentix/core/__init__.py +1 -0
- agentix_cli-0.2.0/src/agentix/core/auth.py +86 -0
- agentix_cli-0.2.0/src/agentix/core/exceptions.py +73 -0
- agentix_cli-0.2.0/src/agentix/core/http.py +223 -0
- agentix_cli-0.2.0/src/agentix/core/output.py +63 -0
- agentix_cli-0.2.0/src/agentix/jenkins/__init__.py +1 -0
- agentix_cli-0.2.0/src/agentix/jenkins/client.py +248 -0
- agentix_cli-0.2.0/src/agentix/jenkins/commands.py +294 -0
- agentix_cli-0.2.0/src/agentix/jenkins/models.py +100 -0
- agentix_cli-0.2.0/src/agentix/jira/__init__.py +1 -0
- agentix_cli-0.2.0/src/agentix/jira/client.py +269 -0
- agentix_cli-0.2.0/src/agentix/jira/commands.py +475 -0
- agentix_cli-0.2.0/src/agentix/jira/models.py +133 -0
- agentix_cli-0.2.0/tests/conftest.py +51 -0
- agentix_cli-0.2.0/tests/test_config.py +97 -0
- agentix_cli-0.2.0/tests/test_confluence/__init__.py +0 -0
- agentix_cli-0.2.0/tests/test_core/__init__.py +0 -0
- agentix_cli-0.2.0/tests/test_core/test_auth.py +57 -0
- agentix_cli-0.2.0/tests/test_core/test_exceptions.py +47 -0
- agentix_cli-0.2.0/tests/test_core/test_http.py +173 -0
- agentix_cli-0.2.0/tests/test_core/test_output.py +74 -0
- agentix_cli-0.2.0/tests/test_jenkins/__init__.py +0 -0
- agentix_cli-0.2.0/tests/test_jira/__init__.py +0 -0
- agentix_cli-0.2.0/tests/test_jira/test_client.py +151 -0
- agentix_cli-0.2.0/tests/test_jira/test_commands.py +152 -0
- agentix_cli-0.2.0/uv.lock +832 -0
- agentix_cli-0.1.1/src/agentix/cli.py +0 -12
- {agentix_cli-0.1.1 → agentix_cli-0.2.0}/.github/workflows/publish.yml +0 -0
- {agentix_cli-0.1.1 → agentix_cli-0.2.0}/.gitignore +0 -0
- {agentix_cli-0.1.1 → agentix_cli-0.2.0}/.python-version +0 -0
- {agentix_cli-0.1.1 → agentix_cli-0.2.0}/CLAUDE.md +0 -0
- {agentix_cli-0.1.1 → agentix_cli-0.2.0}/LICENSE +0 -0
- {agentix_cli-0.1.1 → agentix_cli-0.2.0}/README.md +0 -0
- {agentix_cli-0.1.1 → agentix_cli-0.2.0}/src/agentix/__main__.py +0 -0
- {agentix_cli-0.1.1 → agentix_cli-0.2.0}/tests/__init__.py +0 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
name: Test (Python ${{ matrix.python-version }}, ${{ matrix.os }})
|
|
12
|
+
runs-on: ${{ matrix.os }}
|
|
13
|
+
strategy:
|
|
14
|
+
matrix:
|
|
15
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
16
|
+
python-version: ["3.9", "3.12", "3.13"]
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Install uv
|
|
21
|
+
uses: astral-sh/setup-uv@v5
|
|
22
|
+
with:
|
|
23
|
+
enable-cache: true
|
|
24
|
+
|
|
25
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
26
|
+
run: uv python install ${{ matrix.python-version }}
|
|
27
|
+
|
|
28
|
+
- name: Install dependencies
|
|
29
|
+
run: uv pip install -e ".[dev]"
|
|
30
|
+
|
|
31
|
+
- name: Lint
|
|
32
|
+
run: uv run ruff check src/ tests/
|
|
33
|
+
|
|
34
|
+
- name: Test
|
|
35
|
+
run: uv run pytest tests/ -v --tb=short
|
|
36
|
+
|
|
37
|
+
build:
|
|
38
|
+
name: Build package
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
needs: [test]
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@v4
|
|
43
|
+
|
|
44
|
+
- name: Install uv
|
|
45
|
+
uses: astral-sh/setup-uv@v5
|
|
46
|
+
with:
|
|
47
|
+
enable-cache: true
|
|
48
|
+
|
|
49
|
+
- name: Set up Python
|
|
50
|
+
run: uv python install
|
|
51
|
+
|
|
52
|
+
- name: Build
|
|
53
|
+
run: uv build
|
|
54
|
+
|
|
55
|
+
- name: Verify package
|
|
56
|
+
run: ls -la dist/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentix-cli
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Unified CLI and SDK for Jira, Confluence, and Jenkins
|
|
5
5
|
Project-URL: Homepage, https://github.com/saggl/agentix
|
|
6
6
|
Project-URL: Repository, https://github.com/saggl/agentix.git
|
|
7
7
|
Project-URL: Issues, https://github.com/saggl/agentix/issues
|
|
@@ -18,6 +18,16 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
20
|
Requires-Python: >=3.9
|
|
21
|
+
Requires-Dist: click>=8.0
|
|
22
|
+
Requires-Dist: requests>=2.28
|
|
23
|
+
Requires-Dist: tabulate>=0.9
|
|
24
|
+
Requires-Dist: tomli-w>=1.0
|
|
25
|
+
Requires-Dist: tomli>=1.0; python_version < '3.11'
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: responses>=0.23; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
21
31
|
Description-Content-Type: text/markdown
|
|
22
32
|
|
|
23
33
|
# agentix
|
|
@@ -3,12 +3,17 @@ requires = ["hatchling"]
|
|
|
3
3
|
build-backend = "hatchling.build"
|
|
4
4
|
|
|
5
5
|
[dependency-groups]
|
|
6
|
-
dev = [
|
|
6
|
+
dev = [
|
|
7
|
+
"pytest>=7.0",
|
|
8
|
+
"pytest-cov>=4.0",
|
|
9
|
+
"responses>=0.23",
|
|
10
|
+
"ruff>=0.4",
|
|
11
|
+
]
|
|
7
12
|
|
|
8
13
|
[project]
|
|
9
14
|
name = "agentix-cli"
|
|
10
|
-
version = "0.
|
|
11
|
-
description = "
|
|
15
|
+
version = "0.2.0"
|
|
16
|
+
description = "Unified CLI and SDK for Jira, Confluence, and Jenkins"
|
|
12
17
|
readme = "README.md"
|
|
13
18
|
requires-python = ">=3.9"
|
|
14
19
|
license = {text = "MIT"}
|
|
@@ -26,7 +31,21 @@ classifiers = [
|
|
|
26
31
|
"Programming Language :: Python :: 3.12",
|
|
27
32
|
"Programming Language :: Python :: 3.13",
|
|
28
33
|
]
|
|
29
|
-
dependencies = [
|
|
34
|
+
dependencies = [
|
|
35
|
+
"click>=8.0",
|
|
36
|
+
"requests>=2.28",
|
|
37
|
+
"tomli>=1.0; python_version < '3.11'",
|
|
38
|
+
"tomli-w>=1.0",
|
|
39
|
+
"tabulate>=0.9",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.optional-dependencies]
|
|
43
|
+
dev = [
|
|
44
|
+
"pytest>=7.0",
|
|
45
|
+
"pytest-cov>=4.0",
|
|
46
|
+
"responses>=0.23",
|
|
47
|
+
"ruff>=0.4",
|
|
48
|
+
]
|
|
30
49
|
|
|
31
50
|
[project.scripts]
|
|
32
51
|
agentix = "agentix.__main__:main"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Root CLI for agentix."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from agentix import __version__
|
|
9
|
+
from agentix.config.commands import config_group
|
|
10
|
+
from agentix.config.manager import ConfigManager
|
|
11
|
+
from agentix.core.exceptions import AgentixError
|
|
12
|
+
from agentix.core.output import OutputFormatter
|
|
13
|
+
from agentix.confluence.commands import confluence_group
|
|
14
|
+
from agentix.jenkins.commands import jenkins_group
|
|
15
|
+
from agentix.jira.commands import jira_group
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.group()
|
|
19
|
+
@click.option(
|
|
20
|
+
"--profile",
|
|
21
|
+
default=None,
|
|
22
|
+
envvar="AGENTIX_DEFAULT_PROFILE",
|
|
23
|
+
help="Config profile to use.",
|
|
24
|
+
)
|
|
25
|
+
@click.option(
|
|
26
|
+
"--format",
|
|
27
|
+
"output_format",
|
|
28
|
+
type=click.Choice(["json", "table"]),
|
|
29
|
+
default=None,
|
|
30
|
+
help="Output format (default: json).",
|
|
31
|
+
)
|
|
32
|
+
@click.option("--verbose", is_flag=True, help="Enable verbose logging.")
|
|
33
|
+
@click.version_option(version=__version__, prog_name="agentix")
|
|
34
|
+
@click.pass_context
|
|
35
|
+
def cli(ctx, profile, output_format, verbose):
|
|
36
|
+
"""agentix — Unified CLI for Jira, Confluence, and Jenkins."""
|
|
37
|
+
ctx.ensure_object(dict)
|
|
38
|
+
|
|
39
|
+
if verbose:
|
|
40
|
+
logging.basicConfig(level=logging.DEBUG, stream=sys.stderr)
|
|
41
|
+
|
|
42
|
+
config_manager = ConfigManager()
|
|
43
|
+
resolved_format = (
|
|
44
|
+
output_format
|
|
45
|
+
or config_manager.config.defaults.format
|
|
46
|
+
or "json"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
ctx.obj["profile"] = profile
|
|
50
|
+
ctx.obj["config_manager"] = config_manager
|
|
51
|
+
ctx.obj["formatter"] = OutputFormatter(resolved_format)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Register subgroups
|
|
55
|
+
cli.add_command(config_group)
|
|
56
|
+
cli.add_command(jira_group)
|
|
57
|
+
cli.add_command(confluence_group)
|
|
58
|
+
cli.add_command(jenkins_group)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main():
|
|
62
|
+
"""Entry point for the agentix CLI."""
|
|
63
|
+
try:
|
|
64
|
+
cli(standalone_mode=False)
|
|
65
|
+
except AgentixError as e:
|
|
66
|
+
formatter = OutputFormatter("json")
|
|
67
|
+
formatter.error(e)
|
|
68
|
+
sys.exit(e.exit_code)
|
|
69
|
+
except click.exceptions.Abort:
|
|
70
|
+
sys.exit(130)
|
|
71
|
+
except click.exceptions.Exit as e:
|
|
72
|
+
sys.exit(e.exit_code)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Configuration management for agentix."""
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""CLI commands for agentix config management."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import requests
|
|
7
|
+
from requests.auth import HTTPBasicAuth
|
|
8
|
+
|
|
9
|
+
from agentix.config.models import (
|
|
10
|
+
ConfluenceConfig,
|
|
11
|
+
JenkinsConfig,
|
|
12
|
+
JiraConfig,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group("config")
|
|
17
|
+
def config_group():
|
|
18
|
+
"""Manage agentix configuration."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@config_group.command()
|
|
23
|
+
@click.pass_context
|
|
24
|
+
def init(ctx):
|
|
25
|
+
"""Interactive setup wizard."""
|
|
26
|
+
cm = ctx.obj["config_manager"]
|
|
27
|
+
config = cm.config
|
|
28
|
+
|
|
29
|
+
profile_name = click.prompt("Profile name", default="default")
|
|
30
|
+
profile = config.get_profile(profile_name)
|
|
31
|
+
|
|
32
|
+
# Jira
|
|
33
|
+
if click.confirm("Configure Jira?", default=True):
|
|
34
|
+
profile.jira = _setup_jira()
|
|
35
|
+
|
|
36
|
+
# Confluence
|
|
37
|
+
if click.confirm("Configure Confluence?", default=True):
|
|
38
|
+
profile.confluence = _setup_confluence(profile.jira)
|
|
39
|
+
|
|
40
|
+
# Jenkins
|
|
41
|
+
if click.confirm("Configure Jenkins?", default=False):
|
|
42
|
+
profile.jenkins = _setup_jenkins()
|
|
43
|
+
|
|
44
|
+
config.default_profile = profile_name
|
|
45
|
+
config.profiles[profile_name] = profile
|
|
46
|
+
cm.save(config)
|
|
47
|
+
|
|
48
|
+
formatter = ctx.obj["formatter"]
|
|
49
|
+
formatter.success(f"Configuration saved to {cm.config_path}")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _setup_jira() -> JiraConfig:
|
|
53
|
+
base_url = click.prompt("Jira base URL (e.g., https://company.atlassian.net)")
|
|
54
|
+
email = click.prompt("Jira email")
|
|
55
|
+
api_token = click.prompt("Jira API token", hide_input=True)
|
|
56
|
+
|
|
57
|
+
# Validate
|
|
58
|
+
click.echo("Validating credentials... ", nl=False)
|
|
59
|
+
try:
|
|
60
|
+
resp = requests.get(
|
|
61
|
+
f"{base_url.rstrip('/')}/rest/api/3/myself",
|
|
62
|
+
auth=HTTPBasicAuth(email, api_token),
|
|
63
|
+
timeout=10,
|
|
64
|
+
)
|
|
65
|
+
if resp.ok:
|
|
66
|
+
user = resp.json()
|
|
67
|
+
click.echo(f"OK (authenticated as {user.get('displayName', email)})")
|
|
68
|
+
else:
|
|
69
|
+
click.echo(f"Warning: got HTTP {resp.status_code} — credentials may be invalid")
|
|
70
|
+
except requests.RequestException as e:
|
|
71
|
+
click.echo(f"Warning: could not validate — {e}")
|
|
72
|
+
|
|
73
|
+
return JiraConfig(base_url=base_url.rstrip("/"), email=email, api_token=api_token)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _setup_confluence(jira_config: JiraConfig) -> ConfluenceConfig:
|
|
77
|
+
default_url = ""
|
|
78
|
+
default_email = ""
|
|
79
|
+
default_token = ""
|
|
80
|
+
|
|
81
|
+
if jira_config.base_url:
|
|
82
|
+
default_url = jira_config.base_url.rstrip("/") + "/wiki"
|
|
83
|
+
default_email = jira_config.email
|
|
84
|
+
default_token = jira_config.api_token
|
|
85
|
+
|
|
86
|
+
base_url = click.prompt(
|
|
87
|
+
"Confluence base URL", default=default_url or None
|
|
88
|
+
)
|
|
89
|
+
email = click.prompt("Confluence email", default=default_email or None)
|
|
90
|
+
api_token = click.prompt(
|
|
91
|
+
"Confluence API token (same as Jira for Atlassian Cloud)",
|
|
92
|
+
default=default_token or None,
|
|
93
|
+
hide_input=True,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return ConfluenceConfig(
|
|
97
|
+
base_url=base_url.rstrip("/"), email=email, api_token=api_token
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _setup_jenkins() -> JenkinsConfig:
|
|
102
|
+
base_url = click.prompt("Jenkins base URL (e.g., https://jenkins.company.com)")
|
|
103
|
+
username = click.prompt("Jenkins username")
|
|
104
|
+
api_token = click.prompt("Jenkins API token", hide_input=True)
|
|
105
|
+
|
|
106
|
+
# Validate
|
|
107
|
+
click.echo("Validating credentials... ", nl=False)
|
|
108
|
+
try:
|
|
109
|
+
resp = requests.get(
|
|
110
|
+
f"{base_url.rstrip('/')}/api/json",
|
|
111
|
+
auth=HTTPBasicAuth(username, api_token),
|
|
112
|
+
timeout=10,
|
|
113
|
+
)
|
|
114
|
+
if resp.ok:
|
|
115
|
+
click.echo("OK")
|
|
116
|
+
else:
|
|
117
|
+
click.echo(f"Warning: got HTTP {resp.status_code}")
|
|
118
|
+
except requests.RequestException as e:
|
|
119
|
+
click.echo(f"Warning: could not validate — {e}")
|
|
120
|
+
|
|
121
|
+
return JenkinsConfig(
|
|
122
|
+
base_url=base_url.rstrip("/"), username=username, api_token=api_token
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@config_group.command()
|
|
127
|
+
@click.argument("key")
|
|
128
|
+
@click.argument("value")
|
|
129
|
+
@click.pass_context
|
|
130
|
+
def set(ctx, key, value):
|
|
131
|
+
"""Set a config value (e.g., agentix config set profiles.work.jira.base_url https://...)."""
|
|
132
|
+
cm = ctx.obj["config_manager"]
|
|
133
|
+
cm.set_value(key, value)
|
|
134
|
+
ctx.obj["formatter"].success(f"Set {key} = {value}")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@config_group.command()
|
|
138
|
+
@click.argument("key")
|
|
139
|
+
@click.pass_context
|
|
140
|
+
def get(ctx, key):
|
|
141
|
+
"""Get a config value."""
|
|
142
|
+
cm = ctx.obj["config_manager"]
|
|
143
|
+
try:
|
|
144
|
+
value = cm.get_value(key)
|
|
145
|
+
except Exception as e:
|
|
146
|
+
ctx.obj["formatter"].error(e)
|
|
147
|
+
ctx.exit(2)
|
|
148
|
+
return
|
|
149
|
+
formatter = ctx.obj["formatter"]
|
|
150
|
+
if isinstance(value, dict):
|
|
151
|
+
formatter.output(value)
|
|
152
|
+
else:
|
|
153
|
+
if formatter.fmt == "json":
|
|
154
|
+
print(json.dumps({"key": key, "value": value}))
|
|
155
|
+
else:
|
|
156
|
+
print(value)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@config_group.command()
|
|
160
|
+
@click.pass_context
|
|
161
|
+
def show(ctx):
|
|
162
|
+
"""Show full config with tokens masked."""
|
|
163
|
+
cm = ctx.obj["config_manager"]
|
|
164
|
+
if not cm.exists():
|
|
165
|
+
click.echo("No configuration found. Run 'agentix config init' to create one.")
|
|
166
|
+
ctx.exit(2)
|
|
167
|
+
return
|
|
168
|
+
ctx.obj["formatter"].output(cm.mask_tokens())
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@config_group.command()
|
|
172
|
+
@click.pass_context
|
|
173
|
+
def path(ctx):
|
|
174
|
+
"""Print config file path."""
|
|
175
|
+
cm = ctx.obj["config_manager"]
|
|
176
|
+
formatter = ctx.obj["formatter"]
|
|
177
|
+
if formatter.fmt == "json":
|
|
178
|
+
print(json.dumps({"path": str(cm.config_path), "exists": cm.exists()}))
|
|
179
|
+
else:
|
|
180
|
+
print(cm.config_path)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Configuration manager for agentix."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import stat
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import tomllib
|
|
11
|
+
except ModuleNotFoundError:
|
|
12
|
+
import tomli as tomllib
|
|
13
|
+
|
|
14
|
+
import tomli_w
|
|
15
|
+
|
|
16
|
+
from agentix.core.exceptions import ConfigError
|
|
17
|
+
|
|
18
|
+
from .models import AgentixConfig, get_config_path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ConfigManager:
|
|
22
|
+
"""Load, save, and query agentix configuration."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, config_path: Optional[Path] = None):
|
|
25
|
+
self.config_path = config_path or get_config_path()
|
|
26
|
+
self._config: Optional[AgentixConfig] = None
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def config(self) -> AgentixConfig:
|
|
30
|
+
if self._config is None:
|
|
31
|
+
self._config = self.load()
|
|
32
|
+
return self._config
|
|
33
|
+
|
|
34
|
+
def exists(self) -> bool:
|
|
35
|
+
return self.config_path.exists()
|
|
36
|
+
|
|
37
|
+
def load(self) -> AgentixConfig:
|
|
38
|
+
if not self.config_path.exists():
|
|
39
|
+
return AgentixConfig()
|
|
40
|
+
try:
|
|
41
|
+
data = tomllib.loads(self.config_path.read_text(encoding="utf-8"))
|
|
42
|
+
return AgentixConfig.from_dict(data)
|
|
43
|
+
except Exception as e:
|
|
44
|
+
raise ConfigError(
|
|
45
|
+
f"Failed to load config from {self.config_path}: {e}"
|
|
46
|
+
) from e
|
|
47
|
+
|
|
48
|
+
def save(self, config: Optional[AgentixConfig] = None) -> None:
|
|
49
|
+
cfg = config or self.config
|
|
50
|
+
config_dir = self.config_path.parent
|
|
51
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
|
|
53
|
+
# Set restrictive permissions on directory
|
|
54
|
+
if sys.platform != "win32":
|
|
55
|
+
os.chmod(config_dir, stat.S_IRWXU)
|
|
56
|
+
|
|
57
|
+
self.config_path.write_text(
|
|
58
|
+
tomli_w.dumps(cfg.to_dict()), encoding="utf-8"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Set restrictive permissions on file
|
|
62
|
+
if sys.platform != "win32":
|
|
63
|
+
os.chmod(self.config_path, stat.S_IRUSR | stat.S_IWUSR)
|
|
64
|
+
|
|
65
|
+
self._config = cfg
|
|
66
|
+
|
|
67
|
+
def get_value(self, key: str) -> Any:
|
|
68
|
+
"""Get a config value by dotted key path (e.g., 'profiles.work.jira.base_url')."""
|
|
69
|
+
parts = key.split(".")
|
|
70
|
+
data = self.config.to_dict()
|
|
71
|
+
for part in parts:
|
|
72
|
+
if isinstance(data, dict) and part in data:
|
|
73
|
+
data = data[part]
|
|
74
|
+
else:
|
|
75
|
+
raise ConfigError(f"Config key not found: {key}")
|
|
76
|
+
return data
|
|
77
|
+
|
|
78
|
+
def set_value(self, key: str, value: str) -> None:
|
|
79
|
+
"""Set a config value by dotted key path."""
|
|
80
|
+
parts = key.split(".")
|
|
81
|
+
data = self.config.to_dict()
|
|
82
|
+
|
|
83
|
+
# Navigate to parent
|
|
84
|
+
current = data
|
|
85
|
+
for part in parts[:-1]:
|
|
86
|
+
if part not in current:
|
|
87
|
+
current[part] = {}
|
|
88
|
+
current = current[part]
|
|
89
|
+
current[parts[-1]] = value
|
|
90
|
+
|
|
91
|
+
self._config = AgentixConfig.from_dict(data)
|
|
92
|
+
self.save()
|
|
93
|
+
|
|
94
|
+
def mask_tokens(self) -> dict:
|
|
95
|
+
"""Return config dict with tokens/secrets masked."""
|
|
96
|
+
data = self.config.to_dict()
|
|
97
|
+
secret_keys = {"api_token", "token", "password", "secret"}
|
|
98
|
+
|
|
99
|
+
def _mask(d: Any) -> Any:
|
|
100
|
+
if isinstance(d, dict):
|
|
101
|
+
return {
|
|
102
|
+
k: ("***" if k in secret_keys and v else _mask(v))
|
|
103
|
+
for k, v in d.items()
|
|
104
|
+
}
|
|
105
|
+
if isinstance(d, list):
|
|
106
|
+
return [_mask(i) for i in d]
|
|
107
|
+
return d
|
|
108
|
+
|
|
109
|
+
return _mask(data)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Configuration data models for agentix."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class JiraConfig:
|
|
12
|
+
base_url: str = ""
|
|
13
|
+
email: str = ""
|
|
14
|
+
api_token: str = ""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ConfluenceConfig:
|
|
19
|
+
base_url: str = ""
|
|
20
|
+
email: str = ""
|
|
21
|
+
api_token: str = ""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class JenkinsConfig:
|
|
26
|
+
base_url: str = ""
|
|
27
|
+
username: str = ""
|
|
28
|
+
api_token: str = ""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class Profile:
|
|
33
|
+
jira: JiraConfig = field(default_factory=JiraConfig)
|
|
34
|
+
confluence: ConfluenceConfig = field(default_factory=ConfluenceConfig)
|
|
35
|
+
jenkins: JenkinsConfig = field(default_factory=JenkinsConfig)
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Profile":
|
|
39
|
+
return cls(
|
|
40
|
+
jira=JiraConfig(**data.get("jira", {})),
|
|
41
|
+
confluence=ConfluenceConfig(**data.get("confluence", {})),
|
|
42
|
+
jenkins=JenkinsConfig(**data.get("jenkins", {})),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
46
|
+
result = {}
|
|
47
|
+
for service_name in ("jira", "confluence", "jenkins"):
|
|
48
|
+
cfg = getattr(self, service_name)
|
|
49
|
+
d = {k: v for k, v in cfg.__dict__.items() if v}
|
|
50
|
+
if d:
|
|
51
|
+
result[service_name] = d
|
|
52
|
+
return result
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class Defaults:
|
|
57
|
+
format: str = "json"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class AgentixConfig:
|
|
62
|
+
default_profile: str = "default"
|
|
63
|
+
defaults: Defaults = field(default_factory=Defaults)
|
|
64
|
+
profiles: Dict[str, Profile] = field(default_factory=dict)
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def from_dict(cls, data: Dict[str, Any]) -> "AgentixConfig":
|
|
68
|
+
profiles = {}
|
|
69
|
+
for name, pdata in data.get("profiles", {}).items():
|
|
70
|
+
profiles[name] = Profile.from_dict(pdata)
|
|
71
|
+
return cls(
|
|
72
|
+
default_profile=data.get("default_profile", "default"),
|
|
73
|
+
defaults=Defaults(**data.get("defaults", {})),
|
|
74
|
+
profiles=profiles,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
78
|
+
result: Dict[str, Any] = {"default_profile": self.default_profile}
|
|
79
|
+
defaults_dict = {k: v for k, v in self.defaults.__dict__.items() if v}
|
|
80
|
+
if defaults_dict:
|
|
81
|
+
result["defaults"] = defaults_dict
|
|
82
|
+
if self.profiles:
|
|
83
|
+
result["profiles"] = {
|
|
84
|
+
name: p.to_dict() for name, p in self.profiles.items()
|
|
85
|
+
}
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
def get_profile(self, name: Optional[str] = None) -> Profile:
|
|
89
|
+
profile_name = name or self.default_profile
|
|
90
|
+
if profile_name not in self.profiles:
|
|
91
|
+
self.profiles[profile_name] = Profile()
|
|
92
|
+
return self.profiles[profile_name]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_config_dir() -> Path:
|
|
96
|
+
"""Return the platform-appropriate config directory."""
|
|
97
|
+
if sys.platform == "win32":
|
|
98
|
+
base = Path(os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming"))
|
|
99
|
+
else:
|
|
100
|
+
base = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"))
|
|
101
|
+
return base / "agentix"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_config_path() -> Path:
|
|
105
|
+
return get_config_dir() / "config.toml"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Confluence integration for agentix."""
|