soliplex-plumber 0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ *
@@ -0,0 +1,4 @@
1
+ {
2
+ "data": "\n<!doctype html>\n<html lang=\"en\" class=\"no-js\">\n <head>\n \n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n \n <meta name=\"description\" content=\"Stdlib-only tools for reading and modifying the configuration of an existing Soliplex stack -- the shared dependency for skill projects that wire rooms.\n\">\n \n \n <meta name=\"author\" content=\"Soliplex\">\n \n \n <link rel=\"canonical\" href=\"https://soliplex.github.io/soliplex-plumber/reference/api/\">\n \n \n <link rel=\"prev\" href=\"../..\">\n \n \n \n \n \n \n <link rel=\"icon\" href=\"../../assets/images/favicon.png\">\n <meta name=\"generator\" content=\"zensical-0.0.45\">\n \n \n \n <title>API reference - Soliplex Plumber</title>\n \n \n \n \n \n \n <link rel=\"stylesheet\" href=\"../../assets/stylesheets/modern/main.fba56155.min.css\">\n \n \n \n \n <link rel=\"stylesheet\" href=\"../../assets/stylesheets/modern/palette.dfe2e883.min.css\">\n \n \n\n\n \n \n \n \n \n \n \n \n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Inter:300,300i,400,400i,500,500i,700,700i%7CJetBrains+Mono:400,400i,700,700i&amp;display=fallback\">\n <style>:root{--md-text-font:\"Inter\";--md-code-font:\"JetBrains Mono\"}</style>\n \n \n \n <script>__md_scope=new URL(\"../..\",location),__md_scope.pathname.endsWith(\"/\")||(__md_scope=new URL(__md_scope.pathname+\"/\",location)),__md_hash=e=>[...e].reduce(((e,t)=>(e<<5)-e+t.charCodeAt(0)),0),__md_get=(e,t=localStorage,_=__md_scope)=>JSON.parse(t.getItem(_.pathname+\".\"+e)),__md_set=(e,t,_=localStorage,a=__md_scope)=>{try{_.setItem(a.pathname+\".\"+e,JSON.stringify(t))}catch(e){}},document.documentElement.setAttribute(\"data-platform\",navigator.platform)</script>\n \n \n\n \n \n </head>\n \n \n \n \n \n \n \n \n \n <body dir=\"ltr\" data-md-color-scheme=\"default\" data-md-color-primary=\"indigo\" data-md-color-accent=\"indigo\">\n \n \n <input class=\"md-toggle\" data-md-toggle=\"drawer\" type=\"checkbox\" id=\"__drawer\" autocomplete=\"off\">\n <input class=\"md-toggle\" data-md-toggle=\"search\" type=\"checkbox\" id=\"__search\" autocomplete=\"off\">\n <label class=\"md-overlay\" for=\"__drawer\" aria-label=\"Navigation\"></label>\n <div data-md-component=\"skip\">\n \n \n <a href=\"#api-reference\" class=\"md-skip\">\n Skip to content\n </a>\n \n </div>\n <div data-md-component=\"announce\">\n \n </div>\n \n \n \n\n \n\n<header class=\"md-header md-header--shadow\" data-md-component=\"header\">\n <nav class=\"md-header__inner md-grid\" aria-label=\"Header\">\n <a href=\"../..\" title=\"Soliplex Plumber\" class=\"md-header__button md-logo\" aria-label=\"Soliplex Plumber\" data-md-component=\"logo\">\n \n \n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" class=\"lucide lucide-book-open\" viewBox=\"0 0 24 24\"><path d=\"M12 7v14M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z\"/></svg>\n\n </a>\n <label class=\"md-header__button md-icon\" for=\"__drawer\" aria-label=\"Navigation\">\n \n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" class=\"lucide lucide-menu\" viewBox=\"0 0 24 24\"><path d=\"M4 5h16M4 12h16M4 19h16\"/></svg>\n </label>\n <div class=\"md-header__title\" data-md-component=\"header-title\">\n <div class=\"md-header__ellipsis\">\n <div class=\"md-header__topic\">\n <span class=\"md-ellipsis\">\n Soliplex Plumber\n </span>\n </div>\n <div class=\"md-header__topic\" data-md-component=\"header-topic\">\n <span class=\"md-ellipsis\">\n \n API reference\n \n </span>\n </div>\n </div>\n </div>\n \n \n <form class=\"md-header__option\" data-md-component=\"palette\">\n \n \n \n \n <input class=\"md-option\" data-md-color-media=\"none\" data-md-color-scheme=\"default\" data-md-color-primary=\"indigo\" data-md-color-accent=\"indigo\" aria-label=\"Switch to dark mode\" type=\"radio\" name=\"__palette\" id=\"__palette_0\">\n \n <label class=\"md-header__button md-icon\" title=\"Switch to dark mode\" for=\"__palette_1\" hidden>\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" class=\"lucide lucide-sun\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"4\"/><path d=\"M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41\"/></svg>\n </label>\n \n \n \n \n \n <input class=\"md-option\" data-md-color-media=\"none\" data-md-color-scheme=\"slate\" data-md-color-primary=\"indigo\" data-md-color-accent=\"indigo\" aria-label=\"Switch to light mode\" type=\"radio\" name=\"__palette\" id=\"__palette_1\">\n \n <label class=\"md-header__button md-icon\" title=\"Switch to light mode\" for=\"__palette_0\" hidden>\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" class=\"lucide lucide-moon\" viewBox=\"0 0 24 24\"><path d=\"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401\"/></svg>\n </label>\n \n \n</form>\n \n \n \n <script>var palette=__md_get(\"__palette\");if(palette&&palette.color){if(\"(prefers-color-scheme)\"===palette.color.media){var media=matchMedia(\"(prefers-color-scheme: light)\"),input=document.querySelector(media.matches?\"[data-md-color-media='(prefers-color-scheme: light)']\":\"[data-md-color-media='(prefers-color-scheme: dark)']\");palette.color.media=input.getAttribute(\"data-md-color-media\"),palette.color.scheme=input.getAttribute(\"data-md-color-scheme\"),palette.color.primary=input.getAttribute(\"data-md-color-primary\"),palette.color.accent=input.getAttribute(\"data-md-color-accent\")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute(\"data-md-color-\"+key,value)}</script>\n \n \n \n \n \n <label class=\"md-header__button md-icon\" for=\"__search\" aria-label=\"Search\">\n \n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" class=\"lucide lucide-search\" viewBox=\"0 0 24 24\"><path d=\"m21 21-4.34-4.34\"/><circle cx=\"11\" cy=\"11\" r=\"8\"/></svg>\n </label>\n <div class=\"md-search\" data-md-component=\"search\" role=\"dialog\" aria-label=\"Search\">\n <button type=\"button\" class=\"md-search__button\">\n Search\n </button>\n</div>\n \n \n <div class=\"md-header__source\">\n \n <a href=\"https://github.com/soliplex/soliplex-plumber\" title=\"Go to repository\" class=\"md-source\" data-md-component=\"source\">\n <div class=\"md-source__icon md-icon\">\n \n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 448 512\"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc.--><path fill=\"currentColor\" d=\"M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4\"/></svg>\n </div>\n <div class=\"md-source__repository\">\n soliplex/soliplex-plumber\n </div>\n</a>\n \n </div>\n </nav>\n \n</header>\n \n <div class=\"md-container\" data-md-component=\"container\">\n \n \n \n \n \n \n <main class=\"md-main\" data-md-component=\"main\">\n <div class=\"md-main__inner md-grid\">\n \n \n \n <div class=\"md-sidebar md-sidebar--primary\" data-md-component=\"sidebar\" data-md-type=\"navigation\" >\n <div class=\"md-sidebar__scrollwrap\">\n <div class=\"md-sidebar__inner\">\n \n\n\n\n<nav class=\"md-nav md-nav--primary\" aria-label=\"Navigation\" data-md-level=\"0\">\n <label class=\"md-nav__title\" for=\"__drawer\">\n <a href=\"../..\" title=\"Soliplex Plumber\" class=\"md-nav__button md-logo\" aria-label=\"Soliplex Plumber\" data-md-component=\"logo\">\n \n \n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" class=\"lucide lucide-book-open\" viewBox=\"0 0 24 24\"><path d=\"M12 7v14M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z\"/></svg>\n\n </a>\n Soliplex Plumber\n </label>\n \n <div class=\"md-nav__source\">\n <a href=\"https://github.com/soliplex/soliplex-plumber\" title=\"Go to repository\" class=\"md-source\" data-md-component=\"source\">\n <div class=\"md-source__icon md-icon\">\n \n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 448 512\"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc.--><path fill=\"currentColor\" d=\"M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4\"/></svg>\n </div>\n <div class=\"md-source__repository\">\n soliplex/soliplex-plumber\n </div>\n</a>\n </div>\n \n <ul class=\"md-nav__list\" data-md-scrollfix>\n \n \n \n \n \n \n \n <li class=\"md-nav__item\">\n <a href=\"../..\" class=\"md-nav__link\">\n \n \n \n <span class=\"md-ellipsis\">\n \n \n Home\n\n \n </span>\n \n \n\n </a>\n </li>\n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n <li class=\"md-nav__item md-nav__item--active md-nav__item--section md-nav__item--nested\">\n \n \n \n <input class=\"md-nav__toggle md-toggle \" type=\"checkbox\" id=\"__nav_2\" checked>\n \n \n <label class=\"md-nav__link\" for=\"__nav_2\" id=\"__nav_2_label\" tabindex=\"\">\n \n \n \n <span class=\"md-ellipsis\">\n \n \n API reference\n\n \n </span>\n \n \n\n <span class=\"md-nav__icon md-icon\"></span>\n </label>\n \n <nav class=\"md-nav\" data-md-level=\"1\" aria-labelledby=\"__nav_2_label\" aria-expanded=\"true\">\n <label class=\"md-nav__title\" for=\"__nav_2\">\n <span class=\"md-nav__icon md-icon\"></span>\n \n \n API reference\n\n </label>\n <ul class=\"md-nav__list\" data-md-scrollfix>\n \n \n \n \n \n \n \n \n \n <li class=\"md-nav__item md-nav__item--active\">\n \n \n \n \n \n \n \n <label class=\"md-nav__link md-nav__link--active\" for=\"__toc\">\n \n \n \n <span class=\"md-ellipsis\">\n \n \n API reference\n\n \n </span>\n \n \n\n <span class=\"md-nav__icon md-icon\"></span>\n </label>\n \n <a href=\"././\" class=\"md-nav__link md-nav__link--active\">\n \n \n \n <span class=\"md-ellipsis\">\n \n \n API reference\n\n \n </span>\n \n \n\n </a>\n \n \n\n\n<nav class=\"md-nav md-nav--secondary\" aria-label=\"On this page\">\n \n \n \n \n \n \n <label class=\"md-nav__title\" for=\"__toc\">\n <span class=\"md-nav__icon md-icon\"></span>\n On this page\n </label>\n <ul class=\"md-nav__list\" data-md-component=\"toc\" data-md-scrollfix>\n \n <li class=\"md-nav__item\">\n <a href=\"#rooms-add-a-room-to-a-soliplex-stack\" class=\"md-nav__link\">\n <span class=\"md-ellipsis\">\n <span class=\"md-typeset\">\n <code>rooms</code> — add a room to a Soliplex stack\n </span>\n </span>\n </a>\n \n</li>\n \n </ul>\n \n</nav>\n \n </li>\n \n\n \n \n </ul>\n </nav>\n \n </li>\n \n\n \n </ul>\n</nav>\n </div>\n </div>\n </div>\n \n \n \n <div class=\"md-sidebar md-sidebar--secondary\" data-md-component=\"sidebar\" data-md-type=\"toc\" >\n <div class=\"md-sidebar__scrollwrap\">\n \n \n \n \n \n \n \n <input class=\"md-nav__toggle md-toggle\" type=\"checkbox\" id=\"__toc\">\n <div class=\"md-sidebar-button__wrapper\">\n <label class=\"md-sidebar-button\" for=\"__toc\"></label>\n </div>\n \n \n <div class=\"md-sidebar__inner\">\n \n\n\n<nav class=\"md-nav md-nav--secondary\" aria-label=\"On this page\">\n \n \n \n \n \n \n <label class=\"md-nav__title\" for=\"__toc\">\n <span class=\"md-nav__icon md-icon\"></span>\n On this page\n </label>\n <ul class=\"md-nav__list\" data-md-component=\"toc\" data-md-scrollfix>\n \n <li class=\"md-nav__item\">\n <a href=\"#rooms-add-a-room-to-a-soliplex-stack\" class=\"md-nav__link\">\n <span class=\"md-ellipsis\">\n <span class=\"md-typeset\">\n <code>rooms</code> — add a room to a Soliplex stack\n </span>\n </span>\n </a>\n \n</li>\n \n </ul>\n \n</nav>\n </div>\n </div>\n </div>\n \n \n \n <div class=\"md-content\" data-md-component=\"content\">\n \n \n\n\n\n \n\n\n <nav class=\"md-path\" aria-label=\"Navigation\" >\n <ol class=\"md-path__list\">\n \n \n \n \n <li class=\"md-path__item\">\n <a href=\"../..\" class=\"md-path__link\">\n \n \n <span class=\"md-ellipsis\">\n Home\n </span>\n\n </a>\n </li>\n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n <li class=\"md-path__item\">\n <a href=\"././\" class=\"md-path__link\">\n \n \n <span class=\"md-ellipsis\">\n API reference\n </span>\n\n </a>\n </li>\n \n \n\n \n </ol>\n </nav>\n\n \n <article class=\"md-content__inner md-typeset\">\n \n \n \n \n\n\n<h1 id=\"api-reference\">API reference<a class=\"headerlink\" href=\"#api-reference\" title=\"Permanent link\">&para;</a></h1>\n<div class=\"admonition note\">\n<p class=\"admonition-title\">Implemented</p>\n<p>This is the library's public surface. Every member below is implemented\nand covered by the test suite (100% branch coverage); the modules under\n<code>src/soliplex_plumber/</code> are the source of truth.</p>\n</div>\n<p>There is no re-exporting package <code>__init__</code> — <strong>client code imports the\nsubmodule and uses its members by dotted name</strong> (e.g. <code>from soliplex_plumber\nimport rooms</code> then <code>rooms.install_room(...)</code>).</p>\n<h2 id=\"rooms-add-a-room-to-a-soliplex-stack\"><code>rooms</code> — add a room to a Soliplex stack<a class=\"headerlink\" href=\"#rooms-add-a-room-to-a-soliplex-stack\" title=\"Permanent link\">&para;</a></h2>\n<p>Generic, template-agnostic, stdlib-only logic for wiring a room into a generated\nstack. The shared core behind both the <code>soliplex-template</code> skill's <code>add_room.py</code>\nand the <code>soliplex-concierge</code> installer; it edits <code>installation.yaml</code> line-based so\ncomments and layout are preserved.</p>\n<table>\n<thead>\n<tr>\n<th>Member</th>\n<th>Purpose</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>validate_room_id(room_id)</code></td>\n<td>enforce the room-id / path-segment rule (<code>ROOM_ID_RE</code>); raises <code>AddRoomError</code></td>\n</tr>\n<tr>\n<td><code>resolve_project(project_dir)</code></td>\n<td>resolve + verify the stack root (has <code>COMPOSE_FILE</code> and <code>INSTALLATION_FILE</code>)</td>\n</tr>\n<tr>\n<td><code>resolve_package_name(project, override)</code></td>\n<td>the stack's own package (inferred from <code>src/&lt;pkg&gt;/tools.py</code>) or <code>DEFAULT_PACKAGE_NAME</code></td>\n</tr>\n<tr>\n<td><code>add_room_path(text, room_id) -&gt; (text, action)</code></td>\n<td>ensure <code>room_paths</code> loads the room; action is <code>ADDED</code> / <code>UNCHANGED</code> / <code>COVERED</code></td>\n</tr>\n<tr>\n<td><code>install_room(project, room_id, *, config_text, prompt_text=None, force=False, dry_run=False)</code></td>\n<td>write the room dir + config (+ optional prompt) and apply the <code>room_paths</code> edit</td>\n</tr>\n<tr>\n<td><code>RoomInstalled(config_path, path_action)</code></td>\n<td>the <code>install_room</code> outcome (alias <code>RoomInstall</code> kept for back-compat)</td>\n</tr>\n<tr>\n<td><code>AddRoomError</code></td>\n<td>user-facing error with message-factory classmethods</td>\n</tr>\n<tr>\n<td><code>ADDED</code> / <code>UNCHANGED</code> / <code>COVERED</code> / <code>ROOMS_PARENT_ENTRY</code></td>\n<td>the <code>room_paths</code> action constants and the <code>./rooms</code> auto-discovery entry</td>\n</tr>\n</tbody>\n</table>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n </article>\n </div>\n \n \n <script>var tabs=__md_get(\"__tabs\");if(Array.isArray(tabs))e:for(var set of document.querySelectorAll(\".tabbed-set\")){var labels=set.querySelector(\".tabbed-labels\");for(var tab of tabs)for(var label of labels.getElementsByTagName(\"label\"))if(label.innerText.trim()===tab){var input=document.getElementById(label.htmlFor);input.checked=!0;continue e}}</script>\n\n<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith(\"__tabbed_\"))</script>\n </div>\n \n <button type=\"button\" class=\"md-top md-icon\" data-md-component=\"top\" hidden>\n \n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" class=\"lucide lucide-circle-arrow-up\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m16 12-4-4-4 4M12 16V8\"/></svg>\n Back to top\n</button>\n \n </main>\n \n <footer class=\"md-footer\">\n \n \n \n <nav class=\"md-footer__inner md-grid\" aria-label=\"Footer\" >\n \n \n <a href=\"../..\" class=\"md-footer__link md-footer__link--prev\" aria-label=\"Previous: Home\">\n <div class=\"md-footer__button md-icon\">\n \n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" class=\"lucide lucide-arrow-left\" viewBox=\"0 0 24 24\"><path d=\"m12 19-7-7 7-7M19 12H5\"/></svg>\n </div>\n <div class=\"md-footer__title\">\n <span class=\"md-footer__direction\">\n Previous\n </span>\n <div class=\"md-ellipsis\">\n Home\n </div>\n </div>\n </a>\n \n \n </nav>\n \n \n <div class=\"md-footer-meta md-typeset\">\n <div class=\"md-footer-meta__inner md-grid\">\n <div class=\"md-copyright\">\n \n <div class=\"md-copyright__highlight\">\n Copyright &copy; Soliplex\n\n </div>\n \n \n Made with\n <a href=\"https://zensical.org/\" target=\"_blank\" rel=\"noopener\">\n Zensical\n </a>\n \n</div>\n \n </div>\n </div>\n</footer>\n \n </div>\n <div class=\"md-dialog\" data-md-component=\"dialog\">\n <div class=\"md-dialog__inner md-typeset\"></div>\n </div>\n \n \n \n \n \n <script id=\"__config\" type=\"application/json\">{\"annotate\":null,\"base\":\"../..\",\"features\":[\"announce.dismiss\",\"content.code.annotate\",\"content.code.copy\",\"content.code.select\",\"content.tabs.link\",\"navigation.footer\",\"navigation.indexes\",\"navigation.instant\",\"navigation.instant.prefetch\",\"navigation.path\",\"navigation.sections\",\"navigation.top\",\"navigation.tracking\",\"search.highlight\"],\"search\":\"../../assets/javascripts/workers/search.e2d2d235.min.js\",\"tags\":null,\"translations\":{\"clipboard.copied\":\"Copied to clipboard\",\"clipboard.copy\":\"Copy to clipboard\",\"search.result.more.one\":\"1 more on this page\",\"search.result.more.other\":\"# more on this page\",\"search.result.none\":\"No matching documents\",\"search.result.one\":\"1 matching document\",\"search.result.other\":\"# matching documents\",\"search.result.placeholder\":\"Type to start searching\",\"search.result.term.missing\":\"Missing\",\"select.version\":\"Select version\"},\"version\":null}</script>\n \n \n <script src=\"../../assets/javascripts/bundle.6e5f0216.min.js\"></script>\n \n \n </body>\n</html>",
3
+ "hash": 1485270945752863816
4
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "data": "\n<!doctype html>\n<html lang=\"en\" class=\"no-js\">\n <head>\n \n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n \n <meta name=\"description\" content=\"Stdlib-only tools for reading and modifying the configuration of an existing Soliplex stack -- the shared dependency for skill projects that wire rooms.\n\">\n \n \n <meta name=\"author\" content=\"Soliplex\">\n \n \n <link rel=\"canonical\" href=\"https://soliplex.github.io/soliplex-plumber/\">\n \n \n \n <link rel=\"next\" href=\"./reference/api/\">\n \n \n \n \n \n <link rel=\"icon\" href=\"./assets/images/favicon.png\">\n <meta name=\"generator\" content=\"zensical-0.0.45\">\n \n \n \n <title>Soliplex Plumber - Soliplex Plumber</title>\n \n \n \n \n \n \n <link rel=\"stylesheet\" href=\"./assets/stylesheets/modern/main.fba56155.min.css\">\n \n \n \n \n <link rel=\"stylesheet\" href=\"./assets/stylesheets/modern/palette.dfe2e883.min.css\">\n \n \n\n\n \n \n \n \n \n \n \n \n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n <link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Inter:300,300i,400,400i,500,500i,700,700i%7CJetBrains+Mono:400,400i,700,700i&amp;display=fallback\">\n <style>:root{--md-text-font:\"Inter\";--md-code-font:\"JetBrains Mono\"}</style>\n \n \n \n <script>__md_scope=new URL(\".\",location),__md_scope.pathname.endsWith(\"/\")||(__md_scope=new URL(__md_scope.pathname+\"/\",location)),__md_hash=e=>[...e].reduce(((e,t)=>(e<<5)-e+t.charCodeAt(0)),0),__md_get=(e,t=localStorage,_=__md_scope)=>JSON.parse(t.getItem(_.pathname+\".\"+e)),__md_set=(e,t,_=localStorage,a=__md_scope)=>{try{_.setItem(a.pathname+\".\"+e,JSON.stringify(t))}catch(e){}},document.documentElement.setAttribute(\"data-platform\",navigator.platform)</script>\n \n \n\n \n \n </head>\n \n \n \n \n \n \n \n \n \n <body dir=\"ltr\" data-md-color-scheme=\"default\" data-md-color-primary=\"indigo\" data-md-color-accent=\"indigo\">\n \n \n <input class=\"md-toggle\" data-md-toggle=\"drawer\" type=\"checkbox\" id=\"__drawer\" autocomplete=\"off\">\n <input class=\"md-toggle\" data-md-toggle=\"search\" type=\"checkbox\" id=\"__search\" autocomplete=\"off\">\n <label class=\"md-overlay\" for=\"__drawer\" aria-label=\"Navigation\"></label>\n <div data-md-component=\"skip\">\n \n \n <a href=\"#soliplex-plumber\" class=\"md-skip\">\n Skip to content\n </a>\n \n </div>\n <div data-md-component=\"announce\">\n \n </div>\n \n \n \n\n \n\n<header class=\"md-header md-header--shadow\" data-md-component=\"header\">\n <nav class=\"md-header__inner md-grid\" aria-label=\"Header\">\n <a href=\"\" title=\"Soliplex Plumber\" class=\"md-header__button md-logo\" aria-label=\"Soliplex Plumber\" data-md-component=\"logo\">\n \n \n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" class=\"lucide lucide-book-open\" viewBox=\"0 0 24 24\"><path d=\"M12 7v14M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z\"/></svg>\n\n </a>\n <label class=\"md-header__button md-icon\" for=\"__drawer\" aria-label=\"Navigation\">\n \n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" class=\"lucide lucide-menu\" viewBox=\"0 0 24 24\"><path d=\"M4 5h16M4 12h16M4 19h16\"/></svg>\n </label>\n <div class=\"md-header__title\" data-md-component=\"header-title\">\n <div class=\"md-header__ellipsis\">\n <div class=\"md-header__topic\">\n <span class=\"md-ellipsis\">\n Soliplex Plumber\n </span>\n </div>\n <div class=\"md-header__topic\" data-md-component=\"header-topic\">\n <span class=\"md-ellipsis\">\n \n Soliplex Plumber\n \n </span>\n </div>\n </div>\n </div>\n \n \n <form class=\"md-header__option\" data-md-component=\"palette\">\n \n \n \n \n <input class=\"md-option\" data-md-color-media=\"none\" data-md-color-scheme=\"default\" data-md-color-primary=\"indigo\" data-md-color-accent=\"indigo\" aria-label=\"Switch to dark mode\" type=\"radio\" name=\"__palette\" id=\"__palette_0\">\n \n <label class=\"md-header__button md-icon\" title=\"Switch to dark mode\" for=\"__palette_1\" hidden>\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" class=\"lucide lucide-sun\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"4\"/><path d=\"M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41\"/></svg>\n </label>\n \n \n \n \n \n <input class=\"md-option\" data-md-color-media=\"none\" data-md-color-scheme=\"slate\" data-md-color-primary=\"indigo\" data-md-color-accent=\"indigo\" aria-label=\"Switch to light mode\" type=\"radio\" name=\"__palette\" id=\"__palette_1\">\n \n <label class=\"md-header__button md-icon\" title=\"Switch to light mode\" for=\"__palette_0\" hidden>\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" class=\"lucide lucide-moon\" viewBox=\"0 0 24 24\"><path d=\"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401\"/></svg>\n </label>\n \n \n</form>\n \n \n \n <script>var palette=__md_get(\"__palette\");if(palette&&palette.color){if(\"(prefers-color-scheme)\"===palette.color.media){var media=matchMedia(\"(prefers-color-scheme: light)\"),input=document.querySelector(media.matches?\"[data-md-color-media='(prefers-color-scheme: light)']\":\"[data-md-color-media='(prefers-color-scheme: dark)']\");palette.color.media=input.getAttribute(\"data-md-color-media\"),palette.color.scheme=input.getAttribute(\"data-md-color-scheme\"),palette.color.primary=input.getAttribute(\"data-md-color-primary\"),palette.color.accent=input.getAttribute(\"data-md-color-accent\")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute(\"data-md-color-\"+key,value)}</script>\n \n \n \n \n \n <label class=\"md-header__button md-icon\" for=\"__search\" aria-label=\"Search\">\n \n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" class=\"lucide lucide-search\" viewBox=\"0 0 24 24\"><path d=\"m21 21-4.34-4.34\"/><circle cx=\"11\" cy=\"11\" r=\"8\"/></svg>\n </label>\n <div class=\"md-search\" data-md-component=\"search\" role=\"dialog\" aria-label=\"Search\">\n <button type=\"button\" class=\"md-search__button\">\n Search\n </button>\n</div>\n \n \n <div class=\"md-header__source\">\n \n <a href=\"https://github.com/soliplex/soliplex-plumber\" title=\"Go to repository\" class=\"md-source\" data-md-component=\"source\">\n <div class=\"md-source__icon md-icon\">\n \n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 448 512\"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc.--><path fill=\"currentColor\" d=\"M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4\"/></svg>\n </div>\n <div class=\"md-source__repository\">\n soliplex/soliplex-plumber\n </div>\n</a>\n \n </div>\n </nav>\n \n</header>\n \n <div class=\"md-container\" data-md-component=\"container\">\n \n \n \n \n \n \n <main class=\"md-main\" data-md-component=\"main\">\n <div class=\"md-main__inner md-grid\">\n \n \n \n <div class=\"md-sidebar md-sidebar--primary\" data-md-component=\"sidebar\" data-md-type=\"navigation\" >\n <div class=\"md-sidebar__scrollwrap\">\n <div class=\"md-sidebar__inner\">\n \n\n\n\n<nav class=\"md-nav md-nav--primary\" aria-label=\"Navigation\" data-md-level=\"0\">\n <label class=\"md-nav__title\" for=\"__drawer\">\n <a href=\"\" title=\"Soliplex Plumber\" class=\"md-nav__button md-logo\" aria-label=\"Soliplex Plumber\" data-md-component=\"logo\">\n \n \n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" class=\"lucide lucide-book-open\" viewBox=\"0 0 24 24\"><path d=\"M12 7v14M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z\"/></svg>\n\n </a>\n Soliplex Plumber\n </label>\n \n <div class=\"md-nav__source\">\n <a href=\"https://github.com/soliplex/soliplex-plumber\" title=\"Go to repository\" class=\"md-source\" data-md-component=\"source\">\n <div class=\"md-source__icon md-icon\">\n \n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 448 512\"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc.--><path fill=\"currentColor\" d=\"M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4\"/></svg>\n </div>\n <div class=\"md-source__repository\">\n soliplex/soliplex-plumber\n </div>\n</a>\n </div>\n \n <ul class=\"md-nav__list\" data-md-scrollfix>\n \n \n \n \n \n \n \n \n \n <li class=\"md-nav__item md-nav__item--active\">\n \n \n \n \n \n \n \n <label class=\"md-nav__link md-nav__link--active\" for=\"__toc\">\n \n \n \n <span class=\"md-ellipsis\">\n \n \n Home\n\n \n </span>\n \n \n\n <span class=\"md-nav__icon md-icon\"></span>\n </label>\n \n <a href=\"\" class=\"md-nav__link md-nav__link--active\">\n \n \n \n <span class=\"md-ellipsis\">\n \n \n Home\n\n \n </span>\n \n \n\n </a>\n \n \n\n\n<nav class=\"md-nav md-nav--secondary\" aria-label=\"On this page\">\n \n \n \n \n \n \n <label class=\"md-nav__title\" for=\"__toc\">\n <span class=\"md-nav__icon md-icon\"></span>\n On this page\n </label>\n <ul class=\"md-nav__list\" data-md-component=\"toc\" data-md-scrollfix>\n \n <li class=\"md-nav__item\">\n <a href=\"#what-it-provides\" class=\"md-nav__link\">\n <span class=\"md-ellipsis\">\n <span class=\"md-typeset\">\n What it provides\n </span>\n </span>\n </a>\n \n</li>\n \n </ul>\n \n</nav>\n \n </li>\n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n <li class=\"md-nav__item md-nav__item--section md-nav__item--nested\">\n \n \n \n <input class=\"md-nav__toggle md-toggle \" type=\"checkbox\" id=\"__nav_2\" >\n \n \n <label class=\"md-nav__link\" for=\"__nav_2\" id=\"__nav_2_label\" tabindex=\"\">\n \n \n \n <span class=\"md-ellipsis\">\n \n \n API reference\n\n \n </span>\n \n \n\n <span class=\"md-nav__icon md-icon\"></span>\n </label>\n \n <nav class=\"md-nav\" data-md-level=\"1\" aria-labelledby=\"__nav_2_label\" aria-expanded=\"false\">\n <label class=\"md-nav__title\" for=\"__nav_2\">\n <span class=\"md-nav__icon md-icon\"></span>\n \n \n API reference\n\n </label>\n <ul class=\"md-nav__list\" data-md-scrollfix>\n \n \n \n \n \n \n \n <li class=\"md-nav__item\">\n <a href=\"./reference/api/\" class=\"md-nav__link\">\n \n \n \n <span class=\"md-ellipsis\">\n \n \n API reference\n\n \n </span>\n \n \n\n </a>\n </li>\n \n\n \n \n </ul>\n </nav>\n \n </li>\n \n\n \n </ul>\n</nav>\n </div>\n </div>\n </div>\n \n \n \n <div class=\"md-sidebar md-sidebar--secondary\" data-md-component=\"sidebar\" data-md-type=\"toc\" >\n <div class=\"md-sidebar__scrollwrap\">\n \n \n \n \n \n \n \n <input class=\"md-nav__toggle md-toggle\" type=\"checkbox\" id=\"__toc\">\n <div class=\"md-sidebar-button__wrapper\">\n <label class=\"md-sidebar-button\" for=\"__toc\"></label>\n </div>\n \n \n <div class=\"md-sidebar__inner\">\n \n\n\n<nav class=\"md-nav md-nav--secondary\" aria-label=\"On this page\">\n \n \n \n \n \n \n <label class=\"md-nav__title\" for=\"__toc\">\n <span class=\"md-nav__icon md-icon\"></span>\n On this page\n </label>\n <ul class=\"md-nav__list\" data-md-component=\"toc\" data-md-scrollfix>\n \n <li class=\"md-nav__item\">\n <a href=\"#what-it-provides\" class=\"md-nav__link\">\n <span class=\"md-ellipsis\">\n <span class=\"md-typeset\">\n What it provides\n </span>\n </span>\n </a>\n \n</li>\n \n </ul>\n \n</nav>\n </div>\n </div>\n </div>\n \n \n \n <div class=\"md-content\" data-md-component=\"content\">\n \n \n\n\n\n \n\n\n \n <article class=\"md-content__inner md-typeset\">\n \n \n \n \n\n\n<h1 id=\"soliplex-plumber\">Soliplex Plumber<a class=\"headerlink\" href=\"#soliplex-plumber\" title=\"Permanent link\">&para;</a></h1>\n<p><code>soliplex-plumber</code> is the shared, <strong>stdlib-only</strong> library for reading and\nmodifying the configuration of an <em>existing</em> Soliplex stack. It is the common\ndependency for the skill projects that operate on a generated stack -- the\n<code>soliplex-template</code> skill's <code>add_room.py</code> and the <code>soliplex-concierge</code>\ninstaller -- so the stack-wiring rules live in one place.</p>\n<div class=\"admonition note\">\n<p class=\"admonition-title\">Status</p>\n<p>The library is implemented and tested (100% branch coverage). It does pure\nfilesystem work -- no Docker, no running backend, stdlib only.</p>\n</div>\n<h2 id=\"what-it-provides\">What it provides<a class=\"headerlink\" href=\"#what-it-provides\" title=\"Permanent link\">&para;</a></h2>\n<ul>\n<li><strong><code>rooms</code></strong> -- generic, template-agnostic logic for adding a room to a stack:\n resolve and validate the stack root, infer its package, and ensure\n <code>installation.yaml</code>'s <code>room_paths</code> loads the room (editing line-based, so\n comments and layout are preserved). See the\n <a href=\"reference/api/\">API reference</a>.</li>\n</ul>\n<p>There is no re-exporting package <code>__init__</code>; client code imports the submodule\nand uses its members by dotted name:</p>\n<div class=\"language-python highlight\"><pre><span></span><code><span id=\"__span-0-1\"><a id=\"__codelineno-0-1\" name=\"__codelineno-0-1\" href=\"#__codelineno-0-1\"></a><span class=\"kn\">from</span><span class=\"w\"> </span><span class=\"nn\">soliplex_plumber</span><span class=\"w\"> </span><span class=\"kn\">import</span> <span class=\"n\">rooms</span>\n</span><span id=\"__span-0-2\"><a id=\"__codelineno-0-2\" name=\"__codelineno-0-2\" href=\"#__codelineno-0-2\"></a>\n</span><span id=\"__span-0-3\"><a id=\"__codelineno-0-3\" name=\"__codelineno-0-3\" href=\"#__codelineno-0-3\"></a><span class=\"n\">project</span> <span class=\"o\">=</span> <span class=\"n\">rooms</span><span class=\"o\">.</span><span class=\"n\">resolve_project</span><span class=\"p\">(</span><span class=\"s2\">&quot;/path/to/stack&quot;</span><span class=\"p\">)</span>\n</span><span id=\"__span-0-4\"><a id=\"__codelineno-0-4\" name=\"__codelineno-0-4\" href=\"#__codelineno-0-4\"></a><span class=\"n\">installed</span> <span class=\"o\">=</span> <span class=\"n\">rooms</span><span class=\"o\">.</span><span class=\"n\">install_room</span><span class=\"p\">(</span><span class=\"n\">project</span><span class=\"p\">,</span> <span class=\"s2\">&quot;handbook&quot;</span><span class=\"p\">,</span> <span class=\"n\">config_text</span><span class=\"o\">=</span><span class=\"n\">cfg</span><span class=\"p\">)</span>\n</span></code></pre></div>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n </article>\n </div>\n \n \n <script>var tabs=__md_get(\"__tabs\");if(Array.isArray(tabs))e:for(var set of document.querySelectorAll(\".tabbed-set\")){var labels=set.querySelector(\".tabbed-labels\");for(var tab of tabs)for(var label of labels.getElementsByTagName(\"label\"))if(label.innerText.trim()===tab){var input=document.getElementById(label.htmlFor);input.checked=!0;continue e}}</script>\n\n<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith(\"__tabbed_\"))</script>\n </div>\n \n <button type=\"button\" class=\"md-top md-icon\" data-md-component=\"top\" hidden>\n \n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" class=\"lucide lucide-circle-arrow-up\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m16 12-4-4-4 4M12 16V8\"/></svg>\n Back to top\n</button>\n \n </main>\n \n <footer class=\"md-footer\">\n \n \n \n <nav class=\"md-footer__inner md-grid\" aria-label=\"Footer\" >\n \n \n \n <a href=\"./reference/api/\" class=\"md-footer__link md-footer__link--next\" aria-label=\"Next: API reference\">\n <div class=\"md-footer__title\">\n <span class=\"md-footer__direction\">\n Next\n </span>\n <div class=\"md-ellipsis\">\n API reference\n </div>\n </div>\n <div class=\"md-footer__button md-icon\">\n \n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" class=\"lucide lucide-arrow-right\" viewBox=\"0 0 24 24\"><path d=\"M5 12h14M12 5l7 7-7 7\"/></svg>\n </div>\n </a>\n \n </nav>\n \n \n <div class=\"md-footer-meta md-typeset\">\n <div class=\"md-footer-meta__inner md-grid\">\n <div class=\"md-copyright\">\n \n <div class=\"md-copyright__highlight\">\n Copyright &copy; Soliplex\n\n </div>\n \n \n Made with\n <a href=\"https://zensical.org/\" target=\"_blank\" rel=\"noopener\">\n Zensical\n </a>\n \n</div>\n \n </div>\n </div>\n</footer>\n \n </div>\n <div class=\"md-dialog\" data-md-component=\"dialog\">\n <div class=\"md-dialog__inner md-typeset\"></div>\n </div>\n \n \n \n \n \n <script id=\"__config\" type=\"application/json\">{\"annotate\":null,\"base\":\".\",\"features\":[\"announce.dismiss\",\"content.code.annotate\",\"content.code.copy\",\"content.code.select\",\"content.tabs.link\",\"navigation.footer\",\"navigation.indexes\",\"navigation.instant\",\"navigation.instant.prefetch\",\"navigation.path\",\"navigation.sections\",\"navigation.top\",\"navigation.tracking\",\"search.highlight\"],\"search\":\"./assets/javascripts/workers/search.e2d2d235.min.js\",\"tags\":null,\"translations\":{\"clipboard.copied\":\"Copied to clipboard\",\"clipboard.copy\":\"Copy to clipboard\",\"search.result.more.one\":\"1 more on this page\",\"search.result.more.other\":\"# more on this page\",\"search.result.none\":\"No matching documents\",\"search.result.one\":\"1 matching document\",\"search.result.other\":\"# matching documents\",\"search.result.placeholder\":\"Type to start searching\",\"search.result.term.missing\":\"Missing\",\"select.version\":\"Select version\"},\"version\":null}</script>\n \n \n <script src=\"./assets/javascripts/bundle.6e5f0216.min.js\"></script>\n \n \n </body>\n</html>",
3
+ "hash": 2188577422303779309
4
+ }
@@ -0,0 +1,45 @@
1
+ {
2
+ "data": {
3
+ "meta": {},
4
+ "content": "<h1 id=\"soliplex-plumber\">Soliplex Plumber<a class=\"headerlink\" href=\"#soliplex-plumber\" title=\"Permanent link\">&para;</a></h1>\n<p><code>soliplex-plumber</code> is the shared, <strong>stdlib-only</strong> library for reading and\nmodifying the configuration of an <em>existing</em> Soliplex stack. It is the common\ndependency for the skill projects that operate on a generated stack -- the\n<code>soliplex-template</code> skill's <code>add_room.py</code> and the <code>soliplex-concierge</code>\ninstaller -- so the stack-wiring rules live in one place.</p>\n<div class=\"admonition note\">\n<p class=\"admonition-title\">Status</p>\n<p>The library is implemented and tested (100% branch coverage). It does pure\nfilesystem work -- no Docker, no running backend, stdlib only.</p>\n</div>\n<h2 id=\"what-it-provides\">What it provides<a class=\"headerlink\" href=\"#what-it-provides\" title=\"Permanent link\">&para;</a></h2>\n<ul>\n<li><strong><code>rooms</code></strong> -- generic, template-agnostic logic for adding a room to a stack:\n resolve and validate the stack root, infer its package, and ensure\n <code>installation.yaml</code>'s <code>room_paths</code> loads the room (editing line-based, so\n comments and layout are preserved). See the\n <a href=\"reference/api/\">API reference</a>.</li>\n</ul>\n<p>There is no re-exporting package <code>__init__</code>; client code imports the submodule\nand uses its members by dotted name:</p>\n<div class=\"language-python highlight\"><pre><span></span><code><span id=\"__span-0-1\"><a id=\"__codelineno-0-1\" name=\"__codelineno-0-1\" href=\"#__codelineno-0-1\"></a><span class=\"kn\">from</span><span class=\"w\"> </span><span class=\"nn\">soliplex_plumber</span><span class=\"w\"> </span><span class=\"kn\">import</span> <span class=\"n\">rooms</span>\n</span><span id=\"__span-0-2\"><a id=\"__codelineno-0-2\" name=\"__codelineno-0-2\" href=\"#__codelineno-0-2\"></a>\n</span><span id=\"__span-0-3\"><a id=\"__codelineno-0-3\" name=\"__codelineno-0-3\" href=\"#__codelineno-0-3\"></a><span class=\"n\">project</span> <span class=\"o\">=</span> <span class=\"n\">rooms</span><span class=\"o\">.</span><span class=\"n\">resolve_project</span><span class=\"p\">(</span><span class=\"s2\">&quot;/path/to/stack&quot;</span><span class=\"p\">)</span>\n</span><span id=\"__span-0-4\"><a id=\"__codelineno-0-4\" name=\"__codelineno-0-4\" href=\"#__codelineno-0-4\"></a><span class=\"n\">installed</span> <span class=\"o\">=</span> <span class=\"n\">rooms</span><span class=\"o\">.</span><span class=\"n\">install_room</span><span class=\"p\">(</span><span class=\"n\">project</span><span class=\"p\">,</span> <span class=\"s2\">&quot;handbook&quot;</span><span class=\"p\">,</span> <span class=\"n\">config_text</span><span class=\"o\">=</span><span class=\"n\">cfg</span><span class=\"p\">)</span>\n</span></code></pre></div>",
5
+ "search": [
6
+ {
7
+ "location": null,
8
+ "level": 1,
9
+ "title": "Soliplex Plumber",
10
+ "text": "<p><code>soliplex-plumber</code> is the shared, stdlib-only library for reading and modifying the configuration of an existing Soliplex stack. It is the common dependency for the skill projects that operate on a generated stack -- the <code>soliplex-template</code> skill's <code>add_room.py</code> and the <code>soliplex-concierge</code> installer -- so the stack-wiring rules live in one place.</p> <p>Status</p> <p>The library is implemented and tested (100% branch coverage). It does pure filesystem work -- no Docker, no running backend, stdlib only.</p>",
11
+ "path": [],
12
+ "tags": []
13
+ },
14
+ {
15
+ "location": "what-it-provides",
16
+ "level": 2,
17
+ "title": "What it provides",
18
+ "text": "<ul> <li><code>rooms</code> -- generic, template-agnostic logic for adding a room to a stack: resolve and validate the stack root, infer its package, and ensure <code>installation.yaml</code>'s <code>room_paths</code> loads the room (editing line-based, so comments and layout are preserved). See the API reference.</li> </ul> <p>There is no re-exporting package <code>__init__</code>; client code imports the submodule and uses its members by dotted name:</p> <pre><code>from soliplex_plumber import rooms\n\nproject = rooms.resolve_project(\"/path/to/stack\")\ninstalled = rooms.install_room(project, \"handbook\", config_text=cfg)\n</code></pre>",
19
+ "path": [],
20
+ "tags": []
21
+ }
22
+ ],
23
+ "title": "Soliplex Plumber",
24
+ "toc": [
25
+ {
26
+ "title": "Soliplex Plumber",
27
+ "content": "Soliplex Plumber",
28
+ "id": "soliplex-plumber",
29
+ "url": "#soliplex-plumber",
30
+ "children": [
31
+ {
32
+ "title": "What it provides",
33
+ "content": "What it provides",
34
+ "id": "what-it-provides",
35
+ "url": "#what-it-provides",
36
+ "children": [],
37
+ "level": 2
38
+ }
39
+ ],
40
+ "level": 1
41
+ }
42
+ ]
43
+ },
44
+ "hash": 7823077601700005752
45
+ }
@@ -0,0 +1,45 @@
1
+ {
2
+ "data": {
3
+ "meta": {},
4
+ "content": "<h1 id=\"api-reference\">API reference<a class=\"headerlink\" href=\"#api-reference\" title=\"Permanent link\">&para;</a></h1>\n<div class=\"admonition note\">\n<p class=\"admonition-title\">Implemented</p>\n<p>This is the library's public surface. Every member below is implemented\nand covered by the test suite (100% branch coverage); the modules under\n<code>src/soliplex_plumber/</code> are the source of truth.</p>\n</div>\n<p>There is no re-exporting package <code>__init__</code> — <strong>client code imports the\nsubmodule and uses its members by dotted name</strong> (e.g. <code>from soliplex_plumber\nimport rooms</code> then <code>rooms.install_room(...)</code>).</p>\n<h2 id=\"rooms-add-a-room-to-a-soliplex-stack\"><code>rooms</code> — add a room to a Soliplex stack<a class=\"headerlink\" href=\"#rooms-add-a-room-to-a-soliplex-stack\" title=\"Permanent link\">&para;</a></h2>\n<p>Generic, template-agnostic, stdlib-only logic for wiring a room into a generated\nstack. The shared core behind both the <code>soliplex-template</code> skill's <code>add_room.py</code>\nand the <code>soliplex-concierge</code> installer; it edits <code>installation.yaml</code> line-based so\ncomments and layout are preserved.</p>\n<table>\n<thead>\n<tr>\n<th>Member</th>\n<th>Purpose</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>validate_room_id(room_id)</code></td>\n<td>enforce the room-id / path-segment rule (<code>ROOM_ID_RE</code>); raises <code>AddRoomError</code></td>\n</tr>\n<tr>\n<td><code>resolve_project(project_dir)</code></td>\n<td>resolve + verify the stack root (has <code>COMPOSE_FILE</code> and <code>INSTALLATION_FILE</code>)</td>\n</tr>\n<tr>\n<td><code>resolve_package_name(project, override)</code></td>\n<td>the stack's own package (inferred from <code>src/&lt;pkg&gt;/tools.py</code>) or <code>DEFAULT_PACKAGE_NAME</code></td>\n</tr>\n<tr>\n<td><code>add_room_path(text, room_id) -&gt; (text, action)</code></td>\n<td>ensure <code>room_paths</code> loads the room; action is <code>ADDED</code> / <code>UNCHANGED</code> / <code>COVERED</code></td>\n</tr>\n<tr>\n<td><code>install_room(project, room_id, *, config_text, prompt_text=None, force=False, dry_run=False)</code></td>\n<td>write the room dir + config (+ optional prompt) and apply the <code>room_paths</code> edit</td>\n</tr>\n<tr>\n<td><code>RoomInstalled(config_path, path_action)</code></td>\n<td>the <code>install_room</code> outcome (alias <code>RoomInstall</code> kept for back-compat)</td>\n</tr>\n<tr>\n<td><code>AddRoomError</code></td>\n<td>user-facing error with message-factory classmethods</td>\n</tr>\n<tr>\n<td><code>ADDED</code> / <code>UNCHANGED</code> / <code>COVERED</code> / <code>ROOMS_PARENT_ENTRY</code></td>\n<td>the <code>room_paths</code> action constants and the <code>./rooms</code> auto-discovery entry</td>\n</tr>\n</tbody>\n</table>",
5
+ "search": [
6
+ {
7
+ "location": null,
8
+ "level": 1,
9
+ "title": "API reference",
10
+ "text": "<p>Implemented</p> <p>This is the library's public surface. Every member below is implemented and covered by the test suite (100% branch coverage); the modules under <code>src/soliplex_plumber/</code> are the source of truth.</p> <p>There is no re-exporting package <code>__init__</code> — client code imports the submodule and uses its members by dotted name (e.g. <code>from soliplex_plumber import rooms</code> then <code>rooms.install_room(...)</code>).</p>",
11
+ "path": [],
12
+ "tags": []
13
+ },
14
+ {
15
+ "location": "rooms-add-a-room-to-a-soliplex-stack",
16
+ "level": 2,
17
+ "title": "<code>rooms</code> — add a room to a Soliplex stack",
18
+ "text": "<p>Generic, template-agnostic, stdlib-only logic for wiring a room into a generated stack. The shared core behind both the <code>soliplex-template</code> skill's <code>add_room.py</code> and the <code>soliplex-concierge</code> installer; it edits <code>installation.yaml</code> line-based so comments and layout are preserved.</p> Member Purpose <code>validate_room_id(room_id)</code> enforce the room-id / path-segment rule (<code>ROOM_ID_RE</code>); raises <code>AddRoomError</code> <code>resolve_project(project_dir)</code> resolve + verify the stack root (has <code>COMPOSE_FILE</code> and <code>INSTALLATION_FILE</code>) <code>resolve_package_name(project, override)</code> the stack's own package (inferred from <code>src/&lt;pkg&gt;/tools.py</code>) or <code>DEFAULT_PACKAGE_NAME</code> <code>add_room_path(text, room_id) -&gt; (text, action)</code> ensure <code>room_paths</code> loads the room; action is <code>ADDED</code> / <code>UNCHANGED</code> / <code>COVERED</code> <code>install_room(project, room_id, *, config_text, prompt_text=None, force=False, dry_run=False)</code> write the room dir + config (+ optional prompt) and apply the <code>room_paths</code> edit <code>RoomInstalled(config_path, path_action)</code> the <code>install_room</code> outcome (alias <code>RoomInstall</code> kept for back-compat) <code>AddRoomError</code> user-facing error with message-factory classmethods <code>ADDED</code> / <code>UNCHANGED</code> / <code>COVERED</code> / <code>ROOMS_PARENT_ENTRY</code> the <code>room_paths</code> action constants and the <code>./rooms</code> auto-discovery entry",
19
+ "path": [],
20
+ "tags": []
21
+ }
22
+ ],
23
+ "title": "API reference",
24
+ "toc": [
25
+ {
26
+ "title": "API reference",
27
+ "content": "API reference",
28
+ "id": "api-reference",
29
+ "url": "#api-reference",
30
+ "children": [
31
+ {
32
+ "title": "rooms — add a room to a Soliplex stack",
33
+ "content": "<code>rooms</code> — add a room to a Soliplex stack",
34
+ "id": "rooms-add-a-room-to-a-soliplex-stack",
35
+ "url": "#rooms-add-a-room-to-a-soliplex-stack",
36
+ "children": [],
37
+ "level": 2
38
+ }
39
+ ],
40
+ "level": 1
41
+ }
42
+ ]
43
+ },
44
+ "hash": 10928872380341298869
45
+ }
@@ -0,0 +1,66 @@
1
+ name: Publish docs
2
+
3
+ # Builds the Zensical docs site (sources under docs/, config in zensical.toml)
4
+ # and publishes the generated site/ to the 'gh-pages' branch. Point the
5
+ # repository's Pages "Source" at the 'gh-pages' branch (Settings -> Pages) --
6
+ # a one-time manual step.
7
+
8
+ on:
9
+ push:
10
+ branches: [main]
11
+ paths:
12
+ - 'docs/**'
13
+ - 'zensical.toml'
14
+ - '.github/workflows/docs.yaml'
15
+ workflow_dispatch:
16
+
17
+ permissions:
18
+ contents: write
19
+
20
+ # Allow one publish at a time; let an in-progress run finish.
21
+ concurrency:
22
+ group: publish-docs
23
+ cancel-in-progress: false
24
+
25
+ jobs:
26
+ publish-docs:
27
+ runs-on: ubuntu-latest
28
+ timeout-minutes: 10
29
+ steps:
30
+ - uses: actions/checkout@v4
31
+
32
+ - name: Install uv
33
+ uses: astral-sh/setup-uv@v6
34
+
35
+ - name: Set up Python
36
+ uses: actions/setup-python@v6
37
+ with:
38
+ python-version-file: "pyproject.toml"
39
+
40
+ # The 'docs' group provides zensical (pinned via uv.lock).
41
+ - name: Install dependencies
42
+ run: uv sync --frozen --group docs
43
+
44
+ - name: Build site
45
+ run: uv run zensical build --clean
46
+
47
+ - name: Publish to gh-pages
48
+ uses: peaceiris/actions-gh-pages@v4
49
+ with:
50
+ github_token: ${{ secrets.GITHUB_TOKEN }}
51
+ publish_dir: ./site
52
+ publish_branch: gh-pages
53
+
54
+ - name: Notify Slack on failure
55
+ if: failure()
56
+ uses: slackapi/slack-github-action@v2.1.0
57
+ with:
58
+ webhook: ${{ secrets.SLACK_NOTIFY_URL }}
59
+ webhook-type: incoming-webhook
60
+ payload: |
61
+ {
62
+ "channel": "#soliplex",
63
+ "username": "soliplex",
64
+ "text": ":x: ${{ github.workflow }} failed on ${{ github.head_ref }}:\n${{ github.event.head_commit.message }}\nhttps://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}",
65
+ "icon_emoji": ":ghost:"
66
+ }
@@ -0,0 +1,55 @@
1
+ name: Gitleaks Secret Scan
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - '**'
7
+ workflow_dispatch:
8
+
9
+ concurrency:
10
+ group: ${{ github.workflow }}-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ gitleaks:
15
+ runs-on: ubuntu-latest
16
+ timeout-minutes: 10
17
+ steps:
18
+ - name: Checkout code
19
+ uses: actions/checkout@v6
20
+ with:
21
+ fetch-depth: 0
22
+
23
+ - name: Run Gitleaks
24
+ id: gitleaks
25
+ uses: docker://ghcr.io/gitleaks/gitleaks:latest
26
+ continue-on-error: true
27
+ with:
28
+ args: detect --source=/github/workspace --config=/github/workspace/.gitleaks.toml --no-git --verbose --redact --report-format json --report-path /github/workspace/gitleaks-report.json
29
+
30
+ - name: Parse Gitleaks report
31
+ id: parse-report
32
+ if: steps.gitleaks.outcome == 'failure'
33
+ run: |
34
+ if [ -f gitleaks-report.json ]; then
35
+ FILES=$(jq -r '[.[].File] | unique | .[]' gitleaks-report.json | head -10 | tr '\n' ', ' | sed 's/,$//')
36
+ echo "matched_files=$FILES" >> $GITHUB_OUTPUT
37
+ else
38
+ echo "matched_files=unknown" >> $GITHUB_OUTPUT
39
+ fi
40
+
41
+ - name: Fail if secrets found
42
+ if: steps.gitleaks.outcome == 'failure'
43
+ run: exit 1
44
+
45
+ - name: Notify Slack on failure
46
+ if: failure()
47
+ uses: rtCamp/action-slack-notify@v2
48
+ env:
49
+ SLACK_WEBHOOK: ${{ secrets.SLACK_NOTIFY_URL }}
50
+ SLACK_CHANNEL: soliplex
51
+ SLACK_USERNAME: gitleaks-ci
52
+ SLACK_ICON_EMOJI: ':rotating_light:'
53
+ SLACK_COLOR: danger
54
+ SLACK_TITLE: Gitleaks Secret Scan Failed
55
+ SLACK_MESSAGE: 'Potential secrets detected on ${{ github.ref_name }} in: ${{ steps.parse-report.outputs.matched_files }}\n${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\n $GITHUB_OUTPUT'
@@ -0,0 +1,22 @@
1
+ name: Notify Slack on New issue
2
+
3
+ on:
4
+ issues:
5
+ types: [opened]
6
+
7
+ jobs:
8
+ notify:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - name: Send Slack notification when new issue is opened
12
+ uses: slackapi/slack-github-action@v2.1.0
13
+ with:
14
+ webhook: ${{ secrets.SLACK_NOTIFY_URL }}
15
+ webhook-type: incoming-webhook
16
+ payload: |
17
+ {
18
+ "channel": "#soliplex",
19
+ "username": "soliplex",
20
+ "text": ":bell: New issue by *${{ github.event.issue.user.login }}*:\n*${{ github.event.issue.title }}*\n${{ github.event.issue.html_url }}",
21
+ "icon_emoji": ":ghost:"
22
+ }
@@ -0,0 +1,22 @@
1
+ name: Notify Slack on New PR
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened]
6
+
7
+ jobs:
8
+ notify:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - name: Send Slack notification when new PR is opened
12
+ uses: slackapi/slack-github-action@v2.1.0
13
+ with:
14
+ webhook: ${{ secrets.SLACK_NOTIFY_URL }}
15
+ webhook-type: incoming-webhook
16
+ payload: |
17
+ {
18
+ "channel": "#soliplex",
19
+ "username": "soliplex",
20
+ "text": ":bell: New Pull Request by *${{ github.event.pull_request.user.login }}*:\n*${{ github.event.pull_request.title }}*\n${{ github.event.pull_request.html_url }}",
21
+ "icon_emoji": ":ghost:"
22
+ }
@@ -0,0 +1,30 @@
1
+ name: Build & publish soliplex-plumber to pypi
2
+ on:
3
+ release:
4
+ types: [published]
5
+ workflow_dispatch:
6
+
7
+ permissions:
8
+ contents: read
9
+ # Mint a short-lived OIDC token for PyPI trusted publishing (no API token).
10
+ id-token: write
11
+
12
+ jobs:
13
+ build:
14
+ runs-on: ubuntu-latest
15
+ # Must match the Environment name on the PyPI trusted publisher.
16
+ environment:
17
+ name: pypi
18
+ url: https://pypi.org/p/soliplex-plumber
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ - name: Install uv
22
+ uses: astral-sh/setup-uv@v6
23
+ - name: Set up Python
24
+ uses: actions/setup-python@v6
25
+ with:
26
+ python-version-file: "pyproject.toml"
27
+ - name: Build soliplex-plumber
28
+ run: uv build --sdist --wheel
29
+ - name: Publish soliplex-plumber
30
+ run: uv publish --trusted-publishing always
@@ -0,0 +1,65 @@
1
+ name: Python linting
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ paths:
8
+ - 'pyproject.toml'
9
+ - 'src/soliplex_plumber/**'
10
+ - 'tests/**'
11
+ - '.github/workflows/python-lint.yaml'
12
+ - 'uv.lock'
13
+ pull_request:
14
+ branches:
15
+ - main
16
+ paths:
17
+ - 'pyproject.toml'
18
+ - 'src/soliplex_plumber/**'
19
+ - 'tests/**'
20
+ - '.github/workflows/python-lint.yaml'
21
+ - 'uv.lock'
22
+ workflow_dispatch:
23
+
24
+ jobs:
25
+ lint-soliplex-plumber:
26
+ runs-on: ${{ matrix.os }}
27
+ timeout-minutes: 15
28
+ strategy:
29
+ fail-fast: true
30
+ matrix:
31
+ os: [ubuntu-latest]
32
+ python-version: ["3.13"]
33
+ steps:
34
+ - uses: actions/checkout@v4
35
+
36
+ - name: Install uv
37
+ uses: astral-sh/setup-uv@v6
38
+
39
+ - name: Set up Python ${{ matrix.python-version }}
40
+ uses: actions/setup-python@v6
41
+ with:
42
+ python-version: ${{ matrix.python-version }}
43
+
44
+ - name: Install dependencies
45
+ run: uv sync --frozen --group dev
46
+
47
+ - name: Check formatting
48
+ run: uv run ruff format --check
49
+
50
+ - name: Run ruff linter
51
+ run: uv run ruff check
52
+
53
+ - name: Notify Slack on failure
54
+ if: failure()
55
+ uses: slackapi/slack-github-action@v2.1.0
56
+ with:
57
+ webhook: ${{ secrets.SLACK_NOTIFY_URL }}
58
+ webhook-type: incoming-webhook
59
+ payload: |
60
+ {
61
+ "channel": "#soliplex",
62
+ "username": "soliplex",
63
+ "text": ":x: ${{ github.workflow }} failed on ${{ github.head_ref }}:\n${{ github.event.head_commit.message }}\nhttps://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}",
64
+ "icon_emoji": ":ghost:"
65
+ }
@@ -0,0 +1,66 @@
1
+ name: Python tests/coverage
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ paths:
8
+ - 'pyproject.toml'
9
+ - 'src/soliplex_plumber/**'
10
+ - 'tests/**'
11
+ - '.github/workflows/python-test.yaml'
12
+ - 'uv.lock'
13
+ pull_request:
14
+ branches:
15
+ - main
16
+ paths:
17
+ - 'pyproject.toml'
18
+ - 'src/soliplex_plumber/**'
19
+ - 'tests/**'
20
+ - '.github/workflows/python-test.yaml'
21
+ - 'uv.lock'
22
+ workflow_dispatch:
23
+
24
+ jobs:
25
+ test-soliplex-plumber:
26
+ runs-on: ${{ matrix.os }}
27
+ timeout-minutes: 15
28
+ strategy:
29
+ fail-fast: true
30
+ matrix:
31
+ os: [ubuntu-latest]
32
+ python-version: ["3.12", "3.13", "3.14"]
33
+ include:
34
+ # Only enforce coverage on Python >= 3.13.
35
+ - python-version: "3.12"
36
+ pytest-args: "--no-cov"
37
+ steps:
38
+ - uses: actions/checkout@v4
39
+
40
+ - name: Install uv
41
+ uses: astral-sh/setup-uv@v6
42
+
43
+ - name: Set up Python ${{ matrix.python-version }}
44
+ uses: actions/setup-python@v6
45
+ with:
46
+ python-version: ${{ matrix.python-version }}
47
+
48
+ - name: Install dependencies
49
+ run: uv sync --frozen --group dev
50
+
51
+ - name: Run unit tests with coverage
52
+ run: uv run pytest ${{ matrix.pytest-args }}
53
+
54
+ - name: Notify Slack on failure
55
+ if: failure()
56
+ uses: slackapi/slack-github-action@v2.1.0
57
+ with:
58
+ webhook: ${{ secrets.SLACK_NOTIFY_URL }}
59
+ webhook-type: incoming-webhook
60
+ payload: |
61
+ {
62
+ "channel": "#soliplex",
63
+ "username": "soliplex",
64
+ "text": ":x: ${{ github.workflow }} failed on ${{ github.head_ref }}:\n${{ github.event.head_commit.message }}\nhttps://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}",
65
+ "icon_emoji": ":ghost:"
66
+ }
@@ -0,0 +1,44 @@
1
+ name: TruffleHog Secrets Scan
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - '**'
7
+ workflow_dispatch:
8
+
9
+ concurrency:
10
+ group: ${{ github.workflow }}-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ trufflehog:
15
+ runs-on: ubuntu-latest
16
+ timeout-minutes: 10
17
+ steps:
18
+ - name: Checkout code
19
+ uses: actions/checkout@v6
20
+ with:
21
+ fetch-depth: 0
22
+ - name: TruffleHog scan
23
+ uses: trufflesecurity/trufflehog@main
24
+ with:
25
+ extra_args: --only-verified
26
+
27
+ notify-failure:
28
+ runs-on: ubuntu-latest
29
+ needs: [trufflehog]
30
+ if: failure()
31
+ timeout-minutes: 2
32
+ steps:
33
+ - name: Notify Slack on failure
34
+ uses: slackapi/slack-github-action@v2.1.1
35
+ with:
36
+ webhook: ${{ secrets.SLACK_NOTIFY_URL }}
37
+ webhook-type: incoming-webhook
38
+ payload: |
39
+ {
40
+ "channel": "#soliplex",
41
+ "username": "soliplex",
42
+ "text": ":rotating_light: ${{ github.workflow }} failed on ${{ github.head_ref || github.ref_name }}:\n${{ github.event.head_commit.message }}\nhttps://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}",
43
+ "icon_emoji": ":ghost:"
44
+ }