@softspark/ai-toolkit 4.14.1 → 4.15.1
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.
- package/AGENTS.md +117 -0
- package/CHANGELOG.md +37 -0
- package/README.md +9 -10
- package/app/.claude-plugin/plugin.json +1 -1
- package/app/CLAUDE.md.template +3 -0
- package/app/hooks/_search-capability.sh +3 -2
- package/app/hooks/stop-search-check.sh +2 -1
- package/benchmarks/ecosystem-doctor-snapshot.json +73 -31
- package/kb/procedures/maintenance-sop.md +26 -13
- package/kb/procedures/release-verification-sop.md +41 -36
- package/kb/reference/architecture-overview.md +23 -7
- package/kb/reference/codex-cli-compatibility.md +96 -36
- package/kb/reference/extension-api.md +52 -9
- package/kb/reference/global-install-model.md +56 -21
- package/kb/reference/hooks-catalog.md +44 -8
- package/kb/reference/mcp-editor-compatibility.md +27 -6
- package/kb/reference/mcp-templates.md +12 -6
- package/kb/reference/opencode-compatibility.md +13 -7
- package/kb/reference/plugin-pack-conventions.md +7 -7
- package/kb/reference/skills-catalog.md +3 -3
- package/kb/reference/supported-tools-registry.md +19 -17
- package/kb/reference/windows-support.md +27 -3
- package/llms-full.txt +447 -180
- package/llms.txt +1 -1
- package/manifest.json +1 -1
- package/package.json +2 -2
- package/scripts/codex_skill_adapter.py +448 -198
- package/scripts/copilot_legacy_hashes.json +338 -0
- package/scripts/dir_rules_shared.py +2 -11
- package/scripts/ecosystem_tools.json +29 -8
- package/scripts/emission.py +5 -91
- package/scripts/generate_agents_md.py +4 -87
- package/scripts/generate_codex.py +5 -95
- package/scripts/generate_codex_agents.py +242 -0
- package/scripts/generate_codex_hooks.py +648 -55
- package/scripts/generate_codex_skills.py +15 -6
- package/scripts/generate_copilot.py +1187 -97
- package/scripts/generate_copilot_hooks.py +723 -0
- package/scripts/generate_cursor_hooks.py +453 -121
- package/scripts/generate_opencode_commands.py +4 -6
- package/scripts/inject_hook_cli.py +770 -205
- package/scripts/injection.py +102 -23
- package/scripts/install_steps/ai_tools.py +136 -83
- package/scripts/instruction_core.py +95 -0
- package/scripts/mcp_editors.py +934 -80
- package/scripts/mcp_manager.py +46 -26
- package/scripts/plugin.py +291 -114
- package/scripts/secure_fs.py +538 -0
- package/scripts/uninstall.py +1279 -208
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Generate AGENTS.md
|
|
2
|
+
"""Generate the shared AGENTS.md instruction core.
|
|
3
3
|
|
|
4
|
-
Output is compatible with Codex
|
|
4
|
+
Output is compatible with the Codex and GitHub Copilot AGENTS.md format.
|
|
5
5
|
Usage: ./scripts/generate_agents_md.py > AGENTS.md
|
|
6
6
|
"""
|
|
7
7
|
from __future__ import annotations
|
|
@@ -10,94 +10,11 @@ import sys
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
12
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
-
from _common import agents_dir, frontmatter_field
|
|
13
|
+
from instruction_core import render_instruction_core
|
|
16
14
|
|
|
17
15
|
|
|
18
16
|
def main() -> None:
|
|
19
|
-
print(
|
|
20
|
-
print()
|
|
21
|
-
print(
|
|
22
|
-
"This file describes the specialized AI agents bundled with ai-toolkit."
|
|
23
|
-
)
|
|
24
|
-
print(
|
|
25
|
-
"It is auto-generated from `app/agents/*.md` frontmatter"
|
|
26
|
-
" — do not edit manually."
|
|
27
|
-
)
|
|
28
|
-
print()
|
|
29
|
-
print("To regenerate: `python3 scripts/generate_agents_md.py > AGENTS.md`")
|
|
30
|
-
print()
|
|
31
|
-
print("Compatible with: Claude Code, Codex, OpenCode, Gemini CLI.")
|
|
32
|
-
print()
|
|
33
|
-
print("---")
|
|
34
|
-
print()
|
|
35
|
-
print("## Usage")
|
|
36
|
-
print()
|
|
37
|
-
print("### Claude Code")
|
|
38
|
-
print(
|
|
39
|
-
"Agents are loaded automatically from `.claude/agents/`"
|
|
40
|
-
" after running `install.sh`."
|
|
41
|
-
)
|
|
42
|
-
print("Invoke via the Agent tool:")
|
|
43
|
-
print("```")
|
|
44
|
-
print(
|
|
45
|
-
'Use subagent_type: "backend-specialist" to implement the API endpoint.'
|
|
46
|
-
)
|
|
47
|
-
print("```")
|
|
48
|
-
print()
|
|
49
|
-
print("### Codex / OpenCode")
|
|
50
|
-
print("Reference agents by name in your prompts:")
|
|
51
|
-
print("```")
|
|
52
|
-
print("@backend-specialist implement the payment API")
|
|
53
|
-
print("```")
|
|
54
|
-
print()
|
|
55
|
-
print("### Gemini CLI")
|
|
56
|
-
print("Use agent descriptions as system context:")
|
|
57
|
-
print("```")
|
|
58
|
-
print(
|
|
59
|
-
'gemini --system "$(cat .claude/agents/backend-specialist.md)"'
|
|
60
|
-
' "implement the API"'
|
|
61
|
-
)
|
|
62
|
-
print("```")
|
|
63
|
-
print()
|
|
64
|
-
print("---")
|
|
65
|
-
print()
|
|
66
|
-
print("## Agents")
|
|
67
|
-
print()
|
|
68
|
-
|
|
69
|
-
for agent_file in sorted(agents_dir.glob("*.md")):
|
|
70
|
-
if not agent_file.is_file():
|
|
71
|
-
continue
|
|
72
|
-
|
|
73
|
-
name = frontmatter_field(agent_file, "name")
|
|
74
|
-
description = frontmatter_field(agent_file, "description")
|
|
75
|
-
tools = frontmatter_field(agent_file, "tools")
|
|
76
|
-
|
|
77
|
-
if not name:
|
|
78
|
-
continue
|
|
79
|
-
|
|
80
|
-
print(f"### `{name}`")
|
|
81
|
-
print()
|
|
82
|
-
if description:
|
|
83
|
-
print(description)
|
|
84
|
-
print()
|
|
85
|
-
if tools:
|
|
86
|
-
print(f"**Tools:** `{tools}`")
|
|
87
|
-
print()
|
|
88
|
-
print("---")
|
|
89
|
-
print()
|
|
90
|
-
|
|
91
|
-
# Codex CLI configuration block (agents, skills, guidelines)
|
|
92
|
-
codex_script = Path(__file__).resolve().parent / "generate_codex.py"
|
|
93
|
-
result = subprocess.run(
|
|
94
|
-
["python3", str(codex_script)],
|
|
95
|
-
capture_output=True, text=True,
|
|
96
|
-
)
|
|
97
|
-
if result.returncode == 0 and result.stdout.strip():
|
|
98
|
-
print(result.stdout.rstrip())
|
|
99
|
-
|
|
100
|
-
# Note: custom rules are included via generate_codex.py output above
|
|
17
|
+
print(render_instruction_core(), end="")
|
|
101
18
|
|
|
102
19
|
|
|
103
20
|
if __name__ == "__main__":
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Generate AGENTS.md
|
|
2
|
+
"""Generate the compact shared AGENTS.md policy for OpenAI Codex CLI.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
Agent and skill discovery is installed separately in native directories, so
|
|
5
|
+
the always-on instruction file contains policy rather than duplicated catalogs.
|
|
6
6
|
|
|
7
7
|
Usage: ./scripts/generate_codex.py > AGENTS.md
|
|
8
8
|
"""
|
|
@@ -13,108 +13,18 @@ import sys
|
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
|
|
15
15
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
16
|
-
from codex_skill_adapter import codex_skill_description
|
|
17
|
-
from dir_rules_shared import (
|
|
18
|
-
rule_code_style,
|
|
19
|
-
rule_output_mode,
|
|
20
|
-
rule_security,
|
|
21
|
-
rule_testing,
|
|
22
|
-
)
|
|
23
16
|
from emission import (
|
|
24
|
-
agents_dir,
|
|
25
|
-
skills_dir,
|
|
26
|
-
generate_quality_standards,
|
|
27
|
-
generate_workflow_guidelines,
|
|
28
17
|
print_toolkit_end,
|
|
29
18
|
print_toolkit_start,
|
|
30
19
|
)
|
|
31
|
-
from
|
|
20
|
+
from instruction_core import render_instruction_core
|
|
32
21
|
from paths import RULES_DIR
|
|
33
22
|
|
|
34
23
|
|
|
35
|
-
def _emit_agents() -> str:
|
|
36
|
-
"""Emit agents as bullets."""
|
|
37
|
-
lines: list[str] = []
|
|
38
|
-
for agent_file in sorted(agents_dir.glob("*.md")):
|
|
39
|
-
name = frontmatter_field(agent_file, "name")
|
|
40
|
-
description = frontmatter_field(agent_file, "description")
|
|
41
|
-
if not name or not description:
|
|
42
|
-
continue
|
|
43
|
-
lines.append(f"- **{name}**: {description}")
|
|
44
|
-
return "\n".join(lines)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _emit_skills() -> str:
|
|
48
|
-
"""Emit skills as bullets, adapting Claude-native descriptions for Codex."""
|
|
49
|
-
lines: list[str] = []
|
|
50
|
-
for skill_dir in sorted(skills_dir.iterdir()):
|
|
51
|
-
if skill_dir.name.startswith("_"):
|
|
52
|
-
continue
|
|
53
|
-
skill_file = skill_dir / "SKILL.md"
|
|
54
|
-
if not skill_file.is_file():
|
|
55
|
-
continue
|
|
56
|
-
name = frontmatter_field(skill_file, "name")
|
|
57
|
-
description = codex_skill_description(skill_file)
|
|
58
|
-
if not name or not description:
|
|
59
|
-
continue
|
|
60
|
-
lines.append(f"- **{name}**: {description}")
|
|
61
|
-
return "\n".join(lines)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _emit_coding_rules() -> str:
|
|
65
|
-
"""Inline the universal coding-rule bodies so Codex receives them.
|
|
66
|
-
|
|
67
|
-
Codex reads project instructions only from AGENTS.md (and AGENTS.override.md);
|
|
68
|
-
it does not read a ``.agents/rules/`` directory. Workflow + quality standards
|
|
69
|
-
are already emitted above, so this adds code-style, testing, security, and
|
|
70
|
-
output-mode under a single ``## Coding Rules`` section (H1 demoted to H3).
|
|
71
|
-
"""
|
|
72
|
-
sections: list[str] = []
|
|
73
|
-
for rule_fn in (rule_code_style, rule_testing, rule_security, rule_output_mode):
|
|
74
|
-
body = rule_fn().rstrip()
|
|
75
|
-
if body.startswith("# "):
|
|
76
|
-
body = "### " + body[2:]
|
|
77
|
-
sections.append(body)
|
|
78
|
-
return "## Coding Rules\n\n" + "\n\n".join(sections)
|
|
79
|
-
|
|
80
|
-
|
|
81
24
|
def main() -> None:
|
|
82
25
|
print_toolkit_start()
|
|
83
|
-
|
|
84
|
-
print("# AI Toolkit — Codex CLI Configuration")
|
|
85
|
-
print()
|
|
86
|
-
print(
|
|
87
|
-
"Shared AI development toolkit with specialized agents,"
|
|
88
|
-
" Codex-compatible skills, quality hooks, and a safety constitution."
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
# Agents (all agents are informational — safe to list)
|
|
92
|
-
print()
|
|
93
|
-
print("## Available Agents")
|
|
26
|
+
print(render_instruction_core().rstrip())
|
|
94
27
|
print()
|
|
95
|
-
print("Specialized agent personas — apply their expertise for relevant tasks:")
|
|
96
|
-
print()
|
|
97
|
-
print(_emit_agents())
|
|
98
|
-
|
|
99
|
-
# Skills
|
|
100
|
-
print()
|
|
101
|
-
print("## Available Skills")
|
|
102
|
-
print()
|
|
103
|
-
print("Skills are invocable commands or auto-loaded knowledge sources:")
|
|
104
|
-
print()
|
|
105
|
-
print(_emit_skills())
|
|
106
|
-
|
|
107
|
-
# Guidelines
|
|
108
|
-
print()
|
|
109
|
-
print(generate_quality_standards())
|
|
110
|
-
print()
|
|
111
|
-
print(generate_workflow_guidelines())
|
|
112
|
-
|
|
113
|
-
# Universal coding rules — Codex reads instructions only from AGENTS.md,
|
|
114
|
-
# so inline them here (previously emitted to the unread .agents/rules/).
|
|
115
|
-
print()
|
|
116
|
-
print(_emit_coding_rules())
|
|
117
|
-
|
|
118
28
|
print_toolkit_end()
|
|
119
29
|
|
|
120
30
|
# Registered custom rules from ~/.softspark/ai-toolkit/rules/.
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate native Codex custom-agent TOML files."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
import tomllib
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
14
|
+
from emission import agents_dir
|
|
15
|
+
from frontmatter import frontmatter_field
|
|
16
|
+
|
|
17
|
+
AGENT_PREFIX = "ai-toolkit-"
|
|
18
|
+
MANAGED_MARKER = "# ai-toolkit-managed: codex-agent"
|
|
19
|
+
REQUIRED_FIELDS = {"name", "description", "developer_instructions"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _agent_body(agent_file: Path) -> str:
|
|
23
|
+
text = agent_file.read_text(encoding="utf-8")
|
|
24
|
+
lines = text.splitlines(keepends=True)
|
|
25
|
+
body = text
|
|
26
|
+
if lines and lines[0].rstrip("\r\n") == "---":
|
|
27
|
+
for index, line in enumerate(lines[1:], start=1):
|
|
28
|
+
if line.rstrip("\r\n") == "---":
|
|
29
|
+
body = "".join(lines[index + 1:])
|
|
30
|
+
break
|
|
31
|
+
return body.lstrip("\r\n").rstrip() + "\n"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _toml_string(value: str) -> str:
|
|
35
|
+
return json.dumps(value, ensure_ascii=False)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _render_agent(agent_file: Path) -> str:
|
|
39
|
+
name = frontmatter_field(agent_file, "name")
|
|
40
|
+
description = frontmatter_field(agent_file, "description")
|
|
41
|
+
body = _agent_body(agent_file)
|
|
42
|
+
return "\n".join(
|
|
43
|
+
[
|
|
44
|
+
"# Generated by ai-toolkit. Do not edit.",
|
|
45
|
+
MANAGED_MARKER,
|
|
46
|
+
f"name = {_toml_string(name)}",
|
|
47
|
+
f"description = {_toml_string(description)}",
|
|
48
|
+
f"developer_instructions = {_toml_string(body)}",
|
|
49
|
+
"",
|
|
50
|
+
]
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _is_managed(path: Path) -> bool:
|
|
55
|
+
if path.is_symlink() or not path.is_file():
|
|
56
|
+
return False
|
|
57
|
+
try:
|
|
58
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
59
|
+
except (OSError, UnicodeError):
|
|
60
|
+
return False
|
|
61
|
+
return MANAGED_MARKER in lines[:3]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _warn_preserved(path: Path, reason: str) -> None:
|
|
65
|
+
print(
|
|
66
|
+
f"Warning: preserving user Codex agent '{path}': {reason}",
|
|
67
|
+
file=sys.stderr,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _unmanaged_agent_names(output_dir: Path) -> set[str]:
|
|
72
|
+
names: set[str] = set()
|
|
73
|
+
for path in sorted(output_dir.glob("*.toml")):
|
|
74
|
+
if path.is_symlink():
|
|
75
|
+
_warn_preserved(path, "path is a symlink")
|
|
76
|
+
continue
|
|
77
|
+
if _is_managed(path):
|
|
78
|
+
continue
|
|
79
|
+
try:
|
|
80
|
+
data = tomllib.loads(path.read_text(encoding="utf-8"))
|
|
81
|
+
except (OSError, UnicodeError, tomllib.TOMLDecodeError) as error:
|
|
82
|
+
_warn_preserved(path, f"cannot parse TOML ({error})")
|
|
83
|
+
continue
|
|
84
|
+
name = data.get("name")
|
|
85
|
+
if isinstance(name, str) and name.strip():
|
|
86
|
+
names.add(name)
|
|
87
|
+
return names
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _cleanup_stale(output_dir: Path, expected: set[str]) -> int:
|
|
91
|
+
removed = 0
|
|
92
|
+
for output in sorted(output_dir.glob(f"{AGENT_PREFIX}*.toml")):
|
|
93
|
+
if output.is_symlink():
|
|
94
|
+
continue
|
|
95
|
+
if output.name in expected or not _is_managed(output):
|
|
96
|
+
continue
|
|
97
|
+
output.unlink()
|
|
98
|
+
removed += 1
|
|
99
|
+
return removed
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _assert_safe_output_paths(base: Path, output_dir: Path) -> None:
|
|
103
|
+
if base.is_symlink():
|
|
104
|
+
raise RuntimeError(f"Refusing symlinked Codex config directory: {base}")
|
|
105
|
+
if output_dir.is_symlink():
|
|
106
|
+
raise RuntimeError(f"Refusing symlinked Codex agents directory: {output_dir}")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _prepare_output_dir(base: Path) -> Path:
|
|
110
|
+
output_dir = base / "agents"
|
|
111
|
+
_assert_safe_output_paths(base, output_dir)
|
|
112
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
113
|
+
_assert_safe_output_paths(base, output_dir)
|
|
114
|
+
return output_dir
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _validate_agent_toml(rendered: str, source: Path) -> None:
|
|
118
|
+
data = tomllib.loads(rendered)
|
|
119
|
+
if set(data) != REQUIRED_FIELDS:
|
|
120
|
+
raise ValueError(f"Invalid Codex agent fields rendered from {source}")
|
|
121
|
+
if any(not isinstance(data[field], str) or not data[field] for field in REQUIRED_FIELDS):
|
|
122
|
+
raise ValueError(f"Empty Codex agent field rendered from {source}")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _build_write_plan(
|
|
126
|
+
output_dir: Path,
|
|
127
|
+
unmanaged_names: set[str],
|
|
128
|
+
) -> tuple[list[tuple[Path, str]], set[str]]:
|
|
129
|
+
plan: list[tuple[Path, str]] = []
|
|
130
|
+
expected: set[str] = set()
|
|
131
|
+
|
|
132
|
+
for agent_file in sorted(agents_dir.glob("*.md")):
|
|
133
|
+
name = frontmatter_field(agent_file, "name")
|
|
134
|
+
description = frontmatter_field(agent_file, "description")
|
|
135
|
+
if not name or not description:
|
|
136
|
+
continue
|
|
137
|
+
if name in unmanaged_names:
|
|
138
|
+
continue
|
|
139
|
+
filename = f"{AGENT_PREFIX}{name}.toml"
|
|
140
|
+
expected.add(filename)
|
|
141
|
+
output = output_dir / filename
|
|
142
|
+
if output.is_symlink():
|
|
143
|
+
_warn_preserved(output, "destination is a symlink")
|
|
144
|
+
continue
|
|
145
|
+
if output.exists() and not _is_managed(output):
|
|
146
|
+
continue
|
|
147
|
+
rendered = _render_agent(agent_file)
|
|
148
|
+
_validate_agent_toml(rendered, agent_file)
|
|
149
|
+
plan.append((output, rendered))
|
|
150
|
+
return plan, expected
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _discard_temp(path: Path, output_dir: Path) -> None:
|
|
154
|
+
if path.parent != output_dir:
|
|
155
|
+
return
|
|
156
|
+
try:
|
|
157
|
+
path.unlink(missing_ok=True)
|
|
158
|
+
except OSError:
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _stage_agent(output_dir: Path, output: Path, rendered: str) -> Path:
|
|
163
|
+
fd, temp_name = tempfile.mkstemp(
|
|
164
|
+
dir=output_dir,
|
|
165
|
+
prefix=f".{output.name}.",
|
|
166
|
+
suffix=".tmp",
|
|
167
|
+
)
|
|
168
|
+
temp_path = Path(temp_name)
|
|
169
|
+
try:
|
|
170
|
+
handle = os.fdopen(fd, "w", encoding="utf-8")
|
|
171
|
+
fd = -1
|
|
172
|
+
with handle:
|
|
173
|
+
handle.write(rendered)
|
|
174
|
+
handle.flush()
|
|
175
|
+
os.fsync(handle.fileno())
|
|
176
|
+
_validate_agent_toml(temp_path.read_text(encoding="utf-8"), output)
|
|
177
|
+
return temp_path
|
|
178
|
+
except Exception:
|
|
179
|
+
if fd >= 0:
|
|
180
|
+
os.close(fd)
|
|
181
|
+
_discard_temp(temp_path, output_dir)
|
|
182
|
+
raise
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _stage_all(
|
|
186
|
+
output_dir: Path,
|
|
187
|
+
plan: list[tuple[Path, str]],
|
|
188
|
+
) -> list[tuple[Path, Path]]:
|
|
189
|
+
staged: list[tuple[Path, Path]] = []
|
|
190
|
+
try:
|
|
191
|
+
for output, rendered in plan:
|
|
192
|
+
staged.append((_stage_agent(output_dir, output, rendered), output))
|
|
193
|
+
return staged
|
|
194
|
+
except Exception:
|
|
195
|
+
for temp_path, _ in staged:
|
|
196
|
+
_discard_temp(temp_path, output_dir)
|
|
197
|
+
raise
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _replace_all(
|
|
201
|
+
base: Path,
|
|
202
|
+
output_dir: Path,
|
|
203
|
+
staged: list[tuple[Path, Path]],
|
|
204
|
+
) -> int:
|
|
205
|
+
written = 0
|
|
206
|
+
_assert_safe_output_paths(base, output_dir)
|
|
207
|
+
for temp_path, output in staged:
|
|
208
|
+
if output.is_symlink():
|
|
209
|
+
_warn_preserved(output, "destination became a symlink during generation")
|
|
210
|
+
continue
|
|
211
|
+
if output.exists() and not _is_managed(output):
|
|
212
|
+
_warn_preserved(output, "destination became user-owned during generation")
|
|
213
|
+
continue
|
|
214
|
+
os.replace(temp_path, output)
|
|
215
|
+
written += 1
|
|
216
|
+
return written
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def generate(target_dir: Path, config_root: Path | None = None) -> tuple[int, int]:
|
|
220
|
+
base = config_root if config_root is not None else target_dir / ".codex"
|
|
221
|
+
output_dir = _prepare_output_dir(base)
|
|
222
|
+
unmanaged_names = _unmanaged_agent_names(output_dir)
|
|
223
|
+
plan, expected = _build_write_plan(output_dir, unmanaged_names)
|
|
224
|
+
staged = _stage_all(output_dir, plan)
|
|
225
|
+
try:
|
|
226
|
+
written = _replace_all(base, output_dir, staged)
|
|
227
|
+
_assert_safe_output_paths(base, output_dir)
|
|
228
|
+
finally:
|
|
229
|
+
for temp_path, _ in staged:
|
|
230
|
+
_discard_temp(temp_path, output_dir)
|
|
231
|
+
removed = _cleanup_stale(output_dir, expected)
|
|
232
|
+
return written, removed
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def main() -> None:
|
|
236
|
+
target = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
|
|
237
|
+
written, removed = generate(target)
|
|
238
|
+
print(f"Generated: .codex/agents/ ({written} agents, {removed} stale removed)")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
if __name__ == "__main__":
|
|
242
|
+
main()
|