agentbundle 0.7.0__tar.gz → 0.8.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.
- {agentbundle-0.7.0 → agentbundle-0.8.0}/PKG-INFO +12 -12
- {agentbundle-0.7.0 → agentbundle-0.8.0}/README.md +11 -11
- agentbundle-0.8.0/agentbundle/_data/install-defaults.toml +10 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/adapters/codex.py +1 -1
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/adapters/cursor.py +2 -2
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/adapters/gemini.py +2 -2
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/adapters/kiro.py +1 -1
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/adapters/kiro_ide.py +1 -1
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/main.py +4 -4
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/projections/codex_agent_toml.py +1 -1
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/projections/copilot_agent_md.py +1 -1
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/projections/copilot_hooks_json.py +1 -1
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/projections/gemini_command_toml.py +1 -1
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/projections/kiro_ide_hook.py +1 -1
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/projections/merge_into_agent_json.py +1 -1
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/projections/merge_json.py +1 -1
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/projections/user_merge_json.py +1 -1
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/self_host.py +106 -22
- agentbundle-0.8.0/agentbundle/build/tests/test_self_host_fixture_guard.py +88 -0
- agentbundle-0.8.0/agentbundle/build/tests/test_self_host_recipe_config.py +113 -0
- agentbundle-0.8.0/agentbundle/build/tests/test_writers_emit_lf.py +169 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/cli.py +40 -4
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/_common.py +119 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/config.py +6 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/install.py +30 -6
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/list_packs.py +20 -30
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/list_profiles.py +12 -16
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/upgrade.py +10 -1
- agentbundle-0.8.0/agentbundle/source_defaults.py +342 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/user_config.py +53 -5
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/version.py +1 -1
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle.egg-info/PKG-INFO +12 -12
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle.egg-info/SOURCES.txt +5 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/pyproject.toml +1 -1
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/__init__.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/__main__.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/_data/adapter.schema.json +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/_data/adapter.toml +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/_data/install-marker.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/_data/pack.schema.json +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/_data/plugin-manifest.derived.schema.json +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/_data/plugin-manifest.schema.json +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/_data/profile.schema.json +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/__init__.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/__main__.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/adapter_root_bins.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/adapters/__init__.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/adapters/claude_code.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/adapters/copilot.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/adapters/kiro_cli.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/contract.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/lint_packs.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/phase_order.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/projections/__init__.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/projections/direct_directory.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/projections/hook_id.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/scope_rails.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/shared_libs.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/target_resolver.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/__init__.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_adapter_claude_code.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_adapter_codex.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_adapter_copilot.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_adapter_cursor.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_adapter_gemini.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_adapter_kiro.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_adapter_kiro_alias.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_adapter_kiro_cli.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_adapter_kiro_ide.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_adapter_root_bins_projection.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_architect_design_reviewer_projection.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_architect_design_reviewer_rubric_parity.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_build_ships_seeds.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_contract.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_contract_scope.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_contract_v07.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_contract_v08.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_direct_directory_cleanup.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_end_to_end_build.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_lint_agents_md_diataxis_block.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_lint_agents_md_legacy_block.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_lint_agents_md_risk_block.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_lint_packs.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_load_pack_hook_wiring_safely.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_pack_schema.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_pack_schema_allowed_adapters.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_pack_schema_install.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_pipeline.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_plugin_manifest_schema.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_projectable_subset.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_projections_merge_json.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_scope_rails.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_security.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_self_host_check.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_shared_libs_projection.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_shipped_packs_v07_declarations.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_shipped_packs_v08_declarations.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_user_libs_projection.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/tests/test_validate.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/user_libs.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/validate.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/catalogue.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/categories.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/__init__.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/_drop_warning.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/adapt.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/diff.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/init_state.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/list_targets.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/profile.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/reconcile.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/render.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/scaffold.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/uninstall.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/commands/validate.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/config.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/render.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/safety.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/scope.py +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle.egg-info/dependency_links.txt +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle.egg-info/entry_points.txt +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle.egg-info/top_level.txt +0 -0
- {agentbundle-0.7.0 → agentbundle-0.8.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentbundle
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: npm for your coding agent. Install packs of skills, subagents, and hooks into any repo, for every major agent.
|
|
5
5
|
Author-email: eugenelim <eugenelim@users.noreply.github.com>
|
|
6
6
|
License: Apache-2.0 OR MIT
|
|
@@ -33,37 +33,37 @@ pip install agentbundle
|
|
|
33
33
|
**Install into a repo** — so everyone who clones it gets the pack. `core` is the flagship pack, the loop itself:
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
agentbundle install --pack core
|
|
36
|
+
agentbundle install --pack core
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
It lands in the repo's agent config — subagents and skills included — and you commit it like any other project file. This is the default scope: the pack belongs to the project and the whole team.
|
|
39
|
+
No catalogue argument needed: it defaults to the agent-ready-repo catalogue. It lands in the repo's agent config — subagents and skills included — and you commit it like any other project file. This is the default scope: the pack belongs to the project and the whole team.
|
|
40
40
|
|
|
41
41
|
**Install for yourself, everywhere** — so a pack follows you across every project, with no per-repo setup:
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
agentbundle install --pack research
|
|
44
|
+
agentbundle install --pack research --scope user
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
User-scope packs land in your home directory, not the repo — they're yours, not the team's, and they're there in every project you open.
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
The install auto-detects your agent (`--adapter` overrides). To install from a **different** catalogue, pass it as a trailing argument — a git URL or a local path (`agentbundle install --pack core <catalogue>`); a `config set source <catalogue>` makes that the default, and an editable clone (`pip install -e`) defaults to itself.
|
|
50
50
|
|
|
51
51
|
## More commands
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
|
-
# See what
|
|
55
|
-
agentbundle list-packs
|
|
56
|
-
agentbundle list-profiles
|
|
54
|
+
# See what the catalogue offers (bare uses the default; or name one explicitly)
|
|
55
|
+
agentbundle list-packs
|
|
56
|
+
agentbundle list-profiles
|
|
57
57
|
|
|
58
58
|
# Install a whole curated profile — a single-scope set of packs — in one command
|
|
59
|
-
agentbundle install --profile inception
|
|
59
|
+
agentbundle install --profile inception
|
|
60
60
|
|
|
61
61
|
# Preview any install without writing a file
|
|
62
|
-
agentbundle install --pack core
|
|
62
|
+
agentbundle install --pack core --dry-run
|
|
63
63
|
|
|
64
64
|
# Upgrade to the version the catalogue ships — shows installed → target, asks first
|
|
65
|
-
agentbundle upgrade --pack core
|
|
66
|
-
agentbundle upgrade --pack core
|
|
65
|
+
agentbundle upgrade --pack core
|
|
66
|
+
agentbundle upgrade --pack core --yes # skip the prompt (CI)
|
|
67
67
|
|
|
68
68
|
# Uninstall — previews remove (Tier-1) vs keep (your edits), asks first
|
|
69
69
|
agentbundle uninstall --pack core --dry-run
|
|
@@ -15,37 +15,37 @@ pip install agentbundle
|
|
|
15
15
|
**Install into a repo** — so everyone who clones it gets the pack. `core` is the flagship pack, the loop itself:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
agentbundle install --pack core
|
|
18
|
+
agentbundle install --pack core
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
It lands in the repo's agent config — subagents and skills included — and you commit it like any other project file. This is the default scope: the pack belongs to the project and the whole team.
|
|
21
|
+
No catalogue argument needed: it defaults to the agent-ready-repo catalogue. It lands in the repo's agent config — subagents and skills included — and you commit it like any other project file. This is the default scope: the pack belongs to the project and the whole team.
|
|
22
22
|
|
|
23
23
|
**Install for yourself, everywhere** — so a pack follows you across every project, with no per-repo setup:
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
agentbundle install --pack research
|
|
26
|
+
agentbundle install --pack research --scope user
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
User-scope packs land in your home directory, not the repo — they're yours, not the team's, and they're there in every project you open.
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
The install auto-detects your agent (`--adapter` overrides). To install from a **different** catalogue, pass it as a trailing argument — a git URL or a local path (`agentbundle install --pack core <catalogue>`); a `config set source <catalogue>` makes that the default, and an editable clone (`pip install -e`) defaults to itself.
|
|
32
32
|
|
|
33
33
|
## More commands
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
# See what
|
|
37
|
-
agentbundle list-packs
|
|
38
|
-
agentbundle list-profiles
|
|
36
|
+
# See what the catalogue offers (bare uses the default; or name one explicitly)
|
|
37
|
+
agentbundle list-packs
|
|
38
|
+
agentbundle list-profiles
|
|
39
39
|
|
|
40
40
|
# Install a whole curated profile — a single-scope set of packs — in one command
|
|
41
|
-
agentbundle install --profile inception
|
|
41
|
+
agentbundle install --profile inception
|
|
42
42
|
|
|
43
43
|
# Preview any install without writing a file
|
|
44
|
-
agentbundle install --pack core
|
|
44
|
+
agentbundle install --pack core --dry-run
|
|
45
45
|
|
|
46
46
|
# Upgrade to the version the catalogue ships — shows installed → target, asks first
|
|
47
|
-
agentbundle upgrade --pack core
|
|
48
|
-
agentbundle upgrade --pack core
|
|
47
|
+
agentbundle upgrade --pack core
|
|
48
|
+
agentbundle upgrade --pack core --yes # skip the prompt (CI)
|
|
49
49
|
|
|
50
50
|
# Uninstall — previews remove (Tier-1) vs keep (your edits), asks first
|
|
51
51
|
agentbundle uninstall --pack core --dry-run
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Packaged install default — layer 4 of the catalogue-source resolution chain
|
|
2
|
+
# (RFC-0046 / ADR-0036). When `agentbundle install`/`upgrade` is run with no
|
|
3
|
+
# `catalogue` positional and layers 1–3 (explicit arg, user config, editable
|
|
4
|
+
# detection) yield nothing, this source is used.
|
|
5
|
+
#
|
|
6
|
+
# Private-fork pattern: blank the `source` value below (or delete this file
|
|
7
|
+
# entirely) to disable the packaged GitHub default — layers 1–3 still apply, so
|
|
8
|
+
# an editable clone or a `config set source` still resolves.
|
|
9
|
+
[defaults]
|
|
10
|
+
source = "git+https://github.com/eugenelim/agent-ready-repo"
|
|
@@ -72,7 +72,7 @@ def project_packs(pack_paths: list[Path], contract: dict, output_root: Path) ->
|
|
|
72
72
|
dir=str(agents_md.parent),
|
|
73
73
|
)
|
|
74
74
|
try:
|
|
75
|
-
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
75
|
+
with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as handle:
|
|
76
76
|
handle.write(stripped)
|
|
77
77
|
os.replace(tmp_path, agents_md)
|
|
78
78
|
except BaseException:
|
|
@@ -251,7 +251,7 @@ def _project_agent_as_md(
|
|
|
251
251
|
out_fm["readonly"] = readonly
|
|
252
252
|
|
|
253
253
|
output_text = _serialize_frontmatter_md(out_fm) + body
|
|
254
|
-
(target_dir / entry.name).write_text(output_text, encoding="utf-8")
|
|
254
|
+
(target_dir / entry.name).write_text(output_text, encoding="utf-8", newline="\n")
|
|
255
255
|
|
|
256
256
|
|
|
257
257
|
def _derive_readonly(frontmatter: dict[str, Any]) -> bool | None:
|
|
@@ -357,7 +357,7 @@ def _project_hooks_json(source_dir: Path, output_root: Path, rule: dict) -> None
|
|
|
357
357
|
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
358
358
|
target_path.write_text(
|
|
359
359
|
json.dumps(existing, indent=2, sort_keys=True) + "\n",
|
|
360
|
-
encoding="utf-8",
|
|
360
|
+
encoding="utf-8", newline="\n",
|
|
361
361
|
)
|
|
362
362
|
|
|
363
363
|
|
|
@@ -220,7 +220,7 @@ def _project_agent_as_md(
|
|
|
220
220
|
frontmatter, body = _split_frontmatter(entry.read_text(encoding="utf-8"))
|
|
221
221
|
out_fm = _apply_mapping(frontmatter, mapping, entry.stem)
|
|
222
222
|
output_text = _serialize_frontmatter_md(out_fm) + body
|
|
223
|
-
(target_dir / entry.name).write_text(output_text, encoding="utf-8")
|
|
223
|
+
(target_dir / entry.name).write_text(output_text, encoding="utf-8", newline="\n")
|
|
224
224
|
|
|
225
225
|
|
|
226
226
|
def _apply_mapping(
|
|
@@ -386,7 +386,7 @@ def _project_settings_json(
|
|
|
386
386
|
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
387
387
|
target_path.write_text(
|
|
388
388
|
json.dumps(existing, indent=2, sort_keys=True) + "\n",
|
|
389
|
-
encoding="utf-8",
|
|
389
|
+
encoding="utf-8", newline="\n",
|
|
390
390
|
)
|
|
391
391
|
|
|
392
392
|
|
|
@@ -255,7 +255,7 @@ def _project_agent_as_md(
|
|
|
255
255
|
|
|
256
256
|
output_text = _serialize_frontmatter_md(rewritten) + body
|
|
257
257
|
destination = target_dir / entry.name # preserves .md extension
|
|
258
|
-
destination.write_text(output_text, encoding="utf-8")
|
|
258
|
+
destination.write_text(output_text, encoding="utf-8", newline="\n")
|
|
259
259
|
|
|
260
260
|
|
|
261
261
|
def _serialize_frontmatter_md(fields: dict[str, Any]) -> str:
|
|
@@ -498,7 +498,7 @@ def _run_per_pack_single(
|
|
|
498
498
|
)
|
|
499
499
|
destination.write_text(
|
|
500
500
|
json.dumps(derived, indent=2, sort_keys=False) + "\n",
|
|
501
|
-
encoding="utf-8",
|
|
501
|
+
encoding="utf-8", newline="\n",
|
|
502
502
|
)
|
|
503
503
|
# Defence-in-depth: re-validate the written file against the schema
|
|
504
504
|
# to catch any serialise/parse divergence introduced by json.dumps.
|
|
@@ -547,7 +547,7 @@ def _run_per_pack_apm(recipe: Recipe, packs: list[Pack], output_dir: Path) -> di
|
|
|
547
547
|
pack_metadata = tomllib.loads((pack.path / "pack.toml").read_text(encoding="utf-8"))
|
|
548
548
|
(per_pack_output / "apm.yml").write_text(
|
|
549
549
|
_render_apm_yml(pack_metadata.get("pack", {})),
|
|
550
|
-
encoding="utf-8",
|
|
550
|
+
encoding="utf-8", newline="\n",
|
|
551
551
|
)
|
|
552
552
|
apm_source = pack.path / ".apm"
|
|
553
553
|
if apm_source.exists():
|
|
@@ -568,7 +568,7 @@ def _run_per_pack_apm(recipe: Recipe, packs: list[Pack], output_dir: Path) -> di
|
|
|
568
568
|
(hooks_dir / "install-marker.py").write_bytes(writer_bytes)
|
|
569
569
|
(hooks_dir / "install-marker.json").write_text(
|
|
570
570
|
json.dumps(_APM_INSTALL_MARKER_HOOK_JSON, indent=2) + "\n",
|
|
571
|
-
encoding="utf-8",
|
|
571
|
+
encoding="utf-8", newline="\n",
|
|
572
572
|
)
|
|
573
573
|
|
|
574
574
|
# Project pack.toml verbatim. The writer reads it for
|
|
@@ -630,7 +630,7 @@ def _run_aggregate(recipe: Recipe, output_dir: Path) -> dict:
|
|
|
630
630
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
631
631
|
output_path.write_text(
|
|
632
632
|
json.dumps({"plugins": entries}, indent=2, sort_keys=True) + "\n",
|
|
633
|
-
encoding="utf-8",
|
|
633
|
+
encoding="utf-8", newline="\n",
|
|
634
634
|
)
|
|
635
635
|
return {"recipe": recipe.name, "type": recipe.type, "entries": len(entries)}
|
|
636
636
|
|
|
@@ -331,4 +331,4 @@ def project_codex_agent_toml(
|
|
|
331
331
|
)
|
|
332
332
|
toml_lines = _emit_fields(rewritten, body)
|
|
333
333
|
destination = target_dir / (entry.stem + ".toml")
|
|
334
|
-
destination.write_text("\n".join(toml_lines) + "\n", encoding="utf-8")
|
|
334
|
+
destination.write_text("\n".join(toml_lines) + "\n", encoding="utf-8", newline="\n")
|
|
@@ -208,4 +208,4 @@ def project_copilot_agent_md(
|
|
|
208
208
|
_validate_tools(frontmatter["tools"])
|
|
209
209
|
emitted["tools"] = frontmatter["tools"]
|
|
210
210
|
destination = target_dir / (entry.stem + ".agent.md")
|
|
211
|
-
destination.write_text(_emit(emitted, body), encoding="utf-8")
|
|
211
|
+
destination.write_text(_emit(emitted, body), encoding="utf-8", newline="\n")
|
{agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/projections/gemini_command_toml.py
RENAMED
|
@@ -227,4 +227,4 @@ def project_gemini_command_toml(
|
|
|
227
227
|
|
|
228
228
|
destination = target_root / relative.with_suffix(".toml")
|
|
229
229
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
230
|
-
destination.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
230
|
+
destination.write_text("\n".join(lines) + "\n", encoding="utf-8", newline="\n")
|
{agentbundle-0.7.0 → agentbundle-0.8.0}/agentbundle/build/projections/merge_into_agent_json.py
RENAMED
|
@@ -243,7 +243,7 @@ def _atomic_write(target_path: Path, data: dict) -> None:
|
|
|
243
243
|
serialised = json.dumps(data, indent=2, sort_keys=False) + "\n"
|
|
244
244
|
with tempfile.NamedTemporaryFile(
|
|
245
245
|
mode="w",
|
|
246
|
-
encoding="utf-8",
|
|
246
|
+
encoding="utf-8", newline="\n",
|
|
247
247
|
dir=str(target_path.parent),
|
|
248
248
|
prefix=target_path.name + ".",
|
|
249
249
|
suffix=".tmp",
|
|
@@ -54,5 +54,5 @@ def project_merge_json(source_dir: Path, output_root: Path, rule: dict) -> None:
|
|
|
54
54
|
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
55
55
|
target_path.write_text(
|
|
56
56
|
json.dumps(existing, indent=2, sort_keys=True) + "\n",
|
|
57
|
-
encoding="utf-8",
|
|
57
|
+
encoding="utf-8", newline="\n",
|
|
58
58
|
)
|
|
@@ -296,7 +296,7 @@ def _atomic_write(target_path: Path, data: dict) -> None:
|
|
|
296
296
|
serialised = json.dumps(data, indent=2, sort_keys=False) + "\n"
|
|
297
297
|
with tempfile.NamedTemporaryFile(
|
|
298
298
|
mode="w",
|
|
299
|
-
encoding="utf-8",
|
|
299
|
+
encoding="utf-8", newline="\n",
|
|
300
300
|
dir=str(target_path.parent),
|
|
301
301
|
prefix=target_path.name + ".",
|
|
302
302
|
suffix=".tmp",
|
|
@@ -24,7 +24,9 @@ consumer.
|
|
|
24
24
|
Self-host scope (see docs/specs/self-hosting/spec.md § Phased rollout):
|
|
25
25
|
the `SELF_HOST_ADAPTERS` allow-list runs `claude-code` and `codex`.
|
|
26
26
|
Kiro and Copilot stay distribution-only so self-host does not project
|
|
27
|
-
`.kiro/` or `.github/instructions/`.
|
|
27
|
+
`.kiro/` or `.github/instructions/`. Both `SELF_HOST_ADAPTERS` and
|
|
28
|
+
`SELF_HOST_PACKS` are sourced from `recipes/self-host.toml` (see the
|
|
29
|
+
`_DEFAULT_*` block below); the values named here are the current defaults.
|
|
28
30
|
"""
|
|
29
31
|
|
|
30
32
|
from __future__ import annotations
|
|
@@ -79,27 +81,76 @@ TARGET_PATHS = (
|
|
|
79
81
|
Path("AGENTS.md"),
|
|
80
82
|
)
|
|
81
83
|
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
# are
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
# `monorepo-extras` is repo-only by metadata but the bundle is not a
|
|
94
|
-
# canonical monorepo consumer of `new-package`, so it's left out too.
|
|
95
|
-
# `_aggregate_marketplace` intentionally ignores this filter — the
|
|
96
|
-
# catalogue advertises every pack.
|
|
97
|
-
SELF_HOST_PACKS: tuple[str, ...] = (
|
|
84
|
+
# Built-in fallbacks for the self-host pack / adapter allow-lists. The
|
|
85
|
+
# authoritative values live in `recipes/self-host.toml` (read at import below);
|
|
86
|
+
# these are used only when that recipe is missing a key or can't be read, so
|
|
87
|
+
# module import stays total. Kiro and Copilot remain in the contract for
|
|
88
|
+
# distribution builds but are excluded from the self-host runner. This repo is
|
|
89
|
+
# the catalogue's home, not an adopter, so `make build-self` only projects the
|
|
90
|
+
# in-house packs; `_aggregate_marketplace` intentionally ignores the pack
|
|
91
|
+
# filter — the catalogue advertises every pack. See the recipe for the full
|
|
92
|
+
# rationale behind the pack selection.
|
|
93
|
+
_DEFAULT_SELF_HOST_ADAPTERS: tuple[str, ...] = ("claude-code", "codex")
|
|
94
|
+
_DEFAULT_SELF_HOST_PACKS: tuple[str, ...] = (
|
|
98
95
|
"core",
|
|
99
96
|
"governance-extras",
|
|
100
97
|
"user-guide-diataxis",
|
|
101
98
|
)
|
|
102
99
|
|
|
100
|
+
_SELF_HOST_RECIPE = "self-host"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _read_recipe_text(name: str) -> str | None:
|
|
104
|
+
"""Return the text of `recipes/<name>.toml`, or None if unreadable.
|
|
105
|
+
|
|
106
|
+
Mirrors `build.main`'s recipe resolution: filesystem first (dev / editable
|
|
107
|
+
install), then `importlib.resources` (zipapp, where the package lives inside
|
|
108
|
+
a `.pyz` archive that `Path.exists()` cannot traverse).
|
|
109
|
+
"""
|
|
110
|
+
recipe_path = Path(__file__).resolve().parent / "recipes" / f"{name}.toml"
|
|
111
|
+
if recipe_path.exists():
|
|
112
|
+
return recipe_path.read_text(encoding="utf-8")
|
|
113
|
+
try:
|
|
114
|
+
from importlib.resources import files
|
|
115
|
+
|
|
116
|
+
resource = files("agentbundle.build").joinpath(f"recipes/{name}.toml")
|
|
117
|
+
if resource.is_file():
|
|
118
|
+
return resource.read_text(encoding="utf-8")
|
|
119
|
+
except (FileNotFoundError, ModuleNotFoundError):
|
|
120
|
+
pass
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _extract_self_host_lists(
|
|
125
|
+
recipe: dict,
|
|
126
|
+
) -> tuple[tuple[str, ...], tuple[str, ...]]:
|
|
127
|
+
"""Pull `(packs, adapters)` from a parsed self-host recipe, substituting the
|
|
128
|
+
built-in defaults for any list that is absent or empty."""
|
|
129
|
+
body = recipe.get("recipe", {})
|
|
130
|
+
packs = tuple(body.get("packs", {}).get("include", ())) or _DEFAULT_SELF_HOST_PACKS
|
|
131
|
+
adapters = (
|
|
132
|
+
tuple(body.get("adapters", {}).get("targets", ()))
|
|
133
|
+
or _DEFAULT_SELF_HOST_ADAPTERS
|
|
134
|
+
)
|
|
135
|
+
return packs, adapters
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _load_self_host_lists() -> tuple[tuple[str, ...], tuple[str, ...]]:
|
|
139
|
+
"""Read the self-host allow-lists from the recipe. Total: any read or parse
|
|
140
|
+
failure falls back to the built-in defaults so module import never raises."""
|
|
141
|
+
try:
|
|
142
|
+
text = _read_recipe_text(_SELF_HOST_RECIPE)
|
|
143
|
+
if text is None:
|
|
144
|
+
return _DEFAULT_SELF_HOST_PACKS, _DEFAULT_SELF_HOST_ADAPTERS
|
|
145
|
+
return _extract_self_host_lists(tomllib.loads(text))
|
|
146
|
+
except Exception:
|
|
147
|
+
# Unreadable (non-UTF-8 / permission / IO) or malformed recipe — fall
|
|
148
|
+
# back so module import is total (AC3).
|
|
149
|
+
return _DEFAULT_SELF_HOST_PACKS, _DEFAULT_SELF_HOST_ADAPTERS
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
SELF_HOST_PACKS, SELF_HOST_ADAPTERS = _load_self_host_lists()
|
|
153
|
+
|
|
103
154
|
|
|
104
155
|
def _filter_self_host_packs(pack_paths: list[Path]) -> list[Path]:
|
|
105
156
|
"""Return the subset of *pack_paths* whose directory name is in
|
|
@@ -195,7 +246,7 @@ def resolve_markers(
|
|
|
195
246
|
text,
|
|
196
247
|
)
|
|
197
248
|
if replaced != text:
|
|
198
|
-
path.write_text(replaced, encoding="utf-8")
|
|
249
|
+
path.write_text(replaced, encoding="utf-8", newline="\n")
|
|
199
250
|
modified += 1
|
|
200
251
|
return modified
|
|
201
252
|
|
|
@@ -270,7 +321,7 @@ def _compose_agents_md(
|
|
|
270
321
|
body = body_path.read_text(encoding="utf-8").replace("\r\n", "\n")
|
|
271
322
|
if body and not body.endswith("\n"):
|
|
272
323
|
body += "\n"
|
|
273
|
-
target_path.write_text(body, encoding="utf-8")
|
|
324
|
+
target_path.write_text(body, encoding="utf-8", newline="\n")
|
|
274
325
|
|
|
275
326
|
if footer_path.exists():
|
|
276
327
|
text = target_path.read_text(encoding="utf-8")
|
|
@@ -279,7 +330,7 @@ def _compose_agents_md(
|
|
|
279
330
|
text += "\n"
|
|
280
331
|
if footer and not footer.endswith("\n"):
|
|
281
332
|
footer += "\n"
|
|
282
|
-
target_path.write_text(text + footer, encoding="utf-8")
|
|
333
|
+
target_path.write_text(text + footer, encoding="utf-8", newline="\n")
|
|
283
334
|
return target_path
|
|
284
335
|
|
|
285
336
|
|
|
@@ -329,6 +380,7 @@ EXCLUDED_PATTERNS: tuple[str, ...] = (
|
|
|
329
380
|
"README.md", # root-level; nested README.md not excluded
|
|
330
381
|
"LICENSE-*",
|
|
331
382
|
".gitignore",
|
|
383
|
+
".gitattributes",
|
|
332
384
|
".github/**",
|
|
333
385
|
"AGENTS.local.md",
|
|
334
386
|
"AGENTS.md", # root-level; nested AGENTS.md not excluded
|
|
@@ -535,7 +587,7 @@ def _aggregate_marketplace(
|
|
|
535
587
|
}
|
|
536
588
|
target.write_text(
|
|
537
589
|
json.dumps(payload, indent=2, sort_keys=True) + "\n",
|
|
538
|
-
encoding="utf-8",
|
|
590
|
+
encoding="utf-8", newline="\n",
|
|
539
591
|
)
|
|
540
592
|
return target
|
|
541
593
|
|
|
@@ -1463,10 +1515,42 @@ def run_build_check_drift_gates(
|
|
|
1463
1515
|
return 0
|
|
1464
1516
|
|
|
1465
1517
|
|
|
1518
|
+
def _refuse_fixture_packs_dir(packs_dir: Path, *, dry_run: bool) -> int | None:
|
|
1519
|
+
"""Refuse a real-write self-host whose `packs_dir` points into
|
|
1520
|
+
`tests/fixtures/` (which would overwrite the working tree with fixture
|
|
1521
|
+
data), unless `ALLOW_FIXTURE_PACKS` is set.
|
|
1522
|
+
|
|
1523
|
+
This is the cross-platform home of the guard that used to live only in the
|
|
1524
|
+
Makefile `build-self` recipe — so the make-free entry
|
|
1525
|
+
`python -m agentbundle.build self` (the only way to run build-self on
|
|
1526
|
+
Windows) is protected too. Returns a non-zero exit code to refuse, or
|
|
1527
|
+
`None` to proceed. Dry-run writes to a shadow temp dir, so it is never
|
|
1528
|
+
guarded (matching the `run_self_host` dirty-tree check). `as_posix()`
|
|
1529
|
+
normalises separators so the match is Windows-safe.
|
|
1530
|
+
"""
|
|
1531
|
+
if dry_run or os.environ.get("ALLOW_FIXTURE_PACKS"):
|
|
1532
|
+
return None
|
|
1533
|
+
# Trailing slash mirrors the historical Makefile glob `*tests/fixtures/*`
|
|
1534
|
+
# exactly — so a sibling like `my-tests/fixtures-backup/` doesn't over-match.
|
|
1535
|
+
if "tests/fixtures/" in packs_dir.as_posix():
|
|
1536
|
+
print(
|
|
1537
|
+
"self-host: refusing — --packs-dir points into tests/fixtures/; "
|
|
1538
|
+
"this would overwrite your working tree with fixture data. Set "
|
|
1539
|
+
"ALLOW_FIXTURE_PACKS=1 to override, or use --packs-dir packs.",
|
|
1540
|
+
file=sys.stderr,
|
|
1541
|
+
)
|
|
1542
|
+
return 2
|
|
1543
|
+
return None
|
|
1544
|
+
|
|
1545
|
+
|
|
1466
1546
|
def cmd_self(args) -> int:
|
|
1547
|
+
packs_dir = Path(args.packs_dir).resolve()
|
|
1548
|
+
refusal = _refuse_fixture_packs_dir(packs_dir, dry_run=args.dry_run)
|
|
1549
|
+
if refusal is not None:
|
|
1550
|
+
return refusal
|
|
1467
1551
|
return run_self_host(
|
|
1468
1552
|
working_tree=Path(args.output_dir).resolve(),
|
|
1469
|
-
packs_dir=
|
|
1553
|
+
packs_dir=packs_dir,
|
|
1470
1554
|
dry_run=args.dry_run,
|
|
1471
1555
|
force=args.force,
|
|
1472
1556
|
no_symlink=getattr(args, "no_symlink", False),
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""The tests/fixtures fixture-overwrite guard lives in the CLI handler.
|
|
2
|
+
|
|
3
|
+
`build-self` overwrites the working tree; pointing `--packs-dir` into
|
|
4
|
+
`tests/fixtures/` would overwrite it with fixture data. That guard used to live
|
|
5
|
+
only in the Makefile, so the make-free `python -m agentbundle.build self` entry
|
|
6
|
+
(the only one available on Windows) bypassed it. It now lives in `cmd_self` via
|
|
7
|
+
`_refuse_fixture_packs_dir`, honouring the same `ALLOW_FIXTURE_PACKS` override.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import os
|
|
14
|
+
import unittest
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from unittest import mock
|
|
17
|
+
|
|
18
|
+
import agentbundle.build.self_host as sh
|
|
19
|
+
from agentbundle.build.self_host import _refuse_fixture_packs_dir
|
|
20
|
+
|
|
21
|
+
_FIXTURE_DIR = Path("/repo/packages/agentbundle/tests/fixtures/packs")
|
|
22
|
+
_REAL_DIR = Path("/repo/packs")
|
|
23
|
+
|
|
24
|
+
# Empty string is falsy, so this simulates "unset" for the guard regardless of
|
|
25
|
+
# the ambient environment; "1" simulates the override being set.
|
|
26
|
+
_ENV_UNSET = {"ALLOW_FIXTURE_PACKS": ""}
|
|
27
|
+
_ENV_SET = {"ALLOW_FIXTURE_PACKS": "1"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class RefuseFixturePacksDirTest(unittest.TestCase):
|
|
31
|
+
def test_real_write_into_fixtures_refuses(self):
|
|
32
|
+
with mock.patch.dict(os.environ, _ENV_UNSET):
|
|
33
|
+
rc = _refuse_fixture_packs_dir(_FIXTURE_DIR, dry_run=False)
|
|
34
|
+
self.assertEqual(rc, 2)
|
|
35
|
+
|
|
36
|
+
def test_env_override_proceeds(self):
|
|
37
|
+
with mock.patch.dict(os.environ, _ENV_SET):
|
|
38
|
+
rc = _refuse_fixture_packs_dir(_FIXTURE_DIR, dry_run=False)
|
|
39
|
+
self.assertIsNone(rc)
|
|
40
|
+
|
|
41
|
+
def test_dry_run_is_never_guarded(self):
|
|
42
|
+
with mock.patch.dict(os.environ, _ENV_UNSET):
|
|
43
|
+
rc = _refuse_fixture_packs_dir(_FIXTURE_DIR, dry_run=True)
|
|
44
|
+
self.assertIsNone(rc)
|
|
45
|
+
|
|
46
|
+
def test_non_fixtures_dir_proceeds(self):
|
|
47
|
+
with mock.patch.dict(os.environ, _ENV_UNSET):
|
|
48
|
+
rc = _refuse_fixture_packs_dir(_REAL_DIR, dry_run=False)
|
|
49
|
+
self.assertIsNone(rc)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class CmdSelfGuardWiringTest(unittest.TestCase):
|
|
53
|
+
def test_cmd_self_refuses_and_does_not_run(self):
|
|
54
|
+
args = argparse.Namespace(
|
|
55
|
+
output_dir=".",
|
|
56
|
+
packs_dir=str(_FIXTURE_DIR),
|
|
57
|
+
dry_run=False,
|
|
58
|
+
force=False,
|
|
59
|
+
no_symlink=False,
|
|
60
|
+
)
|
|
61
|
+
with mock.patch.dict(os.environ, _ENV_UNSET), mock.patch.object(
|
|
62
|
+
sh, "run_self_host"
|
|
63
|
+
) as run:
|
|
64
|
+
rc = sh.cmd_self(args)
|
|
65
|
+
self.assertEqual(rc, 2)
|
|
66
|
+
run.assert_not_called()
|
|
67
|
+
|
|
68
|
+
def test_cmd_self_refuses_relative_fixtures_path(self):
|
|
69
|
+
# cmd_self resolves the packs-dir first; a *relative* tests/fixtures
|
|
70
|
+
# path must still match after resolve() + as_posix() (the real input
|
|
71
|
+
# shape, which the pure-helper tests above bypass).
|
|
72
|
+
args = argparse.Namespace(
|
|
73
|
+
output_dir=".",
|
|
74
|
+
packs_dir="agentbundle/build/tests/fixtures/packs",
|
|
75
|
+
dry_run=False,
|
|
76
|
+
force=False,
|
|
77
|
+
no_symlink=False,
|
|
78
|
+
)
|
|
79
|
+
with mock.patch.dict(os.environ, _ENV_UNSET), mock.patch.object(
|
|
80
|
+
sh, "run_self_host"
|
|
81
|
+
) as run:
|
|
82
|
+
rc = sh.cmd_self(args)
|
|
83
|
+
self.assertEqual(rc, 2)
|
|
84
|
+
run.assert_not_called()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
unittest.main()
|