aes-cli 0.4.3__tar.gz → 0.6.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.
- {aes_cli-0.4.3 → aes_cli-0.6.0}/PKG-INFO +1 -1
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/__init__.py +1 -1
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/__main__.py +4 -0
- aes_cli-0.6.0/aes/commands/bom.py +131 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/commands/init.py +11 -1
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/commands/inspect.py +42 -1
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/commands/publish.py +77 -9
- aes_cli-0.6.0/aes/commands/upgrade.py +282 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/config.py +4 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/i18n/_messages.py +48 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/i18n/ja.py +48 -0
- aes_cli-0.6.0/aes/migrations.py +72 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/scaffold/agent.yaml.jinja +20 -2
- aes_cli-0.6.0/aes/scaffold/bom.yaml.jinja +27 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/scaffold/instructions.md.jinja +4 -4
- aes_cli-0.6.0/aes/scaffold/ja/memory_command.md.jinja +99 -0
- aes_cli-0.6.0/aes/scaffold/memory_command.md.jinja +99 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/scaffold/permissions.yaml.jinja +30 -2
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/scaffold/skill.yaml.jinja +2 -2
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/scaffold/workflow.yaml.jinja +1 -1
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/schemas/agent.schema.json +56 -0
- aes_cli-0.6.0/aes/schemas/bom.schema.json +77 -0
- aes_cli-0.6.0/aes/schemas/decision-record.schema.json +78 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/schemas/permissions.schema.json +51 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/targets/claude.py +13 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/validator.py +12 -1
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes_cli.egg-info/PKG-INFO +1 -1
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes_cli.egg-info/SOURCES.txt +10 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/pyproject.toml +1 -1
- aes_cli-0.6.0/tests/test_bom.py +132 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/tests/test_init.py +15 -6
- {aes_cli-0.4.3 → aes_cli-0.6.0}/tests/test_sync.py +34 -0
- aes_cli-0.6.0/tests/test_upgrade.py +430 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/tests/test_validate.py +326 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/README.md +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/analyzer.py +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/commands/__init__.py +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/commands/install.py +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/commands/search.py +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/commands/status.py +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/commands/sync.py +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/commands/validate.py +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/domains.py +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/frameworks.py +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/global_config.py +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/i18n/__init__.py +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/i18n/domains_ja.py +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/mcp_server.py +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/registry.py +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/scaffold/agentignore.jinja +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/scaffold/ja/instructions.md.jinja +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/scaffold/ja/operations.md.jinja +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/scaffold/ja/orchestrator.md.jinja +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/scaffold/ja/setup.md.jinja +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/scaffold/ja/skill.md.jinja +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/scaffold/ja/workflow_command.md.jinja +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/scaffold/local.example.yaml.jinja +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/scaffold/local.yaml.jinja +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/scaffold/operations.md.jinja +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/scaffold/orchestrator.md.jinja +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/scaffold/setup.md.jinja +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/scaffold/skill.md.jinja +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/scaffold/workflow_command.md.jinja +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/schemas/registry.schema.json +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/schemas/skill.schema.json +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/schemas/workflow.schema.json +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/targets/__init__.py +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/targets/_base.py +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/targets/_composer.py +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/targets/copilot.py +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/targets/cursor.py +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes/targets/windsurf.py +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes_cli.egg-info/dependency_links.txt +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes_cli.egg-info/entry_points.txt +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes_cli.egg-info/requires.txt +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/aes_cli.egg-info/top_level.txt +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/setup.cfg +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/tests/test_analyzer.py +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/tests/test_frameworks.py +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/tests/test_inspect.py +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/tests/test_install.py +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/tests/test_mcp_server.py +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/tests/test_publish.py +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/tests/test_registry.py +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/tests/test_search.py +0 -0
- {aes_cli-0.4.3 → aes_cli-0.6.0}/tests/test_status.py +0 -0
|
@@ -16,6 +16,8 @@ from aes.commands.install import install_cmd
|
|
|
16
16
|
from aes.commands.search import search_cmd
|
|
17
17
|
from aes.commands.status import status_cmd
|
|
18
18
|
from aes.commands.sync import sync_cmd
|
|
19
|
+
from aes.commands.upgrade import upgrade_cmd
|
|
20
|
+
from aes.commands.bom import bom_cmd
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
def _prompt_language() -> None:
|
|
@@ -77,6 +79,8 @@ cli.add_command(install_cmd, "install")
|
|
|
77
79
|
cli.add_command(sync_cmd, "sync")
|
|
78
80
|
cli.add_command(status_cmd, "status")
|
|
79
81
|
cli.add_command(search_cmd, "search")
|
|
82
|
+
cli.add_command(upgrade_cmd, "upgrade")
|
|
83
|
+
cli.add_command(bom_cmd, "bom")
|
|
80
84
|
|
|
81
85
|
|
|
82
86
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""aes bom — Display the Agent Bill of Materials."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import yaml
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from aes.config import AGENT_DIR, BOM_FILE
|
|
13
|
+
from aes.i18n import t
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _load_bom(agent_dir: Path) -> dict:
|
|
19
|
+
"""Load and return bom.yaml, or empty dict."""
|
|
20
|
+
bom_path = agent_dir / BOM_FILE
|
|
21
|
+
if not bom_path.exists():
|
|
22
|
+
return {}
|
|
23
|
+
with open(bom_path) as f:
|
|
24
|
+
data = yaml.safe_load(f)
|
|
25
|
+
return data if isinstance(data, dict) else {}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@click.command("bom")
|
|
29
|
+
@click.argument("path", default=".", type=click.Path(exists=True))
|
|
30
|
+
def bom_cmd(path: str) -> None:
|
|
31
|
+
"""Display the Agent Bill of Materials (AI-BOM).
|
|
32
|
+
|
|
33
|
+
Shows all models, frameworks, tools, and data sources
|
|
34
|
+
declared in .agent/bom.yaml.
|
|
35
|
+
|
|
36
|
+
\b
|
|
37
|
+
Examples:
|
|
38
|
+
aes bom # current project
|
|
39
|
+
aes bom ./my-project # specific project
|
|
40
|
+
"""
|
|
41
|
+
project_root = Path(path).resolve()
|
|
42
|
+
agent_dir = project_root / AGENT_DIR
|
|
43
|
+
|
|
44
|
+
if not agent_dir.exists():
|
|
45
|
+
console.print(f"[red]{t('common.error')}:[/] {t('common.no_agent_dir', agent_dir=AGENT_DIR, path=project_root)}")
|
|
46
|
+
raise SystemExit(1)
|
|
47
|
+
|
|
48
|
+
bom = _load_bom(agent_dir)
|
|
49
|
+
if not bom:
|
|
50
|
+
console.print(f"\n [dim]{t('bom.not_found')}[/]\n")
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
console.print(f"\n[bold]{t('bom.title')}[/] [dim](AES {bom.get('aes_bom', '?')})[/]\n")
|
|
54
|
+
|
|
55
|
+
# Models
|
|
56
|
+
models = bom.get("models", [])
|
|
57
|
+
if models:
|
|
58
|
+
table = Table(title=t("bom.models_table"), show_header=True, header_style="bold")
|
|
59
|
+
table.add_column(t("bom.col_name"), style="cyan")
|
|
60
|
+
table.add_column(t("bom.col_provider"))
|
|
61
|
+
table.add_column(t("bom.col_purpose"))
|
|
62
|
+
table.add_column(t("bom.col_license"))
|
|
63
|
+
for m in models:
|
|
64
|
+
table.add_row(
|
|
65
|
+
m.get("name", "?"),
|
|
66
|
+
m.get("provider", "?"),
|
|
67
|
+
m.get("purpose", "-"),
|
|
68
|
+
m.get("license", "-"),
|
|
69
|
+
)
|
|
70
|
+
console.print(table)
|
|
71
|
+
console.print()
|
|
72
|
+
|
|
73
|
+
# Frameworks
|
|
74
|
+
frameworks = bom.get("frameworks", [])
|
|
75
|
+
if frameworks:
|
|
76
|
+
table = Table(title=t("bom.frameworks_table"), show_header=True, header_style="bold")
|
|
77
|
+
table.add_column(t("bom.col_name"), style="cyan")
|
|
78
|
+
table.add_column(t("bom.col_version"))
|
|
79
|
+
table.add_column(t("bom.col_license"))
|
|
80
|
+
for fw in frameworks:
|
|
81
|
+
table.add_row(
|
|
82
|
+
fw.get("name", "?"),
|
|
83
|
+
fw.get("version", "-"),
|
|
84
|
+
fw.get("license", "-"),
|
|
85
|
+
)
|
|
86
|
+
console.print(table)
|
|
87
|
+
console.print()
|
|
88
|
+
|
|
89
|
+
# Tools
|
|
90
|
+
tools = bom.get("tools", [])
|
|
91
|
+
if tools:
|
|
92
|
+
table = Table(title=t("bom.tools_table"), show_header=True, header_style="bold")
|
|
93
|
+
table.add_column(t("bom.col_name"), style="cyan")
|
|
94
|
+
table.add_column(t("bom.col_type"))
|
|
95
|
+
table.add_column(t("bom.col_version"))
|
|
96
|
+
table.add_column(t("bom.col_source"))
|
|
97
|
+
for tool in tools:
|
|
98
|
+
table.add_row(
|
|
99
|
+
tool.get("name", "?"),
|
|
100
|
+
tool.get("type", "?"),
|
|
101
|
+
tool.get("version", "-"),
|
|
102
|
+
tool.get("source", "-"),
|
|
103
|
+
)
|
|
104
|
+
console.print(table)
|
|
105
|
+
console.print()
|
|
106
|
+
|
|
107
|
+
# Data Sources
|
|
108
|
+
data_sources = bom.get("data_sources", [])
|
|
109
|
+
if data_sources:
|
|
110
|
+
table = Table(title=t("bom.data_sources_table"), show_header=True, header_style="bold")
|
|
111
|
+
table.add_column(t("bom.col_name"), style="cyan")
|
|
112
|
+
table.add_column(t("bom.col_type"))
|
|
113
|
+
table.add_column(t("bom.col_uri"))
|
|
114
|
+
table.add_column(t("bom.col_license"))
|
|
115
|
+
for ds in data_sources:
|
|
116
|
+
table.add_row(
|
|
117
|
+
ds.get("name", "?"),
|
|
118
|
+
ds.get("type", "?"),
|
|
119
|
+
ds.get("uri", "-"),
|
|
120
|
+
ds.get("license", "-"),
|
|
121
|
+
)
|
|
122
|
+
console.print(table)
|
|
123
|
+
console.print()
|
|
124
|
+
|
|
125
|
+
# Summary
|
|
126
|
+
console.print(f"[bold]{t('bom.summary')}[/]")
|
|
127
|
+
console.print(f" {t('bom.models_count', count=len(models))}")
|
|
128
|
+
console.print(f" {t('bom.frameworks_count', count=len(frameworks))}")
|
|
129
|
+
console.print(f" {t('bom.tools_count', count=len(tools))}")
|
|
130
|
+
console.print(f" {t('bom.data_sources_count', count=len(data_sources))}")
|
|
131
|
+
console.print()
|
|
@@ -20,6 +20,8 @@ from rich.tree import Tree
|
|
|
20
20
|
from aes.config import (
|
|
21
21
|
AGENT_DIR,
|
|
22
22
|
AGENTIGNORE_FILE,
|
|
23
|
+
BOM_FILE,
|
|
24
|
+
DECISIONS_DIR,
|
|
23
25
|
LOCAL_EXAMPLE_FILE,
|
|
24
26
|
LOCAL_FILE,
|
|
25
27
|
SCAFFOLD_DIR,
|
|
@@ -427,6 +429,7 @@ def _print_post_init_summary(
|
|
|
427
429
|
|
|
428
430
|
cmd_branch = tree.add("commands/")
|
|
429
431
|
cmd_branch.add("setup.md")
|
|
432
|
+
cmd_branch.add("memory.md [dim]/memory[/]")
|
|
430
433
|
if isinstance(domain_config, DomainConfig) and domain_config.workflow_commands:
|
|
431
434
|
for cmd_def in domain_config.workflow_commands:
|
|
432
435
|
cmd_branch.add(f"{cmd_def.id}.md [dim]{cmd_def.trigger}[/]")
|
|
@@ -619,6 +622,7 @@ def init_cmd(
|
|
|
619
622
|
agent_dir.mkdir(exist_ok=True)
|
|
620
623
|
(agent_dir / MEMORY_DIR).mkdir(exist_ok=True)
|
|
621
624
|
(agent_dir / MEMORY_DIR / "sessions").mkdir(exist_ok=True)
|
|
625
|
+
(agent_dir / DECISIONS_DIR).mkdir(parents=True, exist_ok=True)
|
|
622
626
|
(agent_dir / OVERRIDES_DIR).mkdir(exist_ok=True)
|
|
623
627
|
|
|
624
628
|
if skills:
|
|
@@ -663,6 +667,10 @@ def init_cmd(
|
|
|
663
667
|
content = _render_template(env, "permissions.yaml.jinja", context)
|
|
664
668
|
(agent_dir / "permissions.yaml").write_text(content)
|
|
665
669
|
|
|
670
|
+
# bom.yaml
|
|
671
|
+
content = _render_template(env, "bom.yaml.jinja", context)
|
|
672
|
+
(agent_dir / BOM_FILE).write_text(content)
|
|
673
|
+
|
|
666
674
|
# .agentignore
|
|
667
675
|
agentignore_path = project_root / AGENTIGNORE_FILE
|
|
668
676
|
if not agentignore_path.exists():
|
|
@@ -702,10 +710,12 @@ def init_cmd(
|
|
|
702
710
|
memory_content = f"# {name} — Agent Memory\n\n## Project Overview\n\n## Architecture\n\n## Status\n\n## Key Patterns\n"
|
|
703
711
|
(agent_dir / MEMORY_DIR / "project.md").write_text(memory_content)
|
|
704
712
|
|
|
705
|
-
# Commands directory + /setup
|
|
713
|
+
# Commands directory + /setup and /memory runbooks
|
|
706
714
|
(agent_dir / COMMANDS_DIR).mkdir(exist_ok=True)
|
|
707
715
|
content = _render_template(env, "setup.md.jinja", context)
|
|
708
716
|
(agent_dir / COMMANDS_DIR / "setup.md").write_text(content)
|
|
717
|
+
content = _render_template(env, "memory_command.md.jinja", context)
|
|
718
|
+
(agent_dir / COMMANDS_DIR / "memory.md").write_text(content)
|
|
709
719
|
|
|
710
720
|
# Workflow command runbooks
|
|
711
721
|
if domain_config and domain_config.workflow_commands:
|
|
@@ -12,7 +12,7 @@ import yaml
|
|
|
12
12
|
from rich.console import Console
|
|
13
13
|
from rich.table import Table
|
|
14
14
|
|
|
15
|
-
from aes.config import AGENT_DIR
|
|
15
|
+
from aes.config import AGENT_DIR, BOM_FILE, DECISIONS_DIR
|
|
16
16
|
from aes.i18n import t
|
|
17
17
|
from aes.registry import (
|
|
18
18
|
fetch_index,
|
|
@@ -205,6 +205,47 @@ def _inspect_local(path: str) -> None:
|
|
|
205
205
|
console.print(table)
|
|
206
206
|
console.print()
|
|
207
207
|
|
|
208
|
+
# Models
|
|
209
|
+
models = manifest.get("models", [])
|
|
210
|
+
if models:
|
|
211
|
+
console.print(f"[bold]{t('inspect.models_section')}[/]")
|
|
212
|
+
for m in models:
|
|
213
|
+
purpose = m.get("purpose", "")
|
|
214
|
+
purpose_str = f" [dim]({purpose})[/]" if purpose else ""
|
|
215
|
+
console.print(f" {m.get('name', '?')} — {m.get('provider', '?')}{purpose_str}")
|
|
216
|
+
console.print()
|
|
217
|
+
|
|
218
|
+
# Provenance
|
|
219
|
+
provenance = manifest.get("provenance", {})
|
|
220
|
+
if provenance:
|
|
221
|
+
console.print(f"[bold]{t('inspect.provenance_section')}[/]")
|
|
222
|
+
if provenance.get("created_by"):
|
|
223
|
+
console.print(f" {t('inspect.provenance_created_by', value=provenance['created_by'])}")
|
|
224
|
+
if provenance.get("source"):
|
|
225
|
+
console.print(f" {t('inspect.provenance_source', value=provenance['source'])}")
|
|
226
|
+
console.print()
|
|
227
|
+
|
|
228
|
+
# BOM summary
|
|
229
|
+
bom_path = agent_dir / BOM_FILE
|
|
230
|
+
if bom_path.exists():
|
|
231
|
+
bom = _load_yaml(bom_path)
|
|
232
|
+
n_models = len(bom.get("models", []))
|
|
233
|
+
n_frameworks = len(bom.get("frameworks", []))
|
|
234
|
+
n_tools = len(bom.get("tools", []))
|
|
235
|
+
n_data = len(bom.get("data_sources", []))
|
|
236
|
+
console.print(f"[bold]{t('inspect.bom_section')}[/]")
|
|
237
|
+
console.print(f" {t('inspect.bom_summary', models=n_models, frameworks=n_frameworks, tools=n_tools, data=n_data)}")
|
|
238
|
+
console.print()
|
|
239
|
+
|
|
240
|
+
# Decision records count
|
|
241
|
+
decisions_dir = agent_dir / DECISIONS_DIR
|
|
242
|
+
if decisions_dir.exists() and decisions_dir.is_dir():
|
|
243
|
+
dr_count = len(list(decisions_dir.glob("*.yaml")))
|
|
244
|
+
if dr_count > 0:
|
|
245
|
+
console.print(f"[bold]{t('inspect.decisions_section')}[/]")
|
|
246
|
+
console.print(f" {t('inspect.decisions_count', count=dr_count)}")
|
|
247
|
+
console.print()
|
|
248
|
+
|
|
208
249
|
# Summary
|
|
209
250
|
console.print(f"[bold]{t('inspect.summary')}[/]")
|
|
210
251
|
console.print(f" {t('inspect.skills_count', count=len(skills))}")
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import fnmatch
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
6
8
|
import shutil
|
|
7
9
|
import sys
|
|
8
10
|
import tarfile
|
|
@@ -14,7 +16,7 @@ import click
|
|
|
14
16
|
import yaml
|
|
15
17
|
from rich.console import Console
|
|
16
18
|
|
|
17
|
-
from aes.config import AGENT_DIR, MANIFEST_FILE
|
|
19
|
+
from aes.config import AGENT_DIR, BOM_FILE, MANIFEST_FILE
|
|
18
20
|
from aes.i18n import t
|
|
19
21
|
|
|
20
22
|
console = Console()
|
|
@@ -171,6 +173,53 @@ def _validate_before_publish(project_root: Path) -> bool:
|
|
|
171
173
|
return True
|
|
172
174
|
|
|
173
175
|
|
|
176
|
+
def _media_type_for_path(path: str) -> str:
|
|
177
|
+
"""Return a media type string for a file path."""
|
|
178
|
+
if path.endswith(".yaml") or path.endswith(".yml"):
|
|
179
|
+
return "application/vnd.aes.agent-config.v1+yaml"
|
|
180
|
+
if path.endswith(".md"):
|
|
181
|
+
return "text/markdown"
|
|
182
|
+
if path.endswith(".json"):
|
|
183
|
+
return "application/json"
|
|
184
|
+
return "application/octet-stream"
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _build_package_manifest(
|
|
188
|
+
name: str,
|
|
189
|
+
version: str,
|
|
190
|
+
aes_version: str,
|
|
191
|
+
pkg_type: str,
|
|
192
|
+
files: List[tuple],
|
|
193
|
+
) -> dict:
|
|
194
|
+
"""Build an aes-manifest.json dict.
|
|
195
|
+
|
|
196
|
+
*files* is a list of ``(arcname, file_path)`` tuples.
|
|
197
|
+
"""
|
|
198
|
+
layers = []
|
|
199
|
+
for arcname, file_path in files:
|
|
200
|
+
data = file_path.read_bytes()
|
|
201
|
+
digest = hashlib.sha256(data).hexdigest()
|
|
202
|
+
layers.append({
|
|
203
|
+
"mediaType": _media_type_for_path(arcname),
|
|
204
|
+
"digest": f"sha256:{digest}",
|
|
205
|
+
"size": len(data),
|
|
206
|
+
"path": arcname,
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
"schemaVersion": 1,
|
|
211
|
+
"mediaType": "application/vnd.aes.package.v1+tar+gzip",
|
|
212
|
+
"config": {
|
|
213
|
+
"name": name,
|
|
214
|
+
"version": version,
|
|
215
|
+
"type": pkg_type,
|
|
216
|
+
"aes": aes_version,
|
|
217
|
+
},
|
|
218
|
+
"layers": layers,
|
|
219
|
+
"signature": None,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
174
223
|
def _publish_template_dir(
|
|
175
224
|
project_root: Path,
|
|
176
225
|
output_dir: Path,
|
|
@@ -194,6 +243,7 @@ def _publish_template_dir(
|
|
|
194
243
|
|
|
195
244
|
name = manifest.get("name", "unknown")
|
|
196
245
|
version = manifest.get("version", "0.0.0")
|
|
246
|
+
aes_version = manifest.get("aes", "1.0")
|
|
197
247
|
|
|
198
248
|
# Build exclusion list
|
|
199
249
|
if include_all:
|
|
@@ -208,17 +258,35 @@ def _publish_template_dir(
|
|
|
208
258
|
tarball_name = f"{name}-{version}.tar.gz"
|
|
209
259
|
tarball_path = output_dir / tarball_name
|
|
210
260
|
|
|
261
|
+
# Collect files and build package manifest
|
|
262
|
+
included_files: List[tuple] = [] # (rel_path_in_agent, absolute_path)
|
|
263
|
+
|
|
264
|
+
for file_path in sorted(agent_dir.rglob("*")):
|
|
265
|
+
if not file_path.is_file():
|
|
266
|
+
continue
|
|
267
|
+
rel = file_path.relative_to(agent_dir)
|
|
268
|
+
rel_str = str(rel)
|
|
269
|
+
if _is_excluded(rel_str, excludes):
|
|
270
|
+
continue
|
|
271
|
+
included_files.append((f"{AGENT_DIR}/{rel_str}", file_path))
|
|
272
|
+
|
|
273
|
+
# Generate aes-manifest.json
|
|
274
|
+
pkg_manifest = _build_package_manifest(
|
|
275
|
+
name, version, aes_version, "template", included_files,
|
|
276
|
+
)
|
|
277
|
+
|
|
211
278
|
with tarfile.open(tarball_path, "w:gz") as tar:
|
|
212
|
-
for file_path in
|
|
213
|
-
|
|
214
|
-
continue
|
|
215
|
-
rel = file_path.relative_to(agent_dir)
|
|
216
|
-
rel_str = str(rel)
|
|
217
|
-
if _is_excluded(rel_str, excludes):
|
|
218
|
-
continue
|
|
219
|
-
arcname = f"{name}/{AGENT_DIR}/{rel_str}"
|
|
279
|
+
for rel_path, file_path in included_files:
|
|
280
|
+
arcname = f"{name}/{rel_path}"
|
|
220
281
|
tar.add(file_path, arcname=arcname)
|
|
221
282
|
|
|
283
|
+
# Add aes-manifest.json at the package root
|
|
284
|
+
manifest_json = json.dumps(pkg_manifest, indent=2).encode()
|
|
285
|
+
import io
|
|
286
|
+
info = tarfile.TarInfo(name=f"{name}/aes-manifest.json")
|
|
287
|
+
info.size = len(manifest_json)
|
|
288
|
+
tar.addfile(info, io.BytesIO(manifest_json))
|
|
289
|
+
|
|
222
290
|
return tarball_path
|
|
223
291
|
|
|
224
292
|
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""aes upgrade — Upgrade .agent/ to the current AES spec version."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
import yaml
|
|
11
|
+
from jinja2 import ChoiceLoader, Environment, FileSystemLoader
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
|
|
14
|
+
from aes.config import AGENT_DIR, COMMANDS_DIR, MANIFEST_FILE, SCAFFOLD_DIR
|
|
15
|
+
from aes.i18n import t
|
|
16
|
+
from aes.migrations import (
|
|
17
|
+
CURRENT_SPEC_VERSION,
|
|
18
|
+
Migration,
|
|
19
|
+
MigrationFile,
|
|
20
|
+
applicable_migrations,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
console = Console()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# Upgrade plan data structures
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class PlannedFile:
|
|
33
|
+
"""A file to create during upgrade."""
|
|
34
|
+
|
|
35
|
+
migration_file: MigrationFile
|
|
36
|
+
create_file: bool # whether the file needs creating
|
|
37
|
+
add_manifest_entry: bool # whether the manifest entry needs adding
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class UpgradePlan:
|
|
42
|
+
"""The full upgrade plan."""
|
|
43
|
+
|
|
44
|
+
current_version: str
|
|
45
|
+
target_version: str
|
|
46
|
+
migrations: List[Migration] = field(default_factory=list)
|
|
47
|
+
planned_files: List[PlannedFile] = field(default_factory=list)
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def has_changes(self) -> bool:
|
|
51
|
+
return bool(self.planned_files) or self.current_version != self.target_version
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
# Plan computation
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _compute_plan(agent_dir: Path, manifest: dict) -> UpgradePlan:
|
|
60
|
+
"""Compare current .agent/ against migrations to determine needed changes."""
|
|
61
|
+
current_version = manifest.get("aes", "1.0")
|
|
62
|
+
migrations = applicable_migrations(current_version)
|
|
63
|
+
|
|
64
|
+
plan = UpgradePlan(
|
|
65
|
+
current_version=current_version,
|
|
66
|
+
target_version=CURRENT_SPEC_VERSION,
|
|
67
|
+
migrations=migrations,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
for migration in migrations:
|
|
71
|
+
for mf in migration.files:
|
|
72
|
+
file_path = agent_dir / mf.relative_path
|
|
73
|
+
file_exists = file_path.exists()
|
|
74
|
+
|
|
75
|
+
entry_exists = False
|
|
76
|
+
if mf.manifest_entry and mf.manifest_section:
|
|
77
|
+
entry_id = mf.manifest_entry.get("id", "")
|
|
78
|
+
section = manifest.get(mf.manifest_section, [])
|
|
79
|
+
entry_exists = any(
|
|
80
|
+
item.get("id") == entry_id for item in section
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
needs_file = not file_exists
|
|
84
|
+
needs_entry = bool(mf.manifest_entry) and not entry_exists
|
|
85
|
+
|
|
86
|
+
if needs_file or needs_entry:
|
|
87
|
+
plan.planned_files.append(
|
|
88
|
+
PlannedFile(
|
|
89
|
+
migration_file=mf,
|
|
90
|
+
create_file=needs_file,
|
|
91
|
+
add_manifest_entry=needs_entry,
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return plan
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
# Plan display
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _display_plan(plan: UpgradePlan) -> None:
|
|
104
|
+
"""Display the upgrade plan without applying it."""
|
|
105
|
+
console.print()
|
|
106
|
+
console.print(f"[bold]{t('upgrade.title')}[/]")
|
|
107
|
+
console.print()
|
|
108
|
+
console.print(
|
|
109
|
+
f" {t('upgrade.current_version', version=plan.current_version)}"
|
|
110
|
+
)
|
|
111
|
+
console.print(
|
|
112
|
+
f" {t('upgrade.target_version', version=plan.target_version)}"
|
|
113
|
+
)
|
|
114
|
+
console.print()
|
|
115
|
+
|
|
116
|
+
for migration in plan.migrations:
|
|
117
|
+
console.print(
|
|
118
|
+
f" [bold cyan]{t('upgrade.migration_header', from_v=migration.from_version, to_v=migration.to_version, description=migration.description)}[/]"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
for pf in plan.planned_files:
|
|
122
|
+
mf = pf.migration_file
|
|
123
|
+
if pf.create_file:
|
|
124
|
+
console.print(
|
|
125
|
+
f" [green]+[/] {mf.relative_path} [dim]({t('upgrade.new_file')})[/]"
|
|
126
|
+
)
|
|
127
|
+
if pf.add_manifest_entry and mf.manifest_entry:
|
|
128
|
+
entry_id = mf.manifest_entry.get("id", "?")
|
|
129
|
+
console.print(
|
|
130
|
+
f" [green]+[/] agent.yaml: {mf.manifest_section}[] [dim]({t('upgrade.add_entry', id=entry_id)})[/]"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if plan.current_version != plan.target_version:
|
|
134
|
+
console.print(
|
|
135
|
+
f" [yellow]~[/] agent.yaml: aes [dim]({t('upgrade.bump_version', old=plan.current_version, new=plan.target_version)})[/]"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
console.print()
|
|
139
|
+
console.print(f" [dim]{t('upgrade.run_apply')}[/]")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
# Apply logic
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _apply_plan(
|
|
148
|
+
agent_dir: Path,
|
|
149
|
+
manifest: dict,
|
|
150
|
+
plan: UpgradePlan,
|
|
151
|
+
env: Environment,
|
|
152
|
+
context: dict,
|
|
153
|
+
) -> int:
|
|
154
|
+
"""Apply the upgrade plan. Returns number of files created."""
|
|
155
|
+
created = 0
|
|
156
|
+
|
|
157
|
+
for pf in plan.planned_files:
|
|
158
|
+
mf = pf.migration_file
|
|
159
|
+
|
|
160
|
+
# Create missing file from template
|
|
161
|
+
if pf.create_file:
|
|
162
|
+
tmpl = env.get_template(mf.template_name)
|
|
163
|
+
content = tmpl.render(**context)
|
|
164
|
+
file_path = agent_dir / mf.relative_path
|
|
165
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
166
|
+
file_path.write_text(content)
|
|
167
|
+
console.print(
|
|
168
|
+
f" [green]+[/] {t('upgrade.created_file', path=mf.relative_path)}"
|
|
169
|
+
)
|
|
170
|
+
created += 1
|
|
171
|
+
|
|
172
|
+
# Add manifest entry
|
|
173
|
+
if pf.add_manifest_entry and mf.manifest_entry:
|
|
174
|
+
section = manifest.setdefault(mf.manifest_section, [])
|
|
175
|
+
section.append(dict(mf.manifest_entry))
|
|
176
|
+
|
|
177
|
+
# Bump aes version
|
|
178
|
+
if plan.current_version != plan.target_version:
|
|
179
|
+
manifest["aes"] = plan.target_version
|
|
180
|
+
|
|
181
|
+
# Write updated agent.yaml
|
|
182
|
+
yaml_path = agent_dir / MANIFEST_FILE
|
|
183
|
+
with open(yaml_path, "w") as f:
|
|
184
|
+
yaml.safe_dump(
|
|
185
|
+
manifest,
|
|
186
|
+
f,
|
|
187
|
+
default_flow_style=False,
|
|
188
|
+
sort_keys=False,
|
|
189
|
+
allow_unicode=True,
|
|
190
|
+
)
|
|
191
|
+
console.print(
|
|
192
|
+
f" [yellow]~[/] {t('upgrade.updated_manifest', old=plan.current_version, new=plan.target_version)}"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
return created
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# ---------------------------------------------------------------------------
|
|
199
|
+
# CLI command
|
|
200
|
+
# ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@click.command("upgrade")
|
|
204
|
+
@click.argument("path", default=".", type=click.Path(exists=True))
|
|
205
|
+
@click.option("--apply", "do_apply", is_flag=True, help="Apply the upgrade (default: dry-run).")
|
|
206
|
+
def upgrade_cmd(path: str, do_apply: bool) -> None:
|
|
207
|
+
"""Upgrade .agent/ to the current AES spec version.
|
|
208
|
+
|
|
209
|
+
Adds missing files and manifest entries introduced in newer versions
|
|
210
|
+
without touching existing customizations.
|
|
211
|
+
|
|
212
|
+
Without --apply, shows what would change (dry-run).
|
|
213
|
+
|
|
214
|
+
\b
|
|
215
|
+
Examples:
|
|
216
|
+
aes upgrade # show upgrade plan
|
|
217
|
+
aes upgrade --apply # apply and auto-sync
|
|
218
|
+
aes upgrade /path/to/project # target a specific project
|
|
219
|
+
"""
|
|
220
|
+
project_root = Path(path).resolve()
|
|
221
|
+
agent_dir = project_root / AGENT_DIR
|
|
222
|
+
|
|
223
|
+
if not agent_dir.exists() or not (agent_dir / MANIFEST_FILE).exists():
|
|
224
|
+
console.print(
|
|
225
|
+
f"[red]{t('common.error')}:[/] {t('upgrade.no_agent_dir')}"
|
|
226
|
+
)
|
|
227
|
+
console.print(f"[dim]{t('common.run_init_hint')}[/]")
|
|
228
|
+
raise SystemExit(1)
|
|
229
|
+
|
|
230
|
+
# Load manifest
|
|
231
|
+
with open(agent_dir / MANIFEST_FILE) as f:
|
|
232
|
+
manifest = yaml.safe_load(f) or {}
|
|
233
|
+
|
|
234
|
+
# Compute plan
|
|
235
|
+
plan = _compute_plan(agent_dir, manifest)
|
|
236
|
+
|
|
237
|
+
if not plan.has_changes:
|
|
238
|
+
console.print(
|
|
239
|
+
f"\n {t('upgrade.up_to_date', version=plan.target_version)}\n"
|
|
240
|
+
)
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
if not do_apply:
|
|
244
|
+
_display_plan(plan)
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
# Build Jinja2 environment (locale-aware, same as init.py)
|
|
248
|
+
from aes.i18n import get_current_locale
|
|
249
|
+
|
|
250
|
+
locale = get_current_locale()
|
|
251
|
+
loaders = []
|
|
252
|
+
if locale != "en":
|
|
253
|
+
locale_dir = SCAFFOLD_DIR / locale
|
|
254
|
+
if locale_dir.exists():
|
|
255
|
+
loaders.append(FileSystemLoader(str(locale_dir)))
|
|
256
|
+
loaders.append(FileSystemLoader(str(SCAFFOLD_DIR)))
|
|
257
|
+
|
|
258
|
+
env = Environment(
|
|
259
|
+
loader=ChoiceLoader(loaders),
|
|
260
|
+
keep_trailing_newline=True,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Template context — minimal, just what the templates need
|
|
264
|
+
context = {
|
|
265
|
+
"name": manifest.get("name", "project"),
|
|
266
|
+
"domain": manifest.get("domain", "other"),
|
|
267
|
+
"language": manifest.get("runtime", {}).get("language", "other"),
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
console.print()
|
|
271
|
+
created = _apply_plan(agent_dir, manifest, plan, env, context)
|
|
272
|
+
|
|
273
|
+
# Auto-sync
|
|
274
|
+
from aes.commands.sync import run_sync
|
|
275
|
+
|
|
276
|
+
synced = run_sync(project_root, force=True, quiet=True)
|
|
277
|
+
if synced > 0:
|
|
278
|
+
console.print(f" [green]{t('upgrade.synced', count=synced)}[/]")
|
|
279
|
+
|
|
280
|
+
console.print(
|
|
281
|
+
f"\n [bold green]{t('upgrade.applied', count=created)}[/]\n"
|
|
282
|
+
)
|
|
@@ -29,9 +29,11 @@ REGISTRY_DIR = "registry"
|
|
|
29
29
|
WORKFLOWS_DIR = "workflows"
|
|
30
30
|
COMMANDS_DIR = "commands"
|
|
31
31
|
MEMORY_DIR = "memory"
|
|
32
|
+
DECISIONS_DIR = "memory/decisions"
|
|
32
33
|
OVERRIDES_DIR = "overrides"
|
|
33
34
|
LOCAL_FILE = "local.yaml"
|
|
34
35
|
LOCAL_EXAMPLE_FILE = "local.example.yaml"
|
|
36
|
+
BOM_FILE = "bom.yaml"
|
|
35
37
|
|
|
36
38
|
# Schema file mapping
|
|
37
39
|
SCHEMA_MAP = {
|
|
@@ -40,4 +42,6 @@ SCHEMA_MAP = {
|
|
|
40
42
|
"workflow": "workflow.schema.json",
|
|
41
43
|
"registry": "registry.schema.json",
|
|
42
44
|
"permissions": "permissions.schema.json",
|
|
45
|
+
"bom": "bom.schema.json",
|
|
46
|
+
"decision-record": "decision-record.schema.json",
|
|
43
47
|
}
|