feat: full-stack MCP surface — client template, npm CI, 13 repos

- build.py: vendor templates/client into mcp/templates/client with
  client.json manifest (16 kinds); register drone-npm as a third CI shape
  (types: pypi/npm/docker); root *.py stale doc numbers fixed
- registry.py: registry://client resource + list_client_components /
  get_client_component / get_client_template tools; build prompt gains the
  client scaffold step (docs://modules/ui_admin); compliance prompt covers
  npm packages; drone docs describe all three shapes
- repos.json (+mcp): doc-weaver, react-auth, react-openapi (npm Type B,
  package.json version source), vet (app Type C, VERSION)
- docs: 90_drone_pipelines gains Type B npm section; 91_versioning covers
  package.json as a version source; 94_repository_inventory gains the npm
  package table and services/apps Type C; lib index updated

Verified: bundle regen (24 modules, client 16, drone 3, repos 13); registry
client readers exercised against the vendored bundle.
This commit is contained in:
2026-10-20 12:02:31 +05:30
parent 406affcfcf
commit d7c344c9aa
44 changed files with 3251 additions and 94 deletions

130
build.py
View File

@@ -362,14 +362,125 @@ def build_templates(folder: Path, dry_run: bool = False) -> None:
f"({len(manifest['components'])} components)")
# ── Client template bundle ────────────────────────────────────────────────────
# Component kinds shipped in the vendored React client template
# (``templates/client``): the Vite + MUI shell that pairs with ``templates/app``
# and renders the admin UI from the app's openapi.yaml via
# ``@aetoskia/react-openapi`` / ``@aetoskia/react-auth``. No seed tokens —
# ship as-is (see standards "40_ui_admin").
_CLIENT_COMPONENTS: list[dict[str, str]] = [
{"kind": "client-package", "path": "package.json", "placeholder": "",
"description": "npm manifest: @aetoskia deps, scripts (dev/build/preview)."},
{"kind": "client-npmrc", "path": ".npmrc", "placeholder": "",
"description": "Private registry auth pointing at npm.aetoskia.com."},
{"kind": "client-env", "path": ".env.example", "placeholder": "",
"description": "VITE_API_BASE_URL / VITE_AUTH_BASE_URL seed."},
{"kind": "client-vite-config", "path": "vite.config.ts", "placeholder": "",
"description": "Vite config (dev server, build)."},
{"kind": "client-tsconfig", "path": "tsconfig.json", "placeholder": "",
"description": "TypeScript config for the client."},
{"kind": "client-index-html", "path": "index.html", "placeholder": "",
"description": "HTML entry point."},
{"kind": "client-entry", "path": "src/main.jsx", "placeholder": "",
"description": "React entry: router + providers + theme."},
{"kind": "client-openapi-config", "path": "src/openapi-config.ts", "placeholder": "",
"description": "Points the admin renderer at the app spec + auth-server spec."},
{"kind": "client-home", "path": "src/Home.tsx", "placeholder": "",
"description": "Auto-listing home page (ResourceCard + LiveTable per resource)."},
{"kind": "client-header", "path": "src/Header.tsx", "placeholder": "",
"description": "App header bar (brand + nav)."},
{"kind": "client-footer", "path": "src/Footer.tsx", "placeholder": "",
"description": "App footer."},
{"kind": "client-vite-env", "path": "src/vite-env.d.ts", "placeholder": "",
"description": "Vite client type declarations."},
{"kind": "client-theme", "path": "src/shared-theme/AppTheme.tsx", "placeholder": "",
"description": "Theme provider wrapper (light/dark)."},
{"kind": "client-theme-config", "path": "src/shared-theme/themeConfig.ts", "placeholder": "",
"description": "Palette / typography tokens."},
{"kind": "client-theme-primitives", "path": "src/shared-theme/themePrimitives.ts", "placeholder": "",
"description": "MUI theme primitives (shadows, accents)."},
{"kind": "client-theme-shim", "path": "src/shared-theme/mui.d.ts", "placeholder": "",
"description": "MUI v5 type shims (Theme.vars, SelectClasses.focused)."},
]
def build_client_manifest(template_root: Path) -> dict[str, object]:
"""Describe every component in the vendored client template.
Args:
template_root (Path): The vendored ``templates/client`` directory.
Returns:
dict[str, object]: Manifest with the component entries.
"""
components: list[dict[str, str]] = []
for entry in _CLIENT_COMPONENTS:
rel = entry["path"]
if not (template_root / rel).exists():
raise SystemExit(f"[ERR] client component path missing from template: {rel}")
components.append({
"kind": entry["kind"],
"path": rel,
"placeholder": entry["placeholder"],
"description": entry["description"],
})
return {
"project": "standards",
"template": "templates/client",
"components": components,
}
def build_client_templates(folder: Path, dry_run: bool = False) -> None:
"""Vendor ``folder/src/templates/client`` into ``<name>/mcp/templates/client``.
Ships the React client skeleton (the auto-rendered admin shell) in the MCP
bundle so an agent can pull exact, current component code, and emits
``client.json`` describing the kinds. Excludes caches and lock files.
Args:
folder (Path): The hub folder owning ``src/templates/client``.
dry_run (bool): Print the plan without executing.
"""
name = folder.name
src = folder / "src" / "templates" / "client"
if not src.is_dir():
return
out_dir = folder / "mcp" / "templates" / "client"
if dry_run:
print(f"[CLT] {name} {src.relative_to(ROOT)}/** -> {out_dir.relative_to(ROOT)}/**")
return
if out_dir.exists():
shutil.rmtree(out_dir)
out_dir.parent.mkdir(parents=True, exist_ok=True)
shutil.copytree(
src, out_dir,
ignore=shutil.ignore_patterns(
"node_modules", "dist", ".ruff_cache", ".pytest_cache",
".mypy_cache", ".venv", "venv", ".git", "*.egg-info",
),
)
manifest = build_client_manifest(out_dir)
(folder / "mcp" / "client.json").write_text(
json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8"
)
print(f"[CLT] {name} client template vendored to {out_dir.relative_to(ROOT)}/ "
f"({len(manifest['components'])} components)")
# ── Drone standard bundle ─────────────────────────────────────────────────────
# The two CI shapes shipped in ``src/templates/drone``. There are exactly two
# types of ``.drone.yml`` in the org: a Python package (private PyPI) and a
# Docker deployment (build / push / run). See standards "10_drone_pipelines".
# The CI shapes shipped in ``src/templates/drone``. There are exactly three
# types of ``.drone.yml`` in the org: a Python package (private PyPI), an npm
# package (private npm registry), and a Docker deployment (build / push / run).
# See standards "90_drone_pipelines".
_DRONE_COMPONENTS: list[dict[str, str]] = [
{"kind": "drone-pypi", "path": "python-lib.yaml", "placeholder": "",
"description": "Drone pipeline: build + publish a Python package to the private PyPI index."},
{"kind": "drone-npm", "path": "npm-lib.yaml", "placeholder": "",
"description": "Drone pipeline: build + publish an npm package to the private npm registry."},
{"kind": "drone-service", "path": "service-docker.yaml", "placeholder": "",
"description": "Drone pipeline: build, push, and deploy a Docker service."},
]
@@ -398,7 +509,7 @@ def build_drone_manifest(template_root: Path) -> dict[str, object]:
return {
"project": "standards",
"template": "templates/drone",
"types": ["pypi", "docker"],
"types": ["pypi", "npm", "docker"],
"components": components,
}
@@ -406,9 +517,9 @@ def build_drone_manifest(template_root: Path) -> dict[str, object]:
def build_drone_templates(folder: Path, dry_run: bool = False) -> None:
"""Vendor ``folder/src/templates/drone`` into ``<name>/mcp/templates/drone``.
Ships the two ``.drone.yml`` shapes (PyPI package, Docker deployment) in the
MCP bundle so an agent can pull the exact CI file, and emits ``drone.json``
describing them.
Ships the three ``.drone.yml`` shapes (PyPI package, npm package, Docker
deployment) in the MCP bundle so an agent can pull the exact CI file, and
emits ``drone.json`` describing them.
Args:
folder (Path): The hub folder owning ``src/templates/drone``.
@@ -442,7 +553,7 @@ def build_drone_templates(folder: Path, dry_run: bool = False) -> None:
# The release artefacts shipped in ``src/templates/release``. One canonical
# changelog (Keep a Changelog + SemVer) and the service version file. See
# standards "11_versioning_and_changelog".
# standards "91_versioning_and_changelog".
_RELEASE_COMPONENTS: list[dict[str, str]] = [
{"kind": "changelog", "path": "CHANGELOG.md", "placeholder": "",
"description": "Keep a Changelog skeleton: one [Unreleased] section + compare links."},
@@ -523,7 +634,7 @@ def build_repos(folder: Path, dry_run: bool = False) -> None:
The repository inventory (name, type, version source, Drone shape) is a
single hand-authored JSON file, surfaced to agents through the
``registry://repos`` resource and the ``list_repos`` tool. See standards
"15_repository_inventory".
"94_repository_inventory".
Args:
folder (Path): The hub folder owning ``src/repos.json``.
@@ -566,6 +677,7 @@ def main() -> None:
for folder in folders:
build_bundle(folder, dry_run=args.dry_run)
build_templates(folder, dry_run=args.dry_run)
build_client_templates(folder, dry_run=args.dry_run)
build_drone_templates(folder, dry_run=args.dry_run)
build_release_templates(folder, dry_run=args.dry_run)
build_repos(folder, dry_run=args.dry_run)

View File

@@ -9,7 +9,8 @@ Resources:
docs://index, docs://nav, docs://modules/{module} (from the base server)
docs://spec (the pinned auth-server OpenAPI contract)
registry://libraries (the structured catalog)
registry://components (the template component index)
registry://components (the app template component index)
registry://client (the client template component index)
registry://drone (the CI template index)
registry://release (the release template index)
registry://repos (the repository inventory)
@@ -20,7 +21,10 @@ Tools:
list_app_components (the template component kinds)
get_app_component (kind -> exact template file, seed tokens renamed)
get_app_template (the whole app skeleton as a path -> content map)
list_drone_templates (the two CI shapes: PyPI package / Docker deployment)
list_client_components (the client template component kinds)
get_client_component (kind -> exact client template file)
get_client_template (the whole client skeleton as a path -> content map)
list_drone_templates (the three CI shapes: PyPI package / npm package / Docker deployment)
get_drone_template (kind -> exact .drone.yml template)
list_release_templates (the changelog + version-file artefacts)
get_release_template (kind -> exact release template file)
@@ -91,10 +95,14 @@ def _build_text(app_name: str, resources: str, pipelines: str, auth: str) -> str
"4. Scaffold with `get_app_template()` (whole tree) or pull single "
"files with `list_app_components()` and "
"`get_app_component(kind, resource=..., family=...)`.\n"
"5. If auth is needed, read `docs://spec` (the auth-server contract) and "
"5. If a UI is needed, scaffold the client with `get_client_template()` "
"(or pull files via `list_client_components()` / "
"`get_client_component(kind)`) — the admin renders from your spec's x- "
"extensions; see `docs://modules/ui_admin`.\n"
"6. If auth is needed, read `docs://spec` (the auth-server contract) and "
"`docs://modules/auth_integration`; declare `securitySchemes.bearerAuth` "
"and `security` in your spec — do not write auth code.\n"
"6. Author only Tier C; keep Tier B as shipped.\n"
"7. Author only Tier C; keep Tier B as shipped.\n"
)
@@ -103,8 +111,12 @@ _COMPLIANCE_TEXT = (
"Read `docs://modules/drone_pipelines` and `docs://modules/versioning_and_changelog`,\n"
"then enumerate the working set with `registry://repos` (or `list_repos()`).\n\n"
"For each repository, bring it to the standard for its `type`:\n\n"
"- **package** — `version` in `pyproject.toml`; root `CHANGELOG.md`; Drone file\n"
" from `get_drone_template(\"drone-pypi\")` (`python-lib.yaml`, tag-triggered).\n"
"- **package** (Python) — `version` in `pyproject.toml`; root `CHANGELOG.md`;\n"
" Drone file from `get_drone_template(\"drone-pypi\")` (`python-lib.yaml`,\n"
" tag-triggered).\n"
"- **package** (npm) — `version` in `package.json`; root `CHANGELOG.md`;\n"
" Drone file from `get_drone_template(\"drone-npm\")` (`npm-lib.yaml`,\n"
" tag-triggered, publishes to the private npm registry).\n"
"- **service** — root `VERSION`; root `CHANGELOG.md`; Drone file from\n"
" `get_drone_template(\"drone-service\")` (`service-docker.yaml`, tag-triggered\n"
" with a `tag == VERSION` guard). The service must read its version from\n"
@@ -291,6 +303,58 @@ class StandardsMCPServer(MCPServer):
"content": text,
}
def _client(self) -> dict[str, Any]:
"""Load the client template component manifest."""
path = Path(self.mcp_root) / "client.json"
if not path.exists():
return {"error": "not_found", "path": str(path), "components": []}
return json.loads(path.read_bytes())
def _find_client_component(self, kind: str) -> dict[str, Any] | None:
for entry in self._client().get("components", []):
if entry.get("kind") == kind:
return entry
return None
def _read_client_component(self, entry: dict[str, Any]) -> dict[str, Any]:
"""Read a vendored client template file."""
rel = entry["path"]
path = Path(self.mcp_root) / "templates" / "client" / rel
if not path.exists():
return {"error": "not_found", "path": str(path)}
return {
"kind": entry["kind"],
"path": rel,
"placeholder": entry.get("placeholder"),
"content": path.read_text(encoding="utf-8"),
}
def _client_files(self, prefix: str | None = None) -> dict[str, str]:
"""Read the vendored client template tree as a path -> content map.
Args:
prefix (str | None): Optional path prefix filter with POSIX
separators, for example ``"src/shared-theme/"``.
Returns:
dict[str, str]: Relative POSIX path -> UTF-8 file content,
excluding caches and build output.
"""
root = Path(self.mcp_root) / "templates" / "client"
files: dict[str, str] = {}
if not root.is_dir():
return files
for path in sorted(root.rglob("*")):
if not path.is_file():
continue
if "__pycache__" in path.parts or path.suffix == ".pyc":
continue
rel = path.relative_to(root).as_posix()
if prefix and not rel.startswith(prefix):
continue
files[rel] = path.read_text(encoding="utf-8")
return files
# ------------------------------------------------------------------
# Standards surface
# ------------------------------------------------------------------
@@ -304,6 +368,16 @@ class StandardsMCPServer(MCPServer):
def components() -> dict[str, Any]:
return self._components()
@self.app.resource("registry://client")
def client() -> dict[str, Any]:
"""Return the client template component manifest.
The React client skeleton (Vite + MUI) that pairs with the app
template; the admin UI renders from the app's openapi.yaml x-
extensions via @aetoskia/react-openapi.
"""
return self._client()
@self.app.resource("registry://drone")
def drone() -> dict[str, Any]:
return self._drone()
@@ -434,13 +508,77 @@ class StandardsMCPServer(MCPServer):
"files": files,
}
@self.app.tool()
def list_client_components() -> list[dict[str, Any]]:
"""List the reusable component kinds shipped in the client template.
The React client (Vite + MUI) that pairs with the app template:
the admin shell rendering the app's resources from its
``openapi.yaml``. See :func:`get_client_template` for the whole
tree and :func:`list_drone_templates` ("drone-npm") for its CI.
Returns:
list[dict[str, Any]]:
Entries with ``kind``, ``path``, ``placeholder`` and
``description``. Fetch one with :func:`get_client_component`.
"""
return self._client().get("components", [])
@self.app.tool()
def get_client_component(kind: str) -> dict[str, Any]:
"""Return the exact client template file for a component kind.
Use this to copy current, app-neutral client code instead of
inventing it. See :func:`list_client_components` for valid kinds
(e.g. "client-home", "client-header", "client-openapi-config").
Args:
kind (str): Component kind, e.g. "client-openapi-config".
Returns:
dict[str, Any]: ``{kind, path, placeholder, content}`` or an
error entry when the kind is unknown.
"""
entry = self._find_client_component(kind)
if entry is None:
return {
"error": "unknown_kind",
"kind": kind,
"available": [c["kind"] for c in self._client().get("components", [])],
}
return self._read_client_component(entry)
@self.app.tool()
def get_client_template(prefix: str = "") -> dict[str, Any]:
"""Return the whole client skeleton as a path -> content map.
Use this to scaffold the UI in one shot instead of pulling
components one by one; combine with :func:`list_client_components`
for the kind and description metadata.
Args:
prefix (str): Optional path prefix filter with POSIX
separators, for example ``"src/shared-theme/"``. Empty
returns every file.
Returns:
dict[str, Any]: ``{root, file_count, files: {relpath: content}}``.
"""
files = self._client_files(prefix or None)
return {
"root": "templates/client",
"file_count": len(files),
"files": files,
}
@self.app.tool()
def list_drone_templates() -> list[dict[str, Any]]:
"""List the CI (Drone) template shapes.
There are exactly two types of ``.drone.yml`` in the org: a Python
package published to the private PyPI index, and a Docker
deployment that builds, pushes, and runs a container.
There are exactly three types of ``.drone.yml`` in the org: a
Python package published to the private PyPI index, an npm package
published to the private npm registry, and a Docker deployment
that builds, pushes, and runs a container.
Returns:
list[dict[str, Any]]:
@@ -455,10 +593,10 @@ class StandardsMCPServer(MCPServer):
Use this to copy the current ``.drone.yml`` shape instead of
inventing one. See :func:`list_drone_templates` for valid kinds
("drone-pypi", "drone-service").
("drone-pypi", "drone-npm", "drone-service").
Args:
kind (str): Component kind, e.g. "drone-pypi" or
kind (str): Component kind, e.g. "drone-pypi", "drone-npm" or
"drone-service".
Returns:
@@ -480,7 +618,7 @@ class StandardsMCPServer(MCPServer):
These are the files every repo carries for releases: a Keep a
Changelog ``CHANGELOG.md`` and, for services, a root ``VERSION``
file. See standards "11_versioning_and_changelog".
file. See standards "91_versioning_and_changelog".
Returns:
list[dict[str, Any]]:
@@ -517,13 +655,14 @@ class StandardsMCPServer(MCPServer):
def list_repos(type: str | None = None) -> list[dict[str, Any]]:
"""List the repositories covered by the CI and versioning standards.
Each entry has: ``name``, ``type`` ("package" | "service"),
Each entry has: ``name``, ``type`` ("package" | "service" | "app"),
``path`` (relative to the aetos/ workspace root), ``version_source``
("pyproject.toml" | "VERSION"), ``drone`` (the template file),
``trigger`` ("tag"), ``docs`` kinds, and ``mcp``.
("pyproject.toml" | "package.json" | "VERSION"), ``drone`` (the
template file), ``trigger`` ("tag"), ``docs`` kinds, and ``mcp``.
Args:
type (str | None): Optional filter, e.g. "package" or "service".
type (str | None): Optional filter, e.g. "package", "service"
or "app".
Returns:
list[dict[str, Any]]: Matching repository entries.

View File

@@ -611,9 +611,18 @@
</li>
<li class="md-nav__item">
<a href="#type-b-docker-deployment-service-dockeryaml" class="md-nav__link">
<a href="#type-b-npm-package-npm-libyaml" class="md-nav__link">
<span class="md-ellipsis">
Type B — Docker deployment (service-docker.yaml)
Type B — npm package (npm-lib.yaml)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#type-c-docker-deployment-service-dockeryaml" class="md-nav__link">
<span class="md-ellipsis">
Type C — Docker deployment (service-docker.yaml)
</span>
</a>
@@ -656,7 +665,7 @@
<h1 id="drone-pipelines">Drone Pipelines</h1>
<h2 id="summary">Summary</h2>
<p>Every Aetos repository ships a Drone CI definition at <code>.drone.yml</code>. There is
exactly <strong>one standard per repository</strong>, and there are <strong>two types of file</strong>,
exactly <strong>one standard per repository</strong>, and there are <strong>three types of file</strong>,
chosen by what the repository produces:</p>
<table>
<thead>
@@ -675,23 +684,30 @@ chosen by what the repository produces:</p>
<td><code>dagpipe</code>, <code>doc-forge</code>, <code>hexa</code>, <code>mail-intake</code>, <code>mongo-ops</code>, <code>omniread</code>, <code>openapi-first</code>, <code>py-jwt</code></td>
</tr>
<tr>
<td><strong>B — Docker deployment</strong></td>
<td><strong>B — npm package</strong></td>
<td><code>templates/drone/npm-lib.yaml</code></td>
<td>A package published to the private npm registry</td>
<td><code>doc-weaver</code>, <code>react-auth</code>, <code>react-openapi</code></td>
</tr>
<tr>
<td><strong>C — Docker deployment</strong></td>
<td><code>templates/drone/service-docker.yaml</code></td>
<td>An image pushed to the registry and run as a container</td>
<td><code>auth-server</code></td>
<td><code>auth-server</code>, <code>vet</code></td>
</tr>
</tbody>
</table>
<p>Both templates run on <code>platform: linux / arm64</code> with the workspace at
<code>/drone/src</code>. Copy the matching template to <code>&lt;repo&gt;/.drone.yml</code>; the Type A file
needs no edits, the Type B file needs its placeholders filled in (below).</p>
<p>All templates run on <code>platform: linux / arm64</code> with the workspace at
<code>/drone/src</code>. Copy the matching template to <code>&lt;repo&gt;/.drone.yml</code>; the Type A and
Type B files need no edits, the Type C file needs its placeholders filled in
(below).</p>
<blockquote>
<p>Conventions: <code>.drone.yml</code> (not <code>.yaml</code>), one file per repo, secrets injected
with <code>from_secret</code> — never commit credentials.</p>
</blockquote>
<hr />
<h2 id="type-a-pypi-package-python-libyaml">Type A — PyPI package (<code>python-lib.yaml</code>)</h2>
<p>For internal libraries. The file contains <strong>two pipelines</strong>:</p>
<p>For internal Python libraries. The file contains <strong>two pipelines</strong>:</p>
<ol>
<li><strong><code>build-and-publish-pypi</code></strong> — triggered on <code>tag</code>:</li>
<li><code>check-version</code> — <strong>guard</strong>: fail unless <code>$DRONE_TAG</code> equals the version in
@@ -716,7 +732,28 @@ with <code>from_secret</code> — never commit credentials.</p>
same file is reused verbatim across every library.</li>
</ul>
<hr />
<h2 id="type-b-docker-deployment-service-dockeryaml">Type B — Docker deployment (<code>service-docker.yaml</code>)</h2>
<h2 id="type-b-npm-package-npm-libyaml">Type B — npm package (<code>npm-lib.yaml</code>)</h2>
<p>For internal npm libraries (the React packages). One pipeline,
<strong><code>build-and-publish-npm</code></strong>, triggered on <code>tag</code>:</p>
<ul>
<li><code>check-version</code> — <strong>guard</strong>: fail unless <code>$DRONE_TAG</code> equals the version in
<code>package.json</code>; query <code>$NPM_REPO_URL</code> for <code>name@version</code>; if it already
exists, exit <code>78</code> and skip the whole build.</li>
<li><code>quality-gate</code> — write <code>.npmrc</code> from the secrets, <code>npm install</code>, then
<code>npm run quality-gate</code>.</li>
<li><code>build-package</code> — <code>npm install &amp;&amp; npm run build</code> into <code>dist/</code>.</li>
<li><code>publish-to-private-npm</code> — <code>npm publish --access restricted</code> to
<code>$NPM_REPO_URL</code>.</li>
</ul>
<p>Secrets: <code>NPM_REPO_URL</code>, <code>NPM_USERNAME</code>, <code>NPM_PASSWORD</code>. Auth is a generated
<code>.npmrc</code> (<code>registry=</code> + <code>_auth=</code> base64 of <code>user:pass</code>) written from secrets at
runtime — the committed <code>.npmrc</code> in a repo only points at the registry URL.</p>
<ul>
<li><strong>Ship as-is.</strong> Name and version come from <code>package.json</code>, so the same file
is reused verbatim across every npm package.</li>
</ul>
<hr />
<h2 id="type-c-docker-deployment-service-dockeryaml">Type C — Docker deployment (<code>service-docker.yaml</code>)</h2>
<p>For a service that ships a container image — a long-running API/worker, e.g.
<code>auth-server</code>. The file contains <strong>one pipeline</strong>, <code>build-and-deploy-service</code>,
triggered on <code>tag</code>:</p>
@@ -774,15 +811,18 @@ triggered on <code>tag</code>:</p>
<hr />
<h2 id="what-not-to-do">What not to do</h2>
<ul>
<li>Do not hand-roll a third shape. A repo is either Type A or Type B, never both.</li>
<li>Do not hand-roll a fourth shape. A repo is exactly one of Type A, B, or C,
never combinations.</li>
<li>Do not inline secrets; always use <code>from_secret</code>.</li>
<li>Do not drop the <code>check-version</code> / <code>check-remote-image</code> skip — it guards the
index and the running container against duplicate publishes.</li>
<li>Do not drop the tag↔version guard. A package needs its <code>pyproject.toml</code>
version to equal the tag; a service needs a root <code>VERSION</code> file that equals
the tag. See <a href="../91_versioning_and_changelog/">91 – Versioning &amp; Changelog</a>.</li>
<li>Do not drop the tag↔version guard. A Python package needs its
<code>pyproject.toml</code> version to equal the tag; an npm package needs its
<code>package.json</code> version; a service needs a root <code>VERSION</code> file. See
<a href="../91_versioning_and_changelog/">91 – Versioning &amp; Changelog</a>.</li>
<li>Do not rename the pipelines; tooling and runbooks key off
<code>build-and-publish-pypi</code> and <code>build-and-deploy-service</code>.</li>
<code>build-and-publish-pypi</code>, <code>build-and-publish-npm</code>, and
<code>build-and-deploy-service</code>.</li>
</ul>
<hr />
<h2 id="related">Related</h2>

View File

@@ -725,7 +725,7 @@ The <strong>version file is the source of truth</strong>, the <strong>git tag mu
a Drone guard enforces that equality before anything is published. Releases are
cut manually; a tag is what triggers the pipeline.</p>
<p>This page pairs with <a href="../90_drone_pipelines/">90 – Drone Pipelines</a>: the version
and tag contract is exactly what those two CI shapes consume.</p>
and tag contract is exactly what those three CI shapes consume.</p>
<hr />
<h2 id="the-version-contract">The version contract</h2>
<table>
@@ -743,6 +743,11 @@ and tag contract is exactly what those two CI shapes consume.</p>
<td><code>check-version</code> also reads it to skip duplicates</td>
</tr>
<tr>
<td>Library (npm package)</td>
<td><code>version</code> in <code>package.json</code></td>
<td><code>check-version</code> also queries the registry to skip duplicates</td>
</tr>
<tr>
<td>Service (Docker)</td>
<td><code>VERSION</code> file at the repo root</td>
<td>one line, <code>x.y.z</code>; the service <strong>reads</strong> it (build-arg) — never hardcode the version</td>
@@ -838,7 +843,8 @@ bump travels with the change and only the tag is added afterwards:</p>
<li><strong>In the MR</strong> — reconcile <code>[Unreleased]</code> against the change (see
<a href="#reconcile-before-releasing">Reconcile before releasing</a>) and get the user's
approval for any additions. Then set the intended release version: <code>version</code>
in <code>pyproject.toml</code> (or the <code>VERSION</code> file) to <code>x.y.z</code>, and promote the
in <code>pyproject.toml</code> or <code>package.json</code> (or the <code>VERSION</code> file) to <code>x.y.z</code>,
and promote the
changelog (below). Changelog entries and link references name the
<strong>version/tag</strong>, never a commit id.</li>
<li><strong>Merge or squash</strong> the MR into <code>main</code>.</li>
@@ -853,8 +859,10 @@ After the user confirms the MR is merged/squashed and says to proceed:</p>
<ol>
<li><strong>Wait</strong> for that explicit go-ahead.</li>
<li>Find the commit that introduced the version bump:</li>
<li>packages:
<li>Python packages:
<code>git log -S 'version = "x.y.z"' -n 1 --format='%H %s' -- pyproject.toml</code></li>
<li>npm packages:
<code>git log -S '"version": "x.y.z"' -n 1 --format='%H %s' -- package.json</code></li>
<li>services:
<code>git log -S 'x.y.z' -n 1 --format='%H %s' -- VERSION</code></li>
<li>Confirm that commit's version file equals <code>x.y.z</code>, then tag <strong>only</strong> that
@@ -873,13 +881,14 @@ After the user confirms the MR is merged/squashed and says to proceed:</p>
on top.</li>
<li>In the MR: add the two compare-link references at the bottom of the
changelog, naming the version/tag (never a commit id).</li>
<li>In the MR: set the version file — <code>pyproject.toml</code> (<code>version = "x.y.z"</code>) or
<code>VERSION</code> — to the intended release version.</li>
<li>In the MR: set the version file — <code>pyproject.toml</code> (<code>version = "x.y.z"</code>),
<code>package.json</code> (<code>"version": "x.y.z"</code>), or <code>VERSION</code> — to the intended
release version.</li>
<li>Merge or squash the MR into <code>main</code>.</li>
<li>Create the tag on the resulting commit: lightweight <code>git tag x.y.z</code>.</li>
<li>Push the tag (<code>git push origin x.y.z</code>). Watch Drone: Type A publishes the
wheel/sdist, Type B builds and deploys the image; both first assert
<code>tag == version</code>.</li>
wheel/sdist, Type B publishes the npm package, Type C builds and deploys
the image; all three first assert <code>tag == version</code>.</li>
<li>Do not reuse the tag if the run fails — fix and cut <code>x.y.(z+1)</code>.</li>
</ol>
<hr />

View File

@@ -602,18 +602,27 @@
</li>
<li class="md-nav__item">
<a href="#packages-type-a" class="md-nav__link">
<a href="#packages-type-a-pypi" class="md-nav__link">
<span class="md-ellipsis">
Packages (Type A)
Packages (Type A — PyPI)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#services-type-b" class="md-nav__link">
<a href="#packages-type-b-npm" class="md-nav__link">
<span class="md-ellipsis">
Services (Type B)
Packages (Type B — npm)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#services-apps-type-c" class="md-nav__link">
<span class="md-ellipsis">
Services &amp; Apps (Type C)
</span>
</a>
@@ -667,13 +676,13 @@
<p>The canonical list of Aetos repositories covered by the CI and versioning
standards, with the artifact each one produces, its version source, and its
Drone shape. Every entry maps to the contract in
<a href="../91_versioning_and_changelog/">91 – Versioning &amp; Changelog</a> and the two CI
<a href="../91_versioning_and_changelog/">91 – Versioning &amp; Changelog</a> and the three CI
files in <a href="../90_drone_pipelines/">90 – Drone Pipelines</a>.</p>
<p>The same data is published machine-readably as the <code>registry://repos</code> MCP
resource (and the <code>list_repos</code> tool), sourced from <code>standards/src/repos.json</code>.
Paths are relative to the <code>aetos/</code> workspace root.</p>
<hr />
<h2 id="packages-type-a">Packages (Type A)</h2>
<h2 id="packages-type-a-pypi">Packages (Type A — PyPI)</h2>
<p>A wheel + sdist published to the private index. Version source is
<code>pyproject.toml</code>; Drone file is <code>python-lib.yaml</code> (tag-triggered).</p>
<table>
@@ -737,7 +746,41 @@ Paths are relative to the <code>aetos/</code> workspace root.</p>
</tbody>
</table>
<hr />
<h2 id="services-type-b">Services (Type B)</h2>
<h2 id="packages-type-b-npm">Packages (Type B — npm)</h2>
<p>A package published to the private npm registry (<code>npm.aetoskia.com</code>). Version
source is <code>package.json</code>; Drone file is <code>npm-lib.yaml</code> (tag-triggered).</p>
<table>
<thead>
<tr>
<th>Repo</th>
<th>Path</th>
<th>Docs</th>
<th>MCP server</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>doc-weaver</code></td>
<td><code>doc-weaver</code></td>
<td>lib, wiki</td>
<td><code>doc-weaver:8010</code></td>
</tr>
<tr>
<td><code>react-auth</code></td>
<td><code>react/react-auth</code></td>
<td>lib, wiki</td>
<td><code>react-auth:8011</code></td>
</tr>
<tr>
<td><code>react-openapi</code></td>
<td><code>react/react-openapi</code></td>
<td>lib, wiki</td>
<td><code>react-openapi:8012</code></td>
</tr>
</tbody>
</table>
<hr />
<h2 id="services-apps-type-c">Services &amp; Apps (Type C)</h2>
<p>A container image pushed to the registry and run on the Docker host. Version
source is the <code>VERSION</code> file; Drone file is <code>service-docker.yaml</code>.</p>
<table>
@@ -745,6 +788,7 @@ source is the <code>VERSION</code> file; Drone file is <code>service-docker.yaml
<tr>
<th>Repo</th>
<th>Path</th>
<th>Type</th>
<th>Trigger</th>
<th>Docs</th>
</tr>
@@ -753,9 +797,17 @@ source is the <code>VERSION</code> file; Drone file is <code>service-docker.yaml
<tr>
<td><code>auth-server</code></td>
<td><code>auth-server</code></td>
<td>service</td>
<td>tag</td>
<td>api, lib, wiki</td>
</tr>
<tr>
<td><code>vet</code></td>
<td><code>react/vet</code></td>
<td>app</td>
<td>tag</td>
<td>lib, wiki</td>
</tr>
</tbody>
</table>
<p>Tag-triggered and enforces <code>tag == VERSION</code>.</p>
@@ -772,13 +824,19 @@ source is the <code>VERSION</code> file; Drone file is <code>service-docker.yaml
</thead>
<tbody>
<tr>
<td>package</td>
<td>package (PyPI)</td>
<td><code>pyproject.toml</code></td>
<td><code>CHANGELOG.md</code></td>
<td><code>python-lib.yaml</code></td>
</tr>
<tr>
<td>service</td>
<td>package (npm)</td>
<td><code>package.json</code></td>
<td><code>CHANGELOG.md</code></td>
<td><code>npm-lib.yaml</code></td>
</tr>
<tr>
<td>service / app</td>
<td><code>VERSION</code></td>
<td><code>CHANGELOG.md</code></td>
<td><code>service-docker.yaml</code></td>

View File

@@ -821,8 +821,8 @@ and an optional pipelines layer.</p>
</ul>
<h2 id="delivery-meta">Delivery &amp; Meta</h2>
<ul>
<li><strong>Drone Pipelines</strong> — the two CI shapes: a PyPI/npm package or a Docker
deployment.</li>
<li><strong>Drone Pipelines</strong> — the three CI shapes: a PyPI package, an npm package,
or a Docker deployment.</li>
<li><strong>Versioning &amp; Changelog</strong> — SemVer, the tag↔version contract, and the
release changelog.</li>
<li><strong>Repository Inventory</strong> — every covered repo, its type, version source, and

File diff suppressed because one or more lines are too long

Binary file not shown.

102
standards/mcp/client.json Normal file
View File

@@ -0,0 +1,102 @@
{
"project": "standards",
"template": "templates/client",
"components": [
{
"kind": "client-package",
"path": "package.json",
"placeholder": "",
"description": "npm manifest: @aetoskia deps, scripts (dev/build/preview)."
},
{
"kind": "client-npmrc",
"path": ".npmrc",
"placeholder": "",
"description": "Private registry auth pointing at npm.aetoskia.com."
},
{
"kind": "client-env",
"path": ".env.example",
"placeholder": "",
"description": "VITE_API_BASE_URL / VITE_AUTH_BASE_URL seed."
},
{
"kind": "client-vite-config",
"path": "vite.config.ts",
"placeholder": "",
"description": "Vite config (dev server, build)."
},
{
"kind": "client-tsconfig",
"path": "tsconfig.json",
"placeholder": "",
"description": "TypeScript config for the client."
},
{
"kind": "client-index-html",
"path": "index.html",
"placeholder": "",
"description": "HTML entry point."
},
{
"kind": "client-entry",
"path": "src/main.jsx",
"placeholder": "",
"description": "React entry: router + providers + theme."
},
{
"kind": "client-openapi-config",
"path": "src/openapi-config.ts",
"placeholder": "",
"description": "Points the admin renderer at the app spec + auth-server spec."
},
{
"kind": "client-home",
"path": "src/Home.tsx",
"placeholder": "",
"description": "Auto-listing home page (ResourceCard + LiveTable per resource)."
},
{
"kind": "client-header",
"path": "src/Header.tsx",
"placeholder": "",
"description": "App header bar (brand + nav)."
},
{
"kind": "client-footer",
"path": "src/Footer.tsx",
"placeholder": "",
"description": "App footer."
},
{
"kind": "client-vite-env",
"path": "src/vite-env.d.ts",
"placeholder": "",
"description": "Vite client type declarations."
},
{
"kind": "client-theme",
"path": "src/shared-theme/AppTheme.tsx",
"placeholder": "",
"description": "Theme provider wrapper (light/dark)."
},
{
"kind": "client-theme-config",
"path": "src/shared-theme/themeConfig.ts",
"placeholder": "",
"description": "Palette / typography tokens."
},
{
"kind": "client-theme-primitives",
"path": "src/shared-theme/themePrimitives.ts",
"placeholder": "",
"description": "MUI theme primitives (shadows, accents)."
},
{
"kind": "client-theme-shim",
"path": "src/shared-theme/mui.d.ts",
"placeholder": "",
"description": "MUI v5 type shims (Theme.vars, SelectClasses.focused)."
}
]
}

View File

@@ -3,6 +3,7 @@
"template": "templates/drone",
"types": [
"pypi",
"npm",
"docker"
],
"components": [
@@ -12,6 +13,12 @@
"placeholder": "",
"description": "Drone pipeline: build + publish a Python package to the private PyPI index."
},
{
"kind": "drone-npm",
"path": "npm-lib.yaml",
"placeholder": "",
"description": "Drone pipeline: build + publish an npm package to the private npm registry."
},
{
"kind": "drone-service",
"path": "service-docker.yaml",

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,7 @@
"module": "index",
"content": {
"path": "index",
"docstring": "# Standards Reference\n\nThe reference half of the Standards description: the enforceable rules,\npatterns, and the internal library catalog that agents consult while coding.\n\nThe reference is organized as an **anatomy** — one entry checklist and four\npillars describing what an application is made of, plus delivery/meta pages\nand an optional pipelines layer.\n\n## Anatomy\n\n- **Build Recipe** — the ordered, agentic checklist for a new app; the entry\n point that walks the whole anatomy.\n\n## Pillar 1 — Domains\n\n- **Layering Rules** — the strict request → domain → repository layering and\n the rules that enforce it.\n\n## Pillar 2 — OpenAPI Spec (the contract)\n\n- **Composition Root** — a single composition point wiring the whole app.\n- **Spec-First Routing** — the OpenAPI contract as the routing source of truth.\n- **CRUD Template** — the standardized generic resource endpoint scaffold.\n- **Auth Integration** — consuming the shared auth-server service (spec-driven,\n no auth code).\n\n## Pillar 3 — Interfaces & Infra\n\n- **Infra Adapters** — how infrastructure connects without leaking into domain.\n\n## Pillar 4 — UI\n\n- **UI: Auto-Rendered Admin & Custom Pages** — the `@aetoskia/react-openapi`\n admin rendered from the spec's x- extensions, and `@aetoskia/react-auth`\n for auth pages; the client template scaffold.\n\n## Pipelines (optional layer)\n\n- **Pipeline Scaffold** — the deterministic pipeline skeleton for multi-stage\n jobs.\n- **Worker & Progress** — queued work, progress reporting, and visibility.\n- **Human-in-the-Loop** — guardrails and approvals inside automations.\n\n## Delivery & Meta\n\n- **Drone Pipelines** — the two CI shapes: a PyPI/npm package or a Docker\n deployment.\n- **Versioning & Changelog** — SemVer, the tag↔version contract, and the\n release changelog.\n- **Repository Inventory** — every covered repo, its type, version source, and\n Drone shape.\n- **Genericity Matrix** — what is library-backed, template-provided, or\n authored.\n- **Library Registry** — the catalog of internal libraries behind all of it.\n\nRead the **Wiki** for the philosophy and rationale; these pages are the\nreference that mirrors the `standards` MCP bundle.\n",
"docstring": "# Standards Reference\n\nThe reference half of the Standards description: the enforceable rules,\npatterns, and the internal library catalog that agents consult while coding.\n\nThe reference is organized as an **anatomy** — one entry checklist and four\npillars describing what an application is made of, plus delivery/meta pages\nand an optional pipelines layer.\n\n## Anatomy\n\n- **Build Recipe** — the ordered, agentic checklist for a new app; the entry\n point that walks the whole anatomy.\n\n## Pillar 1 — Domains\n\n- **Layering Rules** — the strict request → domain → repository layering and\n the rules that enforce it.\n\n## Pillar 2 — OpenAPI Spec (the contract)\n\n- **Composition Root** — a single composition point wiring the whole app.\n- **Spec-First Routing** — the OpenAPI contract as the routing source of truth.\n- **CRUD Template** — the standardized generic resource endpoint scaffold.\n- **Auth Integration** — consuming the shared auth-server service (spec-driven,\n no auth code).\n\n## Pillar 3 — Interfaces & Infra\n\n- **Infra Adapters** — how infrastructure connects without leaking into domain.\n\n## Pillar 4 — UI\n\n- **UI: Auto-Rendered Admin & Custom Pages** — the `@aetoskia/react-openapi`\n admin rendered from the spec's x- extensions, and `@aetoskia/react-auth`\n for auth pages; the client template scaffold.\n\n## Pipelines (optional layer)\n\n- **Pipeline Scaffold** — the deterministic pipeline skeleton for multi-stage\n jobs.\n- **Worker & Progress** — queued work, progress reporting, and visibility.\n- **Human-in-the-Loop** — guardrails and approvals inside automations.\n\n## Delivery & Meta\n\n- **Drone Pipelines** — the three CI shapes: a PyPI package, an npm package,\n or a Docker deployment.\n- **Versioning & Changelog** — SemVer, the tag↔version contract, and the\n release changelog.\n- **Repository Inventory** — every covered repo, its type, version source, and\n Drone shape.\n- **Genericity Matrix** — what is library-backed, template-provided, or\n authored.\n- **Library Registry** — the catalog of internal libraries behind all of it.\n\nRead the **Wiki** for the philosophy and rationale; these pages are the\nreference that mirrors the `standards` MCP bundle.\n",
"objects": {}
}
}

View File

@@ -2,7 +2,7 @@
"module": "repository_inventory",
"content": {
"path": "repository_inventory",
"docstring": "# Repository Inventory\n\n## Summary\n\nThe canonical list of Aetos repositories covered by the CI and versioning\nstandards, with the artifact each one produces, its version source, and its\nDrone shape. Every entry maps to the contract in\n[91 – Versioning & Changelog](91_versioning_and_changelog.md) and the two CI\nfiles in [90 – Drone Pipelines](90_drone_pipelines.md).\n\nThe same data is published machine-readably as the `registry://repos` MCP\nresource (and the `list_repos` tool), sourced from `standards/src/repos.json`.\nPaths are relative to the `aetos/` workspace root.\n\n---\n\n## Packages (Type A)\n\nA wheel + sdist published to the private index. Version source is\n`pyproject.toml`; Drone file is `python-lib.yaml` (tag-triggered).\n\n| Repo | Path | Docs | MCP server |\n|------|------|------|------------|\n| `dagpipe` | `dagpipe` | lib, wiki | `dagpipe:8006` |\n| `doc-forge` | `doc-forge` | lib, wiki | `docforge:8001` |\n| `hexa` | `hexa` | lib, wiki | `hexa:8008` |\n| `mail-intake` | `mail-intake` | lib, wiki | `mail_intake:8002` |\n| `mongo-ops` | `mongo-ops` | lib, wiki | `mongo_ops:8007` |\n| `omniread` | `omniread` | lib, wiki | `omniread:8003` |\n| `openapi-first` | `openapi-first` | lib, wiki | `openapi_first:8004` |\n| `py-jwt` | `py-jwt` | lib, wiki | `jwtlib:8005` |\n\n---\n\n## Services (Type B)\n\nA container image pushed to the registry and run on the Docker host. Version\nsource is the `VERSION` file; Drone file is `service-docker.yaml`.\n\n| Repo | Path | Trigger | Docs |\n|------|------|---------|------|\n| `auth-server` | `auth-server` | tag | api, lib, wiki |\n\nTag-triggered and enforces `tag == VERSION`.\n\n---\n\n## Required files per repo\n\n| Type | Version source | Changelog | Drone |\n|------|----------------|-----------|-------|\n| package | `pyproject.toml` | `CHANGELOG.md` | `python-lib.yaml` |\n| service | `VERSION` | `CHANGELOG.md` | `service-docker.yaml` |\n\nEvery covered repository must be able to satisfy `tag == version`:\nthe version file (never a commit id) is bumped in the release MR, the MR is\nmerged, and the tag is created on the merged commit.\n\n---\n\n## Reading this at runtime\n\n- Resource: `registry://repos`\n- Tool: `list_repos()`\n- Humans: this page (mirror of `standards/src/repos.json`).\n\n---\n\n## Related\n\n- [90 – Drone Pipelines](90_drone_pipelines.md) · [91 – Versioning & Changelog](91_versioning_and_changelog.md) · [93 – Library Registry](93_library_registry.md)\n",
"docstring": "# Repository Inventory\n\n## Summary\n\nThe canonical list of Aetos repositories covered by the CI and versioning\nstandards, with the artifact each one produces, its version source, and its\nDrone shape. Every entry maps to the contract in\n[91 – Versioning & Changelog](91_versioning_and_changelog.md) and the three CI\nfiles in [90 – Drone Pipelines](90_drone_pipelines.md).\n\nThe same data is published machine-readably as the `registry://repos` MCP\nresource (and the `list_repos` tool), sourced from `standards/src/repos.json`.\nPaths are relative to the `aetos/` workspace root.\n\n---\n\n## Packages (Type A — PyPI)\n\nA wheel + sdist published to the private index. Version source is\n`pyproject.toml`; Drone file is `python-lib.yaml` (tag-triggered).\n\n| Repo | Path | Docs | MCP server |\n|------|------|------|------------|\n| `dagpipe` | `dagpipe` | lib, wiki | `dagpipe:8006` |\n| `doc-forge` | `doc-forge` | lib, wiki | `docforge:8001` |\n| `hexa` | `hexa` | lib, wiki | `hexa:8008` |\n| `mail-intake` | `mail-intake` | lib, wiki | `mail_intake:8002` |\n| `mongo-ops` | `mongo-ops` | lib, wiki | `mongo_ops:8007` |\n| `omniread` | `omniread` | lib, wiki | `omniread:8003` |\n| `openapi-first` | `openapi-first` | lib, wiki | `openapi_first:8004` |\n| `py-jwt` | `py-jwt` | lib, wiki | `jwtlib:8005` |\n\n---\n\n## Packages (Type B — npm)\n\nA package published to the private npm registry (`npm.aetoskia.com`). Version\nsource is `package.json`; Drone file is `npm-lib.yaml` (tag-triggered).\n\n| Repo | Path | Docs | MCP server |\n|------|------|------|------------|\n| `doc-weaver` | `doc-weaver` | lib, wiki | `doc-weaver:8010` |\n| `react-auth` | `react/react-auth` | lib, wiki | `react-auth:8011` |\n| `react-openapi` | `react/react-openapi` | lib, wiki | `react-openapi:8012` |\n\n---\n\n## Services & Apps (Type C)\n\nA container image pushed to the registry and run on the Docker host. Version\nsource is the `VERSION` file; Drone file is `service-docker.yaml`.\n\n| Repo | Path | Type | Trigger | Docs |\n|------|------|------|---------|------|\n| `auth-server` | `auth-server` | service | tag | api, lib, wiki |\n| `vet` | `react/vet` | app | tag | lib, wiki |\n\nTag-triggered and enforces `tag == VERSION`.\n\n---\n\n## Required files per repo\n\n| Type | Version source | Changelog | Drone |\n|------|----------------|-----------|-------|\n| package (PyPI) | `pyproject.toml` | `CHANGELOG.md` | `python-lib.yaml` |\n| package (npm) | `package.json` | `CHANGELOG.md` | `npm-lib.yaml` |\n| service / app | `VERSION` | `CHANGELOG.md` | `service-docker.yaml` |\n\nEvery covered repository must be able to satisfy `tag == version`:\nthe version file (never a commit id) is bumped in the release MR, the MR is\nmerged, and the tag is created on the merged commit.\n\n---\n\n## Reading this at runtime\n\n- Resource: `registry://repos`\n- Tool: `list_repos()`\n- Humans: this page (mirror of `standards/src/repos.json`).\n\n---\n\n## Related\n\n- [90 – Drone Pipelines](90_drone_pipelines.md) · [91 – Versioning & Changelog](91_versioning_and_changelog.md) · [93 – Library Registry](93_library_registry.md)\n",
"objects": {}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{
"project": "standards",
"note": "Machine-readable repository inventory. Mirrors standards/src/lib/15_repository_inventory.md. Read it with the registry://repos resource or the list_repos tool.",
"note": "Machine-readable repository inventory. Mirrors standards/src/lib/94_repository_inventory.md. Read it with the registry://repos resource or the list_repos tool.",
"path_convention": "relative to the aetos/ workspace root",
"repos": [
{
@@ -35,6 +35,22 @@
"port": 8001
}
},
{
"name": "doc-weaver",
"type": "package",
"path": "doc-weaver",
"version_source": "package.json",
"drone": "npm-lib.yaml",
"trigger": "tag",
"docs": [
"lib",
"wiki"
],
"mcp": {
"server": "doc-weaver",
"port": 8010
}
},
{
"name": "hexa",
"type": "package",
@@ -131,6 +147,38 @@
"port": 8005
}
},
{
"name": "react-auth",
"type": "package",
"path": "react/react-auth",
"version_source": "package.json",
"drone": "npm-lib.yaml",
"trigger": "tag",
"docs": [
"lib",
"wiki"
],
"mcp": {
"server": "react-auth",
"port": 8011
}
},
{
"name": "react-openapi",
"type": "package",
"path": "react/react-openapi",
"version_source": "package.json",
"drone": "npm-lib.yaml",
"trigger": "tag",
"docs": [
"lib",
"wiki"
],
"mcp": {
"server": "react-openapi",
"port": 8012
}
},
{
"name": "auth-server",
"type": "service",
@@ -144,6 +192,19 @@
"wiki"
],
"mcp": null
},
{
"name": "vet",
"type": "app",
"path": "react/vet",
"version_source": "VERSION",
"drone": "service-docker.yaml",
"trigger": "tag",
"docs": [
"lib",
"wiki"
],
"mcp": null
}
]
}

View File

@@ -0,0 +1,2 @@
VITE_API_BASE_URL=http://localhost:8000
VITE_AUTH_BASE_URL=http://localhost:8000

View File

@@ -0,0 +1,21 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
.env
# dependencies
node_modules
# React Router
.react-router
build
# misc
.DS_Store
dist
dist-ssr
*.local
.idea
tsconfig.tsbuildinfo
# generated documentation
docs/mcp
site/

View File

@@ -0,0 +1 @@
registry=https://npm.aetoskia.com

View File

@@ -0,0 +1,15 @@
# Changelog
All notable changes to this project are documented here.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
<!--
Keep [Unreleased] current as you land changes. On release, reconcile it against
the change (git log / git diff since the last tag), add any missing user-facing
entries, and get the user's approval before promoting it into a version heading.
See standards: Versioning & Changelog (registry://release).
-->
## [Unreleased]

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>App Title</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

View File

@@ -0,0 +1,34 @@
{
"name": "client-template",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"quality-gate": "tsc --noEmit"
},
"dependencies": {
"@aetoskia/react-auth": "^0.1.0",
"@aetoskia/react-openapi": "^0.1.0",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@mui/icons-material": "^5.16.7",
"@mui/material": "^5.16.7",
"axios": "^1.7.7",
"js-yaml": "^4.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.28.0"
},
"devDependencies": {
"@types/js-yaml": "^4.0.9",
"@types/node": "^25.9.3",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "~5.6.2",
"vite": "^6.0.1"
}
}

View File

@@ -0,0 +1,66 @@
/**
* Site footer.
*
* A fixed bottom `AppBar` (pinned above the drawer via
* `zIndex.drawer + 1`) with a blurred translucent background and a top
* divider, rendering the `Copyright` line. Rendered once by `main.jsx` below
* the routed content.
*
* @packageDocumentation
*/
import * as React from 'react';
import Container from '@mui/material/Container';
import Link from '@mui/material/Link';
import Typography from '@mui/material/Typography';
import { AppBar, Toolbar } from "@mui/material";
/** Copyright line linking to aetoskia.com with the current year. */
function Copyright() {
return (
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
<Link color="text.secondary" href="https://www.aetoskia.com/">
{'Copyright © Aetoskia Internal Infrastructure — All rights reserved.'}
</Link>
&nbsp;
{new Date().getFullYear()}
</Typography>
);
}
/**
* Fixed bottom app bar showing the site copyright.
*
* @returns The footer `AppBar` element.
*/
export default function Footer() {
return (
<AppBar
position="fixed"
color="default"
sx={{
top: 'auto',
bottom: 0,
zIndex: (theme) => theme.zIndex.drawer + 1,
backdropFilter: 'blur(8px)',
boxShadow: 'none',
borderTop: '1px solid',
borderColor: 'divider',
}}
>
<Toolbar>
<Container
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: { xs: 2, sm: 4 },
textAlign: { sm: 'center', md: 'left' },
}}
>
<Copyright />
</Container>
</Toolbar>
</AppBar>
);
}

View File

@@ -0,0 +1,83 @@
/**
* Top navigation bar.
*
* A fixed `AppBar` with blurred translucent background and bottom divider,
* showing the app title/logo, the signed-in user (or a login button), and
* an Admin button. Rendered once by `main.jsx` above the routed content;
* the title navigates to `/` and the Admin button to `/admin`.
*
* @packageDocumentation
*/
import * as React from "react";
import { AppBar, Toolbar, Typography, Button, Chip, IconButton, Tooltip } from "@mui/material";
import { useNavigate } from "react-router-dom";
import DashboardIcon from "@mui/icons-material/Dashboard";
import LogoutIcon from "@mui/icons-material/Logout";
import { useAuth } from "@aetoskia/react-auth";
/**
* Fixed top app bar with the title/logo, current user, and an Admin button.
*
* Uses `useNavigate` and `useAuth`, so it must be rendered inside both a
* router and an `AuthProvider`. Clicking the title navigates to `/`; the
* Admin button navigates to `/admin`; the logout icon clears the session.
*
* @returns The header `AppBar` element.
*/
export default function Header() {
const navigate = useNavigate();
const { currentUser, logout } = useAuth();
return (
<AppBar
position="fixed"
color="default"
sx={{
zIndex: (theme) => theme.zIndex.drawer + 1,
backdropFilter: "blur(8px)",
boxShadow: "none",
borderBottom: "1px solid",
borderColor: "divider",
}}
>
<Toolbar>
<DashboardIcon sx={{ mr: 1.5, color: "primary.main" }} />
<Typography
variant="h6"
noWrap
sx={{ fontWeight: "bold", cursor: "pointer" }}
onClick={() => navigate("/")}
>
App Title
</Typography>
<span style={{ flexGrow: 1 }} />
{currentUser && (
<Chip
size="small"
label={currentUser.username ?? currentUser.email ?? "Signed in"}
sx={{ mr: 1 }}
/>
)}
<Button
color="inherit"
onClick={() => navigate("/admin")}
sx={{ textTransform: "none", fontWeight: 500, mr: 1 }}
>
Admin
</Button>
{currentUser && (
<Tooltip title="Log out">
<IconButton size="small" onClick={() => logout()} aria-label="Log out">
<LogoutIcon fontSize="small" />
</IconButton>
</Tooltip>
)}
</Toolbar>
</AppBar>
);
}

View File

@@ -0,0 +1,214 @@
/**
* Landing page: a grid of resource cards and a live preview table.
*
* Exercises the `@aetoskia/react-openapi` building blocks end-to-end:
* - `ResourceCard` - a grid of every top-level resource with a live record
* count (or SSE connection status for streaming resources).
* - `LiveTable` - preview of the first 5 records of the first non-streaming
* resource.
*
* Add custom pages as sibling routes in `main.jsx`; the auto-generated
* admin lives at `/admin/*` regardless.
*
* @packageDocumentation
*/
import React, { useEffect, useMemo, useState } from "react";
import {
Box, Container, Typography, Paper, Grid, Chip,
Table, TableBody, TableCell, TableContainer, TableHead,
TableRow, CircularProgress,
} from "@mui/material";
import { useNavigate } from "react-router-dom";
import { useAppContext, useResource, ListCellRenderer, SseConnectionStatus, applyDisplayFormat } from "@aetoskia/react-openapi";
import type { ResourceConfig, FieldConfig } from "@aetoskia/react-openapi";
/**
* Clickable card summarizing one resource.
*
* Shows the display name, field count and either a record-count chip (loaded
* via `useResource().list({ limit: 0 })`) or the SSE connection status for
* streaming resources; clicking navigates to `/admin/<resource>`.
*
* Props:
* - `resource` - the `ResourceConfig` to summarize.
*/
function ResourceCard({ resource }: { resource: ResourceConfig }) {
const navigate = useNavigate();
const { list } = useResource(resource.name);
const [count, setCount] = useState<number | null>(null);
useEffect(() => {
if (resource.streaming) return;
list({ limit: 0 }).then((res) => setCount(res.total ?? res.items?.length ?? 0));
}, [resource.name]);
return (
<Paper
elevation={0}
onClick={() => navigate(`/admin/${resource.name}`)}
sx={{
p: 3,
borderRadius: 2,
border: "1px solid",
borderColor: "divider",
cursor: "pointer",
transition: "all 0.2s",
"&:hover": { borderColor: "primary.main", boxShadow: 1 },
}}
>
<Typography variant="subtitle1" fontWeight={700}>
{resource.displayName}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
{resource.fields.length} fields
</Typography>
<Box sx={{ mt: 1.5, display: "flex", alignItems: "center", gap: 1 }}>
{resource.streaming ? (
<SseConnectionStatus resourceName={resource.name} />
) : (
<Chip
label={count != null ? `${count} records` : "..."}
size="small"
color={count != null && count > 0 ? "primary" : "default"}
variant="outlined"
/>
)}
<Chip label={resource.displayFormat} size="small" variant="outlined" />
</Box>
</Paper>
);
}
/**
* Preview table of the first 5 records of a resource.
*
* Columns come from `resource.listColumns`; each cell renders through
* `ListCellRenderer` using the resource display format (or the FK target's
* format for foreign-key columns). Shows a spinner while loading and a
* "No records found." message when empty.
*
* Props:
* - `resource` - the `ResourceConfig` to preview.
*/
function LiveTable({ resource }: { resource: ResourceConfig }) {
const { list } = useResource(resource.name);
const { resources: allResources } = useAppContext();
const [rows, setRows] = useState<any[] | null>(null);
const columns = resource.listColumns
.map((n) => resource.fields.find((f) => f.name === n))
.filter(Boolean) as FieldConfig[];
useEffect(() => {
list({ limit: 5 }).then((res) => setRows(res.items ?? []));
}, [resource.name]);
if (!rows) {
return (
<Box sx={{ display: "flex", justifyContent: "center", py: 4 }}>
<CircularProgress size={24} />
</Box>
);
}
if (rows.length === 0) {
return <Typography variant="body2" color="text.secondary">No records found.</Typography>;
}
return (
<TableContainer component={Paper} variant="outlined">
<Table size="small">
<TableHead>
<TableRow>
{columns.map((col) => (
<TableCell key={col.name} sx={{ fontWeight: 700 }}>
{col.label}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{rows.map((row, i) => (
<TableRow key={i}>
{columns.map((col) => {
const displayFormat = col.fk
? (allResources.find((r) => r.name === col.fk!.resource)?.displayFormat ?? resource.displayFormat)
: resource.displayFormat;
return (
<TableCell key={col.name}>
<ListCellRenderer
field={col}
value={row[col.name]}
displayFormat={displayFormat}
/>
</TableCell>
);
})}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
}
/**
* Render the landing page: resource grid and live preview.
*
* @returns The page container, or a "no resources" notice when the spec
* exposes none.
*/
export default function Home() {
const { resources } = useAppContext();
const topLevel = useMemo(() => resources.filter((r) => !r.parent), [resources]);
const previewResource = useMemo(() => topLevel.find((r) => !r.streaming) ?? topLevel[0], [topLevel]);
if (resources.length === 0) {
return (
<Container maxWidth="lg" sx={{ py: 6, textAlign: "center" }}>
<Typography color="text.secondary">
No resources found in the OpenAPI spec.
</Typography>
</Container>
);
}
return (
<Container maxWidth="lg" sx={{ py: 4 }}>
<Box sx={{ mb: 4, textAlign: "center" }}>
<Typography variant="h4" fontWeight={800} gutterBottom>
App Title
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ maxWidth: 600, mx: "auto" }}>
Auto-generated CRUD from an OpenAPI 3.1 spec using{" "}
<Typography component="span" fontWeight={700} color="primary">
@aetoskia/react-openapi
</Typography>
.
</Typography>
</Box>
<Typography variant="h6" fontWeight={700} sx={{ mb: 2 }}>
Resources
</Typography>
<Grid container spacing={2} sx={{ mb: 5 }}>
{topLevel.map((r) => (
<Grid key={r.name} item xs={12} sm={6} md={3}>
<ResourceCard resource={r} />
</Grid>
))}
</Grid>
<Typography variant="h6" fontWeight={700} sx={{ mb: 2 }}>
Live Preview
{previewResource && (
<Typography variant="caption" color="text.secondary" sx={{ ml: 1 }}>
— first 5 records from <strong>{previewResource.displayName}</strong>
</Typography>
)}
</Typography>
{previewResource && <LiveTable resource={previewResource} />}
</Container>
);
}

View File

@@ -0,0 +1,94 @@
/**
* Application entry point.
*
* Creates the React root from `#root` and mounts the app: `AuthProvider`
* (JWT session against `VITE_AUTH_BASE_URL`) wraps a `LoginGate` (renders
* `AuthPage` until authenticated) which wraps `AppProvider` (fed by
* `specConfiguration`) → `BrowserRouter` → `AppTheme` → `Header` → routed
* content (`Home` at `/`, `Admin` at `/admin/*`) → `Footer`.
*
* @packageDocumentation
*/
import * as React from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { Box, CssBaseline, Toolbar } from "@mui/material";
import { AuthProvider, AuthPage, useAuth } from "@aetoskia/react-auth";
import Home from './Home';
import { Admin, AppProvider } from '@aetoskia/react-openapi';
import { specConfiguration } from './openapi-config';
import Header from './Header';
import Footer from './Footer';
import AppTheme from './shared-theme/AppTheme';
const authServer = import.meta.env.VITE_AUTH_BASE_URL;
const authConfig = {
serverUrl: authServer,
loginPath: "/login",
registerPath: "/register",
logoutPath: "/logout",
mePath: "/me",
introspectPath: "/introspect",
};
/**
* Route guard: renders the split-screen `AuthPage` until a user is
* authenticated, then renders the full application shell.
*
* @returns The login gate or the authenticated app.
*/
function LoginGate() {
const auth = useAuth();
const [mode, setMode] = React.useState("login");
if (!auth.currentUser) {
return (
<AppTheme>
<CssBaseline enableColorScheme />
<AuthPage
mode={mode}
onBack={() => {}}
onSwitchMode={() => setMode((m) => (m === "login" ? "register" : "login"))}
login={auth.login}
register={auth.register}
loading={auth.loading}
error={auth.error}
currentUser={auth.currentUser}
/>
</AppTheme>
);
}
return (
<AppProvider specConfiguration={specConfiguration}>
<BrowserRouter>
<AppTheme>
<CssBaseline enableColorScheme />
<Header />
<Box sx={{ pb: 8 }}>
<Toolbar />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/admin/*" element={<Admin basePath="/admin" />} />
</Routes>
</Box>
<Footer />
</AppTheme>
</BrowserRouter>
</AppProvider>
);
}
const rootElement = document.getElementById('root');
const root = createRoot(rootElement);
root.render(
<AuthProvider authConfig={authConfig}>
<LoginGate />
</AuthProvider>
);

View File

@@ -0,0 +1,29 @@
/**
* Runtime OpenAPI configuration.
*
* Reads the API base URL from `import.meta.env.VITE_API_BASE_URL` and points
* `@aetoskia/react-openapi` at `${base}/openapi.json`, sets the app title,
* and wires the auth token from `@aetoskia/react-auth` into every API call.
*
* @packageDocumentation
*/
import type { SpecConfiguration } from "@aetoskia/react-openapi";
import { tokenStore } from "@aetoskia/react-auth";
const apiBase = import.meta.env.VITE_API_BASE_URL;
/**
* The `SpecConfiguration` passed to `AppProvider`.
*
* - `specUrl` / `baseApiUrl` - derived from `VITE_API_BASE_URL`.
* - `title` - replace "App Title" with your application's name.
* - `getToken` - supplies the JWT from `@aetoskia/react-auth` to every
* request made through `getApi()` / `useResource()`.
*/
export const specConfiguration: SpecConfiguration = {
specUrl: `${apiBase}/openapi.json`,
baseApiUrl: apiBase,
title: "App Title",
getToken: () => tokenStore.get(),
};

View File

@@ -0,0 +1,145 @@
/**
* App theme provider and color-mode context.
*
* Wraps the MUI `ThemeProvider` with the design tokens (`getDesignTokens`),
* semantic colors (`getSemanticColors`) and component customizations
* (`inputsCustomizations`, `dataDisplayCustomizations`,
* `feedbackCustomizations`, `navigationCustomizations`,
* `surfacesCustomizations`). Exposes a light/dark `ColorModeContext` and
* publishes the semantic colors as CSS custom properties (`--bg-page`,
* `--bg-card`, `--text-primary`, ...) for the app subtree.
*
* @packageDocumentation
*/
import * as React from "react";
import {
ThemeProvider,
createTheme,
CssBaseline,
Box,
} from "@mui/material";
import { getDesignTokens } from "./themePrimitives";
import { getSemanticColors } from "./themeConfig";
import { inputsCustomizations } from "./customizations/inputs";
import { dataDisplayCustomizations } from "./customizations/dataDisplay";
import { feedbackCustomizations } from "./customizations/feedback";
import { navigationCustomizations } from "./customizations/navigation";
import { surfacesCustomizations } from "./customizations/surfaces";
/** Color mode of the app theme. */
export type ColorMode = "light" | "dark";
/** Shape of the color-mode context value. */
type ColorModeContextValue = {
/** The current color mode. */
mode: ColorMode;
/** Set the color mode explicitly. */
setMode: (mode: ColorMode) => void;
/** Toggle between light and dark. */
toggleColorMode: () => void;
};
/** Context giving the current color mode and its setters. */
export const ColorModeContext =
React.createContext<ColorModeContextValue>({
mode: "light",
setMode: () => {},
toggleColorMode: () => {},
});
/** Props for `AppTheme`. */
type AppThemeProps = {
/** Subtree that receives the theme and color-mode context. */
children: React.ReactNode;
/** Initial color mode (defaults to `"light"`). */
defaultMode?: ColorMode;
};
/**
* Provide the MUI theme, color-mode context and CSS variables to `children`.
*
* Builds the theme from `getDesignTokens(mode)` + `getSemanticColors(mode)` +
* the component customizations, and re-creates it whenever the mode changes.
* Also sets `--bg-page`, `--bg-card`, `--bg-elevated`, `--border-default`,
* `--border-subtle`, `--text-primary`, `--text-secondary` and `--text-muted`
* on a wrapper `Box`.
*
* Props: `children` and `defaultMode` (see `AppThemeProps`).
*
* @example
* ```tsx
* <AppTheme defaultMode="dark">
* <CssBaseline />
* <App />
* </AppTheme>
* ```
*/
export default function AppTheme({
children,
defaultMode = "light",
}: AppThemeProps) {
const [mode, setMode] =
React.useState<ColorMode>(defaultMode);
const toggleColorMode = React.useCallback(() => {
setMode((prev) =>
prev === "light" ? "dark" : "light"
);
}, []);
const contextValue = React.useMemo(
() => ({
mode,
setMode,
toggleColorMode,
}),
[mode, toggleColorMode]
);
const semantic = React.useMemo(
() => getSemanticColors(mode),
[mode]
);
const theme = React.useMemo(
() =>
createTheme({
...getDesignTokens(mode),
semantic,
components: {
...inputsCustomizations,
...dataDisplayCustomizations,
...feedbackCustomizations,
...navigationCustomizations,
...surfacesCustomizations,
},
}),
[mode, semantic]
);
return (
<ColorModeContext.Provider value={contextValue}>
<ThemeProvider theme={theme}>
<CssBaseline />
<Box
sx={{
"--bg-page": semantic.surface.page,
"--bg-card": semantic.surface.card,
"--bg-elevated": semantic.surface.elevated,
"--border-default": semantic.border.default,
"--border-subtle": semantic.border.subtle,
"--text-primary": semantic.text.primary,
"--text-secondary": semantic.text.secondary,
"--text-muted": semantic.text.muted,
}}
>
{children}
</Box>
</ThemeProvider>
</ColorModeContext.Provider>
);
}

View File

@@ -0,0 +1,243 @@
/**
* MUI style overrides for data-display components.
*
* Exports `dataDisplayCustomizations`, a `Components<Theme>` map applied by
* `AppTheme`, covering lists, chips, table pagination and icons.
*
* @packageDocumentation
*/
import { Theme, alpha, Components } from '@mui/material/styles';
import { svgIconClasses } from '@mui/material/SvgIcon';
import { typographyClasses } from '@mui/material/Typography';
import { buttonBaseClasses } from '@mui/material/ButtonBase';
import { chipClasses } from '@mui/material/Chip';
import { iconButtonClasses } from '@mui/material/IconButton';
import { gray, red, green } from '../themePrimitives';
/* eslint-disable import/prefer-default-export */
/** Component overrides for lists, chips, table pagination and icons. */
export const dataDisplayCustomizations: Components<Theme> = {
MuiList: {
styleOverrides: {
root: {
padding: '8px',
display: 'flex',
flexDirection: 'column',
gap: 0,
},
},
},
MuiListItem: {
styleOverrides: {
root: ({ theme }) => ({
[`& .${svgIconClasses.root}`]: {
width: '1rem',
height: '1rem',
color: (theme.vars || theme).palette.text.secondary,
},
[`& .${typographyClasses.root}`]: {
fontWeight: 500,
},
[`& .${buttonBaseClasses.root}`]: {
display: 'flex',
gap: 8,
padding: '2px 8px',
borderRadius: (theme.vars || theme).shape.borderRadius,
opacity: 0.7,
'&.Mui-selected': {
opacity: 1,
backgroundColor: alpha(theme.palette.action.selected, 0.3),
[`& .${svgIconClasses.root}`]: {
color: (theme.vars || theme).palette.text.primary,
},
'&:focus-visible': {
backgroundColor: alpha(theme.palette.action.selected, 0.3),
},
'&:hover': {
backgroundColor: alpha(theme.palette.action.selected, 0.5),
},
},
'&:focus-visible': {
backgroundColor: 'transparent',
},
},
}),
},
},
MuiListItemText: {
styleOverrides: {
primary: ({ theme }) => ({
fontSize: theme.typography.body2.fontSize,
fontWeight: 500,
lineHeight: theme.typography.body2.lineHeight,
}),
secondary: ({ theme }) => ({
fontSize: theme.typography.caption.fontSize,
lineHeight: theme.typography.caption.lineHeight,
}),
},
},
MuiListSubheader: {
styleOverrides: {
root: ({ theme }) => ({
backgroundColor: 'transparent',
padding: '4px 8px',
fontSize: theme.typography.caption.fontSize,
fontWeight: 500,
lineHeight: theme.typography.caption.lineHeight,
}),
},
},
MuiListItemIcon: {
styleOverrides: {
root: {
minWidth: 0,
},
},
},
MuiChip: {
defaultProps: {
size: 'small',
},
styleOverrides: {
root: ({ theme }) => ({
border: '1px solid',
borderRadius: '999px',
[`& .${chipClasses.label}`]: {
fontWeight: 600,
},
variants: [
{
props: {
color: 'default',
},
style: {
borderColor: gray[200],
backgroundColor: gray[100],
[`& .${chipClasses.label}`]: {
color: gray[500],
},
[`& .${chipClasses.icon}`]: {
color: gray[500],
},
...theme.applyStyles('dark', {
borderColor: gray[700],
backgroundColor: gray[800],
[`& .${chipClasses.label}`]: {
color: gray[300],
},
[`& .${chipClasses.icon}`]: {
color: gray[300],
},
}),
},
},
{
props: {
color: 'success',
},
style: {
borderColor: green[200],
backgroundColor: green[50],
[`& .${chipClasses.label}`]: {
color: green[500],
},
[`& .${chipClasses.icon}`]: {
color: green[500],
},
...theme.applyStyles('dark', {
borderColor: green[800],
backgroundColor: green[900],
[`& .${chipClasses.label}`]: {
color: green[300],
},
[`& .${chipClasses.icon}`]: {
color: green[300],
},
}),
},
},
{
props: {
color: 'error',
},
style: {
borderColor: red[100],
backgroundColor: red[50],
[`& .${chipClasses.label}`]: {
color: red[500],
},
[`& .${chipClasses.icon}`]: {
color: red[500],
},
...theme.applyStyles('dark', {
borderColor: red[800],
backgroundColor: red[900],
[`& .${chipClasses.label}`]: {
color: red[200],
},
[`& .${chipClasses.icon}`]: {
color: red[300],
},
}),
},
},
{
props: { size: 'small' },
style: {
maxHeight: 20,
[`& .${chipClasses.label}`]: {
fontSize: theme.typography.caption.fontSize,
},
[`& .${svgIconClasses.root}`]: {
fontSize: theme.typography.caption.fontSize,
},
},
},
{
props: { size: 'medium' },
style: {
[`& .${chipClasses.label}`]: {
fontSize: theme.typography.caption.fontSize,
},
},
},
],
}),
},
},
MuiTablePagination: {
styleOverrides: {
actions: {
display: 'flex',
gap: 8,
marginRight: 6,
[`& .${iconButtonClasses.root}`]: {
minWidth: 0,
width: 36,
height: 36,
},
},
},
},
MuiIcon: {
defaultProps: {
fontSize: 'small',
},
styleOverrides: {
root: {
variants: [
{
props: {
fontSize: 'small',
},
style: {
fontSize: '1rem',
},
},
],
},
},
},
};

View File

@@ -0,0 +1,56 @@
/**
* MUI style overrides for feedback components.
*
* Exports `feedbackCustomizations`, a `Components<Theme>` map applied by
* `AppTheme`, covering alerts, dialogs and linear progress.
*
* @packageDocumentation
*/
import { Theme, alpha, Components } from '@mui/material/styles';
import { gray, orange } from '../themePrimitives';
/* eslint-disable import/prefer-default-export */
/** Component overrides for alerts, dialogs and linear progress. */
export const feedbackCustomizations: Components<Theme> = {
MuiAlert: {
styleOverrides: {
root: ({ theme }) => ({
borderRadius: 10,
backgroundColor: orange[100],
color: (theme.vars || theme).palette.text.primary,
border: `1px solid ${alpha(orange[300], 0.5)}`,
'& .MuiAlert-icon': {
color: orange[500],
},
...theme.applyStyles('dark', {
backgroundColor: alpha(orange[900], 0.35),
border: `1px solid ${alpha(orange[800], 0.3)}`,
}),
}),
},
},
MuiDialog: {
styleOverrides: {
root: ({ theme }) => ({
'& .MuiDialog-paper': {
borderRadius: '10px',
border: '1px solid',
borderColor: (theme.vars || theme).palette.divider,
},
}),
},
},
MuiLinearProgress: {
styleOverrides: {
root: ({ theme }) => ({
height: 8,
borderRadius: 8,
backgroundColor: gray[200],
...theme.applyStyles('dark', {
backgroundColor: gray[800],
}),
}),
},
},
};

View File

@@ -0,0 +1,456 @@
/**
* MUI style overrides for form inputs and buttons.
*
* Exports `inputsCustomizations`, a `Components<Theme>` map applied by
* `AppTheme`, covering buttons, icon buttons, toggles, checkbox, outlined
* inputs and form labels with light/dark variants.
*
* @packageDocumentation
*/
import * as React from 'react';
import { alpha, Theme, Components } from '@mui/material/styles';
import { outlinedInputClasses } from '@mui/material/OutlinedInput';
import { svgIconClasses } from '@mui/material/SvgIcon';
import { toggleButtonGroupClasses } from '@mui/material/ToggleButtonGroup';
import { toggleButtonClasses } from '@mui/material/ToggleButton';
import CheckBoxOutlineBlankRoundedIcon from '@mui/icons-material/CheckBoxOutlineBlankRounded';
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
import RemoveRoundedIcon from '@mui/icons-material/RemoveRounded';
import { gray, brand } from '../themePrimitives';
/* eslint-disable import/prefer-default-export */
/** Component overrides for buttons, toggles, inputs and labels. */
export const inputsCustomizations: Components<Theme> = {
MuiButtonBase: {
defaultProps: {
disableTouchRipple: true,
disableRipple: true,
},
styleOverrides: {
root: ({ theme }) => ({
boxSizing: 'border-box',
transition: 'all 100ms ease-in',
'&:focus-visible': {
outline: `3px solid ${alpha(theme.palette.primary.main, 0.5)}`,
outlineOffset: '2px',
},
}),
},
},
MuiButton: {
styleOverrides: {
root: ({ theme }) => ({
boxShadow: 'none',
borderRadius: (theme.vars || theme).shape.borderRadius,
textTransform: 'none',
variants: [
{
props: {
size: 'small',
},
style: {
height: '2.25rem',
padding: '8px 12px',
},
},
{
props: {
size: 'medium',
},
style: {
height: '2.5rem', // 40px
},
},
{
props: {
color: 'primary',
variant: 'contained',
},
style: {
color: 'white',
backgroundColor: gray[900],
backgroundImage: `linear-gradient(to bottom, ${gray[700]}, ${gray[800]})`,
boxShadow: `inset 0 1px 0 ${gray[600]}, inset 0 -1px 0 1px hsl(220, 0%, 0%)`,
border: `1px solid ${gray[700]}`,
'&:hover': {
backgroundImage: 'none',
backgroundColor: gray[700],
boxShadow: 'none',
},
'&:active': {
backgroundColor: gray[800],
},
...theme.applyStyles('dark', {
color: 'black',
backgroundColor: gray[50],
backgroundImage: `linear-gradient(to bottom, ${gray[100]}, ${gray[50]})`,
boxShadow: 'inset 0 -1px 0 hsl(220, 30%, 80%)',
border: `1px solid ${gray[50]}`,
'&:hover': {
backgroundImage: 'none',
backgroundColor: gray[300],
boxShadow: 'none',
},
'&:active': {
backgroundColor: gray[400],
},
}),
},
},
{
props: {
color: 'secondary',
variant: 'contained',
},
style: {
color: 'white',
backgroundColor: brand[300],
backgroundImage: `linear-gradient(to bottom, ${alpha(brand[400], 0.8)}, ${brand[500]})`,
boxShadow: `inset 0 2px 0 ${alpha(brand[200], 0.2)}, inset 0 -2px 0 ${alpha(brand[700], 0.4)}`,
border: `1px solid ${brand[500]}`,
'&:hover': {
backgroundColor: brand[700],
boxShadow: 'none',
},
'&:active': {
backgroundColor: brand[700],
backgroundImage: 'none',
},
},
},
{
props: {
variant: 'outlined',
},
style: {
color: (theme.vars || theme).palette.text.primary,
border: '1px solid',
borderColor: gray[200],
backgroundColor: alpha(gray[50], 0.3),
'&:hover': {
backgroundColor: gray[100],
borderColor: gray[300],
},
'&:active': {
backgroundColor: gray[200],
},
...theme.applyStyles('dark', {
backgroundColor: 'hsla(0, 0%, 100%, 0.06)',
borderColor: (theme.vars || theme).palette.divider,
'&:hover': {
backgroundColor: 'hsla(0, 0%, 100%, 0.1)',
borderColor: 'hsla(0, 0%, 100%, 0.15)',
},
'&:active': {
backgroundColor: 'hsla(0, 0%, 100%, 0.1)',
},
}),
},
},
{
props: {
color: 'secondary',
variant: 'outlined',
},
style: {
color: brand[700],
border: '1px solid',
borderColor: brand[200],
backgroundColor: brand[50],
'&:hover': {
backgroundColor: brand[100],
borderColor: brand[400],
},
'&:active': {
backgroundColor: alpha(brand[200], 0.7),
},
...theme.applyStyles('dark', {
color: brand[50],
border: '1px solid',
borderColor: brand[900],
backgroundColor: alpha(brand[900], 0.3),
'&:hover': {
borderColor: brand[700],
backgroundColor: alpha(brand[900], 0.6),
},
'&:active': {
backgroundColor: alpha(brand[900], 0.5),
},
}),
},
},
{
props: {
variant: 'text',
},
style: {
color: gray[600],
'&:hover': {
backgroundColor: gray[100],
},
'&:active': {
backgroundColor: gray[200],
},
...theme.applyStyles('dark', {
color: 'hsl(0, 0%, 92%)',
'&:hover': {
backgroundColor: 'hsla(0, 0%, 100%, 0.08)',
},
'&:active': {
backgroundColor: 'hsla(0, 0%, 100%, 0.12)',
},
}),
},
},
{
props: {
color: 'secondary',
variant: 'text',
},
style: {
color: brand[700],
'&:hover': {
backgroundColor: alpha(brand[100], 0.5),
},
'&:active': {
backgroundColor: alpha(brand[200], 0.7),
},
...theme.applyStyles('dark', {
color: brand[100],
'&:hover': {
backgroundColor: alpha(brand[900], 0.5),
},
'&:active': {
backgroundColor: alpha(brand[900], 0.3),
},
}),
},
},
],
}),
},
},
MuiIconButton: {
styleOverrides: {
root: ({ theme }) => ({
boxShadow: 'none',
borderRadius: (theme.vars || theme).shape.borderRadius,
textTransform: 'none',
fontWeight: theme.typography.fontWeightMedium,
letterSpacing: 0,
color: (theme.vars || theme).palette.text.primary,
border: '1px solid ',
borderColor: gray[200],
backgroundColor: alpha(gray[50], 0.3),
'&:hover': {
backgroundColor: gray[100],
borderColor: gray[300],
},
'&:active': {
backgroundColor: gray[200],
},
...theme.applyStyles('dark', {
backgroundColor: 'hsla(0, 0%, 100%, 0.06)',
borderColor: (theme.vars || theme).palette.divider,
'&:hover': {
backgroundColor: 'hsla(0, 0%, 100%, 0.1)',
borderColor: 'hsla(0, 0%, 100%, 0.15)',
},
'&:active': {
backgroundColor: 'hsla(0, 0%, 100%, 0.1)',
},
}),
variants: [
{
props: {
size: 'small',
},
style: {
width: '2.25rem',
height: '2.25rem',
padding: '0.25rem',
[`& .${svgIconClasses.root}`]: { fontSize: '1rem' },
},
},
{
props: {
size: 'medium',
},
style: {
width: '2.5rem',
height: '2.5rem',
},
},
],
}),
},
},
MuiToggleButtonGroup: {
styleOverrides: {
root: ({ theme }) => ({
borderRadius: '10px',
boxShadow: `0 4px 16px ${alpha(gray[400], 0.2)}`,
[`& .${toggleButtonGroupClasses.selected}`]: {
color: brand[500],
},
...theme.applyStyles('dark', {
[`& .${toggleButtonGroupClasses.selected}`]: {
color: '#fff',
},
boxShadow: `0 2px 8px ${alpha(brand[700], 0.3)}`,
}),
}),
},
},
MuiToggleButton: {
styleOverrides: {
root: ({ theme }) => ({
padding: '12px 16px',
textTransform: 'none',
borderRadius: '10px',
fontWeight: 500,
...theme.applyStyles('dark', {
color: gray[400],
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.25)',
[`&.${toggleButtonClasses.selected}`]: {
color: brand[300],
},
}),
}),
},
},
MuiCheckbox: {
defaultProps: {
disableRipple: true,
icon: (
<CheckBoxOutlineBlankRoundedIcon sx={{ color: 'hsla(210, 0%, 0%, 0.0)' }} />
),
checkedIcon: <CheckRoundedIcon sx={{ height: 14, width: 14 }} />,
indeterminateIcon: <RemoveRoundedIcon sx={{ height: 14, width: 14 }} />,
},
styleOverrides: {
root: ({ theme }) => ({
margin: 10,
height: 16,
width: 16,
borderRadius: 5,
border: '1px solid ',
borderColor: alpha(gray[300], 0.8),
boxShadow: '0 0 0 1.5px hsla(210, 0%, 0%, 0.04) inset',
backgroundColor: alpha(gray[100], 0.4),
transition: 'border-color, background-color, 120ms ease-in',
'&:hover': {
borderColor: brand[300],
},
'&.Mui-focusVisible': {
outline: `3px solid ${alpha(brand[500], 0.5)}`,
outlineOffset: '2px',
borderColor: brand[400],
},
'&.Mui-checked': {
color: 'white',
backgroundColor: brand[500],
borderColor: brand[500],
boxShadow: `none`,
'&:hover': {
backgroundColor: brand[600],
},
},
...theme.applyStyles('dark', {
borderColor: alpha(gray[700], 0.8),
boxShadow: '0 0 0 1.5px hsl(210, 0%, 0%) inset',
backgroundColor: alpha(gray[900], 0.8),
'&:hover': {
borderColor: brand[300],
},
'&.Mui-focusVisible': {
borderColor: brand[400],
outline: `3px solid ${alpha(brand[500], 0.5)}`,
outlineOffset: '2px',
},
}),
}),
},
},
MuiInputBase: {
styleOverrides: {
root: {
border: 'none',
},
input: {
'&::placeholder': {
opacity: 0.7,
color: gray[500],
},
},
},
},
MuiOutlinedInput: {
styleOverrides: {
input: {
padding: 0,
},
root: ({ theme }) => ({
padding: '8px 12px',
color: (theme.vars || theme).palette.text.primary,
borderRadius: (theme.vars || theme).shape.borderRadius,
border: `1px solid ${(theme.vars || theme).palette.divider}`,
backgroundColor: (theme.vars || theme).palette.background.default,
transition: 'border 120ms ease-in',
'&:hover': {
borderColor: gray[400],
},
[`&.${outlinedInputClasses.focused}`]: {
outline: `3px solid ${alpha(brand[500], 0.5)}`,
borderColor: brand[400],
},
...theme.applyStyles('dark', {
'&:hover': {
borderColor: gray[500],
},
}),
variants: [
{
props: {
size: 'small',
},
style: {
height: '2.25rem',
},
},
{
props: {
size: 'medium',
},
style: {
height: '2.5rem',
},
},
],
}),
notchedOutline: {
border: 'none',
},
},
},
MuiInputAdornment: {
styleOverrides: {
root: ({ theme }) => ({
color: (theme.vars || theme).palette.grey[500],
...theme.applyStyles('dark', {
color: (theme.vars || theme).palette.grey[400],
}),
}),
},
},
MuiFormLabel: {
styleOverrides: {
root: ({ theme }) => ({
typography: theme.typography.caption,
marginBottom: 8,
}),
},
},
};

View File

@@ -0,0 +1,290 @@
/**
* MUI style overrides for navigation components.
*
* Exports `navigationCustomizations`, a `Components<Theme>` map applied by
* `AppTheme`, covering menus, selects, links, drawers, pagination, tabs and
* steppers.
*
* @packageDocumentation
*/
import * as React from 'react';
import { Theme, alpha, Components } from '@mui/material/styles';
import { SvgIconProps } from '@mui/material/SvgIcon';
import { buttonBaseClasses } from '@mui/material/ButtonBase';
import { dividerClasses } from '@mui/material/Divider';
import { menuItemClasses } from '@mui/material/MenuItem';
import { selectClasses } from '@mui/material/Select';
import { tabClasses } from '@mui/material/Tab';
import UnfoldMoreRoundedIcon from '@mui/icons-material/UnfoldMoreRounded';
import { gray, brand } from '../themePrimitives';
/* eslint-disable import/prefer-default-export */
/** Component overrides for menus, selects, links, drawers, pagination and tabs. */
export const navigationCustomizations: Components<Theme> = {
MuiMenuItem: {
styleOverrides: {
root: ({ theme }) => ({
borderRadius: (theme.vars || theme).shape.borderRadius,
padding: '6px 8px',
[`&.${menuItemClasses.focusVisible}`]: {
backgroundColor: 'transparent',
},
[`&.${menuItemClasses.selected}`]: {
[`&.${menuItemClasses.focusVisible}`]: {
backgroundColor: alpha(theme.palette.action.selected, 0.3),
},
},
}),
},
},
MuiMenu: {
styleOverrides: {
list: {
gap: '0px',
[`&.${dividerClasses.root}`]: {
margin: '0 -8px',
},
},
paper: ({ theme }) => ({
marginTop: '4px',
borderRadius: (theme.vars || theme).shape.borderRadius,
border: `1px solid ${(theme.vars || theme).palette.divider}`,
backgroundImage: 'none',
background: 'hsl(0, 0%, 100%)',
boxShadow:
'hsla(220, 30%, 5%, 0.07) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.07) 0px 8px 16px -5px',
[`& .${buttonBaseClasses.root}`]: {
'&.Mui-selected': {
backgroundColor: alpha(theme.palette.action.selected, 0.3),
},
},
...theme.applyStyles('dark', {
background: (theme.vars || theme).palette.background.paper,
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.3)',
}),
}),
},
},
MuiSelect: {
defaultProps: {
IconComponent: React.forwardRef<SVGSVGElement, SvgIconProps>((props, ref) => (
<UnfoldMoreRoundedIcon fontSize="small" {...props} ref={ref} />
)),
},
styleOverrides: {
root: ({ theme }) => ({
borderRadius: (theme.vars || theme).shape.borderRadius,
border: '1px solid',
borderColor: gray[200],
backgroundColor: (theme.vars || theme).palette.background.paper,
boxShadow: `inset 0 1px 0 1px hsla(220, 0%, 100%, 0.6), inset 0 -1px 0 1px hsla(220, 35%, 90%, 0.5)`,
'&:hover': {
borderColor: gray[300],
backgroundColor: (theme.vars || theme).palette.background.paper,
boxShadow: 'none',
},
[`&.${selectClasses.focused}`]: {
outlineOffset: 0,
borderColor: gray[400],
},
'&:before, &:after': {
display: 'none',
},
...theme.applyStyles('dark', {
borderRadius: (theme.vars || theme).shape.borderRadius,
borderColor: (theme.vars || theme).palette.divider,
backgroundColor: (theme.vars || theme).palette.background.paper,
boxShadow: 'inset 0 1px 0 hsla(0, 0%, 100%, 0.05)',
'&:hover': {
borderColor: 'hsla(0, 0%, 100%, 0.15)',
backgroundColor: (theme.vars || theme).palette.background.paper,
boxShadow: 'none',
},
[`&.${selectClasses.focused}`]: {
outlineOffset: 0,
borderColor: 'hsl(210, 55%, 55%)',
},
'&:before, &:after': {
display: 'none',
},
}),
}),
select: ({ theme }) => ({
display: 'flex',
alignItems: 'center',
...theme.applyStyles('dark', {
display: 'flex',
alignItems: 'center',
'&:focus-visible': {
backgroundColor: (theme.vars || theme).palette.background.default,
},
}),
}),
},
},
MuiLink: {
defaultProps: {
underline: 'none',
},
styleOverrides: {
root: ({ theme }) => ({
color: (theme.vars || theme).palette.text.primary,
fontWeight: 500,
position: 'relative',
textDecoration: 'none',
width: 'fit-content',
'&::before': {
content: '""',
position: 'absolute',
width: '100%',
height: '1px',
bottom: 0,
left: 0,
backgroundColor: (theme.vars || theme).palette.text.secondary,
opacity: 0.3,
transition: 'width 0.3s ease, opacity 0.3s ease',
},
'&:hover::before': {
width: 0,
},
'&:focus-visible': {
outline: `3px solid ${alpha(brand[500], 0.5)}`,
outlineOffset: '4px',
borderRadius: '2px',
},
}),
},
},
MuiDrawer: {
styleOverrides: {
paper: ({ theme }) => ({
backgroundColor: (theme.vars || theme).palette.background.default,
borderRight: `1px solid ${(theme.vars || theme).palette.divider}`,
}),
},
},
MuiPaginationItem: {
styleOverrides: {
root: ({ theme }) => ({
'&.Mui-selected': {
color: 'white',
backgroundColor: (theme.vars || theme).palette.grey[900],
},
...theme.applyStyles('dark', {
'&.Mui-selected': {
color: 'black',
backgroundColor: (theme.vars || theme).palette.grey[50],
},
}),
}),
},
},
MuiTabs: {
styleOverrides: {
root: { minHeight: 'fit-content' },
indicator: ({ theme }) => ({
backgroundColor: (theme.vars || theme).palette.grey[800],
...theme.applyStyles('dark', {
backgroundColor: (theme.vars || theme).palette.grey[200],
}),
}),
},
},
MuiTab: {
styleOverrides: {
root: ({ theme }) => ({
padding: '6px 8px',
marginBottom: '8px',
textTransform: 'none',
minWidth: 'fit-content',
minHeight: 'fit-content',
color: (theme.vars || theme).palette.text.secondary,
borderRadius: (theme.vars || theme).shape.borderRadius,
border: '1px solid',
borderColor: 'transparent',
':hover': {
color: (theme.vars || theme).palette.text.primary,
backgroundColor: gray[100],
borderColor: gray[200],
},
[`&.${tabClasses.selected}`]: {
color: gray[900],
},
...theme.applyStyles('dark', {
':hover': {
color: (theme.vars || theme).palette.text.primary,
backgroundColor: alpha((theme.vars || theme).palette.common.white, 0.08),
borderColor: (theme.vars || theme).palette.divider,
},
[`&.${tabClasses.selected}`]: {
color: '#fff',
},
}),
}),
},
},
MuiStepConnector: {
styleOverrides: {
line: ({ theme }) => ({
borderTop: '1px solid',
borderColor: (theme.vars || theme).palette.divider,
flex: 1,
borderRadius: '99px',
}),
},
},
MuiStepIcon: {
styleOverrides: {
root: ({ theme }) => ({
color: 'transparent',
border: `1px solid ${gray[400]}`,
width: 12,
height: 12,
borderRadius: '50%',
'& text': {
display: 'none',
},
'&.Mui-active': {
border: 'none',
color: (theme.vars || theme).palette.primary.main,
},
'&.Mui-completed': {
border: 'none',
color: (theme.vars || theme).palette.success.main,
},
...theme.applyStyles('dark', {
border: `1px solid ${gray[700]}`,
'&.Mui-active': {
border: 'none',
color: (theme.vars || theme).palette.primary.light,
},
'&.Mui-completed': {
border: 'none',
color: (theme.vars || theme).palette.success.light,
},
}),
variants: [
{
props: { completed: true },
style: {
width: 12,
height: 12,
},
},
],
}),
},
},
MuiStepLabel: {
styleOverrides: {
label: ({ theme }) => ({
'&.Mui-completed': {
opacity: 0.6,
...theme.applyStyles('dark', { opacity: 0.5 }),
},
}),
},
},
};

View File

@@ -0,0 +1,123 @@
/**
* MUI style overrides for surface components.
*
* Exports `surfacesCustomizations`, a `Components<Theme>` map applied by
* `AppTheme`, covering accordions, paper, cards and card sections.
*
* @packageDocumentation
*/
import { alpha, Theme, Components } from '@mui/material/styles';
import { gray } from '../themePrimitives';
/* eslint-disable import/prefer-default-export */
/** Component overrides for accordions, paper and cards. */
export const surfacesCustomizations: Components<Theme> = {
MuiAccordion: {
defaultProps: {
elevation: 0,
disableGutters: true,
},
styleOverrides: {
root: ({ theme }) => ({
padding: 4,
overflow: 'clip',
backgroundColor: (theme.vars || theme).palette.background.default,
border: '1px solid',
borderColor: (theme.vars || theme).palette.divider,
':before': {
backgroundColor: 'transparent',
},
'&:not(:last-of-type)': {
borderBottom: 'none',
},
'&:first-of-type': {
borderTopLeftRadius: (theme.vars || theme).shape.borderRadius,
borderTopRightRadius: (theme.vars || theme).shape.borderRadius,
},
'&:last-of-type': {
borderBottomLeftRadius: (theme.vars || theme).shape.borderRadius,
borderBottomRightRadius: (theme.vars || theme).shape.borderRadius,
},
}),
},
},
MuiAccordionSummary: {
styleOverrides: {
root: ({ theme }) => ({
border: 'none',
borderRadius: 8,
'&:hover': { backgroundColor: gray[50] },
'&:focus-visible': { backgroundColor: 'transparent' },
...theme.applyStyles('dark', {
'&:hover': { backgroundColor: alpha(theme.palette.common.white, 0.06) },
}),
}),
},
},
MuiAccordionDetails: {
styleOverrides: {
root: { mb: 20, border: 'none' },
},
},
MuiPaper: {
defaultProps: {
elevation: 0,
},
},
MuiCard: {
styleOverrides: {
root: ({ theme }) => {
return {
padding: 16,
gap: 16,
transition: 'all 100ms ease',
backgroundColor: gray[50],
borderRadius: (theme.vars || theme).shape.borderRadius,
border: `1px solid ${(theme.vars || theme).palette.divider}`,
boxShadow: 'none',
...theme.applyStyles('dark', {
backgroundColor: (theme.vars || theme).palette.background.paper,
}),
variants: [
{
props: {
variant: 'outlined',
},
style: {
border: `1px solid ${(theme.vars || theme).palette.divider}`,
boxShadow: 'none',
background: 'hsl(0, 0%, 100%)',
...theme.applyStyles('dark', {
background: alpha((theme.vars || theme).palette.background.paper, 0.6),
}),
},
},
],
};
},
},
},
MuiCardContent: {
styleOverrides: {
root: {
padding: 0,
'&:last-child': { paddingBottom: 0 },
},
},
},
MuiCardHeader: {
styleOverrides: {
root: {
padding: 0,
},
},
},
MuiCardActions: {
styleOverrides: {
root: {
padding: 0,
},
},
},
};

View File

@@ -0,0 +1,27 @@
/**
* MUI v5 type shims for the shared theme.
*
* The component customizations use the MUI v6 CSS-variable style
* (`theme.vars || theme`), which is runtime-safe on v5 (falls back to the
* plain theme) but not covered by the v5 type definitions. These
* augmentations teach the v5 types about that pattern so `tsc` passes on
* both majors.
*
* @packageDocumentation
*/
import "@mui/material/styles";
declare module "@mui/material/styles" {
interface Theme {
/** CSS-variable slice; undefined on v5 at runtime — always use `(theme.vars || theme)`. */
vars: Theme;
}
}
declare module "@mui/material/Select" {
interface SelectClasses {
/** Present in MUI v6+; undefined on v5 (dead selector, harmless). */
focused: string;
}
}

View File

@@ -0,0 +1,112 @@
/**
* Semantic (mode-aware) color tokens.
*
* Adds a `semantic` slice to the MUI theme (augmenting `@mui/material/styles`)
* with surface/border/text colors, resolved per light/dark mode by
* `getSemanticColors`. `AppTheme` publishes these as CSS custom properties.
*
* @packageDocumentation
*/
import { gray } from "./themePrimitives";
import { alpha } from "@mui/material/styles";
declare module "@mui/material/styles" {
interface Theme {
semantic: SemanticColors;
}
interface ThemeOptions {
semantic?: SemanticColors;
}
}
/** Color mode supported by the semantic tokens. */
export type SemanticColorMode = "light" | "dark";
/**
* Mode-aware semantic color groups attached to the MUI theme as
* `theme.semantic` and exposed as CSS custom properties by `AppTheme`.
*/
export interface SemanticColors {
/** Background surface colors. */
surface: {
/** Page background (`--bg-page`). */
page: string;
/** Card background (`--bg-card`). */
card: string;
/** Elevated surface, e.g. popovers (`--bg-elevated`). */
elevated: string;
};
/** Border colors. */
border: {
/** Default border (`--border-default`). */
default: string;
/** Subtle border (`--border-subtle`). */
subtle: string;
};
/** Text colors. */
text: {
/** Primary text (`--text-primary`). */
primary: string;
/** Secondary text (`--text-secondary`). */
secondary: string;
/** Muted text (`--text-muted`). */
muted: string;
};
}
const darkBg = 'hsl(0, 0%, 9%)';
const darkPaper = 'hsl(0, 0%, 14%)';
const darkElevated = 'hsl(0, 0%, 19%)';
/**
* Resolve the semantic color groups for a color mode.
*
* Dark mode uses fixed HSL values; light mode derives borders/text from the
* `gray` ramp (with `alpha`).
*
* @param mode - `"light"` or `"dark"`.
* @returns The `SemanticColors` for that mode.
* @example
* ```ts
* const colors = getSemanticColors("dark");
* colors.surface.page; // "hsl(0, 0%, 9%)"
* ```
*/
export function getSemanticColors(mode: SemanticColorMode): SemanticColors {
if (mode === "dark") {
return {
surface: {
page: darkBg,
card: darkPaper,
elevated: darkElevated,
},
border: {
default: 'hsla(0, 0%, 100%, 0.08)',
subtle: 'hsla(0, 0%, 100%, 0.04)',
},
text: {
primary: 'hsl(0, 0%, 92%)',
secondary: 'hsl(0, 0%, 60%)',
muted: 'hsl(0, 0%, 45%)',
},
};
}
return {
surface: {
page: "hsl(0, 0%, 99%)",
card: "hsl(220, 35%, 97%)",
elevated: gray[100],
},
border: {
default: alpha(gray[300], 0.4),
subtle: alpha(gray[200], 0.3),
},
text: {
primary: gray[800],
secondary: gray[600],
muted: gray[500],
},
};
}

View File

@@ -0,0 +1,480 @@
/**
* Base design tokens and palettes for the app theme.
*
* Defines the raw color ramps (`brand`, `gray`, `green`, `orange`, `red`), the
* light/dark design tokens (`getDesignTokens`), and the `colorSchemes`,
* `typography`, `shape` and `shadows` primitives. Also augments MUI's theme
* types with the `ColorRange`, `flows` and `baseShadow` extensions.
*
* @packageDocumentation
*/
import { createTheme, alpha, Shadows } from '@mui/material/styles';
import type { PaletteMode } from '@mui/material';
declare module '@mui/material/Paper' {
interface PaperPropsVariantOverrides {
highlighted: true;
}
}
declare module '@mui/material/styles' {
interface ColorRange {
50: string;
100: string;
200: string;
300: string;
400: string;
500: string;
600: string;
700: string;
800: string;
900: string;
}
interface PaletteColor extends ColorRange {}
interface Palette {
baseShadow: string;
flows: {
outflows: { primary: string; surface: string; text: string };
inflows: { primary: string; surface: string; text: string };
};
}
}
const defaultTheme = createTheme();
const customShadows: Shadows = [...defaultTheme.shadows];
/** Brand blue color ramp (50-900). */
export const brand = {
50: 'hsl(210, 100%, 95%)',
100: 'hsl(210, 100%, 92%)',
200: 'hsl(210, 100%, 80%)',
300: 'hsl(210, 100%, 65%)',
400: 'hsl(210, 98%, 48%)',
500: 'hsl(210, 98%, 42%)',
600: 'hsl(210, 98%, 55%)',
700: 'hsl(210, 100%, 35%)',
800: 'hsl(210, 100%, 16%)',
900: 'hsl(210, 100%, 21%)',
};
/** Neutral gray color ramp (50-900, plus 750/850). */
export const gray = {
50: 'hsl(220, 35%, 97%)',
100: 'hsl(220, 30%, 94%)',
200: 'hsl(220, 20%, 88%)',
300: 'hsl(220, 20%, 80%)',
400: 'hsl(220, 20%, 65%)',
500: 'hsl(220, 20%, 42%)',
600: 'hsl(220, 20%, 35%)',
700: 'hsl(220, 20%, 25%)',
750: 'hsl(220, 20%, 18%)',
800: 'hsl(220, 30%, 6%)',
850: 'hsl(220, 22%, 11%)',
900: 'hsl(220, 35%, 3%)',
};
/** Green (success) color ramp (50-900). */
export const green = {
50: 'hsl(120, 80%, 98%)',
100: 'hsl(120, 75%, 94%)',
200: 'hsl(120, 75%, 87%)',
300: 'hsl(120, 61%, 77%)',
400: 'hsl(120, 44%, 53%)',
500: 'hsl(120, 59%, 30%)',
600: 'hsl(120, 70%, 25%)',
700: 'hsl(120, 75%, 16%)',
800: 'hsl(120, 84%, 10%)',
900: 'hsl(120, 87%, 6%)',
};
/** Orange (warning) color ramp (50-900). */
export const orange = {
50: 'hsl(45, 100%, 97%)',
100: 'hsl(45, 92%, 90%)',
200: 'hsl(45, 94%, 80%)',
300: 'hsl(45, 90%, 65%)',
400: 'hsl(45, 90%, 40%)',
500: 'hsl(45, 90%, 35%)',
600: 'hsl(45, 91%, 25%)',
700: 'hsl(45, 94%, 20%)',
800: 'hsl(45, 95%, 16%)',
900: 'hsl(45, 93%, 12%)',
};
/** Red (error) color ramp (50-900). */
export const red = {
50: 'hsl(0, 100%, 97%)',
100: 'hsl(0, 92%, 90%)',
200: 'hsl(0, 94%, 80%)',
300: 'hsl(0, 90%, 65%)',
400: 'hsl(0, 90%, 40%)',
500: 'hsl(0, 90%, 30%)',
600: 'hsl(0, 91%, 25%)',
700: 'hsl(0, 94%, 18%)',
800: 'hsl(0, 95%, 12%)',
900: 'hsl(0, 93%, 6%)',
};
const darkBg = 'hsl(0, 0%, 9%)';
const darkPaper = 'hsl(0, 0%, 14%)';
const darkElevated = 'hsl(0, 0%, 19%)';
/**
* Build the MUI design tokens (palette, typography, shape, shadows) for a mode.
*
* Light and dark palettes share the ramps but override specific slots in dark
* mode. Also sets `shadows[1]` (the "baseShadow") per mode. The returned object
* is spread into `createTheme` by `AppTheme`.
*
* @param mode - `"light"` or `"dark"`.
* @returns A MUI `ThemeOptions`-shaped object (palette, typography, shape,
* shadows).
*/
export const getDesignTokens = (mode: PaletteMode) => {
customShadows[1] =
mode === 'dark'
? '0 4px 16px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.3)'
: 'hsla(220, 30%, 5%, 0.07) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.07) 0px 8px 16px -5px';
return {
palette: {
mode,
primary: {
light: brand[200],
main: brand[400],
dark: brand[700],
contrastText: brand[50],
...(mode === 'dark' && {
contrastText: brand[50],
light: 'hsl(210, 50%, 65%)',
main: 'hsl(210, 55%, 55%)',
dark: 'hsl(210, 50%, 35%)',
}),
},
info: {
light: brand[100],
main: brand[300],
dark: brand[600],
contrastText: gray[50],
...(mode === 'dark' && {
contrastText: 'hsl(210, 30%, 80%)',
light: 'hsl(210, 40%, 50%)',
main: 'hsl(210, 35%, 40%)',
dark: 'hsl(210, 30%, 25%)',
}),
},
warning: {
light: orange[300],
main: orange[400],
dark: orange[800],
...(mode === 'dark' && {
light: 'hsl(45, 60%, 55%)',
main: 'hsl(45, 55%, 45%)',
dark: 'hsl(45, 50%, 30%)',
}),
},
error: {
light: red[300],
main: red[400],
dark: red[800],
...(mode === 'dark' && {
light: 'hsl(0, 55%, 60%)',
main: 'hsl(0, 55%, 50%)',
dark: 'hsl(0, 50%, 35%)',
}),
},
success: {
light: green[300],
main: green[400],
dark: green[800],
...(mode === 'dark' && {
light: 'hsl(120, 40%, 55%)',
main: 'hsl(120, 40%, 45%)',
dark: 'hsl(120, 35%, 30%)',
}),
},
grey: {
...gray,
},
divider: mode === 'dark' ? 'hsla(0, 0%, 100%, 0.08)' : alpha(gray[300], 0.4),
background: {
default: 'hsl(0, 0%, 99%)',
paper: 'hsl(220, 35%, 97%)',
...(mode === 'dark' && { default: darkBg, paper: darkPaper }),
},
text: {
primary: gray[800],
secondary: gray[600],
warning: orange[400],
...(mode === 'dark' && { primary: 'hsl(0, 0%, 92%)', secondary: 'hsl(0, 0%, 60%)' }),
},
action: {
hover: alpha(gray[200], 0.2),
selected: `${alpha(gray[200], 0.3)}`,
...(mode === 'dark' && {
hover: 'hsla(0, 0%, 100%, 0.06)',
selected: 'hsla(0, 0%, 100%, 0.1)',
}),
},
flows: {
outflows: {
primary: mode === 'dark' ? 'hsl(0, 55%, 60%)' : '#d32f2f',
surface: mode === 'dark' ? 'hsla(0, 35%, 25%, 0.6)' : '#fdecea',
text: mode === 'dark' ? 'hsl(0, 60%, 80%)' : '#b71c1c',
},
inflows: {
primary: mode === 'dark' ? 'hsl(120, 40%, 55%)' : '#2e7d32',
surface: mode === 'dark' ? 'hsla(120, 25%, 22%, 0.6)' : '#e8f5e9',
text: mode === 'dark' ? 'hsl(120, 40%, 78%)' : '#1b5e20',
},
},
},
typography: {
fontFamily: 'Inter, sans-serif',
h1: {
fontSize: defaultTheme.typography.pxToRem(48),
fontWeight: 600,
lineHeight: 1.2,
letterSpacing: -0.5,
},
h2: {
fontSize: defaultTheme.typography.pxToRem(36),
fontWeight: 600,
lineHeight: 1.2,
},
h3: {
fontSize: defaultTheme.typography.pxToRem(30),
lineHeight: 1.2,
},
h4: {
fontSize: defaultTheme.typography.pxToRem(24),
fontWeight: 600,
lineHeight: 1.5,
},
h5: {
fontSize: defaultTheme.typography.pxToRem(20),
fontWeight: 600,
},
h6: {
fontSize: defaultTheme.typography.pxToRem(18),
fontWeight: 600,
},
subtitle1: {
fontSize: defaultTheme.typography.pxToRem(18),
},
subtitle2: {
fontSize: defaultTheme.typography.pxToRem(14),
fontWeight: 500,
},
body1: {
fontSize: defaultTheme.typography.pxToRem(14),
},
body2: {
fontSize: defaultTheme.typography.pxToRem(14),
fontWeight: 400,
},
caption: {
fontSize: defaultTheme.typography.pxToRem(12),
fontWeight: 400,
},
},
shape: {
borderRadius: 8,
},
shadows: customShadows,
};
};
/** Explicit light/dark `colorSchemes` palette definitions. */
export const colorSchemes = {
light: {
palette: {
primary: {
light: brand[200],
main: brand[400],
dark: brand[700],
contrastText: brand[50],
},
info: {
light: brand[100],
main: brand[300],
dark: brand[600],
contrastText: gray[50],
},
warning: {
light: orange[300],
main: orange[400],
dark: orange[800],
},
error: {
light: red[300],
main: red[400],
dark: red[800],
},
success: {
light: green[300],
main: green[400],
dark: green[800],
},
grey: {
...gray,
},
divider: alpha(gray[300], 0.4),
background: {
default: 'hsl(0, 0%, 99%)',
paper: 'hsl(220, 35%, 97%)',
},
text: {
primary: gray[800],
secondary: gray[600],
warning: orange[400],
},
action: {
hover: alpha(gray[200], 0.2),
selected: `${alpha(gray[200], 0.3)}`,
},
flows: {
outflows: {
primary: '#d32f2f',
surface: '#fdecea',
text: '#b71c1c',
},
inflows: {
primary: '#2e7d32',
surface: '#e8f5e9',
text: '#1b5e20',
},
},
baseShadow:
'hsla(220, 30%, 5%, 0.07) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.07) 0px 8px 16px -5px',
},
},
dark: {
palette: {
primary: {
contrastText: brand[50],
light: 'hsl(210, 50%, 65%)',
main: 'hsl(210, 55%, 55%)',
dark: 'hsl(210, 50%, 35%)',
},
info: {
contrastText: 'hsl(210, 30%, 80%)',
light: 'hsl(210, 40%, 50%)',
main: 'hsl(210, 35%, 40%)',
dark: 'hsl(210, 30%, 25%)',
},
warning: {
light: 'hsl(45, 60%, 55%)',
main: 'hsl(45, 55%, 45%)',
dark: 'hsl(45, 50%, 30%)',
},
error: {
light: 'hsl(0, 55%, 60%)',
main: 'hsl(0, 55%, 50%)',
dark: 'hsl(0, 50%, 35%)',
},
success: {
light: 'hsl(120, 40%, 55%)',
main: 'hsl(120, 40%, 45%)',
dark: 'hsl(120, 35%, 30%)',
},
grey: {
...gray,
},
divider: 'hsla(0, 0%, 100%, 0.08)',
background: {
default: darkBg,
paper: darkPaper,
},
text: {
primary: 'hsl(0, 0%, 92%)',
secondary: 'hsl(0, 0%, 60%)',
},
action: {
hover: 'hsla(0, 0%, 100%, 0.06)',
selected: 'hsla(0, 0%, 100%, 0.1)',
},
flows: {
outflows: {
primary: 'hsl(0, 55%, 60%)',
surface: 'hsla(0, 35%, 25%, 0.6)',
text: 'hsl(0, 60%, 80%)',
},
inflows: {
primary: 'hsl(120, 40%, 55%)',
surface: 'hsla(120, 25%, 22%, 0.6)',
text: 'hsl(120, 40%, 78%)',
},
},
baseShadow: '0 4px 16px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.3)',
},
},
};
/** Typography scale (Inter) used by the theme. */
export const typography = {
fontFamily: 'Inter, sans-serif',
h1: {
fontSize: defaultTheme.typography.pxToRem(48),
fontWeight: 600,
lineHeight: 1.2,
letterSpacing: -0.5,
},
h2: {
fontSize: defaultTheme.typography.pxToRem(36),
fontWeight: 600,
lineHeight: 1.2,
},
h3: {
fontSize: defaultTheme.typography.pxToRem(30),
lineHeight: 1.2,
},
h4: {
fontSize: defaultTheme.typography.pxToRem(24),
fontWeight: 600,
lineHeight: 1.5,
},
h5: {
fontSize: defaultTheme.typography.pxToRem(20),
fontWeight: 600,
},
h6: {
fontSize: defaultTheme.typography.pxToRem(18),
fontWeight: 600,
},
subtitle1: {
fontSize: defaultTheme.typography.pxToRem(18),
},
subtitle2: {
fontSize: defaultTheme.typography.pxToRem(14),
fontWeight: 500,
},
body1: {
fontSize: defaultTheme.typography.pxToRem(14),
},
body2: {
fontSize: defaultTheme.typography.pxToRem(14),
fontWeight: 400,
},
caption: {
fontSize: defaultTheme.typography.pxToRem(12),
fontWeight: 400,
},
};
/** Shape tokens (global border radius). */
export const shape = {
borderRadius: 8,
};
// @ts-ignore
const defaultShadows: Shadows = [
'none',
'var(--template-palette-baseShadow)',
...defaultTheme.shadows.slice(2),
];
/** Shadow scale (index 0 `none`, index 1 the template `baseShadow`). */
export const shadows = defaultShadows;

View File

@@ -0,0 +1,10 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string;
readonly VITE_AUTH_BASE_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"allowJs": true,
"resolveJsonModule": true
},
"include": ["src"]
}

View File

@@ -0,0 +1,6 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
});

View File

@@ -3,17 +3,19 @@
## Summary
Every Aetos repository ships a Drone CI definition at `.drone.yml`. There is
exactly **one standard per repository**, and there are **two types of file**,
exactly **one standard per repository**, and there are **three types of file**,
chosen by what the repository produces:
| Type | File | Produces | Repos |
|------|------|----------|-------|
| **A — PyPI package** | `templates/drone/python-lib.yaml` | A wheel + sdist on the private index | `dagpipe`, `doc-forge`, `hexa`, `mail-intake`, `mongo-ops`, `omniread`, `openapi-first`, `py-jwt` |
| **B — Docker deployment** | `templates/drone/service-docker.yaml` | An image pushed to the registry and run as a container | `auth-server` |
| **B — npm package** | `templates/drone/npm-lib.yaml` | A package published to the private npm registry | `doc-weaver`, `react-auth`, `react-openapi` |
| **C — Docker deployment** | `templates/drone/service-docker.yaml` | An image pushed to the registry and run as a container | `auth-server`, `vet` |
Both templates run on `platform: linux / arm64` with the workspace at
`/drone/src`. Copy the matching template to `<repo>/.drone.yml`; the Type A file
needs no edits, the Type B file needs its placeholders filled in (below).
All templates run on `platform: linux / arm64` with the workspace at
`/drone/src`. Copy the matching template to `<repo>/.drone.yml`; the Type A and
Type B files need no edits, the Type C file needs its placeholders filled in
(below).
> Conventions: `.drone.yml` (not `.yaml`), one file per repo, secrets injected
> with `from_secret` — never commit credentials.
@@ -22,7 +24,7 @@ needs no edits, the Type B file needs its placeholders filled in (below).
## Type A — PyPI package (`python-lib.yaml`)
For internal libraries. The file contains **two pipelines**:
For internal Python libraries. The file contains **two pipelines**:
1. **`build-and-publish-pypi`** — triggered on `tag`:
- `check-version` — **guard**: fail unless `$DRONE_TAG` equals the version in
@@ -48,7 +50,30 @@ Secrets: `PIP_REPO_URL`, `PIP_USERNAME`, `PIP_PASSWORD`.
---
## Type B — Docker deployment (`service-docker.yaml`)
## Type B — npm package (`npm-lib.yaml`)
For internal npm libraries (the React packages). One pipeline,
**`build-and-publish-npm`**, triggered on `tag`:
- `check-version` — **guard**: fail unless `$DRONE_TAG` equals the version in
`package.json`; query `$NPM_REPO_URL` for `name@version`; if it already
exists, exit `78` and skip the whole build.
- `quality-gate` — write `.npmrc` from the secrets, `npm install`, then
`npm run quality-gate`.
- `build-package` — `npm install && npm run build` into `dist/`.
- `publish-to-private-npm` — `npm publish --access restricted` to
`$NPM_REPO_URL`.
Secrets: `NPM_REPO_URL`, `NPM_USERNAME`, `NPM_PASSWORD`. Auth is a generated
`.npmrc` (`registry=` + `_auth=` base64 of `user:pass`) written from secrets at
runtime — the committed `.npmrc` in a repo only points at the registry URL.
- **Ship as-is.** Name and version come from `package.json`, so the same file
is reused verbatim across every npm package.
---
## Type C — Docker deployment (`service-docker.yaml`)
For a service that ships a container image — a long-running API/worker, e.g.
`auth-server`. The file contains **one pipeline**, `build-and-deploy-service`,
@@ -86,15 +111,18 @@ Placeholders to fill in per repo:
## What not to do
- Do not hand-roll a third shape. A repo is either Type A or Type B, never both.
- Do not hand-roll a fourth shape. A repo is exactly one of Type A, B, or C,
never combinations.
- Do not inline secrets; always use `from_secret`.
- Do not drop the `check-version` / `check-remote-image` skip — it guards the
index and the running container against duplicate publishes.
- Do not drop the tag↔version guard. A package needs its `pyproject.toml`
version to equal the tag; a service needs a root `VERSION` file that equals
the tag. See [91 – Versioning & Changelog](91_versioning_and_changelog.md).
- Do not drop the tag↔version guard. A Python package needs its
`pyproject.toml` version to equal the tag; an npm package needs its
`package.json` version; a service needs a root `VERSION` file. See
[91 – Versioning & Changelog](91_versioning_and_changelog.md).
- Do not rename the pipelines; tooling and runbooks key off
`build-and-publish-pypi` and `build-and-deploy-service`.
`build-and-publish-pypi`, `build-and-publish-npm`, and
`build-and-deploy-service`.
---

View File

@@ -10,7 +10,7 @@ a Drone guard enforces that equality before anything is published. Releases are
cut manually; a tag is what triggers the pipeline.
This page pairs with [90 – Drone Pipelines](90_drone_pipelines.md): the version
and tag contract is exactly what those two CI shapes consume.
and tag contract is exactly what those three CI shapes consume.
---
@@ -19,6 +19,7 @@ and tag contract is exactly what those two CI shapes consume.
| Artifact | Source of truth | Notes |
|----------|-----------------|-------|
| Library (PyPI package) | `version` in `pyproject.toml` | `check-version` also reads it to skip duplicates |
| Library (npm package) | `version` in `package.json` | `check-version` also queries the registry to skip duplicates |
| Service (Docker) | `VERSION` file at the repo root | one line, `x.y.z`; the service **reads** it (build-arg) — never hardcode the version |
Rules:
@@ -116,7 +117,8 @@ bump travels with the change and only the tag is added afterwards:
1. **In the MR** — reconcile `[Unreleased]` against the change (see
[Reconcile before releasing](#reconcile-before-releasing)) and get the user's
approval for any additions. Then set the intended release version: `version`
in `pyproject.toml` (or the `VERSION` file) to `x.y.z`, and promote the
in `pyproject.toml` or `package.json` (or the `VERSION` file) to `x.y.z`,
and promote the
changelog (below). Changelog entries and link references name the
**version/tag**, never a commit id.
2. **Merge or squash** the MR into `main`.
@@ -133,8 +135,10 @@ After the user confirms the MR is merged/squashed and says to proceed:
1. **Wait** for that explicit go-ahead.
2. Find the commit that introduced the version bump:
- packages:
- Python packages:
`git log -S 'version = "x.y.z"' -n 1 --format='%H %s' -- pyproject.toml`
- npm packages:
`git log -S '"version": "x.y.z"' -n 1 --format='%H %s' -- package.json`
- services:
`git log -S 'x.y.z' -n 1 --format='%H %s' -- VERSION`
3. Confirm that commit's version file equals `x.y.z`, then tag **only** that
@@ -154,13 +158,14 @@ After the user confirms the MR is merged/squashed and says to proceed:
on top.
3. In the MR: add the two compare-link references at the bottom of the
changelog, naming the version/tag (never a commit id).
4. In the MR: set the version file — `pyproject.toml` (`version = "x.y.z"`) or
`VERSION` — to the intended release version.
4. In the MR: set the version file — `pyproject.toml` (`version = "x.y.z"`),
`package.json` (`"version": "x.y.z"`), or `VERSION` — to the intended
release version.
5. Merge or squash the MR into `main`.
6. Create the tag on the resulting commit: lightweight `git tag x.y.z`.
7. Push the tag (`git push origin x.y.z`). Watch Drone: Type A publishes the
wheel/sdist, Type B builds and deploys the image; both first assert
`tag == version`.
wheel/sdist, Type B publishes the npm package, Type C builds and deploys
the image; all three first assert `tag == version`.
8. Do not reuse the tag if the run fails — fix and cut `x.y.(z+1)`.
---

View File

@@ -5,7 +5,7 @@
The canonical list of Aetos repositories covered by the CI and versioning
standards, with the artifact each one produces, its version source, and its
Drone shape. Every entry maps to the contract in
[91 – Versioning & Changelog](91_versioning_and_changelog.md) and the two CI
[91 – Versioning & Changelog](91_versioning_and_changelog.md) and the three CI
files in [90 – Drone Pipelines](90_drone_pipelines.md).
The same data is published machine-readably as the `registry://repos` MCP
@@ -14,7 +14,7 @@ Paths are relative to the `aetos/` workspace root.
---
## Packages (Type A)
## Packages (Type A — PyPI)
A wheel + sdist published to the private index. Version source is
`pyproject.toml`; Drone file is `python-lib.yaml` (tag-triggered).
@@ -32,14 +32,28 @@ A wheel + sdist published to the private index. Version source is
---
## Services (Type B)
## Packages (Type B — npm)
A package published to the private npm registry (`npm.aetoskia.com`). Version
source is `package.json`; Drone file is `npm-lib.yaml` (tag-triggered).
| Repo | Path | Docs | MCP server |
|------|------|------|------------|
| `doc-weaver` | `doc-weaver` | lib, wiki | `doc-weaver:8010` |
| `react-auth` | `react/react-auth` | lib, wiki | `react-auth:8011` |
| `react-openapi` | `react/react-openapi` | lib, wiki | `react-openapi:8012` |
---
## Services & Apps (Type C)
A container image pushed to the registry and run on the Docker host. Version
source is the `VERSION` file; Drone file is `service-docker.yaml`.
| Repo | Path | Trigger | Docs |
|------|------|---------|------|
| `auth-server` | `auth-server` | tag | api, lib, wiki |
| Repo | Path | Type | Trigger | Docs |
|------|------|------|---------|------|
| `auth-server` | `auth-server` | service | tag | api, lib, wiki |
| `vet` | `react/vet` | app | tag | lib, wiki |
Tag-triggered and enforces `tag == VERSION`.
@@ -49,8 +63,9 @@ Tag-triggered and enforces `tag == VERSION`.
| Type | Version source | Changelog | Drone |
|------|----------------|-----------|-------|
| package | `pyproject.toml` | `CHANGELOG.md` | `python-lib.yaml` |
| service | `VERSION` | `CHANGELOG.md` | `service-docker.yaml` |
| package (PyPI) | `pyproject.toml` | `CHANGELOG.md` | `python-lib.yaml` |
| package (npm) | `package.json` | `CHANGELOG.md` | `npm-lib.yaml` |
| service / app | `VERSION` | `CHANGELOG.md` | `service-docker.yaml` |
Every covered repository must be able to satisfy `tag == version`:
the version file (never a commit id) is bumped in the release MR, the MR is

View File

@@ -44,8 +44,8 @@ and an optional pipelines layer.
## Delivery & Meta
- **Drone Pipelines** — the two CI shapes: a PyPI/npm package or a Docker
deployment.
- **Drone Pipelines** — the three CI shapes: a PyPI package, an npm package,
or a Docker deployment.
- **Versioning & Changelog** — SemVer, the tag↔version contract, and the
release changelog.
- **Repository Inventory** — every covered repo, its type, version source, and

View File

@@ -1,6 +1,6 @@
{
"project": "standards",
"note": "Machine-readable repository inventory. Mirrors standards/src/lib/15_repository_inventory.md. Read it with the registry://repos resource or the list_repos tool.",
"note": "Machine-readable repository inventory. Mirrors standards/src/lib/94_repository_inventory.md. Read it with the registry://repos resource or the list_repos tool.",
"path_convention": "relative to the aetos/ workspace root",
"repos": [
{
@@ -23,6 +23,16 @@
"docs": ["lib", "wiki"],
"mcp": { "server": "docforge", "port": 8001 }
},
{
"name": "doc-weaver",
"type": "package",
"path": "doc-weaver",
"version_source": "package.json",
"drone": "npm-lib.yaml",
"trigger": "tag",
"docs": ["lib", "wiki"],
"mcp": { "server": "doc-weaver", "port": 8010 }
},
{
"name": "hexa",
"type": "package",
@@ -83,6 +93,26 @@
"docs": ["lib", "wiki"],
"mcp": { "server": "jwtlib", "port": 8005 }
},
{
"name": "react-auth",
"type": "package",
"path": "react/react-auth",
"version_source": "package.json",
"drone": "npm-lib.yaml",
"trigger": "tag",
"docs": ["lib", "wiki"],
"mcp": { "server": "react-auth", "port": 8011 }
},
{
"name": "react-openapi",
"type": "package",
"path": "react/react-openapi",
"version_source": "package.json",
"drone": "npm-lib.yaml",
"trigger": "tag",
"docs": ["lib", "wiki"],
"mcp": { "server": "react-openapi", "port": 8012 }
},
{
"name": "auth-server",
"type": "service",
@@ -92,6 +122,16 @@
"trigger": "tag",
"docs": ["api", "lib", "wiki"],
"mcp": null
},
{
"name": "vet",
"type": "app",
"path": "react/vet",
"version_source": "VERSION",
"drone": "service-docker.yaml",
"trigger": "tag",
"docs": ["lib", "wiki"],
"mcp": null
}
]
}

Binary file not shown.