eai-supcore 0.5.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.
@@ -0,0 +1,3 @@
1
+ """Shared steady-state bootstrap APIs for the eai-sup* CLI family."""
2
+
3
+ __version__ = "0.5.0"
@@ -0,0 +1,269 @@
1
+ """Shared steady-state bootstrap steps for eai-supinit and eai-supmig."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+
12
+ from eai_supcore.git_ops import GitError, add_submodule, is_git_repository, submodule_exists
13
+ from eai_supcore.io_util import read_text, write_text
14
+ from eai_supcore.paths import (
15
+ AI_RULES_SUBMODULE,
16
+ CURSOR_RULES_DIR,
17
+ DEFAULT_AI_RULES_URL,
18
+ ESYSREPO_MANIFEST_NAME,
19
+ STEADY_STATE_DIRS,
20
+ TOOLS_LOCAL_GITIGNORE_ENTRY,
21
+ )
22
+ from eai_supcore.templates import resolve_consumer_templates, resolve_template_file
23
+
24
+ CONSUMER_GITIGNORE_ENTRIES = (
25
+ TOOLS_LOCAL_GITIGNORE_ENTRY,
26
+ ".venv/",
27
+ "venv/",
28
+ "build/",
29
+ "cmake-build-*/",
30
+ "out/",
31
+ ".idea/",
32
+ ".vscode/",
33
+ "*.swp",
34
+ ".DS_Store",
35
+ "Thumbs.db",
36
+ )
37
+
38
+
39
+ @dataclass
40
+ class SharedBootstrapResult:
41
+ created_dirs: list[str] = field(default_factory=list)
42
+ created_files: list[str] = field(default_factory=list)
43
+ submodule_added: bool = False
44
+ ai_rules_found: bool = False
45
+ cursor_rules_materialized: list[str] = field(default_factory=list)
46
+ warnings: list[str] = field(default_factory=list)
47
+ errors: list[str] = field(default_factory=list)
48
+
49
+
50
+ def ensure_steady_state_dirs(workspace: Path, result: SharedBootstrapResult) -> None:
51
+ """Create steady-state layout dirs (src/, libs/, docs/adr/, prompts/)."""
52
+ for rel in STEADY_STATE_DIRS:
53
+ target = workspace / rel
54
+ if not target.exists():
55
+ target.mkdir(parents=True, exist_ok=True)
56
+ result.created_dirs.append(rel.as_posix())
57
+
58
+
59
+ def _merge_gitignore_entries(
60
+ workspace: Path, entries: tuple[str, ...], result: SharedBootstrapResult
61
+ ) -> None:
62
+ gitignore = workspace / ".gitignore"
63
+ existing_lines: list[str] = []
64
+ if gitignore.is_file():
65
+ content = read_text(gitignore)
66
+ existing_lines = content.splitlines()
67
+ present = {line.strip() for line in existing_lines}
68
+ else:
69
+ content = ""
70
+ present = set()
71
+
72
+ missing = [e for e in entries if e not in present and e.rstrip("/") not in present]
73
+ if not missing:
74
+ return
75
+
76
+ blocks: list[str] = []
77
+ if not gitignore.is_file():
78
+ blocks.append("# Generated / updated by eai-sup* bootstrap")
79
+ elif content and not content.endswith("\n"):
80
+ existing_lines.append("")
81
+
82
+ # Keep tools_local comment when adding that entry
83
+ for entry in missing:
84
+ if entry == TOOLS_LOCAL_GITIGNORE_ENTRY and "# eai-sup* local installs" not in "\n".join(
85
+ existing_lines
86
+ ):
87
+ blocks.append("# eai-sup* local installs (ADR-0024)")
88
+ blocks.append(entry)
89
+
90
+ new_body = "\n".join([*existing_lines, *blocks]).rstrip() + "\n"
91
+ write_text(gitignore, new_body)
92
+ if ".gitignore" not in result.created_files:
93
+ result.created_files.append(".gitignore")
94
+
95
+
96
+ def ensure_tools_local_gitignore(workspace: Path, result: SharedBootstrapResult) -> None:
97
+ """Ensure ``.tools_local/`` is listed in consumer ``.gitignore``."""
98
+ _merge_gitignore_entries(workspace, (TOOLS_LOCAL_GITIGNORE_ENTRY,), result)
99
+
100
+
101
+ def ensure_consumer_gitignore(workspace: Path, result: SharedBootstrapResult) -> None:
102
+ """Merge language-agnostic consumer ``.gitignore`` entries."""
103
+ _merge_gitignore_entries(workspace, CONSUMER_GITIGNORE_ENTRIES, result)
104
+
105
+
106
+ def _write_new_from_template(
107
+ workspace: Path,
108
+ dest_rel: str,
109
+ template_root: Path,
110
+ result: SharedBootstrapResult,
111
+ ) -> None:
112
+ dest = workspace / dest_rel
113
+ if dest.exists():
114
+ return
115
+ src = resolve_template_file(template_root, dest_rel)
116
+ if src is None:
117
+ result.warnings.append(f"Template missing: {dest_rel}")
118
+ return
119
+ dest.parent.mkdir(parents=True, exist_ok=True)
120
+ shutil.copy2(src, dest)
121
+ result.created_files.append(dest_rel.replace("\\", "/"))
122
+
123
+
124
+ def ensure_esysrepo_manifest(
125
+ workspace: Path, result: SharedBootstrapResult, *, template_root: Path
126
+ ) -> None:
127
+ """Scaffold basic ``.esysrepo.manifest.xml`` once if missing."""
128
+ _write_new_from_template(workspace, ESYSREPO_MANIFEST_NAME, template_root, result)
129
+
130
+
131
+ def install_dev_hooks(workspace: Path, result: SharedBootstrapResult) -> None:
132
+ """Run consumer ``scripts/setup-dev.py`` when Python + git are available (warnings on fail)."""
133
+ script = workspace / "scripts" / "setup-dev.py"
134
+ if not script.is_file():
135
+ result.warnings.append("scripts/setup-dev.py missing — skipped hook install")
136
+ return
137
+ if shutil.which("git") is None:
138
+ result.warnings.append("git not on PATH — skipped hook install")
139
+ return
140
+ py = Path(sys.executable) if sys.executable else None
141
+ if py is None or not py.is_file():
142
+ result.warnings.append("Python not available — skipped hook install")
143
+ return
144
+ try:
145
+ proc = subprocess.run(
146
+ [str(py), str(script)],
147
+ cwd=workspace,
148
+ capture_output=True,
149
+ text=True,
150
+ check=False,
151
+ env={**os.environ},
152
+ )
153
+ if proc.returncode != 0:
154
+ detail = (proc.stderr or proc.stdout or "").strip()
155
+ result.warnings.append(
156
+ f"setup-dev.py failed (exit {proc.returncode})" + (f": {detail}" if detail else "")
157
+ )
158
+ except OSError as exc:
159
+ result.warnings.append(f"setup-dev.py failed: {exc}")
160
+
161
+
162
+ def scaffold_consumer_dev_rails(
163
+ workspace: Path,
164
+ result: SharedBootstrapResult | None = None,
165
+ *,
166
+ with_hooks: bool = True,
167
+ with_manifest: bool = True,
168
+ install_hooks: bool = False,
169
+ ) -> SharedBootstrapResult:
170
+ """Materialize language-agnostic consumer rails (hooks, gitignore, setup-dev, manifest)."""
171
+ if result is None:
172
+ result = SharedBootstrapResult()
173
+ try:
174
+ template_root = resolve_consumer_templates(workspace)
175
+ except FileNotFoundError as exc:
176
+ result.errors.append(str(exc))
177
+ return result
178
+
179
+ ensure_consumer_gitignore(workspace, result)
180
+
181
+ if with_hooks:
182
+ _write_new_from_template(workspace, ".pre-commit-config.yaml", template_root, result)
183
+ _write_new_from_template(workspace, ".gitlint", template_root, result)
184
+ scripts_dir = workspace / "scripts"
185
+ if not scripts_dir.exists():
186
+ scripts_dir.mkdir(parents=True, exist_ok=True)
187
+ result.created_dirs.append("scripts")
188
+ _write_new_from_template(workspace, "scripts/setup-dev.py", template_root, result)
189
+
190
+ if with_manifest:
191
+ ensure_esysrepo_manifest(workspace, result, template_root=template_root)
192
+
193
+ if install_hooks and with_hooks:
194
+ install_dev_hooks(workspace, result)
195
+
196
+ return result
197
+
198
+
199
+ def mount_ai_rules(
200
+ workspace: Path,
201
+ result: SharedBootstrapResult,
202
+ *,
203
+ ai_rules_url: str = DEFAULT_AI_RULES_URL,
204
+ ) -> None:
205
+ """Mount ``tools/ai-rules`` as submodule or reuse an existing git checkout."""
206
+ ai_rules_path = workspace / AI_RULES_SUBMODULE
207
+ if ai_rules_path.is_dir() and is_git_repository(ai_rules_path):
208
+ result.ai_rules_found = True
209
+ return
210
+ if submodule_exists(workspace, AI_RULES_SUBMODULE):
211
+ result.warnings.append("tools/ai-rules submodule already configured")
212
+ return
213
+ if ai_rules_path.exists():
214
+ result.warnings.append(
215
+ "tools/ai-rules exists but is not a git repository; skipping submodule add"
216
+ )
217
+ return
218
+ try:
219
+ add_submodule(workspace, ai_rules_url, AI_RULES_SUBMODULE)
220
+ result.submodule_added = True
221
+ except GitError as exc:
222
+ result.errors.append(str(exc))
223
+
224
+
225
+ def materialize_cursor_rules(
226
+ workspace: Path,
227
+ result: SharedBootstrapResult,
228
+ ) -> None:
229
+ """Copy shared Cursor ``.mdc`` rules from ``tools/ai-rules/cursor/rules``."""
230
+ ai_rules_path = workspace / AI_RULES_SUBMODULE
231
+ shared_rules = ai_rules_path / "cursor" / "rules"
232
+ cursor_dir = workspace / CURSOR_RULES_DIR
233
+ if not shared_rules.is_dir():
234
+ result.warnings.append(
235
+ f"Shared Cursor rules not found at {shared_rules}; run with --with-ai-rules first"
236
+ )
237
+ return
238
+ cursor_dir.mkdir(parents=True, exist_ok=True)
239
+ for src in sorted(shared_rules.glob("*.mdc")):
240
+ dest = cursor_dir / src.name
241
+ if dest.exists():
242
+ result.warnings.append(f"Skipped existing Cursor rule: {dest.name}")
243
+ continue
244
+ shutil.copy2(src, dest)
245
+ result.cursor_rules_materialized.append(dest.name)
246
+
247
+
248
+ def write_cursor_overlay(
249
+ workspace: Path,
250
+ result: SharedBootstrapResult,
251
+ *,
252
+ overlay_name: str,
253
+ content: str,
254
+ ) -> None:
255
+ """Write a local Cursor overlay if missing."""
256
+ cursor_dir = workspace / CURSOR_RULES_DIR
257
+ cursor_dir.mkdir(parents=True, exist_ok=True)
258
+ overlay = cursor_dir / overlay_name
259
+ if overlay.exists():
260
+ return
261
+ write_text(overlay, content)
262
+ result.created_files.append(str(overlay.relative_to(workspace)).replace("\\", "/"))
263
+
264
+
265
+ def materialized_cursor_rule_names(workspace: Path) -> list[str]:
266
+ shared_rules = workspace / AI_RULES_SUBMODULE / "cursor" / "rules"
267
+ if not shared_rules.is_dir():
268
+ return []
269
+ return sorted(item.name for item in shared_rules.glob("*.mdc") if item.is_file())
@@ -0,0 +1 @@
1
+ """Package data marker for bundled consumer / extension templates."""
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <esysrepo_manifest kind="embedded" schema-version="1.1">
3
+ <default revision="refs/heads/develop" sync-j="4" />
4
+ <location name="origin" addr="ssh://git@gitlab.com/YOUR_GROUP" alt_addr="https://gitlab.com/YOUR_GROUP">
5
+ <!-- Add repositories under src/ and libs/ as members are added -->
6
+ </location>
7
+ </esysrepo_manifest>
@@ -0,0 +1,18 @@
1
+ # eai-sup* local installs (ADR-0024)
2
+ .tools_local/
3
+
4
+ # Local Python environment (created by scripts/setup-dev.py)
5
+ .venv/
6
+ venv/
7
+
8
+ # Build artifacts
9
+ build/
10
+ cmake-build-*/
11
+ out/
12
+
13
+ # Editor / OS
14
+ .idea/
15
+ .vscode/
16
+ *.swp
17
+ .DS_Store
18
+ Thumbs.db
@@ -0,0 +1,11 @@
1
+ [general]
2
+ ignore=body-is-missing
3
+
4
+ [title-max-length]
5
+ line-length=72
6
+
7
+ [title-match-regex]
8
+ regex=^(WIP: )?((adr|build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9._/-]+\))?(!)?): [A-Z0-9].+$
9
+
10
+ [body-max-line-length]
11
+ line-length=100
@@ -0,0 +1,30 @@
1
+ ci:
2
+ autoupdate_commit_msg: "chore: Update pre-commit hooks"
3
+ autofix_commit_msg: "style: Apply pre-commit fixes"
4
+
5
+ repos:
6
+ - repo: https://github.com/pre-commit/pre-commit-hooks
7
+ rev: v4.6.0
8
+ hooks:
9
+ - id: check-case-conflict
10
+ - id: check-merge-conflict
11
+ - id: check-yaml
12
+ - id: end-of-file-fixer
13
+ exclude: ^\.cursor/
14
+ - id: mixed-line-ending
15
+ args: [--fix=lf]
16
+ - id: trailing-whitespace
17
+ exclude: ^\.cursor/
18
+
19
+ - repo: https://github.com/Lucas-C/pre-commit-hooks
20
+ rev: v1.5.5
21
+ hooks:
22
+ - id: remove-tabs
23
+ files: "\\.(md|mdc|ya?ml)$"
24
+
25
+ - repo: https://github.com/jorisroovers/gitlint
26
+ rev: v0.19.1
27
+ hooks:
28
+ - id: gitlint
29
+ stages: [commit-msg]
30
+ args: ["--msg-filename"]
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env python3
2
+ """Create/use .venv, install pre-commit, register pre-commit + commit-msg hooks.
3
+
4
+ Usage (from repository root):
5
+ python scripts/setup-dev.py
6
+ python3 scripts/setup-dev.py
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import shutil
13
+ import subprocess
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ ROOT = Path(__file__).resolve().parent.parent
18
+ VENV_DIR = ROOT / ".venv"
19
+
20
+
21
+ def log(msg: str) -> None:
22
+ print(f"[setup-dev] {msg}")
23
+
24
+
25
+ def die(msg: str) -> None:
26
+ print(f"[setup-dev] ERROR: {msg}", file=sys.stderr)
27
+ raise SystemExit(1)
28
+
29
+
30
+ def venv_python(venv_dir: Path) -> Path:
31
+ if sys.platform == "win32":
32
+ return venv_dir / "Scripts" / "python.exe"
33
+ return venv_dir / "bin" / "python"
34
+
35
+
36
+ def resolve_python() -> Path:
37
+ active = os.environ.get("VIRTUAL_ENV")
38
+ if active:
39
+ candidate = venv_python(Path(active))
40
+ if candidate.is_file():
41
+ return candidate
42
+ existing = venv_python(VENV_DIR)
43
+ if existing.is_file():
44
+ return existing
45
+ host = Path(sys.executable) if sys.executable else None
46
+ if host is None or not host.is_file():
47
+ for name in ("python3", "python"):
48
+ found = shutil.which(name)
49
+ if found:
50
+ host = Path(found)
51
+ break
52
+ if host is None or not host.is_file():
53
+ die("Python 3.11+ required")
54
+ log(f"Creating {VENV_DIR.name}/ ...")
55
+ subprocess.run([str(host), "-m", "venv", str(VENV_DIR)], cwd=ROOT, check=True)
56
+ py = venv_python(VENV_DIR)
57
+ if not py.is_file():
58
+ die(f"venv created but python missing at {py}")
59
+ return py
60
+
61
+
62
+ def main() -> None:
63
+ os.chdir(ROOT)
64
+ if not (ROOT / ".pre-commit-config.yaml").is_file():
65
+ die(".pre-commit-config.yaml missing — run eai-supinit/eai-supmig bootstrap first")
66
+ if shutil.which("git") is None:
67
+ die("git required on PATH")
68
+ py = resolve_python()
69
+ log(f"Using {py}")
70
+ log("Installing pre-commit ...")
71
+ subprocess.run([str(py), "-m", "pip", "install", "--upgrade", "pip"], cwd=ROOT, check=False)
72
+ subprocess.run([str(py), "-m", "pip", "install", "pre-commit"], cwd=ROOT, check=True)
73
+ log("Installing pre-commit hooks (pre-commit + commit-msg/gitlint) ...")
74
+ subprocess.run([str(py), "-m", "pre_commit", "install"], cwd=ROOT, check=True)
75
+ subprocess.run(
76
+ [str(py), "-m", "pre_commit", "install", "--hook-type", "commit-msg"],
77
+ cwd=ROOT,
78
+ check=True,
79
+ )
80
+ log("Done. Re-run after clone or when .pre-commit-config.yaml changes.")
81
+
82
+
83
+ if __name__ == "__main__":
84
+ main()
@@ -0,0 +1,11 @@
1
+ # cmake-format defaults for eai multi-repo workspaces (extension: cmake)
2
+ # See https://cmake-format.readthedocs.io/
3
+ format:
4
+ line_width: 100
5
+ tab_size: 2
6
+ max_subgroups_hwrap: 2
7
+ max_pargs_hwrap: 6
8
+ dangle_parens: false
9
+ separate_ctrl_name_with_space: false
10
+ separate_fn_name_with_space: false
11
+ raise_body_indent: false
@@ -0,0 +1,7 @@
1
+ # BEGIN eai-sup extension: cmake
2
+ - repo: https://github.com/cheshirekow/cmake-format-precommit
3
+ rev: v0.6.13
4
+ hooks:
5
+ - id: cmake-format
6
+ - id: cmake-lint
7
+ # END eai-sup extension: cmake
@@ -0,0 +1,64 @@
1
+ """Doctor checks shared across eai-sup* for consumer rails and extensions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from eai_supcore.extensions import status_extensions
8
+ from eai_supcore.io_util import read_text
9
+ from eai_supcore.paths import ESYSREPO_MANIFEST_NAME, TOOLS_LOCAL_GITIGNORE_ENTRY
10
+
11
+
12
+ def add_rails_doctor_checks(report, workspace: Path) -> None:
13
+ """Append language-agnostic rails checks to a doctor report (``add`` method)."""
14
+ pre_commit = workspace / ".pre-commit-config.yaml"
15
+ gitlint = workspace / ".gitlint"
16
+ setup_dev = workspace / "scripts" / "setup-dev.py"
17
+ manifest = workspace / ESYSREPO_MANIFEST_NAME
18
+ gitignore = workspace / ".gitignore"
19
+
20
+ report.add(
21
+ ".pre-commit-config.yaml",
22
+ pre_commit.is_file(),
23
+ "run bootstrap (default --with-dev-hooks)" if not pre_commit.is_file() else str(pre_commit),
24
+ )
25
+ report.add(
26
+ ".gitlint",
27
+ gitlint.is_file(),
28
+ "run bootstrap (default --with-dev-hooks)" if not gitlint.is_file() else str(gitlint),
29
+ )
30
+ report.add(
31
+ "scripts/setup-dev.py",
32
+ setup_dev.is_file(),
33
+ "run bootstrap" if not setup_dev.is_file() else str(setup_dev),
34
+ )
35
+ report.add(
36
+ ESYSREPO_MANIFEST_NAME,
37
+ manifest.is_file(),
38
+ "run bootstrap (default --with-esysrepo-manifest)"
39
+ if not manifest.is_file()
40
+ else str(manifest),
41
+ )
42
+
43
+ tools_local_ok = False
44
+ detail = f"add {TOOLS_LOCAL_GITIGNORE_ENTRY} via bootstrap"
45
+ if gitignore.is_file():
46
+ lines = {line.strip() for line in read_text(gitignore).splitlines()}
47
+ tools_local_ok = (
48
+ TOOLS_LOCAL_GITIGNORE_ENTRY in lines or TOOLS_LOCAL_GITIGNORE_ENTRY.rstrip("/") in lines
49
+ )
50
+ detail = str(gitignore) if tools_local_ok else detail
51
+ report.add(".tools_local/ in .gitignore", tools_local_ok, detail)
52
+
53
+ for status in status_extensions(workspace):
54
+ if not status.recorded and not status.pre_commit_block_present:
55
+ continue
56
+ name = f"extension:{status.extension_id}"
57
+ if status.drift:
58
+ report.add(name, False, "; ".join(status.notes) or "drift detected")
59
+ else:
60
+ report.add(
61
+ name,
62
+ True,
63
+ "applied" if status.recorded else "hooks present (unrecorded)",
64
+ )
@@ -0,0 +1,161 @@
1
+ """Typer command group for workspace extensions (shared wiring)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Annotated
8
+
9
+ import typer
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+
13
+ from eai_supcore.extensions import add_extension, list_extensions, status_extensions
14
+
15
+
16
+ def attach_extension_commands(parent: typer.Typer, *, resolve_workspace) -> None:
17
+ """Register both ``extension`` and ``ext`` groups on ``parent``.
18
+
19
+ ``resolve_workspace`` is a callable ``(Path | None) -> Path``.
20
+ """
21
+ console = Console(stderr=True)
22
+
23
+ def _make_group() -> typer.Typer:
24
+ group = typer.Typer(
25
+ help="Workspace extensions (e.g. cmake)",
26
+ no_args_is_help=True,
27
+ )
28
+
29
+ @group.command("list")
30
+ def list_cmd(
31
+ workspace: Annotated[
32
+ Path | None,
33
+ typer.Option("--workspace", "-C", help="Workspace root"),
34
+ ] = None,
35
+ json_output: Annotated[bool, typer.Option("--json")] = False,
36
+ ) -> None:
37
+ """List available extensions and whether each is applied."""
38
+ root = resolve_workspace(workspace)
39
+ items = list_extensions(root)
40
+ if json_output:
41
+ typer.echo(
42
+ json.dumps(
43
+ [
44
+ {
45
+ "id": i.id,
46
+ "description": i.description,
47
+ "applied": i.applied,
48
+ }
49
+ for i in items
50
+ ],
51
+ indent=2,
52
+ )
53
+ )
54
+ return
55
+ table = Table(title="Workspace extensions")
56
+ table.add_column("id")
57
+ table.add_column("applied")
58
+ table.add_column("description")
59
+ for item in items:
60
+ table.add_row(item.id, "yes" if item.applied else "no", item.description)
61
+ console.print(table)
62
+
63
+ @group.command("add")
64
+ def add_cmd(
65
+ extension_id: Annotated[str, typer.Argument(help="Extension id (e.g. cmake)")],
66
+ workspace: Annotated[
67
+ Path | None,
68
+ typer.Option("--workspace", "-C", help="Workspace root"),
69
+ ] = None,
70
+ force: Annotated[
71
+ bool, typer.Option("--force", help="Refresh managed snippets / config")
72
+ ] = False,
73
+ json_output: Annotated[bool, typer.Option("--json")] = False,
74
+ ) -> None:
75
+ """Apply one workspace extension."""
76
+ root = resolve_workspace(workspace)
77
+ result = add_extension(root, extension_id, force=force)
78
+ if json_output:
79
+ typer.echo(
80
+ json.dumps(
81
+ {
82
+ "extension_id": result.extension_id,
83
+ "ok": result.ok,
84
+ "already_applied": result.already_applied,
85
+ "created_files": result.created_files,
86
+ "updated_files": result.updated_files,
87
+ "warnings": result.warnings,
88
+ "errors": result.errors,
89
+ },
90
+ indent=2,
91
+ )
92
+ )
93
+ else:
94
+ if result.already_applied:
95
+ console.print(
96
+ f"[green]Extension `{extension_id}` already applied[/green] "
97
+ "(use --force to refresh)"
98
+ )
99
+ elif result.ok:
100
+ console.print(f"[green]Applied extension[/green] `{extension_id}`")
101
+ for path in result.created_files:
102
+ console.print(f" created: {path}")
103
+ for path in result.updated_files:
104
+ console.print(f" updated: {path}")
105
+ for warning in result.warnings:
106
+ console.print(f"[yellow]Warning:[/yellow] {warning}")
107
+ for error in result.errors:
108
+ console.print(f"[red]Error:[/red] {error}")
109
+ if not result.ok:
110
+ raise typer.Exit(code=1)
111
+
112
+ @group.command("status")
113
+ def status_cmd(
114
+ workspace: Annotated[
115
+ Path | None,
116
+ typer.Option("--workspace", "-C", help="Workspace root"),
117
+ ] = None,
118
+ json_output: Annotated[bool, typer.Option("--json")] = False,
119
+ ) -> None:
120
+ """Show applied extensions and drift."""
121
+ root = resolve_workspace(workspace)
122
+ rows = status_extensions(root)
123
+ if json_output:
124
+ typer.echo(
125
+ json.dumps(
126
+ [
127
+ {
128
+ "extension_id": r.extension_id,
129
+ "recorded": r.recorded,
130
+ "pre_commit_block_present": r.pre_commit_block_present,
131
+ "config_files_present": r.config_files_present,
132
+ "config_files_missing": r.config_files_missing,
133
+ "drift": r.drift,
134
+ "notes": r.notes,
135
+ }
136
+ for r in rows
137
+ ],
138
+ indent=2,
139
+ )
140
+ )
141
+ return
142
+ table = Table(title="Extension status")
143
+ table.add_column("id")
144
+ table.add_column("recorded")
145
+ table.add_column("hooks")
146
+ table.add_column("drift")
147
+ table.add_column("notes")
148
+ for row in rows:
149
+ table.add_row(
150
+ row.extension_id,
151
+ "yes" if row.recorded else "no",
152
+ "yes" if row.pre_commit_block_present else "no",
153
+ "yes" if row.drift else "no",
154
+ "; ".join(row.notes) if row.notes else "—",
155
+ )
156
+ console.print(table)
157
+
158
+ return group
159
+
160
+ parent.add_typer(_make_group(), name="extension")
161
+ parent.add_typer(_make_group(), name="ext")
@@ -0,0 +1,267 @@
1
+ """Workspace extension registry (cmake, …) shared by eai-sup* CLIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass, field
7
+ from datetime import UTC, datetime
8
+ from pathlib import Path
9
+
10
+ import yaml
11
+
12
+ from eai_supcore.io_util import read_text, write_text
13
+ from eai_supcore.paths import EXTENSIONS_STATE_PATH
14
+ from eai_supcore.templates import resolve_extension_templates
15
+
16
+ BEGIN_MARK = "# BEGIN eai-sup extension: {id}"
17
+ END_MARK = "# END eai-sup extension: {id}"
18
+
19
+
20
+ @dataclass
21
+ class ExtensionInfo:
22
+ id: str
23
+ description: str
24
+ applied: bool = False
25
+
26
+
27
+ @dataclass
28
+ class ExtensionApplyResult:
29
+ extension_id: str
30
+ created_files: list[str] = field(default_factory=list)
31
+ updated_files: list[str] = field(default_factory=list)
32
+ warnings: list[str] = field(default_factory=list)
33
+ errors: list[str] = field(default_factory=list)
34
+ already_applied: bool = False
35
+
36
+ @property
37
+ def ok(self) -> bool:
38
+ return not self.errors
39
+
40
+
41
+ @dataclass
42
+ class ExtensionStatus:
43
+ extension_id: str
44
+ recorded: bool
45
+ pre_commit_block_present: bool
46
+ config_files_present: list[str] = field(default_factory=list)
47
+ config_files_missing: list[str] = field(default_factory=list)
48
+ drift: bool = False
49
+ notes: list[str] = field(default_factory=list)
50
+
51
+
52
+ def _now_iso() -> str:
53
+ return datetime.now(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z")
54
+
55
+
56
+ def extensions_state_path(workspace: Path) -> Path:
57
+ return workspace / EXTENSIONS_STATE_PATH
58
+
59
+
60
+ def load_extensions_state(workspace: Path) -> dict:
61
+ path = extensions_state_path(workspace)
62
+ if not path.is_file():
63
+ return {"version": 1, "applied": {}}
64
+ data = yaml.safe_load(read_text(path)) or {}
65
+ if not isinstance(data, dict):
66
+ return {"version": 1, "applied": {}}
67
+ data.setdefault("version", 1)
68
+ data.setdefault("applied", {})
69
+ if not isinstance(data["applied"], dict):
70
+ data["applied"] = {}
71
+ return data
72
+
73
+
74
+ def save_extensions_state(workspace: Path, state: dict) -> None:
75
+ path = extensions_state_path(workspace)
76
+ path.parent.mkdir(parents=True, exist_ok=True)
77
+ write_text(path, yaml.safe_dump(state, sort_keys=False))
78
+
79
+
80
+ def record_extension_applied(workspace: Path, extension_id: str) -> None:
81
+ state = load_extensions_state(workspace)
82
+ state["applied"][extension_id] = {"applied_at": _now_iso()}
83
+ save_extensions_state(workspace, state)
84
+
85
+
86
+ def is_extension_recorded(workspace: Path, extension_id: str) -> bool:
87
+ return extension_id in load_extensions_state(workspace).get("applied", {})
88
+
89
+
90
+ def baseline_hooks_present(workspace: Path) -> bool:
91
+ return (workspace / ".pre-commit-config.yaml").is_file()
92
+
93
+
94
+ def _managed_block_present(pre_commit_text: str, extension_id: str) -> bool:
95
+ begin = BEGIN_MARK.format(id=extension_id)
96
+ end = END_MARK.format(id=extension_id)
97
+ return begin in pre_commit_text and end in pre_commit_text
98
+
99
+
100
+ def _merge_pre_commit_snippet(
101
+ workspace: Path,
102
+ extension_id: str,
103
+ snippet: str,
104
+ *,
105
+ force: bool,
106
+ ) -> tuple[bool, str | None]:
107
+ """Return ``(changed, error)``."""
108
+ path = workspace / ".pre-commit-config.yaml"
109
+ if not path.is_file():
110
+ return False, "`.pre-commit-config.yaml` missing — run bootstrap first"
111
+ text = read_text(path)
112
+ begin = BEGIN_MARK.format(id=extension_id)
113
+ end = END_MARK.format(id=extension_id)
114
+ if _managed_block_present(text, extension_id):
115
+ if not force:
116
+ return False, None
117
+ start = text.index(begin)
118
+ stop = text.index(end) + len(end)
119
+ if stop < len(text) and text[stop] == "\n":
120
+ stop += 1
121
+ text = text[:start] + snippet.rstrip() + "\n" + text[stop:]
122
+ write_text(path, text if text.endswith("\n") else text + "\n")
123
+ return True, None
124
+
125
+ body = text.rstrip() + "\n\n" + snippet.rstrip() + "\n"
126
+ write_text(path, body)
127
+ return True, None
128
+
129
+
130
+ def _apply_cmake(workspace: Path, *, force: bool = False) -> ExtensionApplyResult:
131
+ result = ExtensionApplyResult(extension_id="cmake")
132
+ if not baseline_hooks_present(workspace):
133
+ result.errors.append(
134
+ "Baseline rails missing (`.pre-commit-config.yaml`) — run "
135
+ "`eai-supinit bootstrap` or `eai-supmig bootstrap` first"
136
+ )
137
+ return result
138
+
139
+ try:
140
+ templates = resolve_extension_templates(workspace, "cmake")
141
+ except FileNotFoundError as exc:
142
+ result.errors.append(str(exc))
143
+ return result
144
+
145
+ snippet_path = templates / "pre-commit-snippet.yaml"
146
+ config_src = templates / "cmake-format.yaml"
147
+ if not config_src.is_file():
148
+ config_src = templates / ".cmake-format.yaml"
149
+ if not snippet_path.is_file():
150
+ result.errors.append(f"Missing snippet template: {snippet_path}")
151
+ return result
152
+
153
+ snippet = read_text(snippet_path)
154
+ begin = BEGIN_MARK.format(id="cmake")
155
+ end = END_MARK.format(id="cmake")
156
+ if begin not in snippet:
157
+ snippet = f"{begin}\n{snippet.rstrip()}\n{end}\n"
158
+
159
+ recorded = is_extension_recorded(workspace, "cmake")
160
+ pre_commit = workspace / ".pre-commit-config.yaml"
161
+ block_already = _managed_block_present(read_text(pre_commit), "cmake")
162
+ config_dest = workspace / ".cmake-format.yaml"
163
+ config_exists = config_dest.is_file()
164
+
165
+ if recorded and block_already and config_exists and not force:
166
+ result.already_applied = True
167
+ return result
168
+
169
+ changed, err = _merge_pre_commit_snippet(workspace, "cmake", snippet, force=force)
170
+ if err:
171
+ result.errors.append(err)
172
+ return result
173
+ if changed:
174
+ result.updated_files.append(".pre-commit-config.yaml")
175
+
176
+ if config_src.is_file() and (not config_exists or force):
177
+ write_text(config_dest, read_text(config_src))
178
+ if not config_exists:
179
+ result.created_files.append(".cmake-format.yaml")
180
+ else:
181
+ result.updated_files.append(".cmake-format.yaml")
182
+
183
+ state_path = extensions_state_path(workspace)
184
+ state_existed = state_path.is_file()
185
+ record_extension_applied(workspace, "cmake")
186
+ rel_state = EXTENSIONS_STATE_PATH.as_posix()
187
+ if state_existed:
188
+ result.updated_files.append(rel_state)
189
+ else:
190
+ result.created_files.append(rel_state)
191
+ return result
192
+
193
+
194
+ def _status_cmake(workspace: Path) -> ExtensionStatus:
195
+ recorded = is_extension_recorded(workspace, "cmake")
196
+ pre_commit = workspace / ".pre-commit-config.yaml"
197
+ block = False
198
+ if pre_commit.is_file():
199
+ block = _managed_block_present(read_text(pre_commit), "cmake")
200
+ config = workspace / ".cmake-format.yaml"
201
+ present = [".cmake-format.yaml"] if config.is_file() else []
202
+ missing = [] if config.is_file() else [".cmake-format.yaml"]
203
+ notes: list[str] = []
204
+ if recorded and not block:
205
+ notes.append("managed pre-commit block missing")
206
+ if recorded and missing:
207
+ notes.append("config file(s) missing: " + ", ".join(missing))
208
+ if block and not recorded:
209
+ notes.append("pre-commit block present but not recorded in extensions.yaml")
210
+ drift = bool(notes)
211
+ return ExtensionStatus(
212
+ extension_id="cmake",
213
+ recorded=recorded,
214
+ pre_commit_block_present=block,
215
+ config_files_present=present,
216
+ config_files_missing=missing,
217
+ drift=drift,
218
+ notes=notes,
219
+ )
220
+
221
+
222
+ @dataclass(frozen=True)
223
+ class _ExtensionSpec:
224
+ id: str
225
+ description: str
226
+ apply_force: Callable[[Path, bool], ExtensionApplyResult]
227
+ status: Callable[[Path], ExtensionStatus]
228
+
229
+
230
+ REGISTRY: dict[str, _ExtensionSpec] = {
231
+ "cmake": _ExtensionSpec(
232
+ id="cmake",
233
+ description="CMake builds — cmake-format + cmake-lint pre-commit hooks",
234
+ apply_force=lambda ws, force: _apply_cmake(ws, force=force),
235
+ status=_status_cmake,
236
+ ),
237
+ }
238
+
239
+
240
+ def list_extensions(workspace: Path | None = None) -> list[ExtensionInfo]:
241
+ applied: dict = {}
242
+ if workspace is not None:
243
+ applied = load_extensions_state(workspace).get("applied", {})
244
+ return [
245
+ ExtensionInfo(
246
+ id=spec.id,
247
+ description=spec.description,
248
+ applied=spec.id in applied,
249
+ )
250
+ for spec in REGISTRY.values()
251
+ ]
252
+
253
+
254
+ def add_extension(
255
+ workspace: Path, extension_id: str, *, force: bool = False
256
+ ) -> ExtensionApplyResult:
257
+ spec = REGISTRY.get(extension_id)
258
+ if spec is None:
259
+ result = ExtensionApplyResult(extension_id=extension_id)
260
+ known = ", ".join(sorted(REGISTRY)) or "(none)"
261
+ result.errors.append(f"Unknown extension {extension_id!r}. Known: {known}")
262
+ return result
263
+ return spec.apply_force(workspace, force)
264
+
265
+
266
+ def status_extensions(workspace: Path) -> list[ExtensionStatus]:
267
+ return [spec.status(workspace) for spec in REGISTRY.values()]
eai_supcore/git_ops.py ADDED
@@ -0,0 +1,107 @@
1
+ """Cross-platform Git subprocess helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from pathlib import Path
7
+
8
+ from eai_supcore.paths import AI_RULES_SUBMODULE
9
+
10
+ AI_RULES_CHECKOUT_MANUAL = "manual_clone"
11
+ AI_RULES_CHECKOUT_SUBMODULE = "submodule"
12
+ AI_RULES_CHECKOUT_MISSING = "missing"
13
+ AI_RULES_CHECKOUT_NON_GIT = "non_git"
14
+
15
+
16
+ class GitError(RuntimeError):
17
+ pass
18
+
19
+
20
+ def run_git(args: list[str], *, cwd: Path, check: bool = True) -> subprocess.CompletedProcess[str]:
21
+ cmd = ["git", *args]
22
+ try:
23
+ result = subprocess.run(
24
+ cmd,
25
+ cwd=cwd,
26
+ capture_output=True,
27
+ text=True,
28
+ encoding="utf-8",
29
+ check=False,
30
+ )
31
+ except FileNotFoundError as exc:
32
+ raise GitError("git executable not found on PATH") from exc
33
+ if check and result.returncode != 0:
34
+ stderr = (result.stderr or "").strip()
35
+ raise GitError(f"git {' '.join(args)} failed: {stderr}")
36
+ return result
37
+
38
+
39
+ def git_output(args: list[str], *, cwd: Path) -> str:
40
+ result = run_git(args, cwd=cwd)
41
+ return (result.stdout or "").strip()
42
+
43
+
44
+ def submodule_exists(workspace: Path, path: Path) -> bool:
45
+ gitmodules = workspace / ".gitmodules"
46
+ if not gitmodules.is_file():
47
+ return False
48
+ rel = path.as_posix()
49
+ content = gitmodules.read_text(encoding="utf-8", errors="replace")
50
+ return f"path = {rel}" in content or f"path = {path}" in content
51
+
52
+
53
+ def is_git_repository(path: Path) -> bool:
54
+ if not path.is_dir():
55
+ return False
56
+ try:
57
+ git_output(["rev-parse", "--git-dir"], cwd=path)
58
+ except GitError:
59
+ return False
60
+ return True
61
+
62
+
63
+ def paths_with_pending_changes(workspace: Path, rel_paths: list[str]) -> list[str]:
64
+ """Return workspace-relative paths with staged, modified, or untracked changes."""
65
+ if not rel_paths or not is_git_repository(workspace):
66
+ return []
67
+ try:
68
+ porcelain = git_output(["status", "--porcelain", "--", *rel_paths], cwd=workspace)
69
+ except GitError:
70
+ return []
71
+ if not porcelain:
72
+ return []
73
+ pending: set[str] = set()
74
+ for line in porcelain.splitlines():
75
+ if len(line) < 4:
76
+ continue
77
+ rel = line[3:].strip().replace("\\", "/")
78
+ if rel:
79
+ pending.add(rel)
80
+ return sorted(pending)
81
+
82
+
83
+ def commit_paths(workspace: Path, rel_paths: list[str], message: str) -> str:
84
+ """Stage ``rel_paths`` and create a commit; return the new commit SHA."""
85
+ if not rel_paths:
86
+ raise GitError("no paths to commit")
87
+ run_git(["add", "--", *rel_paths], cwd=workspace)
88
+ run_git(["commit", "-m", message], cwd=workspace)
89
+ return git_output(["rev-parse", "HEAD"], cwd=workspace)
90
+
91
+
92
+ def classify_ai_rules_checkout(workspace: Path, ai_rules_path: Path) -> str:
93
+ """Classify ``tools/ai-rules`` for reset and bootstrap messaging."""
94
+ if not ai_rules_path.exists():
95
+ return AI_RULES_CHECKOUT_MISSING
96
+ if submodule_exists(workspace, AI_RULES_SUBMODULE):
97
+ return AI_RULES_CHECKOUT_SUBMODULE
98
+ if is_git_repository(ai_rules_path):
99
+ return AI_RULES_CHECKOUT_MANUAL
100
+ return AI_RULES_CHECKOUT_NON_GIT
101
+
102
+
103
+ def add_submodule(workspace: Path, url: str, path: Path) -> None:
104
+ rel = path.as_posix()
105
+ if submodule_exists(workspace, path):
106
+ return
107
+ run_git(["submodule", "add", url, rel], cwd=workspace)
eai_supcore/io_util.py ADDED
@@ -0,0 +1,14 @@
1
+ """UTF-8 file I/O helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+
8
+ def read_text(path: Path) -> str:
9
+ return path.read_text(encoding="utf-8")
10
+
11
+
12
+ def write_text(path: Path, content: str) -> None:
13
+ path.parent.mkdir(parents=True, exist_ok=True)
14
+ path.write_text(content, encoding="utf-8", newline="\n")
eai_supcore/paths.py ADDED
@@ -0,0 +1,22 @@
1
+ """Shared path constants for the eai-sup* family."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ AI_RULES_SUBMODULE = Path("tools") / "ai-rules"
8
+ CURSOR_RULES_DIR = Path(".cursor") / "rules"
9
+ DEFAULT_AI_RULES_URL = "ssh://git@gitlab.com/libesys/ai-workflows/superbuild-workflow.git"
10
+
11
+ # Steady-state layout roles (repository-layout.md)
12
+ STEADY_STATE_DIRS = (
13
+ Path("src"),
14
+ Path("libs"),
15
+ Path("docs") / "adr",
16
+ Path("prompts"),
17
+ )
18
+
19
+ TOOLS_LOCAL_GITIGNORE_ENTRY = ".tools_local/"
20
+ ESYSREPO_MANIFEST_NAME = ".esysrepo.manifest.xml"
21
+ # Shared eai-supcore tool state (extensions apply to the whole workspace)
22
+ EXTENSIONS_STATE_PATH = Path(".tools") / "esys" / "eai-supcore" / "extensions.yaml"
@@ -0,0 +1,70 @@
1
+ """Resolve consumer / extension template roots (method mount vs bundled)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib import resources
6
+ from pathlib import Path
7
+
8
+ from eai_supcore.paths import AI_RULES_SUBMODULE
9
+
10
+ # Dest path in workspace → template filename (no leading dots — setuptools package-data)
11
+ CONSUMER_TEMPLATE_FILES: dict[str, str] = {
12
+ ".pre-commit-config.yaml": "pre-commit-config.yaml",
13
+ ".gitlint": "gitlint",
14
+ ".esysrepo.manifest.xml": "esysrepo.manifest.xml",
15
+ "scripts/setup-dev.py": "scripts/setup-dev.py",
16
+ }
17
+
18
+
19
+ def bundled_data_root() -> Path:
20
+ """Return the filesystem path to ``eai_supcore.data``."""
21
+ root = resources.files("eai_supcore") / "data"
22
+ return Path(str(root))
23
+
24
+
25
+ def resolve_template_file(template_root: Path, dest_rel: str) -> Path | None:
26
+ """Locate a template file by destination name or undotted package-data name."""
27
+ dest_rel = dest_rel.replace("\\", "/")
28
+ candidates = [dest_rel]
29
+ mapped = CONSUMER_TEMPLATE_FILES.get(dest_rel)
30
+ if mapped and mapped not in candidates:
31
+ candidates.append(mapped)
32
+ name = Path(dest_rel).name
33
+ if name.startswith("."):
34
+ parent = Path(dest_rel).parent
35
+ undotted = name[1:] if str(parent) == "." else f"{parent.as_posix()}/{name[1:]}"
36
+ if undotted not in candidates:
37
+ candidates.append(undotted)
38
+ for rel in candidates:
39
+ path = template_root / rel
40
+ if path.is_file():
41
+ return path
42
+ return None
43
+
44
+
45
+ def _has_consumer_hooks_template(root: Path) -> bool:
46
+ return resolve_template_file(root, ".pre-commit-config.yaml") is not None
47
+
48
+
49
+ def resolve_consumer_templates(workspace: Path) -> Path:
50
+ """Prefer ``tools/ai-rules/templates/consumer``; else bundled package data."""
51
+ mounted = workspace / AI_RULES_SUBMODULE / "templates" / "consumer"
52
+ if mounted.is_dir() and _has_consumer_hooks_template(mounted):
53
+ return mounted
54
+ bundled = bundled_data_root() / "consumer"
55
+ if not _has_consumer_hooks_template(bundled):
56
+ raise FileNotFoundError(
57
+ "Consumer templates not found (mount tools/ai-rules or reinstall eai-supcore)"
58
+ )
59
+ return bundled
60
+
61
+
62
+ def resolve_extension_templates(workspace: Path, extension_id: str) -> Path:
63
+ """Prefer method-tree extension templates; else bundled package data."""
64
+ mounted = workspace / AI_RULES_SUBMODULE / "templates" / "extensions" / extension_id
65
+ if mounted.is_dir():
66
+ return mounted
67
+ bundled = bundled_data_root() / "extensions" / extension_id
68
+ if not bundled.is_dir():
69
+ raise FileNotFoundError(f"Extension templates missing for id={extension_id!r}")
70
+ return bundled
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.4
2
+ Name: eai-supcore
3
+ Version: 0.5.0
4
+ Summary: Shared steady-state bootstrap APIs for the eai-sup* CLI family
5
+ Author-email: Michel Gillet <michel.gillet@libesys.org>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://gitlab.com/libesys/ai-workflows/superbuild-workflow
8
+ Project-URL: Repository, https://gitlab.com/libesys/ai-workflows/superbuild-workflow.git
9
+ Keywords: superbuild,cmake,conan,bootstrap,cli
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Build Tools
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: pyyaml>=6.0
20
+ Requires-Dist: typer>=0.12
21
+ Requires-Dist: rich>=13.7
22
+
23
+ # eai-supcore
24
+
25
+ Shared steady-state bootstrap and consumer-rails APIs for **`eai-supinit`** and **`eai-supmig`**.
26
+
27
+ Includes:
28
+
29
+ - Layout / AI-rules / Cursor helpers
30
+ - Language-agnostic consumer rails (`scaffold_consumer_dev_rails`)
31
+ - Workspace extensions registry (`cmake`, …) and shared `extension`/`ext` CLI wiring
32
+
33
+ See [docs/init-assistant.md](../../docs/init-assistant.md) and
34
+ [ADR-0002](../../docs/adr/0002-consumer-rails-and-workspace-extensions.md).
@@ -0,0 +1,21 @@
1
+ eai_supcore/__init__.py,sha256=rSY1dlFbovl2W61BBPRUaJm05iarvtxkUPVfV2wnVyk,93
2
+ eai_supcore/bootstrap.py,sha256=CpqQwXNhkWRKCWzQUUZ__QlhQImIMddGFcczreCwOSM,9267
3
+ eai_supcore/doctor_rails.py,sha256=Geu4rP_bhIQBGbsoDityFZUsOD9dzmz72fLobIFlqaY,2357
4
+ eai_supcore/extension_cli.py,sha256=lnVP7YKPktDdGYWh7uutesNUAtkAoZlERHyNQn37JjE,6324
5
+ eai_supcore/extensions.py,sha256=8FuRrjsm1Wd9L-SccLiIXVfv6H3T0KgASgAu94Kh_pM,8812
6
+ eai_supcore/git_ops.py,sha256=srLGT3KlptAcVpB9q7u-HMfbSqPGEDG2hfL6CQjqdgo,3369
7
+ eai_supcore/io_util.py,sha256=9d96omb8fo9eDPBIxFRW5O6q8lPIYi81t0uVt2IrxGE,336
8
+ eai_supcore/paths.py,sha256=K2MUabor9Fw_AwiNNXZn-VT6TYQeP2ruZ23U9L6oGdk,713
9
+ eai_supcore/templates.py,sha256=8dI_KXwRpWdAQAIceK0bOoQzgTGkFVt9zn-M0l8AJEQ,2668
10
+ eai_supcore/data/__init__.py,sha256=CpPVFhqXLUUTQCA2G_zMrGeSAukMvZ8mRjiitXi2pAU,70
11
+ eai_supcore/data/consumer/esysrepo.manifest.xml,sha256=G5D64XqJWVRJA8baguh3zYzkh5z_npbYL9VTmK4609s,365
12
+ eai_supcore/data/consumer/gitignore,sha256=FhawrDyGw_CPPBgRn6wNdvzmL4Bh_P4drVRkJOu9bCc,229
13
+ eai_supcore/data/consumer/gitlint,sha256=_sKCZZyGat1Ne01MDRNG77PXnowtGj5zoPcpZZrJfmc,250
14
+ eai_supcore/data/consumer/pre-commit-config.yaml,sha256=0snq0YPUsCJpMsLvpWoWFtYc6V9JTY3DJ2iHGkSYpcI,772
15
+ eai_supcore/data/consumer/scripts/setup-dev.py,sha256=Uybtbf8gjlAsD64BXJhbBEDrmyJuamVNejVDpV2S1z8,2560
16
+ eai_supcore/data/extensions/cmake/cmake-format.yaml,sha256=-ri3N82xbfnPjFgNk02qiKECwNKTx-AkJGtaCcciYEI,328
17
+ eai_supcore/data/extensions/cmake/pre-commit-snippet.yaml,sha256=z1uxU9p4YPu7wJLIK-CAhUsXTUNnSldXFLsxYnGbGHY,204
18
+ eai_supcore-0.5.0.dist-info/METADATA,sha256=_Gk-f7ARjMbqvjm9dmUAhbUBoMmQ9Kl_PFmHOWlI1zE,1368
19
+ eai_supcore-0.5.0.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
20
+ eai_supcore-0.5.0.dist-info/top_level.txt,sha256=Pfm5UJULDM0tne4404L5azsrkC_BV2YfF0NHCKi0RrU,12
21
+ eai_supcore-0.5.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (79.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ eai_supcore