quickhatch 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quickhatch/__init__.py +11 -0
- quickhatch/__main__.py +6 -0
- quickhatch/ai/__init__.py +1 -0
- quickhatch/ai/install_primer.py +337 -0
- quickhatch/ai/instruction_gen.py +231 -0
- quickhatch/ai/model_list.py +140 -0
- quickhatch/ai/recipe_apply.py +178 -0
- quickhatch/analysis/__init__.py +1 -0
- quickhatch/analysis/app_catalog.py +628 -0
- quickhatch/analysis/app_mapper.py +193 -0
- quickhatch/analysis/distro_engine.py +84 -0
- quickhatch/analysis/driver_check.py +83 -0
- quickhatch/bridge/__init__.py +1 -0
- quickhatch/bridge/display.py +136 -0
- quickhatch/bridge/server.py +886 -0
- quickhatch/cli.py +1188 -0
- quickhatch/config.py +463 -0
- quickhatch/console.py +59 -0
- quickhatch/data/app_alternatives.json +222 -0
- quickhatch/data/app_catalog.generated.json +90774 -0
- quickhatch/data/app_catalog.overrides.json +54159 -0
- quickhatch/data/distros.json +110 -0
- quickhatch/data/driver_db.json +18 -0
- quickhatch/data/gallery/github-1amsimp1e-dots.webp +0 -0
- quickhatch/data/gallery/github-2kabhishek-dots2k.webp +0 -0
- quickhatch/data/gallery/github-2ssk-dot-files.webp +0 -0
- quickhatch/data/gallery/github-3d3f-ii-sddm-theme.webp +0 -0
- quickhatch/data/gallery/github-acaibowlz-niri-setup.webp +0 -0
- quickhatch/data/gallery/github-ad1822-hyprdots.webp +0 -0
- quickhatch/data/gallery/github-adriankarlen-bar-wezterm.webp +0 -0
- quickhatch/data/gallery/github-ahmad9059-hyprflux.webp +0 -0
- quickhatch/data/gallery/github-allaman-nvim.webp +0 -0
- quickhatch/data/gallery/github-anotherhadi-nixy.webp +0 -0
- quickhatch/data/gallery/github-artart222-codeart.webp +0 -0
- quickhatch/data/gallery/github-ascaniolamp-hyprlain.webp +0 -0
- quickhatch/data/gallery/github-ashish0kumar-windots.webp +0 -0
- quickhatch/data/gallery/github-athxrvx-idempotentsystems.webp +0 -0
- quickhatch/data/gallery/github-avivace-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-avtzis-awesome-linux-ricing.webp +0 -0
- quickhatch/data/gallery/github-ayamir-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-ayamir-nvimdots.webp +0 -0
- quickhatch/data/gallery/github-aymanlyesri-archeclipse.webp +0 -0
- quickhatch/data/gallery/github-benmezger-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-bertof-nix-rice.webp +0 -0
- quickhatch/data/gallery/github-bluemancz-hyprmod.webp +0 -0
- quickhatch/data/gallery/github-caelestia-dots-shell.webp +0 -0
- quickhatch/data/gallery/github-caioax-lyne-dots.webp +0 -0
- quickhatch/data/gallery/github-cebem1nt-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-christianlempa-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-command-z-z-arch-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-cosmicnvim-cosmicnvim.webp +0 -0
- quickhatch/data/gallery/github-crissnb-dynamic-island-sketchybar.webp +0 -0
- quickhatch/data/gallery/github-cxorz-dotfiles-hyprland.webp +0 -0
- quickhatch/data/gallery/github-cybersnake223-hypr.webp +0 -0
- quickhatch/data/gallery/github-cybrcore-cybrdots.webp +0 -0
- quickhatch/data/gallery/github-danihek-hellwal.webp +0 -0
- quickhatch/data/gallery/github-danihek-themecord.webp +0 -0
- quickhatch/data/gallery/github-darkkal44-qylock.webp +0 -0
- quickhatch/data/gallery/github-datsfilipe-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-deathbeam-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-debuggyo-exo.webp +0 -0
- quickhatch/data/gallery/github-denisse-dev-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-deridray-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-diinki-diinki-aero.webp +0 -0
- quickhatch/data/gallery/github-diinki-diinki-retrofuture.webp +0 -0
- quickhatch/data/gallery/github-diinki-linux-retroism.webp +0 -0
- quickhatch/data/gallery/github-dikiaap-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-dileep-kishore-nixos-config.webp +0 -0
- quickhatch/data/gallery/github-dmadisetti-dots.webp +0 -0
- quickhatch/data/gallery/github-driesvints-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-dusklinux-dusky.webp +0 -0
- quickhatch/data/gallery/github-dustinlyons-nixos-config.webp +0 -0
- quickhatch/data/gallery/github-dybdeskarphet-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-ecosse3-nvim.webp +0 -0
- quickhatch/data/gallery/github-edu-flores-linux-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-elifouts-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-elkowar-yolk.webp +0 -0
- quickhatch/data/gallery/github-elpritchos-omadora.webp +0 -0
- quickhatch/data/gallery/github-elysiaos-elysiaos.webp +0 -0
- quickhatch/data/gallery/github-end-4-dots-hyprland.webp +0 -0
- quickhatch/data/gallery/github-endeavouros-team-endeavouros-i3wm-setup.webp +0 -0
- quickhatch/data/gallery/github-exploitoverload-pwnixos.webp +0 -0
- quickhatch/data/gallery/github-felipecrs-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-firestreaker2-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-flyingcakes85-lovebites.webp +0 -0
- quickhatch/data/gallery/github-folke-dot.webp +0 -0
- quickhatch/data/gallery/github-frogprog09-my-linux.webp +0 -0
- quickhatch/data/gallery/github-frost-phoenix-nixos-config.webp +0 -0
- quickhatch/data/gallery/github-gabrieltenma-dotfiles-gnm.webp +0 -0
- quickhatch/data/gallery/github-gf3-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-gh0stzk-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-gnuunixchad-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-gpakosz-tmux.webp +0 -0
- quickhatch/data/gallery/github-gvolpe-nix-config.webp +0 -0
- quickhatch/data/gallery/github-haaarshsingh-dots.webp +0 -0
- quickhatch/data/gallery/github-hancore-linux-waybar-themes.webp +0 -0
- quickhatch/data/gallery/github-heapbytes-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-heisenburgh-pixarch.webp +0 -0
- quickhatch/data/gallery/github-hlissner-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-hyprland-community-hyprls.webp +0 -0
- quickhatch/data/gallery/github-ikz87-dots-2-0.webp +0 -0
- quickhatch/data/gallery/github-iogamaster-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-issmirnov-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-jakoolit-arch-hyprland.webp +0 -0
- quickhatch/data/gallery/github-jakoolit-fedora-hyprland.webp +0 -0
- quickhatch/data/gallery/github-jakoolit-hyprland-dots.webp +0 -0
- quickhatch/data/gallery/github-jakoolit-opensuse-hyprland.webp +0 -0
- quickhatch/data/gallery/github-javalsai-lidm.webp +0 -0
- quickhatch/data/gallery/github-jessfraz-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-jguer-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-johackim-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-justus0405-i3wm-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-keyitdev-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-keyitdev-sddm-astronaut-theme.webp +0 -0
- quickhatch/data/gallery/github-klaxalk-linux-setup.webp +0 -0
- quickhatch/data/gallery/github-koeqaife-hyprland-material-you.webp +0 -0
- quickhatch/data/gallery/github-kutsan-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-lolei-razer-cli.webp +0 -0
- quickhatch/data/gallery/github-m4xshen-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-madic-creates-sway-de.webp +0 -0
- quickhatch/data/gallery/github-marcusmix-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-martins3-my-linux-config.webp +0 -0
- quickhatch/data/gallery/github-mastermindzh-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-mathix420-free-the-web-apps.webp +0 -0
- quickhatch/data/gallery/github-mattmc3-antidote.webp +0 -0
- quickhatch/data/gallery/github-maytermux-mytermux.webp +0 -0
- quickhatch/data/gallery/github-meain-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-meowrch-meowrch.webp +0 -0
- quickhatch/data/gallery/github-misterio77-nix-config.webp +0 -0
- quickhatch/data/gallery/github-mrusme-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-mrusme-gomphotherium.webp +0 -0
- quickhatch/data/gallery/github-mubin-thinks-minimal-wm-config.webp +0 -0
- quickhatch/data/gallery/github-mutewinter-dot-vim.webp +0 -0
- quickhatch/data/gallery/github-myamusashi-vast-shell.webp +0 -0
- quickhatch/data/gallery/github-mylinuxforwork-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-n6v26r-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-neurarian-matshell.webp +0 -0
- quickhatch/data/gallery/github-nickjj-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-nicknisi-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-nicksp-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-nix-darwin-nix-darwin.webp +0 -0
- quickhatch/data/gallery/github-noctalia-dev-noctalia-shell.webp +0 -0
- quickhatch/data/gallery/github-nocturnussx-hyprland-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-notmugil-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-notneelpatel-wallpaperthemeconverter.webp +0 -0
- quickhatch/data/gallery/github-nwg-piotr-nwg-shell-config.webp +0 -0
- quickhatch/data/gallery/github-opensuse-opensuseway.webp +0 -0
- quickhatch/data/gallery/github-orhun-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-oughie-clock-rs.webp +0 -0
- quickhatch/data/gallery/github-ozwaldorf-lutgen-rs.webp +0 -0
- quickhatch/data/gallery/github-pablonoya-awesomewm-configuration.webp +0 -0
- quickhatch/data/gallery/github-pazl27-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-pinpox-base16-universal-manager.webp +0 -0
- quickhatch/data/gallery/github-potamides-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-prasanthrangan-hyprdots.webp +0 -0
- quickhatch/data/gallery/github-qxb3-conf.webp +0 -0
- quickhatch/data/gallery/github-qxb3-fum.webp +0 -0
- quickhatch/data/gallery/github-r0naldoom-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-ray-x-nvim.webp +0 -0
- quickhatch/data/gallery/github-rayhaanfay-xfce-creation-of-adam.webp +0 -0
- quickhatch/data/gallery/github-redyf-nixdots.webp +0 -0
- quickhatch/data/gallery/github-retrozinndev-colorshell.webp +0 -0
- quickhatch/data/gallery/github-riccardopp-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-ryan4yin-nix-config.webp +0 -0
- quickhatch/data/gallery/github-ryan4yin-nix-darwin-kickstarter.webp +0 -0
- quickhatch/data/gallery/github-s4nkalp-hyprland.webp +0 -0
- quickhatch/data/gallery/github-s4nkalp-modus.webp +0 -0
- quickhatch/data/gallery/github-sangrokjung-claude-forge.webp +0 -0
- quickhatch/data/gallery/github-saschagrunert-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-sbalghari-sbdots.webp +0 -0
- quickhatch/data/gallery/github-schmeekygeek-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-sejjy-mechabar.webp +0 -0
- quickhatch/data/gallery/github-sekiryl-hyprdots.webp +0 -0
- quickhatch/data/gallery/github-sh1zicus-dots-hyprland.webp +0 -0
- quickhatch/data/gallery/github-shell-ninja-hyprconf-v2.webp +0 -0
- quickhatch/data/gallery/github-shell-ninja-hyprconf.webp +0 -0
- quickhatch/data/gallery/github-shikikan-neko08-nyartix-rice.webp +0 -0
- quickhatch/data/gallery/github-siduck-chadwm.webp +0 -0
- quickhatch/data/gallery/github-simonvic-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-sirethanator-hyprland-dots.webp +0 -0
- quickhatch/data/gallery/github-sly-harvey-nixos.webp +0 -0
- quickhatch/data/gallery/github-snowarch-inir.webp +0 -0
- quickhatch/data/gallery/github-swarsel-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-swaykh-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-szorfein-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-szorfein-dots.webp +0 -0
- quickhatch/data/gallery/github-techdufus-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-tsm-061-ctos.webp +0 -0
- quickhatch/data/gallery/github-vallen217-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-victorsosamx-vshyprland-manager.webp +0 -0
- quickhatch/data/gallery/github-victorsosamx-vswaybar-studio.webp +0 -0
- quickhatch/data/gallery/github-viegphunt-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-vyrx-dev-symphony.webp +0 -0
- quickhatch/data/gallery/github-xero-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-xnm1-linux-nixos-hyprland-config-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-youwes09-ateon.webp +0 -0
- quickhatch/data/gallery/github-yunfachi-nix-config.webp +0 -0
- quickhatch/data/gallery/github-yurihikari-garuda-hyprdots.webp +0 -0
- quickhatch/data/gallery/github-zemmsoares-awesome-rices.webp +0 -0
- quickhatch/data/gallery/github-zhaleff-blacknode.webp +0 -0
- quickhatch/data/gallery/github-ziap-dotfiles.webp +0 -0
- quickhatch/data/gallery/github-zproger-bspwm-dotfiles.webp +0 -0
- quickhatch/data/gallery/gnome-12dampb.webp +0 -0
- quickhatch/data/gallery/hyprland-1b4izs2.webp +0 -0
- quickhatch/data/gallery/hyprland-1fp86p7.webp +0 -0
- quickhatch/data/gallery/hyprland-1inxosk.webp +0 -0
- quickhatch/data/gallery/hyprland-1jtjljz.webp +0 -0
- quickhatch/data/gallery/hyprland-1jv48oq.webp +0 -0
- quickhatch/data/gallery/i3wm-m7w790.webp +0 -0
- quickhatch/data/gallery/unixporn-1k7c8i1.webp +0 -0
- quickhatch/data/gallery/unixporn-1kcebee.webp +0 -0
- quickhatch/data/gallery/unixporn-5crgng.webp +0 -0
- quickhatch/data/gallery/unixporn-nhomed.webp +0 -0
- quickhatch/data/gallery/unixporn-r5ot7x.webp +0 -0
- quickhatch/data/gallery.json +8850 -0
- quickhatch/data/gallery_descriptions.json +426 -0
- quickhatch/data/install_primers/_index.json +253 -0
- quickhatch/data/install_primers/alpine.md +118 -0
- quickhatch/data/install_primers/arch.md +30 -0
- quickhatch/data/install_primers/artix.md +69 -0
- quickhatch/data/install_primers/bazzite.md +61 -0
- quickhatch/data/install_primers/cachyos.md +53 -0
- quickhatch/data/install_primers/common-pre-reboot-rules.md +200 -0
- quickhatch/data/install_primers/debian.md +73 -0
- quickhatch/data/install_primers/elementary.md +67 -0
- quickhatch/data/install_primers/endeavouros.md +39 -0
- quickhatch/data/install_primers/families/arch-family.md +294 -0
- quickhatch/data/install_primers/families/debian-family.md +147 -0
- quickhatch/data/install_primers/families/fedora-family.md +240 -0
- quickhatch/data/install_primers/fedora-silverblue.md +78 -0
- quickhatch/data/install_primers/fedora.md +51 -0
- quickhatch/data/install_primers/garuda.md +90 -0
- quickhatch/data/install_primers/gentoo.md +160 -0
- quickhatch/data/install_primers/kali.md +97 -0
- quickhatch/data/install_primers/kde-neon.md +61 -0
- quickhatch/data/install_primers/linux-mint.md +49 -0
- quickhatch/data/install_primers/manjaro.md +80 -0
- quickhatch/data/install_primers/mx-linux.md +65 -0
- quickhatch/data/install_primers/nixos.md +135 -0
- quickhatch/data/install_primers/nobara.md +55 -0
- quickhatch/data/install_primers/opensuse-leap.md +44 -0
- quickhatch/data/install_primers/opensuse-tumbleweed.md +143 -0
- quickhatch/data/install_primers/parrot.md +70 -0
- quickhatch/data/install_primers/pop-os.md +80 -0
- quickhatch/data/install_primers/rhel-clones.md +78 -0
- quickhatch/data/install_primers/ubuntu.md +99 -0
- quickhatch/data/install_primers/void.md +121 -0
- quickhatch/data/install_primers/zorin.md +49 -0
- quickhatch/export/__init__.py +1 -0
- quickhatch/gallery/__init__.py +15 -0
- quickhatch/gallery/build.py +91 -0
- quickhatch/gallery/common.py +492 -0
- quickhatch/gallery/components.py +475 -0
- quickhatch/gallery/curate.py +398 -0
- quickhatch/gallery/dedupe.py +93 -0
- quickhatch/gallery/describe.py +204 -0
- quickhatch/gallery/extract.py +467 -0
- quickhatch/gallery/filter.py +492 -0
- quickhatch/gallery/library.py +35 -0
- quickhatch/gallery/llm_extract.py +495 -0
- quickhatch/gallery/scrape.py +278 -0
- quickhatch/gallery/verify.py +60 -0
- quickhatch/gui/__init__.py +1 -0
- quickhatch/gui/app.html +5212 -0
- quickhatch/gui/index.html +38 -0
- quickhatch/gui/server.py +8775 -0
- quickhatch/media/__init__.py +1 -0
- quickhatch/media/iso_download.py +94 -0
- quickhatch/media/usb_writer.py +184 -0
- quickhatch/scanners/__init__.py +1 -0
- quickhatch/scanners/apps.py +458 -0
- quickhatch/scanners/files.py +118 -0
- quickhatch/scanners/hardware.py +713 -0
- quickhatch/scanners/settings.py +286 -0
- quickhatch/secrets.py +86 -0
- quickhatch/setup/__init__.py +65 -0
- quickhatch/setup/attempts.py +199 -0
- quickhatch/setup/audit.py +1245 -0
- quickhatch/setup/capabilities.py +1617 -0
- quickhatch/setup/certify.py +527 -0
- quickhatch/setup/debugtools.py +139 -0
- quickhatch/setup/facts.py +646 -0
- quickhatch/setup/failure.py +137 -0
- quickhatch/setup/families/__init__.py +39 -0
- quickhatch/setup/families/arch.py +44 -0
- quickhatch/setup/families/base.py +362 -0
- quickhatch/setup/families/debian.py +80 -0
- quickhatch/setup/families/fedora.py +83 -0
- quickhatch/setup/families/ubuntu.py +54 -0
- quickhatch/setup/fuzz.py +478 -0
- quickhatch/setup/health.py +869 -0
- quickhatch/setup/operations.py +91 -0
- quickhatch/setup/orchestrator.py +11042 -0
- quickhatch/setup/planner.py +1470 -0
- quickhatch/setup/preflight.py +1479 -0
- quickhatch/setup/release_smoke.py +297 -0
- quickhatch/setup/replay.py +1126 -0
- quickhatch/setup/resolver.py +342 -0
- quickhatch/setup/runlog.py +281 -0
- quickhatch/setup/support_bundle.py +594 -0
- quickhatch/setup/types.py +281 -0
- quickhatch/wizard/__init__.py +1 -0
- quickhatch/wizard/additional.py +36 -0
- quickhatch/wizard/ai_setup.py +292 -0
- quickhatch/wizard/bridge.py +231 -0
- quickhatch/wizard/compatibility.py +99 -0
- quickhatch/wizard/distro.py +110 -0
- quickhatch/wizard/export.py +579 -0
- quickhatch/wizard/questionnaire.py +132 -0
- quickhatch/wizard/scan.py +75 -0
- quickhatch/wizard/ssh.py +172 -0
- quickhatch/wizard/usb.py +252 -0
- quickhatch/wizard/vm_verify.py +1473 -0
- quickhatch/wizard/welcome.py +43 -0
- quickhatch-0.1.0.dist-info/METADATA +295 -0
- quickhatch-0.1.0.dist-info/RECORD +319 -0
- quickhatch-0.1.0.dist-info/WHEEL +4 -0
- quickhatch-0.1.0.dist-info/entry_points.txt +2 -0
- quickhatch-0.1.0.dist-info/licenses/LICENSE +50 -0
quickhatch/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""QuickHatch - Windows-to-Linux migration wizard powered by AI."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("quickhatch")
|
|
7
|
+
except PackageNotFoundError:
|
|
8
|
+
# Source checkout without an installed package (e.g. running from a clone)
|
|
9
|
+
__version__ = "0.0.0+dev"
|
|
10
|
+
|
|
11
|
+
__all__ = ["__version__"]
|
quickhatch/__main__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""AI assistant integration modules."""
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"""Lookup + rendering for distro install primers shipped with QuickHatch.
|
|
2
|
+
|
|
3
|
+
The harness agent installs the target distro from a live ISO via SSH. To
|
|
4
|
+
avoid the agent hallucinating flags (e.g. ``pacstrap`` options that changed
|
|
5
|
+
between Arch versions), each supported distro ships a condensed install
|
|
6
|
+
primer under ``data/install_primers/``. This module resolves the user's
|
|
7
|
+
chosen distro (fuzzy alias match) and returns its primer text so it can be
|
|
8
|
+
injected into the remote-setup prompt.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from functools import lru_cache
|
|
15
|
+
from importlib import resources
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
_PRIMER_PACKAGE = "quickhatch.data.install_primers"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@lru_cache(maxsize=1)
|
|
22
|
+
def _load_index() -> dict[str, Any]:
|
|
23
|
+
"""Load the primer index. Cached on first access."""
|
|
24
|
+
try:
|
|
25
|
+
with (
|
|
26
|
+
resources.files(_PRIMER_PACKAGE)
|
|
27
|
+
.joinpath("_index.json")
|
|
28
|
+
.open("r", encoding="utf-8") as handle
|
|
29
|
+
):
|
|
30
|
+
return json.load(handle)
|
|
31
|
+
except (OSError, json.JSONDecodeError, ModuleNotFoundError):
|
|
32
|
+
return {"version": 1, "distros": []}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def list_supported_distros() -> list[dict[str, Any]]:
|
|
36
|
+
"""Return every distro entry in the primer index."""
|
|
37
|
+
return list(_load_index().get("distros", []))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _normalize(value: str) -> str:
|
|
41
|
+
return value.strip().lower().replace(" ", "-").replace("_", "-").replace("!", "")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def resolve_primer(distro: str | None, preferred: str | None = None) -> dict[str, Any] | None:
|
|
45
|
+
"""Given any user-typed distro hint, find the matching primer entry.
|
|
46
|
+
|
|
47
|
+
Matches against canonical names and aliases with light normalization
|
|
48
|
+
(lowercase, hyphenate whitespace). Returns the index entry dict
|
|
49
|
+
(with file path, family, release model) or ``None`` if no match.
|
|
50
|
+
|
|
51
|
+
``preferred`` is the user's stated distro preference — used as a
|
|
52
|
+
strong tiebreaker when the free-text hint mentions multiple distros
|
|
53
|
+
(e.g. the analysis output names Arch as the recommendation and then
|
|
54
|
+
mentions Fedora in a negative comparison). If the preferred distro
|
|
55
|
+
appears anywhere in the hint, it wins.
|
|
56
|
+
"""
|
|
57
|
+
if not distro:
|
|
58
|
+
return None
|
|
59
|
+
# Defensive: callers occasionally pass non-string values (dicts from
|
|
60
|
+
# partially-parsed LLM output, Path objects, etc.). Coerce or bail.
|
|
61
|
+
if not isinstance(distro, str):
|
|
62
|
+
try:
|
|
63
|
+
distro = str(distro)
|
|
64
|
+
except Exception:
|
|
65
|
+
return None
|
|
66
|
+
needle = _normalize(distro)
|
|
67
|
+
if not needle:
|
|
68
|
+
return None
|
|
69
|
+
for entry in _load_index().get("distros", []):
|
|
70
|
+
canonical = _normalize(entry.get("canonical", ""))
|
|
71
|
+
if canonical == needle:
|
|
72
|
+
return entry
|
|
73
|
+
if any(_normalize(alias) == needle for alias in entry.get("aliases", [])):
|
|
74
|
+
return entry
|
|
75
|
+
|
|
76
|
+
# No exact match — try forgiving variants.
|
|
77
|
+
# 1. Strip "linux"/"os" suffixes the user may have appended.
|
|
78
|
+
# ("archlinux" → "arch", "fedoralinux" → "fedora", "endeavouros" stays).
|
|
79
|
+
import re
|
|
80
|
+
|
|
81
|
+
# Only strip "linux" / "-linux" suffixes, NOT bare "os" — the latter
|
|
82
|
+
# over-strips ("chromeos" → "chrome", "archos" → "arch" which would
|
|
83
|
+
# spuriously match Arch's primer). Distro-"os" variants (endeavouros,
|
|
84
|
+
# centos) stay reachable through their explicit aliases.
|
|
85
|
+
stripped = re.sub(r"-?linux$", "", needle)
|
|
86
|
+
if stripped and stripped != needle:
|
|
87
|
+
for entry in _load_index().get("distros", []):
|
|
88
|
+
if _normalize(entry.get("canonical", "")) == stripped:
|
|
89
|
+
return entry
|
|
90
|
+
if any(_normalize(alias) == stripped for alias in entry.get("aliases", [])):
|
|
91
|
+
return entry
|
|
92
|
+
|
|
93
|
+
# Helper: is a distro token mentioned in a NEGATIVE context? The model
|
|
94
|
+
# often contrasts the pick with alternatives ("unlike Ubuntu", "not
|
|
95
|
+
# Fedora", "the bloat of distributions like Ubuntu or Fedora") — those
|
|
96
|
+
# mentions must not win the resolver.
|
|
97
|
+
def _is_negative_context(text: str, match_start: int, match_end: int) -> bool:
|
|
98
|
+
# Look back only within the current local clause. A negative cue
|
|
99
|
+
# from an earlier clause ("not fedora. go with arch") must not
|
|
100
|
+
# poison the later positive recommendation.
|
|
101
|
+
raw_window = text[max(0, match_start - 60) : match_start + 1]
|
|
102
|
+
clause_window = re.split(r"[.;:!?]", raw_window)[-1]
|
|
103
|
+
window = clause_window or raw_window
|
|
104
|
+
# Word-boundary cues so "likely" doesn't trip "like" and
|
|
105
|
+
# "Linux-Mint" doesn't trip "not". Each cue is wrapped so it
|
|
106
|
+
# matches only at word boundaries inside the hyphenated needle.
|
|
107
|
+
negative_cue_words = (
|
|
108
|
+
"unlike",
|
|
109
|
+
"not",
|
|
110
|
+
"rather",
|
|
111
|
+
"instead",
|
|
112
|
+
"avoid",
|
|
113
|
+
"such",
|
|
114
|
+
"compared",
|
|
115
|
+
"versus",
|
|
116
|
+
"vs",
|
|
117
|
+
"over", # "chose X over Y"
|
|
118
|
+
"than", # "better than Y"
|
|
119
|
+
"bloat",
|
|
120
|
+
"constraints",
|
|
121
|
+
)
|
|
122
|
+
for cue in negative_cue_words:
|
|
123
|
+
pattern = rf"(?:^|[^a-z0-9]){re.escape(cue)}(?:$|[^a-z0-9])"
|
|
124
|
+
if re.search(pattern, window):
|
|
125
|
+
return True
|
|
126
|
+
# "like" is ambiguous — "I'd like arch" (positive) vs
|
|
127
|
+
# "distributions like X" (negative). Only treat as negative when
|
|
128
|
+
# preceded by a list-noun within ~20 chars.
|
|
129
|
+
list_noun_like = re.compile(
|
|
130
|
+
r"(?:distributions?|distros?|systems?|options?|oses|choices?|"
|
|
131
|
+
r"alternatives?)[^a-z0-9]{1,5}like(?:$|[^a-z0-9])",
|
|
132
|
+
re.IGNORECASE,
|
|
133
|
+
)
|
|
134
|
+
if list_noun_like.search(window):
|
|
135
|
+
return True
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
# 2a. STRONGEST SIGNAL: if the caller passed a preferred distro AND it
|
|
139
|
+
# appears in the text as a non-negative mention, return it. This
|
|
140
|
+
# prevents the comparison-trailing-mention bug where "Arch is better
|
|
141
|
+
# than Fedora" resolves to Fedora.
|
|
142
|
+
if preferred:
|
|
143
|
+
pref_entry: dict[str, Any] | None = None
|
|
144
|
+
pref_needle = _normalize(preferred)
|
|
145
|
+
for entry in _load_index().get("distros", []):
|
|
146
|
+
canonical = _normalize(entry.get("canonical", ""))
|
|
147
|
+
aliases = [_normalize(a) for a in entry.get("aliases", [])]
|
|
148
|
+
if pref_needle == canonical or pref_needle in aliases:
|
|
149
|
+
pref_entry = entry
|
|
150
|
+
break
|
|
151
|
+
if pref_entry is not None:
|
|
152
|
+
for candidate in [
|
|
153
|
+
_normalize(pref_entry.get("canonical", "")),
|
|
154
|
+
*(_normalize(a) for a in pref_entry.get("aliases", [])),
|
|
155
|
+
]:
|
|
156
|
+
if not candidate:
|
|
157
|
+
continue
|
|
158
|
+
pattern = rf"(?:^|[^a-z0-9]){re.escape(candidate)}(?:$|[^a-z0-9])"
|
|
159
|
+
for mobj in re.finditer(pattern, needle):
|
|
160
|
+
if not _is_negative_context(needle, mobj.start(), mobj.end()):
|
|
161
|
+
return pref_entry
|
|
162
|
+
|
|
163
|
+
# 2b. Word-boundary search within a longer free-text hint
|
|
164
|
+
# ("I'd like arch btw" → matches "arch"). Still rejects
|
|
165
|
+
# accidental-substring cases like "templeos" matching "eos".
|
|
166
|
+
#
|
|
167
|
+
# When multiple distros appear in the text (common when the model
|
|
168
|
+
# compares options before settling), we want the one the model
|
|
169
|
+
# ACTUALLY picks — usually the last occurrence after "recommend",
|
|
170
|
+
# "recommended", "choose", or "go with". First try a targeted
|
|
171
|
+
# recommendation-phrase scan; fall back to last-occurrence.
|
|
172
|
+
#
|
|
173
|
+
# The recommend scan now looks for ANY distro token within 80 chars
|
|
174
|
+
# after a recommendation verb — not just the immediately-next word.
|
|
175
|
+
# Previously "recommended distribution: arch linux" captured the
|
|
176
|
+
# word "distribution" (junk), failed to match, and fell through to
|
|
177
|
+
# the last-mention heuristic (which picked up "Fedora" from a
|
|
178
|
+
# negative comparison at the end of the paragraph).
|
|
179
|
+
_RECOMMEND_RE = re.compile(
|
|
180
|
+
r"(?:recommend(?:ation|ed|ing)?|i\s+suggest|i\s+pick|i\s+choose|"
|
|
181
|
+
r"go\s+with|final\s+choice[: ]|best\s+fit[: ])"
|
|
182
|
+
r"([\s\S]{0,80})",
|
|
183
|
+
re.IGNORECASE,
|
|
184
|
+
)
|
|
185
|
+
rec_matches = list(_RECOMMEND_RE.finditer(needle))
|
|
186
|
+
if rec_matches:
|
|
187
|
+
for rm in reversed(rec_matches):
|
|
188
|
+
window = rm.group(1)
|
|
189
|
+
# Recompute absolute offsets so negative-context detection
|
|
190
|
+
# works against the full needle.
|
|
191
|
+
window_start = rm.start(1)
|
|
192
|
+
for entry in _load_index().get("distros", []):
|
|
193
|
+
for candidate in (
|
|
194
|
+
_normalize(entry.get("canonical", "")),
|
|
195
|
+
*(_normalize(alias) for alias in entry.get("aliases", [])),
|
|
196
|
+
):
|
|
197
|
+
if not candidate:
|
|
198
|
+
continue
|
|
199
|
+
pattern = rf"(?:^|[^a-z0-9]){re.escape(candidate)}(?:$|[^a-z0-9])"
|
|
200
|
+
m = re.search(pattern, window)
|
|
201
|
+
if m and not _is_negative_context(
|
|
202
|
+
needle, window_start + m.start(), window_start + m.end()
|
|
203
|
+
):
|
|
204
|
+
return entry
|
|
205
|
+
|
|
206
|
+
# No recommendation phrase matched a known distro — fall back to
|
|
207
|
+
# scanning the WHOLE text but return the LAST distro mentioned that
|
|
208
|
+
# isn't in a negative context.
|
|
209
|
+
last_match: tuple[int, dict[str, Any]] | None = None
|
|
210
|
+
for entry in _load_index().get("distros", []):
|
|
211
|
+
haystack = [
|
|
212
|
+
_normalize(entry.get("canonical", "")),
|
|
213
|
+
*(_normalize(alias) for alias in entry.get("aliases", [])),
|
|
214
|
+
]
|
|
215
|
+
for candidate in haystack:
|
|
216
|
+
if not candidate:
|
|
217
|
+
continue
|
|
218
|
+
pattern = rf"(?:^|[^a-z0-9]){re.escape(candidate)}(?:$|[^a-z0-9])"
|
|
219
|
+
for mobj in re.finditer(pattern, needle):
|
|
220
|
+
if _is_negative_context(needle, mobj.start(), mobj.end()):
|
|
221
|
+
continue
|
|
222
|
+
if last_match is None or mobj.start() > last_match[0]:
|
|
223
|
+
last_match = (mobj.start(), entry)
|
|
224
|
+
if last_match is not None:
|
|
225
|
+
return last_match[1]
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _read_primer_file(filename: str) -> str | None:
|
|
230
|
+
"""Read a primer markdown file from the package data directory."""
|
|
231
|
+
if not filename:
|
|
232
|
+
return None
|
|
233
|
+
try:
|
|
234
|
+
with (
|
|
235
|
+
resources.files(_PRIMER_PACKAGE)
|
|
236
|
+
.joinpath(filename)
|
|
237
|
+
.open("r", encoding="utf-8") as handle
|
|
238
|
+
):
|
|
239
|
+
return handle.read()
|
|
240
|
+
except (OSError, ModuleNotFoundError):
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def load_family_text(family: str | None) -> str | None:
|
|
245
|
+
"""Read a family primer by key (e.g. ``debian`` -> families/debian-family.md)."""
|
|
246
|
+
if not family:
|
|
247
|
+
return None
|
|
248
|
+
families = _load_index().get("families") or {}
|
|
249
|
+
rel = families.get(family)
|
|
250
|
+
if not rel:
|
|
251
|
+
return None
|
|
252
|
+
return _read_primer_file(rel)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def load_primer_text(distro: str | None) -> str | None:
|
|
256
|
+
"""Return the full primer Markdown for ``distro``, or None if no match.
|
|
257
|
+
|
|
258
|
+
Stitches the family primer (shared install procedure) with the distro
|
|
259
|
+
card (distro-specific values + overrides) when the distro references a
|
|
260
|
+
family. Standalone distros (NixOS, Alpine, etc.) return their card
|
|
261
|
+
directly.
|
|
262
|
+
"""
|
|
263
|
+
entry = resolve_primer(distro)
|
|
264
|
+
if entry is None:
|
|
265
|
+
return None
|
|
266
|
+
card = _read_primer_file(entry.get("file", ""))
|
|
267
|
+
if card is None:
|
|
268
|
+
return None
|
|
269
|
+
family_body = load_family_text(entry.get("family"))
|
|
270
|
+
if family_body is None:
|
|
271
|
+
return card
|
|
272
|
+
# Family primer first, then distro card. Separator makes the boundary
|
|
273
|
+
# visually obvious for the LLM reading the prompt.
|
|
274
|
+
return f"{family_body}\n\n---\n\n{card}"
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def supported_distros_summary() -> str:
|
|
278
|
+
"""Short Markdown list of every supported distro with one-line notes.
|
|
279
|
+
|
|
280
|
+
Used in prompts where we want to tell the LLM "these are the distros
|
|
281
|
+
we have install primers for" without injecting the full primer text.
|
|
282
|
+
"""
|
|
283
|
+
lines: list[str] = ["## QuickHatch-supported distros", ""]
|
|
284
|
+
for entry in list_supported_distros():
|
|
285
|
+
canonical = entry.get("canonical", "?")
|
|
286
|
+
family = entry.get("family", "?")
|
|
287
|
+
release = entry.get("release_model", "?")
|
|
288
|
+
lines.append(f"- **{canonical}** — family: {family}, release: {release}")
|
|
289
|
+
return "\n".join(lines)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def primer_prompt_block(distro: str | None) -> str:
|
|
293
|
+
"""Wrap the selected primer in a clear header for injection into prompts.
|
|
294
|
+
|
|
295
|
+
If no distro match, returns a generic "consult distro docs" note so the
|
|
296
|
+
agent still has guidance (fall back to the LLM's general knowledge).
|
|
297
|
+
"""
|
|
298
|
+
primer = load_primer_text(distro)
|
|
299
|
+
if primer is None:
|
|
300
|
+
# Don't print the literal string "None" when no distro was
|
|
301
|
+
# selected — say so explicitly instead.
|
|
302
|
+
distro_label = distro if distro else "(not selected)"
|
|
303
|
+
fallback = (
|
|
304
|
+
f"The requested distro ``{distro_label}`` is not in QuickHatch's "
|
|
305
|
+
"curated primer library. "
|
|
306
|
+
"Proceed using your training-data knowledge of that distro's "
|
|
307
|
+
"standard install procedure (live ISO → partition → install → "
|
|
308
|
+
"configure → reboot → reconnect). "
|
|
309
|
+
"If you are uncertain about a flag or command, ask the user "
|
|
310
|
+
"via the bridge chat before executing.\n\n"
|
|
311
|
+
)
|
|
312
|
+
return (
|
|
313
|
+
"== DISTRO INSTALL PRIMER (not found) ==\n\n"
|
|
314
|
+
+ fallback
|
|
315
|
+
+ supported_distros_summary()
|
|
316
|
+
+ "\n"
|
|
317
|
+
)
|
|
318
|
+
entry = resolve_primer(distro)
|
|
319
|
+
assert entry is not None # load_primer_text returned non-None
|
|
320
|
+
header = (
|
|
321
|
+
f"== DISTRO INSTALL PRIMER: {entry['canonical']} ==\n\n"
|
|
322
|
+
"The following Markdown is QuickHatch's curated install guide for this "
|
|
323
|
+
"distro. It is authoritative — follow the command ordering and flag "
|
|
324
|
+
"choices here rather than improvising. Placeholders in <ANGLE_BRACKETS> "
|
|
325
|
+
"must be substituted with values from the user profile (username, "
|
|
326
|
+
"hostname, target disk, timezone, public SSH key, passwords).\n\n"
|
|
327
|
+
)
|
|
328
|
+
return header + primer + "\n"
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
__all__ = [
|
|
332
|
+
"list_supported_distros",
|
|
333
|
+
"load_primer_text",
|
|
334
|
+
"primer_prompt_block",
|
|
335
|
+
"resolve_primer",
|
|
336
|
+
"supported_distros_summary",
|
|
337
|
+
]
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Generate the exported AI reference brief for QuickHatch bundles.
|
|
2
|
+
|
|
3
|
+
This file is intentionally NOT the live analysis prompt used by the web
|
|
4
|
+
wizard. The real interactive analysis/setup flow lives in
|
|
5
|
+
``quickhatch.gui.server`` and now uses sectioned, structured contracts.
|
|
6
|
+
|
|
7
|
+
The export bundle still benefits from a human-readable AI brief, but it
|
|
8
|
+
should describe the current product behavior rather than preserve an older
|
|
9
|
+
"one giant narrative prompt" model.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from dataclasses import asdict
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from quickhatch.config import UserProfile
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _redacted_profile_dict(profile: UserProfile) -> dict[str, Any]:
|
|
23
|
+
data = asdict(profile)
|
|
24
|
+
if data.get("ai_config", {}).get("api_key"):
|
|
25
|
+
data["ai_config"]["api_key"] = "***REDACTED***"
|
|
26
|
+
return data
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _compact_profile_json(profile: UserProfile) -> str:
|
|
30
|
+
"""Smaller, export-friendly profile snapshot.
|
|
31
|
+
|
|
32
|
+
Keep the fields that matter for an external agent or manual review,
|
|
33
|
+
but don't dump every bit of scan data into a second drift-prone mega-prompt.
|
|
34
|
+
"""
|
|
35
|
+
data = _redacted_profile_dict(profile)
|
|
36
|
+
prefs = data.get("preferences", {})
|
|
37
|
+
hardware = data.get("hardware", {})
|
|
38
|
+
apps = data.get("apps", [])
|
|
39
|
+
files_summary = data.get("files_summary", {})
|
|
40
|
+
|
|
41
|
+
trimmed = {
|
|
42
|
+
"preferences": prefs,
|
|
43
|
+
"ai_config": data.get("ai_config", {}),
|
|
44
|
+
"hardware": {
|
|
45
|
+
"cpu": hardware.get("cpu", ""),
|
|
46
|
+
"cpu_cores": hardware.get("cpu_cores", 0),
|
|
47
|
+
"ram_gb": hardware.get("ram_gb", 0),
|
|
48
|
+
"gpus": hardware.get("gpus", []),
|
|
49
|
+
"wifi_adapters": hardware.get("wifi_adapters", []),
|
|
50
|
+
"bluetooth": hardware.get("bluetooth", False),
|
|
51
|
+
"disks": hardware.get("disks", []),
|
|
52
|
+
"efi": hardware.get("efi", False),
|
|
53
|
+
},
|
|
54
|
+
"apps": apps[:80],
|
|
55
|
+
"apps_to_install": data.get("apps_to_install", []),
|
|
56
|
+
"windows_settings": data.get("windows_settings", {}),
|
|
57
|
+
"files_summary": {
|
|
58
|
+
"total_files": files_summary.get("total_files", 0),
|
|
59
|
+
"total_size_mb": files_summary.get("total_size_mb", 0),
|
|
60
|
+
"top_types": files_summary.get("top_types", {}),
|
|
61
|
+
"browser_profiles": files_summary.get("browser_profiles", {}),
|
|
62
|
+
},
|
|
63
|
+
"data_transfer_method": data.get("data_transfer_method", ""),
|
|
64
|
+
"data_transfer_paths": data.get("data_transfer_paths", []),
|
|
65
|
+
"data_description": data.get("data_description", ""),
|
|
66
|
+
"linux_ip": data.get("linux_ip", ""),
|
|
67
|
+
"linux_user": data.get("linux_user", ""),
|
|
68
|
+
"target_hardware_override": data.get("target_hardware_override", ""),
|
|
69
|
+
"additional_info": data.get("additional_info", ""),
|
|
70
|
+
}
|
|
71
|
+
return json.dumps(trimmed, indent=2, default=str)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _mode_section(profile: UserProfile) -> str:
|
|
75
|
+
prefs = profile.preferences
|
|
76
|
+
if prefs.migration_type == "customize_linux":
|
|
77
|
+
return (
|
|
78
|
+
"## Mode\n\n"
|
|
79
|
+
"Customization mode. You are acting on an already-installed Linux system.\n"
|
|
80
|
+
"Operate locally and verify changes directly on that machine.\n"
|
|
81
|
+
)
|
|
82
|
+
return (
|
|
83
|
+
"## Mode\n\n"
|
|
84
|
+
"Migration mode. QuickHatch runs from Windows, then connects to the Linux target over SSH.\n"
|
|
85
|
+
"Do not assume local execution on Linux until SSH access is ready.\n"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _operating_rules(profile: UserProfile) -> str:
|
|
90
|
+
prefs = profile.preferences
|
|
91
|
+
rules = [
|
|
92
|
+
"## Operating Rules",
|
|
93
|
+
"",
|
|
94
|
+
"1. Explicit user choices override stylistic preference. If the user named a distro, use that distro unless it is not a real Linux distribution.",
|
|
95
|
+
"2. Latest user feedback wins over earlier assumptions.",
|
|
96
|
+
"3. Describe and verify the full end-state, not just package installation.",
|
|
97
|
+
"4. If `gallery_picks` is non-empty, those recipes are authoritative design references. Personalize within them instead of inventing a different desktop direction.",
|
|
98
|
+
"5. When `data_transfer_method` is `keep_windows`, QuickHatch can copy selected `data_transfer_paths` from the host to the installed target after setup. For external-drive/cloud methods, only claim Linux-side preparation and instructions.",
|
|
99
|
+
"6. If `target_hardware_override` is non-empty, treat it as authoritative over scanned hardware for driver and install decisions.",
|
|
100
|
+
"7. Remote setup commands run on the Linux target. They cannot read Windows-local paths unless Windows exposes them over SSH/SMB, the disk is mounted, or the user moved the files first.",
|
|
101
|
+
]
|
|
102
|
+
if prefs.migration_type == "customize_linux":
|
|
103
|
+
rules.append("8. In customization mode, no distro recommendation is needed.")
|
|
104
|
+
else:
|
|
105
|
+
rules.append(
|
|
106
|
+
"8. In migration mode, plan around SSH access from Windows to the Linux target."
|
|
107
|
+
)
|
|
108
|
+
return "\n".join(rules) + "\n"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _workflow_section(profile: UserProfile) -> str:
|
|
112
|
+
prefs = profile.preferences
|
|
113
|
+
if prefs.migration_type == "customize_linux":
|
|
114
|
+
return (
|
|
115
|
+
"## Expected Workflow\n\n"
|
|
116
|
+
"1. Inspect the current Linux setup directly.\n"
|
|
117
|
+
"2. Align the desktop with the user's look/preferences and any pinned gallery recipes.\n"
|
|
118
|
+
"3. Install requested software and configure it.\n"
|
|
119
|
+
"4. Handle drivers, locale, keyboard, fonts, and other system tuning.\n"
|
|
120
|
+
"5. Verify the resulting desktop, apps, and hardware behavior.\n"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
lines = [
|
|
124
|
+
"## Expected Workflow",
|
|
125
|
+
"",
|
|
126
|
+
"1. Review the profile and clarify any missing requirements.",
|
|
127
|
+
"2. Recommend or confirm the distro choice.",
|
|
128
|
+
"3. Map requested/scanned Windows apps to Linux-native packages, alternatives, or Wine/Proton paths.",
|
|
129
|
+
"4. Plan drivers, desktop setup, data migration, and post-install verification.",
|
|
130
|
+
"5. Once SSH is ready, connect to the Linux target and apply the plan.",
|
|
131
|
+
"6. Verify apps, desktop state, and hardware before declaring completion.",
|
|
132
|
+
]
|
|
133
|
+
if profile.preferences.gallery_picks:
|
|
134
|
+
lines.append(
|
|
135
|
+
"7. Apply the desktop direction around the pinned gallery recipe(s), not around a fresh invention."
|
|
136
|
+
)
|
|
137
|
+
return "\n".join(lines) + "\n"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _bridge_section(profile: UserProfile) -> str:
|
|
141
|
+
ai = profile.ai_config
|
|
142
|
+
endpoint = ai.endpoint or ""
|
|
143
|
+
if "localhost" not in endpoint:
|
|
144
|
+
return ""
|
|
145
|
+
return (
|
|
146
|
+
"## QuickHatch Bridge API\n\n"
|
|
147
|
+
f"Base URL: `{endpoint}`\n\n"
|
|
148
|
+
"This local API is how the interactive agent reports progress and asks follow-up questions.\n"
|
|
149
|
+
"The current live analysis flow is sectioned (`distro`, `apps`, `drivers`, `desktop`, `plan`, `summary`, and sometimes `install_script`) rather than one giant response.\n\n"
|
|
150
|
+
"Core endpoints:\n"
|
|
151
|
+
"- `GET /api/profile` — current profile snapshot\n"
|
|
152
|
+
"- `GET /api/suggestions` — accumulated section outputs\n"
|
|
153
|
+
"- `POST /api/suggest` — add a section output or progress card\n"
|
|
154
|
+
"- `POST /api/ask` — ask the user a blocking question\n"
|
|
155
|
+
"- `POST /api/message` — post a status message\n"
|
|
156
|
+
"- `POST /api/done` — signal completion\n"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _gallery_section(profile: UserProfile) -> str:
|
|
161
|
+
picks = profile.preferences.gallery_picks
|
|
162
|
+
if not picks:
|
|
163
|
+
return (
|
|
164
|
+
"## Desktop Design Inputs\n\n"
|
|
165
|
+
"No gallery recipe is pinned. Use the user's desktop preferences as guidance, but keep the design grounded and reproducible.\n"
|
|
166
|
+
)
|
|
167
|
+
joined = ", ".join(f"`{item}`" for item in picks)
|
|
168
|
+
return (
|
|
169
|
+
"## Desktop Design Inputs\n\n"
|
|
170
|
+
f"Pinned gallery recipe IDs: {joined}\n\n"
|
|
171
|
+
"Treat these as the authoritative desktop references for theme / icon / font / wallpaper direction.\n"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _ssh_section(profile: UserProfile) -> str:
|
|
176
|
+
if not profile.ssh_public_key_path:
|
|
177
|
+
return (
|
|
178
|
+
"## SSH Access\n\n"
|
|
179
|
+
"No SSH key path is stored yet. If you need remote execution, establish SSH access first.\n"
|
|
180
|
+
)
|
|
181
|
+
priv_key = str(Path(profile.ssh_public_key_path).with_suffix(""))
|
|
182
|
+
return (
|
|
183
|
+
"## SSH Access\n\n"
|
|
184
|
+
f"- Private key on the Windows host: `{priv_key}`\n"
|
|
185
|
+
f"- Public key path: `{profile.ssh_public_key_path}`\n"
|
|
186
|
+
"- The target Linux machine must have SSH enabled and the public key installed before remote setup can proceed.\n"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def generate_instructions(profile: UserProfile) -> str:
|
|
191
|
+
"""Generate the exported AI reference brief.
|
|
192
|
+
|
|
193
|
+
This document is bundled for human review or external-agent use. It is
|
|
194
|
+
intentionally aligned with the current QuickHatch flow instead of acting as
|
|
195
|
+
a second, divergent runtime prompt.
|
|
196
|
+
"""
|
|
197
|
+
prefs = profile.preferences
|
|
198
|
+
title = (
|
|
199
|
+
"# QuickHatch Reference Brief — Linux Customization\n\n"
|
|
200
|
+
if prefs.migration_type == "customize_linux"
|
|
201
|
+
else "# QuickHatch Reference Brief — Windows to Linux Migration\n\n"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
intro = (
|
|
205
|
+
"This file is a reference brief exported by QuickHatch. It summarizes the user's profile,\n"
|
|
206
|
+
"the current product workflow, and the rules an external agent or human operator should follow.\n"
|
|
207
|
+
"It is not the authoritative live analysis prompt used by the web wizard; the live wizard now uses\n"
|
|
208
|
+
"sectioned prompts with structured outputs.\n\n"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
sections = [
|
|
212
|
+
title + intro,
|
|
213
|
+
_mode_section(profile),
|
|
214
|
+
_operating_rules(profile),
|
|
215
|
+
_workflow_section(profile),
|
|
216
|
+
_gallery_section(profile),
|
|
217
|
+
_ssh_section(profile),
|
|
218
|
+
_bridge_section(profile),
|
|
219
|
+
"## Profile Snapshot\n\n```json\n" + _compact_profile_json(profile) + "\n```\n",
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
return "\n".join(part for part in sections if part).strip() + "\n"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def save_instructions(profile: UserProfile, output_dir: Path) -> Path:
|
|
226
|
+
"""Generate and save the exported reference brief."""
|
|
227
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
228
|
+
instructions = generate_instructions(profile)
|
|
229
|
+
inst_path = output_dir / "MIGRATION_INSTRUCTIONS.md"
|
|
230
|
+
inst_path.write_text(instructions, encoding="utf-8")
|
|
231
|
+
return inst_path
|