dotagents-cli 0.3.0__py3-none-any.whl
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.
- dotagents/AGENTS.md +125 -0
- dotagents/__init__.py +3 -0
- dotagents/__main__.py +6 -0
- dotagents/_agents.py +627 -0
- dotagents/_context.py +311 -0
- dotagents/_env.py +600 -0
- dotagents/_hooks.py +172 -0
- dotagents/_merge.py +122 -0
- dotagents/_overlay/AGENTS.md +31 -0
- dotagents/_overlay/CLAUDE.md +3 -0
- dotagents/_overlay/README.md +41 -0
- dotagents/_overlay/dotagents/DECISIONS.md +20 -0
- dotagents/_overlay/dotagents/cmds/README.md +46 -0
- dotagents/_overlays.py +412 -0
- dotagents/_resolve.py +81 -0
- dotagents/_scope.py +265 -0
- dotagents/_skills.py +236 -0
- dotagents/_sync.py +72 -0
- dotagents/_wrappers.py +112 -0
- dotagents/cli/__init__.py +354 -0
- dotagents/cli/_common.py +331 -0
- dotagents/cli/build_pyz.py +107 -0
- dotagents/cli/context.py +129 -0
- dotagents/cli/env.py +168 -0
- dotagents/cli/init.py +117 -0
- dotagents/cli/overlays.py +366 -0
- dotagents_cli-0.3.0.dist-info/METADATA +291 -0
- dotagents_cli-0.3.0.dist-info/RECORD +31 -0
- dotagents_cli-0.3.0.dist-info/WHEEL +4 -0
- dotagents_cli-0.3.0.dist-info/entry_points.txt +2 -0
- dotagents_cli-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""`dotagents build-pyz` -- vendor deps and package a self-contained pyz."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from duho import Cmd, LoggingArgs
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BuildPyz(LoggingArgs, Cmd):
|
|
14
|
+
"""Vendor duho/pathlib_next via pip --target and package a self-contained dotagents.pyz."""
|
|
15
|
+
|
|
16
|
+
_parsername_ = "build-pyz"
|
|
17
|
+
|
|
18
|
+
out: Path = Path("dist") / "dotagents.pyz"
|
|
19
|
+
"Output path for the built pyz."
|
|
20
|
+
("--out",)
|
|
21
|
+
|
|
22
|
+
python: str = "/usr/bin/env python3"
|
|
23
|
+
"Shebang line to embed in the pyz."
|
|
24
|
+
("--python",)
|
|
25
|
+
|
|
26
|
+
duho_version: str = "0.4.0"
|
|
27
|
+
"Pinned duho version to vendor."
|
|
28
|
+
("--duho-version",)
|
|
29
|
+
|
|
30
|
+
pathlib_next_version: str = "0.8.0"
|
|
31
|
+
"Pinned pathlib_next version to vendor."
|
|
32
|
+
("--pathlib-next-version",)
|
|
33
|
+
|
|
34
|
+
tools_dir: Optional[Path] = None
|
|
35
|
+
"Repo tools/ dir (required tooling) to bundle as _tools (default: autodetected)."
|
|
36
|
+
("--tools-dir",)
|
|
37
|
+
|
|
38
|
+
def __call__(self) -> int:
|
|
39
|
+
import zipapp
|
|
40
|
+
|
|
41
|
+
# This module lives at src/dotagents/cli/build_pyz.py, so the repo root
|
|
42
|
+
# is parents[3] (cli -> dotagents -> src -> repo) and the dotagents
|
|
43
|
+
# package dir is parents[1].
|
|
44
|
+
repo_root = Path(__file__).resolve().parents[3]
|
|
45
|
+
tools_src = Path(self.tools_dir) if self.tools_dir else (repo_root / "tools")
|
|
46
|
+
if not tools_src.exists():
|
|
47
|
+
raise SystemExit("error: repo tools/ not found at %s (pass --tools-dir)" % tools_src)
|
|
48
|
+
|
|
49
|
+
with tempfile.TemporaryDirectory(prefix="dotagents-pyz-") as tmp:
|
|
50
|
+
stage = Path(tmp) / "stage"
|
|
51
|
+
stage.mkdir()
|
|
52
|
+
|
|
53
|
+
self._logger_.info(
|
|
54
|
+
"vendoring duho==%s pathlib_next==%s via pip --target",
|
|
55
|
+
self.duho_version,
|
|
56
|
+
self.pathlib_next_version,
|
|
57
|
+
)
|
|
58
|
+
rc = subprocess.call(
|
|
59
|
+
[
|
|
60
|
+
sys.executable,
|
|
61
|
+
"-m",
|
|
62
|
+
"pip",
|
|
63
|
+
"install",
|
|
64
|
+
"--target",
|
|
65
|
+
str(stage),
|
|
66
|
+
"duho==%s" % self.duho_version,
|
|
67
|
+
"pathlib_next==%s" % self.pathlib_next_version,
|
|
68
|
+
]
|
|
69
|
+
)
|
|
70
|
+
if rc != 0:
|
|
71
|
+
return rc
|
|
72
|
+
|
|
73
|
+
dotagents_pkg_src = Path(__file__).resolve().parents[1]
|
|
74
|
+
dotagents_pkg_dest = stage / "dotagents"
|
|
75
|
+
shutil.copytree(
|
|
76
|
+
dotagents_pkg_src,
|
|
77
|
+
dotagents_pkg_dest,
|
|
78
|
+
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
tools_dest = dotagents_pkg_dest / "_tools"
|
|
82
|
+
shutil.copytree(
|
|
83
|
+
tools_src,
|
|
84
|
+
tools_dest,
|
|
85
|
+
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
for path in stage.rglob("*.dist-info"):
|
|
89
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
90
|
+
for path in stage.rglob("__pycache__"):
|
|
91
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
92
|
+
for path in stage.rglob("tests"):
|
|
93
|
+
if path.is_dir():
|
|
94
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
95
|
+
|
|
96
|
+
main_py = stage / "__main__.py"
|
|
97
|
+
main_py.write_text(
|
|
98
|
+
"from dotagents.cli import main\n\nraise SystemExit(main())\n",
|
|
99
|
+
encoding="utf-8",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
out_path = Path(self.out)
|
|
103
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
104
|
+
zipapp.create_archive(str(stage), target=str(out_path), interpreter=self.python)
|
|
105
|
+
self._logger_.info("built %s", out_path)
|
|
106
|
+
|
|
107
|
+
return 0
|
dotagents/cli/context.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""`dotagents context` -- assemble the effective context for agents (Plan 04)."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from duho import Cmd, LoggingArgs
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _write_stdout(text: str) -> None:
|
|
11
|
+
"""Write to stdout as UTF-8, whatever the console's encoding claims to be.
|
|
12
|
+
|
|
13
|
+
A bare `print()` encodes with the console codepage -- cp1252 on a default
|
|
14
|
+
Windows shell -- so a single character outside Latin-1 (an arrow, a box-drawing
|
|
15
|
+
rule, a curly quote, any emoji) raises UnicodeEncodeError and the command dies
|
|
16
|
+
having emitted nothing. Context files routinely contain such characters, and
|
|
17
|
+
this is the SessionStart hook's payload, so the failure is both likely and
|
|
18
|
+
silent. Write bytes through the underlying buffer instead, replacing anything
|
|
19
|
+
even UTF-8 cannot represent rather than aborting.
|
|
20
|
+
"""
|
|
21
|
+
data = text.encode("utf-8", errors="replace")
|
|
22
|
+
buffer = getattr(sys.stdout, "buffer", None)
|
|
23
|
+
if buffer is None: # pragma: no cover -- captured/replaced stdout in tests
|
|
24
|
+
sys.stdout.write(text)
|
|
25
|
+
return
|
|
26
|
+
buffer.write(data)
|
|
27
|
+
buffer.flush()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Context(LoggingArgs, Cmd):
|
|
31
|
+
"""Assemble the effective context for agents (Plan 04)."""
|
|
32
|
+
|
|
33
|
+
_parsername_ = "context"
|
|
34
|
+
|
|
35
|
+
format: str = "markdown"
|
|
36
|
+
"Output format: markdown, system-reminder, or json."
|
|
37
|
+
("--format",)
|
|
38
|
+
|
|
39
|
+
global_scope: bool = False
|
|
40
|
+
"Use global scope."
|
|
41
|
+
("--global", "-g")
|
|
42
|
+
|
|
43
|
+
agents: "list[str]" = []
|
|
44
|
+
"List of agents to generate context for (e.g. claude,gemini). Default: active agent."
|
|
45
|
+
("--agents",)
|
|
46
|
+
|
|
47
|
+
out: str = "-"
|
|
48
|
+
"Output path (positional). Default '-' = stdout; a path writes that file; use "
|
|
49
|
+
"--write-agent to write each agent's native config file instead."
|
|
50
|
+
("out",)
|
|
51
|
+
|
|
52
|
+
write_agent: bool = False
|
|
53
|
+
"Write each agent's native config file (e.g. Claude's CONTEXT.md) instead of a path."
|
|
54
|
+
("--write-agent",)
|
|
55
|
+
|
|
56
|
+
def __call__(self) -> int:
|
|
57
|
+
from dotagents import _agents
|
|
58
|
+
from dotagents import _context
|
|
59
|
+
import json
|
|
60
|
+
import os
|
|
61
|
+
|
|
62
|
+
project_root = Path.cwd()
|
|
63
|
+
agents_dir = Path.home() / ".agents"
|
|
64
|
+
|
|
65
|
+
agent_names = []
|
|
66
|
+
if self.agents:
|
|
67
|
+
for a in self.agents:
|
|
68
|
+
agent_names.extend([x.strip() for x in a.split(",") if x.strip()])
|
|
69
|
+
|
|
70
|
+
if agent_names:
|
|
71
|
+
active_agents = []
|
|
72
|
+
for name in agent_names:
|
|
73
|
+
a = _agents.get_agent(name)
|
|
74
|
+
if a:
|
|
75
|
+
active_agents.append(a)
|
|
76
|
+
else:
|
|
77
|
+
self._logger_.warning("Unknown agent: %s", name)
|
|
78
|
+
else:
|
|
79
|
+
# Default target = the active agent (env-var detection / $AGENTS_HARNESS
|
|
80
|
+
# stamp / config-file detect), not "all detected".
|
|
81
|
+
active_agents = [
|
|
82
|
+
_agents.resolve_active_agent(os.environ, root=project_root)
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
# --- JSON: emit structured data (object for one agent, array for many);
|
|
86
|
+
# never writes native config files. ---
|
|
87
|
+
if self.format == "json":
|
|
88
|
+
payloads = [
|
|
89
|
+
_context.assemble_context_data(
|
|
90
|
+
agent, agents_dir, project_root, global_scope=self.global_scope
|
|
91
|
+
)
|
|
92
|
+
for agent in active_agents
|
|
93
|
+
]
|
|
94
|
+
out_obj = payloads[0] if len(payloads) == 1 else payloads
|
|
95
|
+
blob = json.dumps(out_obj, indent=2, ensure_ascii=False)
|
|
96
|
+
if self.out and self.out != "-":
|
|
97
|
+
Path(self.out).write_text(blob, encoding="utf-8")
|
|
98
|
+
self._logger_.info("Wrote JSON context to %s", self.out)
|
|
99
|
+
else:
|
|
100
|
+
print(blob) # default '-' -> stdout (json never writes native configs)
|
|
101
|
+
return 0
|
|
102
|
+
|
|
103
|
+
# --- markdown / system-reminder text paths ---
|
|
104
|
+
for agent in active_agents:
|
|
105
|
+
text = _context.assemble_context(
|
|
106
|
+
agent, agents_dir, project_root, global_scope=self.global_scope
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if self.format == "system-reminder":
|
|
110
|
+
text = (
|
|
111
|
+
"<!-- system-reminder: begin -->\n"
|
|
112
|
+
+ text
|
|
113
|
+
+ "\n<!-- system-reminder: end -->"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if self.write_agent:
|
|
117
|
+
agent.write_context(agents_dir, text, force=False, dry_run=False, logger=self._logger_)
|
|
118
|
+
elif self.out == "-":
|
|
119
|
+
# Just the context on stdout. A per-agent delimiter is emitted ONLY when
|
|
120
|
+
# more than one agent is generated, so a single-agent run (the default)
|
|
121
|
+
# is clean, pipeable output with no decoration to strip.
|
|
122
|
+
if len(active_agents) > 1:
|
|
123
|
+
_write_stdout("# --- %s ---\n" % agent.name)
|
|
124
|
+
_write_stdout(text + "\n")
|
|
125
|
+
else:
|
|
126
|
+
Path(self.out).write_text(text, encoding="utf-8")
|
|
127
|
+
self._logger_.info(f"Wrote {agent.name} context to {self.out}")
|
|
128
|
+
|
|
129
|
+
return 0
|
dotagents/cli/env.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""`dotagents env` -- chained env-file assembly + env.py execution (plan 07).
|
|
2
|
+
|
|
3
|
+
Self-contained block: all logic lives in `_env.py` (frozen contract B) and
|
|
4
|
+
`_agents.stamp_identity` (plan 08). The only umbrella touch is registering
|
|
5
|
+
`Env` on `Dotagents._subcommands_` (in `cli/__init__.py`). Never logs
|
|
6
|
+
DOTAGENTS_*/AGENTS_* VALUES -- output goes to stdout for the caller to consume;
|
|
7
|
+
the logger only ever names vars (Leakage rule).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from duho import Cmd, LoggingArgs
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _dotenv_value(v: str) -> str:
|
|
16
|
+
"""Dotenv quoting: bare unless the value needs quoting.
|
|
17
|
+
|
|
18
|
+
A value with whitespace, ``#``, ``"`` or a newline is wrapped in double
|
|
19
|
+
quotes with ``"``, ``\\`` and newline backslash-escaped; otherwise emitted
|
|
20
|
+
bare (the ``.env`` / ``docker --env-file`` convention).
|
|
21
|
+
"""
|
|
22
|
+
if v == "":
|
|
23
|
+
return ""
|
|
24
|
+
if any(c in v for c in " \t#\"\n") or v.strip() != v:
|
|
25
|
+
esc = v.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
|
|
26
|
+
return '"%s"' % esc
|
|
27
|
+
return v
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _format_env(env: "dict[str, str]", output_format: str) -> str:
|
|
31
|
+
"""Render the assembled env in the requested (canonical or aliased) format.
|
|
32
|
+
|
|
33
|
+
Shell-sourceable / assignment forms, one var per line:
|
|
34
|
+
|
|
35
|
+
* ``export`` (aliases ``posix``/``sh``/``bash``) -- ``export KEY="value"``,
|
|
36
|
+
value JSON-quoted; the POSIX default a SessionStart hook sources.
|
|
37
|
+
* ``dotenv`` (alias ``env``) -- bare ``KEY=value`` (no ``export``), value
|
|
38
|
+
quoted only when it contains whitespace/``#``/``"``/newline (``.env`` /
|
|
39
|
+
``docker --env-file`` rules). Distinct from ``export``: assigns, doesn't
|
|
40
|
+
source+export.
|
|
41
|
+
* ``powershell`` (aliases ``pwsh``/``ps``) -- ``$env:KEY = 'value'``,
|
|
42
|
+
single-quoted, ``'`` escaped as ``''``.
|
|
43
|
+
* ``cmd`` (aliases ``bat``/``batch``) -- ``set "KEY=value"``. cmd has NO way
|
|
44
|
+
to escape a literal ``"`` inside a value; any ``"`` is emitted as ``""``
|
|
45
|
+
best-effort (documented limitation).
|
|
46
|
+
* ``fish`` -- ``set -gx KEY value``, single-quoted, ``'`` escaped as ``\\'``.
|
|
47
|
+
|
|
48
|
+
Data forms:
|
|
49
|
+
|
|
50
|
+
* ``json`` -- a sorted JSON object.
|
|
51
|
+
* ``ini`` -- ``KEY=value`` lines under a ``[env]`` section.
|
|
52
|
+
* ``yaml`` -- ``KEY: value`` lines (values quoted when ambiguous).
|
|
53
|
+
|
|
54
|
+
Aliases are normalized here via :data:`dotagents._env.FORMAT_ALIASES`. Values
|
|
55
|
+
are emitted verbatim (this is the point of the command); callers must treat
|
|
56
|
+
the output as sensitive.
|
|
57
|
+
"""
|
|
58
|
+
import json as _json
|
|
59
|
+
|
|
60
|
+
from dotagents._env import FORMAT_ALIASES
|
|
61
|
+
|
|
62
|
+
fmt = FORMAT_ALIASES.get(output_format, output_format)
|
|
63
|
+
keys = sorted(env)
|
|
64
|
+
|
|
65
|
+
if fmt == "json":
|
|
66
|
+
return _json.dumps({k: env[k] for k in keys}, indent=2, sort_keys=True)
|
|
67
|
+
if fmt == "ini":
|
|
68
|
+
return "\n".join(["[env]"] + ["%s=%s" % (k, env[k]) for k in keys])
|
|
69
|
+
if fmt == "yaml":
|
|
70
|
+
lines = []
|
|
71
|
+
for k in keys:
|
|
72
|
+
v = env[k]
|
|
73
|
+
if v == "" or any(c in v for c in ":#'\"\n") or v.strip() != v:
|
|
74
|
+
v = _json.dumps(v)
|
|
75
|
+
lines.append("%s: %s" % (k, v))
|
|
76
|
+
return "\n".join(lines)
|
|
77
|
+
if fmt == "dotenv":
|
|
78
|
+
return "\n".join("%s=%s" % (k, _dotenv_value(env[k])) for k in keys)
|
|
79
|
+
if fmt == "powershell":
|
|
80
|
+
return "\n".join(
|
|
81
|
+
"$env:%s = '%s'" % (k, env[k].replace("'", "''")) for k in keys
|
|
82
|
+
)
|
|
83
|
+
if fmt == "cmd":
|
|
84
|
+
return "\n".join(
|
|
85
|
+
'set "%s=%s"' % (k, env[k].replace('"', '""')) for k in keys
|
|
86
|
+
)
|
|
87
|
+
if fmt == "fish":
|
|
88
|
+
return "\n".join(
|
|
89
|
+
"set -gx %s '%s'" % (k, env[k].replace("'", "\\'")) for k in keys
|
|
90
|
+
)
|
|
91
|
+
# default / "export"
|
|
92
|
+
return "\n".join("export %s=%s" % (k, _json.dumps(env[k])) for k in keys)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class Env(LoggingArgs, Cmd):
|
|
96
|
+
"""Assemble the chained env (env files + env.py execution) under contract B.
|
|
97
|
+
|
|
98
|
+
Prepends overlay/level ``bin`` dirs to ``PATH`` first, then evaluates the
|
|
99
|
+
``pre.*`` tier and the main tier in precedence order (overlays -> user ->
|
|
100
|
+
project -> project-root), chaining each file over the accumulated env so
|
|
101
|
+
later files win. ``.py`` files are EXECUTED and emit JSON env changes; plain
|
|
102
|
+
files are sourced. Standardized ``AGENTS_*``/``AGENT`` identity vars and the
|
|
103
|
+
``AGENTS_PROXY`` model are wired in.
|
|
104
|
+
|
|
105
|
+
``--diff`` emits only the vars that differ from the current environment (what
|
|
106
|
+
a SessionStart hook injects); the default emits the full assembled env merged
|
|
107
|
+
over the current one. Output is sensitive -- it may carry secret values.
|
|
108
|
+
|
|
109
|
+
``--format`` selects the emitted syntax and defaults to ``auto``, which
|
|
110
|
+
detects the CALLING shell (parent-process chain) and picks a matching format
|
|
111
|
+
so the output is sourceable where it runs: ``export`` (aliases
|
|
112
|
+
``posix``/``sh``/``bash``), ``dotenv`` (``env``), ``powershell``
|
|
113
|
+
(``pwsh``/``ps``), ``cmd`` (``bat``/``batch``), ``fish``, plus the data
|
|
114
|
+
forms ``json``/``ini``/``yaml``. An explicit ``--format`` always wins."""
|
|
115
|
+
|
|
116
|
+
_parsername_ = "env"
|
|
117
|
+
|
|
118
|
+
format: str = "auto"
|
|
119
|
+
(
|
|
120
|
+
"Output format. Default 'auto' detects the calling shell. Shell forms: "
|
|
121
|
+
"export (aliases posix/sh/bash), dotenv (env), powershell (pwsh/ps), "
|
|
122
|
+
"cmd (bat/batch), fish. Data forms: json, ini, yaml."
|
|
123
|
+
)
|
|
124
|
+
("--format",)
|
|
125
|
+
|
|
126
|
+
diff: bool = False
|
|
127
|
+
"Emit only vars that differ from the current environment."
|
|
128
|
+
("--diff",)
|
|
129
|
+
|
|
130
|
+
global_scope: bool = False
|
|
131
|
+
"Use global scope (skip project-level env files)."
|
|
132
|
+
("--global", "-g")
|
|
133
|
+
|
|
134
|
+
def __call__(self) -> int:
|
|
135
|
+
import os
|
|
136
|
+
|
|
137
|
+
from dotagents import _env
|
|
138
|
+
|
|
139
|
+
project_root = Path.cwd()
|
|
140
|
+
agents_dir = Path.home() / ".agents"
|
|
141
|
+
base = dict(os.environ)
|
|
142
|
+
|
|
143
|
+
if self.format not in _env.KNOWN_FORMATS:
|
|
144
|
+
raise SystemExit(
|
|
145
|
+
"error: --format must be one of %s (got %r)"
|
|
146
|
+
% (", ".join(_env.KNOWN_FORMATS), self.format)
|
|
147
|
+
)
|
|
148
|
+
# `auto` (the default) resolves to the calling shell's format; an explicit
|
|
149
|
+
# --format always wins. Detection reads process names only (never values).
|
|
150
|
+
output_format = self.format
|
|
151
|
+
if output_format == "auto":
|
|
152
|
+
output_format = _env.detect_shell_format()
|
|
153
|
+
|
|
154
|
+
if self.diff:
|
|
155
|
+
env = _env.get_diff(
|
|
156
|
+
agents_dir=agents_dir, project_root=project_root,
|
|
157
|
+
base_env=base, global_scope=self.global_scope, logger=self._logger_,
|
|
158
|
+
)
|
|
159
|
+
else:
|
|
160
|
+
changes = _env.get_environment(
|
|
161
|
+
agents_dir=agents_dir, project_root=project_root,
|
|
162
|
+
base_env=base, global_scope=self.global_scope, logger=self._logger_,
|
|
163
|
+
)
|
|
164
|
+
env = dict(base)
|
|
165
|
+
env.update(changes)
|
|
166
|
+
|
|
167
|
+
print(_format_env(env, output_format))
|
|
168
|
+
return 0
|
dotagents/cli/init.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""`dotagents init` -- lay down the neutral base config (+ optional wrappers)."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from duho import Cmd, LoggingArgs
|
|
8
|
+
|
|
9
|
+
from dotagents.cli._common import BASE_ROOT, _apply_base, _resolve_from
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Init(LoggingArgs, Cmd):
|
|
13
|
+
"""Lay down the neutral base config -- the `AGENTS.md` scaffolding and design-log
|
|
14
|
+
convention, never the opinionated overlays (those come from `overlays add`).
|
|
15
|
+
|
|
16
|
+
Scope: **project** by default (``<cwd>/.agents``), or the **user** store with
|
|
17
|
+
``-g/--global`` (``~/.agents``). ``--dest`` overrides the resolved location.
|
|
18
|
+
``--bin-dir`` additionally writes ``dotagents`` wrapper scripts there so the
|
|
19
|
+
command is on your PATH (only meaningful when running from a built ``.pyz``).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
_parsername_ = "init"
|
|
23
|
+
|
|
24
|
+
global_scope: bool = False
|
|
25
|
+
"Init the user store (~/.agents) instead of this project's <cwd>/.agents."
|
|
26
|
+
("--global", "-g")
|
|
27
|
+
|
|
28
|
+
agents_dir: Optional[Path] = None
|
|
29
|
+
"User store location for -g (default ~/.agents; configurable via $AGENTS_STORE_DIR)."
|
|
30
|
+
("--agents-dir",)
|
|
31
|
+
|
|
32
|
+
dest: Optional[Path] = None
|
|
33
|
+
"Explicit destination, overriding the resolved scope (project/user)."
|
|
34
|
+
("--dest",)
|
|
35
|
+
|
|
36
|
+
from_: Optional[str] = None
|
|
37
|
+
"Source directory/URI for the base overlay (default: bundled overlay)."
|
|
38
|
+
("--from",)
|
|
39
|
+
|
|
40
|
+
bin_dir: Optional[Path] = None
|
|
41
|
+
"Also write dotagents/dotagents.cmd wrapper scripts here (puts the command on PATH)."
|
|
42
|
+
("--bin-dir",)
|
|
43
|
+
|
|
44
|
+
dry_run: bool = False
|
|
45
|
+
"Show what would be written without touching anything."
|
|
46
|
+
("--dry-run",)
|
|
47
|
+
|
|
48
|
+
force: bool = False
|
|
49
|
+
"Replace AGENTS.md/CLAUDE.md wholesale (with backup) instead of block-merging."
|
|
50
|
+
("--force",)
|
|
51
|
+
|
|
52
|
+
agents: "list[str]" = []
|
|
53
|
+
"List of agents to install for (e.g. claude,gemini). Default: auto-detect + claude."
|
|
54
|
+
("--agents",)
|
|
55
|
+
|
|
56
|
+
no_hooks: bool = False
|
|
57
|
+
"Skip wiring agent hooks and the shared skills link into the agent's config dir."
|
|
58
|
+
("--no-hooks",)
|
|
59
|
+
|
|
60
|
+
def __call__(self) -> int:
|
|
61
|
+
src = _resolve_from(self.from_, BASE_ROOT)
|
|
62
|
+
if self.dest is not None:
|
|
63
|
+
dest = Path(self.dest).expanduser().resolve()
|
|
64
|
+
else:
|
|
65
|
+
from dotagents import _scope
|
|
66
|
+
|
|
67
|
+
scope = _scope.resolve_scope(self.global_scope, agents_dir=self.agents_dir)
|
|
68
|
+
dest = Path(scope.agents_root).expanduser().resolve()
|
|
69
|
+
self._logger_.info("scope: %s (%s)", scope.level, dest)
|
|
70
|
+
|
|
71
|
+
agent_names = []
|
|
72
|
+
if self.agents:
|
|
73
|
+
for a in self.agents:
|
|
74
|
+
agent_names.extend([x.strip() for x in a.split(",") if x.strip()])
|
|
75
|
+
|
|
76
|
+
_apply_base(
|
|
77
|
+
Path(src), dest, self.force, self.dry_run, self._logger_,
|
|
78
|
+
agents=agent_names if agent_names else None,
|
|
79
|
+
wire_hooks=not self.no_hooks,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if not self.dry_run:
|
|
83
|
+
from dotagents._wrappers import check_path_warning, write_wrappers
|
|
84
|
+
|
|
85
|
+
pyz_path = Path(sys.argv[0]).resolve()
|
|
86
|
+
if pyz_path.suffix != ".pyz":
|
|
87
|
+
# Running from a plain install (not a pyz): the wrappers point at
|
|
88
|
+
# `python -m dotagents` instead of a nonexistent pyz path.
|
|
89
|
+
pyz_path = None
|
|
90
|
+
|
|
91
|
+
# `<scope>/bin/` is always populated, with a path relative to the scope
|
|
92
|
+
# so the store stays relocatable. Everything downstream of `init` --
|
|
93
|
+
# the SessionStart hook, overlay `bin/` PATH entries, an overlay setup
|
|
94
|
+
# script calling a sibling -- shells out to `dotagents` by name, so a
|
|
95
|
+
# scope without it is a scope where those silently fail.
|
|
96
|
+
targets = [(dest / "bin", True)]
|
|
97
|
+
if self.bin_dir is not None:
|
|
98
|
+
targets.append((Path(self.bin_dir), False))
|
|
99
|
+
|
|
100
|
+
for bin_dir, relative in targets:
|
|
101
|
+
if pyz_path is not None:
|
|
102
|
+
for w in write_wrappers(bin_dir, pyz_path, relative=relative):
|
|
103
|
+
self._logger_.info("wrapper: %s", w)
|
|
104
|
+
else:
|
|
105
|
+
self._logger_.info(
|
|
106
|
+
"skipped wrapper install in %s: not running from a .pyz "
|
|
107
|
+
"(use build-pyz first)", bin_dir,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if self.bin_dir is not None:
|
|
111
|
+
warning = check_path_warning(Path(self.bin_dir))
|
|
112
|
+
if warning:
|
|
113
|
+
self._logger_.warning(warning)
|
|
114
|
+
|
|
115
|
+
if self.dry_run:
|
|
116
|
+
self._logger_.info("dry-run: no files were written")
|
|
117
|
+
return 0
|