@craftsamo/git-tools 0.0.0-stage → 0.2.0

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/README.md CHANGED
@@ -1,3 +1,407 @@
1
- # Temporary Holding Version
1
+ # git-tools
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Git tools for coding agents: state, commits, rewrites and stacked branches, worktrees and pushing.
4
+
5
+ The tools cover the state of a working tree, commits, history rewrites and
6
+ stacked branches, worktrees and pushing, against any git remote. Pull requests
7
+ and native stacks on GitHub are
8
+ [github-tools](https://github.com/craft-agent/github-tools)'. The toolsets are
9
+ listed under [Tools and skills](#tools-and-skills).
10
+
11
+ | Harness | Delivery | Pinned by |
12
+ | -------------- | ---------------------------------------------- | ------------------------------ |
13
+ | OpenCode V2 | native plugin (npm package, with skills) | the version in the package |
14
+ | OpenCode V1 | native plugin (npm package, no skills) | the version in the package |
15
+ | Claude Code | plugin: MCP server and skills | the release tag, as the ref |
16
+ | Codex | plugin: MCP server and skills | the release tag, as the ref |
17
+ | Grok Build | plugin: MCP server and skills | the release tag, as `@ref` |
18
+ | Antigravity | plugin: MCP server and skills | the release tag's tree URL |
19
+ | Hermes Agent | native plugin (Python, calls the command line) | the release commit, as the SHA |
20
+ | other MCP host | MCP server from npm | the version in the package |
21
+
22
+ The tools need [Node.js](https://nodejs.org/) 24 or later and `git`. Where
23
+ `origin` is on GitHub, a few safety checks also read it, authenticated as you.
24
+ [`docs/conventions.md`](docs/conventions.md) describes how the tools are named
25
+ and how they behave.
26
+
27
+ ## Install
28
+
29
+ Install from a release, never from the default branch: a release is a tag whose
30
+ commit carries the built `dist/`, and the default branch does not. In the
31
+ commands below, `<version>` is a release's version (`1.2.3`) and `<sha>` is the
32
+ commit its tag names:
33
+
34
+ ```sh
35
+ gh api repos/craft-agent/git-tools/commits/v<version> --jq .sha
36
+ ```
37
+
38
+ ### OpenCode
39
+
40
+ OpenCode V2 takes the package and its options in `opencode.json`:
41
+
42
+ ```json
43
+ {
44
+ "plugins": [{ "package": "@craftsamo/git-tools@<version>", "options": { "read_only": false } }],
45
+ "permissions": [{ "action": "git_*", "resource": "*", "effect": "ask" }]
46
+ }
47
+ ```
48
+
49
+ OpenCode V1 takes the package as a string, or as a pair with its options:
50
+
51
+ ```json
52
+ {
53
+ "plugin": [["@craftsamo/git-tools@<version>", { "read_only": false }]],
54
+ "permission": { "git_*": "ask" }
55
+ }
56
+ ```
57
+
58
+ OpenCode allows any action no rule names, so the plugin's request is granted
59
+ silently unless a rule says `ask`. Read tools never ask, so the `git_*` glob
60
+ is safe.
61
+
62
+ With the `worktree` toolset on and the profile not read-only, the V2 plugin
63
+ also registers a worktree strategy, so OpenCode's own new worktree and removal
64
+ follow the layout and the landed rule of the worktree tools: the name OpenCode
65
+ asks for becomes the branch, and a removal git refuses for changed files offers
66
+ OpenCode's force prompt where the harness supports it, while the tools
67
+ themselves never force. After `git_worktree_add` or `git_worktree_remove` the
68
+ plugin refreshes OpenCode's worktree list, so the change shows at once.
69
+ OpenCode's built-in worktree tools are left as they are; turn them off with
70
+ permission rules. V1 has no worktree strategies.
71
+
72
+ V1 registers no skills. Unpack the package and copy its `skills/*` folders into
73
+ `~/.config/opencode/skills/`:
74
+
75
+ ```sh
76
+ npm pack @craftsamo/git-tools@<version>
77
+ tar -xzf craftsamo-git-tools-<version>.tgz
78
+ mkdir -p ~/.config/opencode/skills
79
+ cp -r package/skills/* ~/.config/opencode/skills/
80
+ rm -rf package craftsamo-git-tools-<version>.tgz
81
+ ```
82
+
83
+ ### Claude Code
84
+
85
+ ```sh
86
+ claude plugin marketplace add craft-agent/git-tools#v<version>
87
+ claude plugin install git-tools@craft-agent
88
+ ```
89
+
90
+ ### Codex
91
+
92
+ ```sh
93
+ codex plugin marketplace add craft-agent/git-tools --ref v<version>
94
+ codex plugin add git-tools@craft-agent
95
+ ```
96
+
97
+ Codex turns `-` into `_` in server names, so it shows the tools as
98
+ `mcp__git_tools__git_*`. It starts the MCP server in the plugin's
99
+ install directory and passes no roots, so the server takes each call's directory
100
+ from what Codex sends with the call: its sandbox state, which the server asks
101
+ for, else the one workspace it names.
102
+
103
+ The plugin declares `default_tools_approval_mode = "writes"`: Codex asks before
104
+ every tool not marked read-only. A user's
105
+ `[plugins."<plugin>".mcp_servers.<server>]` setting can only tighten it. Setting
106
+ `auto` there yields `prompt` (reads asked too), since Codex treats `auto` and
107
+ `writes` as incomparable.
108
+
109
+ ### Grok Build
110
+
111
+ ```sh
112
+ grok plugin install craft-agent/git-tools@v<version> --trust
113
+ ```
114
+
115
+ Grok starts the MCP server in the session's working directory.
116
+
117
+ ### Antigravity
118
+
119
+ Install from the tree of the release tag, never from the default branch, which
120
+ has no built `dist/`; Antigravity clones the tag into
121
+ `~/.gemini/config/plugins/git-tools`.
122
+
123
+ ```sh
124
+ agy plugin install https://github.com/craft-agent/git-tools/tree/v<version>
125
+ ```
126
+
127
+ ### Hermes Agent
128
+
129
+ Hermes installs a commit, not a tag, and the plugin installs disabled:
130
+
131
+ ```sh
132
+ hermes plugins install craft-agent/git-tools --ref <sha>
133
+ hermes plugins enable git-tools
134
+ ```
135
+
136
+ If a Hermes gateway is already running, restart it (`hermes gateway restart`) to
137
+ load the plugin.
138
+
139
+ The plugin runs the Node it found when Hermes started. If a version manager
140
+ removes that version while Hermes runs, a call looks for Node again; when it
141
+ finds none, restart Hermes from a shell where `node` runs.
142
+
143
+ ### Any other MCP host
144
+
145
+ Add the server to the host's MCP configuration. Flags go in `args`:
146
+
147
+ ```json
148
+ {
149
+ "mcpServers": {
150
+ "git-tools": {
151
+ "command": "npx",
152
+ "args": ["-y", "@craftsamo/git-tools@<version>", "--read-only"]
153
+ }
154
+ }
155
+ }
156
+ ```
157
+
158
+ ## Tools and skills
159
+
160
+ Each toolset comes with a skill that tells an agent how to use its tools.
161
+ There is one toolset per domain; `repo`, `commit` and `rebase` are on by
162
+ default, `worktree` and `push` are opt-in.
163
+
164
+ A skill names its toolset under `metadata` in its frontmatter
165
+ (`metadata: { toolset: commit }`, written as a block). OpenCode V2 and Hermes
166
+ register it only when the settings register at least one of that toolset's
167
+ tools, so a profile with the toolset off, or with all its tools removed, does
168
+ not see it; a skill without a toolset is always registered. Claude Code,
169
+ Codex, Grok Build and Antigravity install the skill folder whole and pass the
170
+ settings only to the MCP server, so they list every skill whatever the
171
+ settings select. OpenCode V1 registers no skills, and its copied skills are
172
+ all listed too.
173
+
174
+ | Toolset | Covers |
175
+ | ---------- | ------------------------------------------------------------------------ |
176
+ | `repo` | local reads: status, history, where a change came from, conflicts |
177
+ | `commit` | staging, the checks before a commit, committing, amending and fixups |
178
+ | `rebase` | rebasing, stacked branches rebased together, and the rewrite rule |
179
+ | `worktree` | creating, listing and removing worktrees |
180
+ | `push` | pushing one or more branches, against the remote commit each one expects |
181
+
182
+ ### `repo`
183
+
184
+ One read tool, which changes nothing and reaches no remote. The `git-repo`
185
+ skill says when to use each method.
186
+
187
+ | Tool | Method | Reads |
188
+ | --------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
189
+ | `git_repo_read` | `status` | the branch and its upstream, how it stands against the remote default branch, files by state, an operation in progress, stashes, worktrees, the last fetch |
190
+ | | `conflicts` | what a merge, rebase, cherry-pick or revert left: its sides, and each conflicted file's marker regions or its lockfile command |
191
+ | | `conventions` | recent commit subjects with their conventional types and scopes, and the commit config files and pull request template in the tree |
192
+ | | `origin` | the commit a change came from, by sha, blame or pickaxe, with its full message |
193
+
194
+ ### `commit`
195
+
196
+ One read tool for the checks before a commit, and four write tools. The
197
+ `git-commit` skill says how they fit together.
198
+
199
+ | Tool | Class | Does |
200
+ | ------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
201
+ | `git_commit_read` | read | `lint`: a candidate message against the neutral rules and the repository's commitlint; `secrets`: credentials in the staged changes, the unstaged changes or a range; `hunks`: the unstaged hunks with the digest of their diff; `amend`: whether to correct a commit by amend, fixup or a new commit |
202
+ | `git_commit_stage` | idempotent change | stages the hunks of a listing, pinned by its digest |
203
+ | `git_commit_create` | add | commits what is staged with the checked message, or a fixup of an earlier commit |
204
+ | `git_commit_amend` | overwrite | replaces HEAD with the staged changes and a message, under the rule for rewriting commits |
205
+ | `git_commit_verify` | administrative | runs a command at each commit of the branch in a throwaway worktree |
206
+
207
+ ### `rebase`
208
+
209
+ One read tool for the stack of the checked-out branch, and three write tools.
210
+ A stack is read from ancestry, so a branch alone is a stack of one layer. The
211
+ `git-rebase` skill says how they fit together.
212
+
213
+ | Tool | Class | Does |
214
+ | ------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
215
+ | `git_rebase_read` | read | `stack`: the layers of the branch's line, bottom to top, with their commits and fixups, how each stands to its remote branch, and whether the whole stack may be rewritten |
216
+ | `git_rebase_start` | overwrite | rebases every layer onto the latest default branch, or folds the fixups in place, under the rule for rewriting commits; a stop on conflicts is a paused result |
217
+ | `git_rebase_resume` | overwrite | continues, skips or aborts a stopped rebase |
218
+ | `git_rebase_split` | add | cuts the branch into lower layer branches at given commits, or adds an empty layer on top |
219
+
220
+ ### `worktree`
221
+
222
+ One read tool that lists the worktrees, and two write tools that add one where
223
+ the layout puts it and remove one without losing work. The toolset is opt-in.
224
+ The `git-worktree` skill says how they fit together, and the layout is under
225
+ [Settings](#settings).
226
+
227
+ | Tool | Class | Does |
228
+ | --------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
229
+ | `git_worktree_read` | read | `list`: every worktree of the repository from git alone: its directory, branch and commit, whether it is the main one or the caller's, locked or gone, and whether it sits where the layout puts it |
230
+ | `git_worktree_add` | add | adds a worktree for a branch at `<root>/<repository>/<branch>`: an existing branch as it is, a new one cut from `from`, else from origin's fetched default branch, with no upstream and no hooks on the checkout |
231
+ | `git_worktree_remove` | overwrite | removes a worktree, never forced: refuses changed files, commits no ref holds and an operation in progress; deletes its branch only once proven landed (ancestry, or a merged pull request on GitHub), and says why a branch stays |
232
+
233
+ ### `push`
234
+
235
+ One read tool that previews a push, and one write tool that pushes exactly
236
+ what the preview returned, leased on the remote commit each branch expects.
237
+ The toolset is opt-in. The `git-push` skill says how they fit together.
238
+
239
+ | Tool | Class | Does |
240
+ | --------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
241
+ | `git_push_read` | read | `preview`: per branch of the line, read live from the remote, the kind of push, the commits the remote lacks and what a force drops; refusals, warnings, the secret scan and the published-text rules; and `send`, the arguments that push exactly this |
242
+ | `git_push_send` | administrative | pushes the preview's `send` in one atomic push with a lease per branch, after checking it again; a branch or remote that moved is `stale`, and nothing is pushed |
243
+
244
+ ## Adding a tool
245
+
246
+ 1. Write the spec in `src/core/<domain>/<verb>.ts` with `defineTool`: an ID
247
+ `<prefix>_<domain>_<verb>`, a title and description, the Zod argument and
248
+ output schemas, its toolset and write class, and `execute(args, context)`.
249
+ A write tool also gives `targets(args, context)`: what the call writes to,
250
+ as a permission rule should name it.
251
+ 2. Test it beside the spec (`<verb>.test.ts`), calling it through `invoke` as
252
+ the entry points do. A tool that runs a program is tested against a fake of
253
+ that program on `PATH`; no test reaches a real service.
254
+ 3. List it in `src/tools.ts`. A new toolset goes into `toolsets` in
255
+ `src/project.ts` (and `defaultToolsets` when it is on by default), a
256
+ setting of the package's own into `options` (and `OPTIONS` in `project.py`
257
+ with `config_schema` in `plugin.yaml`), and a variable the tool reads
258
+ besides the settings into `toolVariables`.
259
+ 4. Add or update the toolset's skill, `skills/<prefix>-<toolset>/SKILL.md`,
260
+ whose frontmatter names the toolset under `metadata`.
261
+ 5. Run `pnpm run build && pnpm run manifests` to list the tool in
262
+ `plugin.yaml` and its variables in Codex's manifest, then describe it under
263
+ "Tools and skills".
264
+
265
+ [`docs/conventions.md`](docs/conventions.md) has the rules each step follows.
266
+
267
+ ## Reads and writes
268
+
269
+ Every read tool is named `<prefix>_<domain>_read`. Reads can run unasked;
270
+ writes are confirmed by the harness.
271
+
272
+ | Harness | Reads without asking |
273
+ | ----------- | ------------------------------------------------------------------------------------------------------ |
274
+ | OpenCode | always; the plugin asks only before a write |
275
+ | Claude Code | the rule `mcp__plugin_git-tools_git-tools__*_read` in `permissions.allow` of `~/.claude/settings.json` |
276
+ | Codex | always; Codex runs tools that declare `readOnlyHint` unasked, as the read tools do |
277
+ | Grok Build | `grok --allow 'MCPTool(git-tools__*_read)' --allow 'MCPTool(git_*_read)'` (both rules; see below) |
278
+ | Antigravity | the rule `mcp(git-tools_git-tools/<tool id>)` in `permissions.allow`, one for each read tool |
279
+ | Hermes | always; the plugin asks only before a write |
280
+
281
+ Where a write is asked about, and where it is not:
282
+
283
+ - **OpenCode** grants any action no rule names, so a write runs unasked unless
284
+ a rule asks for it: add the `git_*` rule shown under [OpenCode](#opencode).
285
+ - **Codex** asks before every write because of the plugin's approval mode.
286
+ Under `codex exec` it refuses with "MCP tool call requires approval, but
287
+ approval policy is never".
288
+ - **Grok Build** under `grok -p` refuses a write it cannot ask about, unless it
289
+ is allowed beforehand.
290
+ - **Grok Build** calls the plugin's tools through its `use_tool` tool, and the
291
+ agent often names a tool without its server first. Grok checks the rules
292
+ against the name as given, so the server-qualified rule alone does not match
293
+ that first call. The second rule matches the short name; a write still
294
+ matches neither.
295
+ - **Antigravity** asks before any MCP tool no rule allows, reads included, and
296
+ names the plugin's server `git-tools_git-tools` (plugin,
297
+ then server). `agy -p` refuses a tool it cannot ask about.
298
+ - **Hermes** `hermes chat -q` refuses a write it cannot ask about; `hermes -z`
299
+ and `--yolo` skip approval, so writes then run unasked.
300
+
301
+ ## Settings
302
+
303
+ Every entry point takes the same settings. A flag wins over its environment
304
+ variable, and an option or setting wins over the variable too.
305
+
306
+ | Setting | Flag | Variable | Meaning |
307
+ | ---------------------- | ------------------------ | -------------------------------- | ---------------------------------------------------------------------------------------------------------- |
308
+ | `toolsets` | `--toolsets` | `GIT_TOOLS_TOOLSETS` | the toolsets that are on, or `all`; default those `src/project.ts` names |
309
+ | `read_only` | `--read-only` | `GIT_TOOLS_READ_ONLY` | registers no tool that writes; wins over every other setting |
310
+ | `tools` | `--tools` | `GIT_TOOLS_TOOLS` | single tools to add by ID, whatever their toolset |
311
+ | `exclude_tools` | `--exclude-tools` | `GIT_TOOLS_EXCLUDE_TOOLS` | single tools to remove by ID; wins over `tools` and `toolsets` |
312
+ | `confirm` | `--confirm` | `GIT_TOOLS_CONFIRM` | MCP server only: `harness` (default) or `elicitation`, as the conventions say |
313
+ | `language` | `--language` | `GIT_TOOLS_LANGUAGE` | native plugins only: the approval prompt's language, such as `ja` |
314
+ | `emoji` | — | — | Hermes only: the emoji shown beside each toolset, by toolset name (`""` for none) |
315
+ | `published_text_rules` | `--published-text-rules` | `GIT_TOOLS_PUBLISHED_TEXT_RULES` | the absolute path of a JSON file of published-text rules (see the push toolset); unset, nothing is checked |
316
+ | `worktree_root` | `--worktree-root` | `GIT_TOOLS_WORKTREE_ROOT` | the absolute path where task worktrees go (`~` allowed); unset, `~/worktrees` |
317
+
318
+ Lists are comma-separated in a flag or a variable. A package's own settings,
319
+ listed in `options` in `src/project.ts`, take a row each here and are read the
320
+ same way: `published_text_rules` and `worktree_root` hold text, trimmed and
321
+ blank meaning unset, and the tools read them themselves. The layout is
322
+ `<root>/<repository>/<branch>`, with each `/` of the branch written `-`; the
323
+ repository is the last part of `origin`'s URL without `.git`, else the folder
324
+ name of the main checkout. Where each harness reads them:
325
+
326
+ - **OpenCode** takes the plugin `options` shown above, with lists as arrays and
327
+ `read_only` as a boolean, and falls back to the variables. V2 runs plugins in
328
+ its background service, which keeps the environment it started with: a
329
+ variable set where `opencode run` starts has no effect until the service is
330
+ restarted in that environment (`opencode service restart`). V1 runs plugins
331
+ in its own process.
332
+ - **Hermes** takes `plugins.entries.git-tools.settings` in
333
+ `config.yaml`:
334
+
335
+ ```yaml
336
+ plugins:
337
+ entries:
338
+ git-tools:
339
+ settings:
340
+ read_only: false
341
+ exclude_tools: []
342
+ published_text_rules: /home/me/.config/git-tools/published-text.json
343
+ worktree_root: ~/worktrees
344
+ ```
345
+
346
+ Without a `language` setting, the approval card follows Hermes's own
347
+ `display.language` (or `HERMES_LANGUAGE`), the language of its approval
348
+ buttons, and else stays in English. The plugin passes its own environment
349
+ to the command line it runs, so the variables work too. A running gateway keeps the environment it started with;
350
+ restart it (`hermes gateway restart`) after changing one.
351
+
352
+ - **Claude Code, Grok Build and Antigravity** start the plugin's server without
353
+ flags and pass it the client's environment, so set the variables in the
354
+ environment the harness runs in.
355
+ - **Codex** passes a plugin's MCP server only a few basic variables (such as
356
+ `PATH` and `HOME`) and those the plugin's manifest names, which are the
357
+ variables above: set them in the environment Codex runs in. A variable the
358
+ tools read besides the settings belongs in `toolVariables` in
359
+ `src/project.ts`; `pnpm run manifests` adds it to the manifest after the
360
+ settings' variables. To pass flags, or to give the server a variable the
361
+ package does not name, add the server under `[mcp_servers]` in Codex's
362
+ `config.toml` instead of using the plugin: flags go in `args`, `env_vars`
363
+ names the variables to forward from Codex's environment, and `env` sets
364
+ fixed values.
365
+
366
+ ```toml
367
+ [mcp_servers.git-tools]
368
+ command = "npx"
369
+ args = ["-y", "@craftsamo/git-tools@<version>", "--read-only"]
370
+ env_vars = ["GIT_TOOLS_TOOLSETS"]
371
+ ```
372
+
373
+ ## Layout
374
+
375
+ - `src/project.ts` — the package's name, tool ID prefix, setting prefix,
376
+ toolsets, tool options and the other variables its tools read.
377
+ - `src/core/` — the tool contract and the tools, free of any harness and of
378
+ the project's names.
379
+ - `src/tools.ts` — the list of every tool, which each entry point registers.
380
+ - `src/words.ts` — the translations of the tools' words, per language, which
381
+ the native plugins use in their approval prompts.
382
+ - `src/mcp/` — the MCP server, built to `dist/mcp.js` and run by
383
+ `git-tools`.
384
+ - `src/opencode/` — the OpenCode plugin, built to `dist/opencode.js`.
385
+ - `src/cli/` — the command line the Hermes plugin calls, built to `dist/cli.js`
386
+ and run by `git-tools-cli`.
387
+ - `src/hermes/`, `plugin.yaml` and `__init__.py` — the Hermes plugin.
388
+ - `skills/` — one skill per toolset, `skills/<name>/SKILL.md`, which every
389
+ harness that takes skills loads from the package or plugin root; the native
390
+ plugins load only those of the toolsets that are on.
391
+ - `.claude-plugin/`, `.codex-plugin/`, `.agents/plugins/`, `.mcp.json`,
392
+ `.antigravity-plugin/` and `mcp_config.json` — the plugin manifests of the
393
+ other harnesses. Codex reads `.codex-plugin/plugin.json`; a root
394
+ `plugin.json` would make it load the plugin as an Agent Plugin, whose tool
395
+ budget hides tools past a fixed size. The root `.mcp.json` is the
396
+ configuration Grok Build reads (with `${CLAUDE_PLUGIN_ROOT}`, the variable
397
+ both Grok Build and Claude Code expand); when working on this repository in
398
+ Claude Code, decline the project MCP server it offers from that file.
399
+ - `opencode.jsonc` — OpenCode's configuration for working on this repository,
400
+ not part of the package: it references the starter it was made from and
401
+ github-tools, so an agent can read them beside this code.
402
+
403
+ How a release is made is in [`RELEASING.md`](RELEASING.md).
404
+
405
+ ## License
406
+
407
+ [AGPL-3.0 with Commons Clause](LICENSE)
package/__init__.py ADDED
@@ -0,0 +1,36 @@
1
+ """Loads the plugin package from ``src/hermes/git_tools_hermes`` for Hermes, which imports this file.
2
+
3
+ The package is loaded as ``<this module's name>._impl``, so Hermes, which evicts
4
+ everything under a plugin's module name when it reloads it or loads it for another
5
+ profile, evicts the package too, and each install path gets a package of its own.
6
+ """
7
+
8
+ import importlib.util
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ _NAME = f"{__name__}._impl"
13
+ _PACKAGE_DIR = Path(__file__).resolve().parent / "src" / "hermes" / "git_tools_hermes"
14
+
15
+
16
+ def _evict() -> None:
17
+ """Drop the package and its submodules from ``sys.modules``."""
18
+ for name in list(sys.modules):
19
+ if name == _NAME or name.startswith(f"{_NAME}."):
20
+ sys.modules.pop(name, None)
21
+
22
+
23
+ _spec = importlib.util.spec_from_file_location(
24
+ _NAME, _PACKAGE_DIR / "__init__.py", submodule_search_locations=[str(_PACKAGE_DIR)]
25
+ )
26
+ _module = importlib.util.module_from_spec(_spec)
27
+ _evict()
28
+ sys.modules[_NAME] = _module
29
+ try:
30
+ _spec.loader.exec_module(_module)
31
+ except BaseException:
32
+ _evict()
33
+ raise
34
+
35
+ register = _module.register
36
+ __all__ = ["register"]