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.
Files changed (319) hide show
  1. quickhatch/__init__.py +11 -0
  2. quickhatch/__main__.py +6 -0
  3. quickhatch/ai/__init__.py +1 -0
  4. quickhatch/ai/install_primer.py +337 -0
  5. quickhatch/ai/instruction_gen.py +231 -0
  6. quickhatch/ai/model_list.py +140 -0
  7. quickhatch/ai/recipe_apply.py +178 -0
  8. quickhatch/analysis/__init__.py +1 -0
  9. quickhatch/analysis/app_catalog.py +628 -0
  10. quickhatch/analysis/app_mapper.py +193 -0
  11. quickhatch/analysis/distro_engine.py +84 -0
  12. quickhatch/analysis/driver_check.py +83 -0
  13. quickhatch/bridge/__init__.py +1 -0
  14. quickhatch/bridge/display.py +136 -0
  15. quickhatch/bridge/server.py +886 -0
  16. quickhatch/cli.py +1188 -0
  17. quickhatch/config.py +463 -0
  18. quickhatch/console.py +59 -0
  19. quickhatch/data/app_alternatives.json +222 -0
  20. quickhatch/data/app_catalog.generated.json +90774 -0
  21. quickhatch/data/app_catalog.overrides.json +54159 -0
  22. quickhatch/data/distros.json +110 -0
  23. quickhatch/data/driver_db.json +18 -0
  24. quickhatch/data/gallery/github-1amsimp1e-dots.webp +0 -0
  25. quickhatch/data/gallery/github-2kabhishek-dots2k.webp +0 -0
  26. quickhatch/data/gallery/github-2ssk-dot-files.webp +0 -0
  27. quickhatch/data/gallery/github-3d3f-ii-sddm-theme.webp +0 -0
  28. quickhatch/data/gallery/github-acaibowlz-niri-setup.webp +0 -0
  29. quickhatch/data/gallery/github-ad1822-hyprdots.webp +0 -0
  30. quickhatch/data/gallery/github-adriankarlen-bar-wezterm.webp +0 -0
  31. quickhatch/data/gallery/github-ahmad9059-hyprflux.webp +0 -0
  32. quickhatch/data/gallery/github-allaman-nvim.webp +0 -0
  33. quickhatch/data/gallery/github-anotherhadi-nixy.webp +0 -0
  34. quickhatch/data/gallery/github-artart222-codeart.webp +0 -0
  35. quickhatch/data/gallery/github-ascaniolamp-hyprlain.webp +0 -0
  36. quickhatch/data/gallery/github-ashish0kumar-windots.webp +0 -0
  37. quickhatch/data/gallery/github-athxrvx-idempotentsystems.webp +0 -0
  38. quickhatch/data/gallery/github-avivace-dotfiles.webp +0 -0
  39. quickhatch/data/gallery/github-avtzis-awesome-linux-ricing.webp +0 -0
  40. quickhatch/data/gallery/github-ayamir-dotfiles.webp +0 -0
  41. quickhatch/data/gallery/github-ayamir-nvimdots.webp +0 -0
  42. quickhatch/data/gallery/github-aymanlyesri-archeclipse.webp +0 -0
  43. quickhatch/data/gallery/github-benmezger-dotfiles.webp +0 -0
  44. quickhatch/data/gallery/github-bertof-nix-rice.webp +0 -0
  45. quickhatch/data/gallery/github-bluemancz-hyprmod.webp +0 -0
  46. quickhatch/data/gallery/github-caelestia-dots-shell.webp +0 -0
  47. quickhatch/data/gallery/github-caioax-lyne-dots.webp +0 -0
  48. quickhatch/data/gallery/github-cebem1nt-dotfiles.webp +0 -0
  49. quickhatch/data/gallery/github-christianlempa-dotfiles.webp +0 -0
  50. quickhatch/data/gallery/github-command-z-z-arch-dotfiles.webp +0 -0
  51. quickhatch/data/gallery/github-cosmicnvim-cosmicnvim.webp +0 -0
  52. quickhatch/data/gallery/github-crissnb-dynamic-island-sketchybar.webp +0 -0
  53. quickhatch/data/gallery/github-cxorz-dotfiles-hyprland.webp +0 -0
  54. quickhatch/data/gallery/github-cybersnake223-hypr.webp +0 -0
  55. quickhatch/data/gallery/github-cybrcore-cybrdots.webp +0 -0
  56. quickhatch/data/gallery/github-danihek-hellwal.webp +0 -0
  57. quickhatch/data/gallery/github-danihek-themecord.webp +0 -0
  58. quickhatch/data/gallery/github-darkkal44-qylock.webp +0 -0
  59. quickhatch/data/gallery/github-datsfilipe-dotfiles.webp +0 -0
  60. quickhatch/data/gallery/github-deathbeam-dotfiles.webp +0 -0
  61. quickhatch/data/gallery/github-debuggyo-exo.webp +0 -0
  62. quickhatch/data/gallery/github-denisse-dev-dotfiles.webp +0 -0
  63. quickhatch/data/gallery/github-deridray-dotfiles.webp +0 -0
  64. quickhatch/data/gallery/github-diinki-diinki-aero.webp +0 -0
  65. quickhatch/data/gallery/github-diinki-diinki-retrofuture.webp +0 -0
  66. quickhatch/data/gallery/github-diinki-linux-retroism.webp +0 -0
  67. quickhatch/data/gallery/github-dikiaap-dotfiles.webp +0 -0
  68. quickhatch/data/gallery/github-dileep-kishore-nixos-config.webp +0 -0
  69. quickhatch/data/gallery/github-dmadisetti-dots.webp +0 -0
  70. quickhatch/data/gallery/github-driesvints-dotfiles.webp +0 -0
  71. quickhatch/data/gallery/github-dusklinux-dusky.webp +0 -0
  72. quickhatch/data/gallery/github-dustinlyons-nixos-config.webp +0 -0
  73. quickhatch/data/gallery/github-dybdeskarphet-dotfiles.webp +0 -0
  74. quickhatch/data/gallery/github-ecosse3-nvim.webp +0 -0
  75. quickhatch/data/gallery/github-edu-flores-linux-dotfiles.webp +0 -0
  76. quickhatch/data/gallery/github-elifouts-dotfiles.webp +0 -0
  77. quickhatch/data/gallery/github-elkowar-yolk.webp +0 -0
  78. quickhatch/data/gallery/github-elpritchos-omadora.webp +0 -0
  79. quickhatch/data/gallery/github-elysiaos-elysiaos.webp +0 -0
  80. quickhatch/data/gallery/github-end-4-dots-hyprland.webp +0 -0
  81. quickhatch/data/gallery/github-endeavouros-team-endeavouros-i3wm-setup.webp +0 -0
  82. quickhatch/data/gallery/github-exploitoverload-pwnixos.webp +0 -0
  83. quickhatch/data/gallery/github-felipecrs-dotfiles.webp +0 -0
  84. quickhatch/data/gallery/github-firestreaker2-dotfiles.webp +0 -0
  85. quickhatch/data/gallery/github-flyingcakes85-lovebites.webp +0 -0
  86. quickhatch/data/gallery/github-folke-dot.webp +0 -0
  87. quickhatch/data/gallery/github-frogprog09-my-linux.webp +0 -0
  88. quickhatch/data/gallery/github-frost-phoenix-nixos-config.webp +0 -0
  89. quickhatch/data/gallery/github-gabrieltenma-dotfiles-gnm.webp +0 -0
  90. quickhatch/data/gallery/github-gf3-dotfiles.webp +0 -0
  91. quickhatch/data/gallery/github-gh0stzk-dotfiles.webp +0 -0
  92. quickhatch/data/gallery/github-gnuunixchad-dotfiles.webp +0 -0
  93. quickhatch/data/gallery/github-gpakosz-tmux.webp +0 -0
  94. quickhatch/data/gallery/github-gvolpe-nix-config.webp +0 -0
  95. quickhatch/data/gallery/github-haaarshsingh-dots.webp +0 -0
  96. quickhatch/data/gallery/github-hancore-linux-waybar-themes.webp +0 -0
  97. quickhatch/data/gallery/github-heapbytes-dotfiles.webp +0 -0
  98. quickhatch/data/gallery/github-heisenburgh-pixarch.webp +0 -0
  99. quickhatch/data/gallery/github-hlissner-dotfiles.webp +0 -0
  100. quickhatch/data/gallery/github-hyprland-community-hyprls.webp +0 -0
  101. quickhatch/data/gallery/github-ikz87-dots-2-0.webp +0 -0
  102. quickhatch/data/gallery/github-iogamaster-dotfiles.webp +0 -0
  103. quickhatch/data/gallery/github-issmirnov-dotfiles.webp +0 -0
  104. quickhatch/data/gallery/github-jakoolit-arch-hyprland.webp +0 -0
  105. quickhatch/data/gallery/github-jakoolit-fedora-hyprland.webp +0 -0
  106. quickhatch/data/gallery/github-jakoolit-hyprland-dots.webp +0 -0
  107. quickhatch/data/gallery/github-jakoolit-opensuse-hyprland.webp +0 -0
  108. quickhatch/data/gallery/github-javalsai-lidm.webp +0 -0
  109. quickhatch/data/gallery/github-jessfraz-dotfiles.webp +0 -0
  110. quickhatch/data/gallery/github-jguer-dotfiles.webp +0 -0
  111. quickhatch/data/gallery/github-johackim-dotfiles.webp +0 -0
  112. quickhatch/data/gallery/github-justus0405-i3wm-dotfiles.webp +0 -0
  113. quickhatch/data/gallery/github-keyitdev-dotfiles.webp +0 -0
  114. quickhatch/data/gallery/github-keyitdev-sddm-astronaut-theme.webp +0 -0
  115. quickhatch/data/gallery/github-klaxalk-linux-setup.webp +0 -0
  116. quickhatch/data/gallery/github-koeqaife-hyprland-material-you.webp +0 -0
  117. quickhatch/data/gallery/github-kutsan-dotfiles.webp +0 -0
  118. quickhatch/data/gallery/github-lolei-razer-cli.webp +0 -0
  119. quickhatch/data/gallery/github-m4xshen-dotfiles.webp +0 -0
  120. quickhatch/data/gallery/github-madic-creates-sway-de.webp +0 -0
  121. quickhatch/data/gallery/github-marcusmix-dotfiles.webp +0 -0
  122. quickhatch/data/gallery/github-martins3-my-linux-config.webp +0 -0
  123. quickhatch/data/gallery/github-mastermindzh-dotfiles.webp +0 -0
  124. quickhatch/data/gallery/github-mathix420-free-the-web-apps.webp +0 -0
  125. quickhatch/data/gallery/github-mattmc3-antidote.webp +0 -0
  126. quickhatch/data/gallery/github-maytermux-mytermux.webp +0 -0
  127. quickhatch/data/gallery/github-meain-dotfiles.webp +0 -0
  128. quickhatch/data/gallery/github-meowrch-meowrch.webp +0 -0
  129. quickhatch/data/gallery/github-misterio77-nix-config.webp +0 -0
  130. quickhatch/data/gallery/github-mrusme-dotfiles.webp +0 -0
  131. quickhatch/data/gallery/github-mrusme-gomphotherium.webp +0 -0
  132. quickhatch/data/gallery/github-mubin-thinks-minimal-wm-config.webp +0 -0
  133. quickhatch/data/gallery/github-mutewinter-dot-vim.webp +0 -0
  134. quickhatch/data/gallery/github-myamusashi-vast-shell.webp +0 -0
  135. quickhatch/data/gallery/github-mylinuxforwork-dotfiles.webp +0 -0
  136. quickhatch/data/gallery/github-n6v26r-dotfiles.webp +0 -0
  137. quickhatch/data/gallery/github-neurarian-matshell.webp +0 -0
  138. quickhatch/data/gallery/github-nickjj-dotfiles.webp +0 -0
  139. quickhatch/data/gallery/github-nicknisi-dotfiles.webp +0 -0
  140. quickhatch/data/gallery/github-nicksp-dotfiles.webp +0 -0
  141. quickhatch/data/gallery/github-nix-darwin-nix-darwin.webp +0 -0
  142. quickhatch/data/gallery/github-noctalia-dev-noctalia-shell.webp +0 -0
  143. quickhatch/data/gallery/github-nocturnussx-hyprland-dotfiles.webp +0 -0
  144. quickhatch/data/gallery/github-notmugil-dotfiles.webp +0 -0
  145. quickhatch/data/gallery/github-notneelpatel-wallpaperthemeconverter.webp +0 -0
  146. quickhatch/data/gallery/github-nwg-piotr-nwg-shell-config.webp +0 -0
  147. quickhatch/data/gallery/github-opensuse-opensuseway.webp +0 -0
  148. quickhatch/data/gallery/github-orhun-dotfiles.webp +0 -0
  149. quickhatch/data/gallery/github-oughie-clock-rs.webp +0 -0
  150. quickhatch/data/gallery/github-ozwaldorf-lutgen-rs.webp +0 -0
  151. quickhatch/data/gallery/github-pablonoya-awesomewm-configuration.webp +0 -0
  152. quickhatch/data/gallery/github-pazl27-dotfiles.webp +0 -0
  153. quickhatch/data/gallery/github-pinpox-base16-universal-manager.webp +0 -0
  154. quickhatch/data/gallery/github-potamides-dotfiles.webp +0 -0
  155. quickhatch/data/gallery/github-prasanthrangan-hyprdots.webp +0 -0
  156. quickhatch/data/gallery/github-qxb3-conf.webp +0 -0
  157. quickhatch/data/gallery/github-qxb3-fum.webp +0 -0
  158. quickhatch/data/gallery/github-r0naldoom-dotfiles.webp +0 -0
  159. quickhatch/data/gallery/github-ray-x-nvim.webp +0 -0
  160. quickhatch/data/gallery/github-rayhaanfay-xfce-creation-of-adam.webp +0 -0
  161. quickhatch/data/gallery/github-redyf-nixdots.webp +0 -0
  162. quickhatch/data/gallery/github-retrozinndev-colorshell.webp +0 -0
  163. quickhatch/data/gallery/github-riccardopp-dotfiles.webp +0 -0
  164. quickhatch/data/gallery/github-ryan4yin-nix-config.webp +0 -0
  165. quickhatch/data/gallery/github-ryan4yin-nix-darwin-kickstarter.webp +0 -0
  166. quickhatch/data/gallery/github-s4nkalp-hyprland.webp +0 -0
  167. quickhatch/data/gallery/github-s4nkalp-modus.webp +0 -0
  168. quickhatch/data/gallery/github-sangrokjung-claude-forge.webp +0 -0
  169. quickhatch/data/gallery/github-saschagrunert-dotfiles.webp +0 -0
  170. quickhatch/data/gallery/github-sbalghari-sbdots.webp +0 -0
  171. quickhatch/data/gallery/github-schmeekygeek-dotfiles.webp +0 -0
  172. quickhatch/data/gallery/github-sejjy-mechabar.webp +0 -0
  173. quickhatch/data/gallery/github-sekiryl-hyprdots.webp +0 -0
  174. quickhatch/data/gallery/github-sh1zicus-dots-hyprland.webp +0 -0
  175. quickhatch/data/gallery/github-shell-ninja-hyprconf-v2.webp +0 -0
  176. quickhatch/data/gallery/github-shell-ninja-hyprconf.webp +0 -0
  177. quickhatch/data/gallery/github-shikikan-neko08-nyartix-rice.webp +0 -0
  178. quickhatch/data/gallery/github-siduck-chadwm.webp +0 -0
  179. quickhatch/data/gallery/github-simonvic-dotfiles.webp +0 -0
  180. quickhatch/data/gallery/github-sirethanator-hyprland-dots.webp +0 -0
  181. quickhatch/data/gallery/github-sly-harvey-nixos.webp +0 -0
  182. quickhatch/data/gallery/github-snowarch-inir.webp +0 -0
  183. quickhatch/data/gallery/github-swarsel-dotfiles.webp +0 -0
  184. quickhatch/data/gallery/github-swaykh-dotfiles.webp +0 -0
  185. quickhatch/data/gallery/github-szorfein-dotfiles.webp +0 -0
  186. quickhatch/data/gallery/github-szorfein-dots.webp +0 -0
  187. quickhatch/data/gallery/github-techdufus-dotfiles.webp +0 -0
  188. quickhatch/data/gallery/github-tsm-061-ctos.webp +0 -0
  189. quickhatch/data/gallery/github-vallen217-dotfiles.webp +0 -0
  190. quickhatch/data/gallery/github-victorsosamx-vshyprland-manager.webp +0 -0
  191. quickhatch/data/gallery/github-victorsosamx-vswaybar-studio.webp +0 -0
  192. quickhatch/data/gallery/github-viegphunt-dotfiles.webp +0 -0
  193. quickhatch/data/gallery/github-vyrx-dev-symphony.webp +0 -0
  194. quickhatch/data/gallery/github-xero-dotfiles.webp +0 -0
  195. quickhatch/data/gallery/github-xnm1-linux-nixos-hyprland-config-dotfiles.webp +0 -0
  196. quickhatch/data/gallery/github-youwes09-ateon.webp +0 -0
  197. quickhatch/data/gallery/github-yunfachi-nix-config.webp +0 -0
  198. quickhatch/data/gallery/github-yurihikari-garuda-hyprdots.webp +0 -0
  199. quickhatch/data/gallery/github-zemmsoares-awesome-rices.webp +0 -0
  200. quickhatch/data/gallery/github-zhaleff-blacknode.webp +0 -0
  201. quickhatch/data/gallery/github-ziap-dotfiles.webp +0 -0
  202. quickhatch/data/gallery/github-zproger-bspwm-dotfiles.webp +0 -0
  203. quickhatch/data/gallery/gnome-12dampb.webp +0 -0
  204. quickhatch/data/gallery/hyprland-1b4izs2.webp +0 -0
  205. quickhatch/data/gallery/hyprland-1fp86p7.webp +0 -0
  206. quickhatch/data/gallery/hyprland-1inxosk.webp +0 -0
  207. quickhatch/data/gallery/hyprland-1jtjljz.webp +0 -0
  208. quickhatch/data/gallery/hyprland-1jv48oq.webp +0 -0
  209. quickhatch/data/gallery/i3wm-m7w790.webp +0 -0
  210. quickhatch/data/gallery/unixporn-1k7c8i1.webp +0 -0
  211. quickhatch/data/gallery/unixporn-1kcebee.webp +0 -0
  212. quickhatch/data/gallery/unixporn-5crgng.webp +0 -0
  213. quickhatch/data/gallery/unixporn-nhomed.webp +0 -0
  214. quickhatch/data/gallery/unixporn-r5ot7x.webp +0 -0
  215. quickhatch/data/gallery.json +8850 -0
  216. quickhatch/data/gallery_descriptions.json +426 -0
  217. quickhatch/data/install_primers/_index.json +253 -0
  218. quickhatch/data/install_primers/alpine.md +118 -0
  219. quickhatch/data/install_primers/arch.md +30 -0
  220. quickhatch/data/install_primers/artix.md +69 -0
  221. quickhatch/data/install_primers/bazzite.md +61 -0
  222. quickhatch/data/install_primers/cachyos.md +53 -0
  223. quickhatch/data/install_primers/common-pre-reboot-rules.md +200 -0
  224. quickhatch/data/install_primers/debian.md +73 -0
  225. quickhatch/data/install_primers/elementary.md +67 -0
  226. quickhatch/data/install_primers/endeavouros.md +39 -0
  227. quickhatch/data/install_primers/families/arch-family.md +294 -0
  228. quickhatch/data/install_primers/families/debian-family.md +147 -0
  229. quickhatch/data/install_primers/families/fedora-family.md +240 -0
  230. quickhatch/data/install_primers/fedora-silverblue.md +78 -0
  231. quickhatch/data/install_primers/fedora.md +51 -0
  232. quickhatch/data/install_primers/garuda.md +90 -0
  233. quickhatch/data/install_primers/gentoo.md +160 -0
  234. quickhatch/data/install_primers/kali.md +97 -0
  235. quickhatch/data/install_primers/kde-neon.md +61 -0
  236. quickhatch/data/install_primers/linux-mint.md +49 -0
  237. quickhatch/data/install_primers/manjaro.md +80 -0
  238. quickhatch/data/install_primers/mx-linux.md +65 -0
  239. quickhatch/data/install_primers/nixos.md +135 -0
  240. quickhatch/data/install_primers/nobara.md +55 -0
  241. quickhatch/data/install_primers/opensuse-leap.md +44 -0
  242. quickhatch/data/install_primers/opensuse-tumbleweed.md +143 -0
  243. quickhatch/data/install_primers/parrot.md +70 -0
  244. quickhatch/data/install_primers/pop-os.md +80 -0
  245. quickhatch/data/install_primers/rhel-clones.md +78 -0
  246. quickhatch/data/install_primers/ubuntu.md +99 -0
  247. quickhatch/data/install_primers/void.md +121 -0
  248. quickhatch/data/install_primers/zorin.md +49 -0
  249. quickhatch/export/__init__.py +1 -0
  250. quickhatch/gallery/__init__.py +15 -0
  251. quickhatch/gallery/build.py +91 -0
  252. quickhatch/gallery/common.py +492 -0
  253. quickhatch/gallery/components.py +475 -0
  254. quickhatch/gallery/curate.py +398 -0
  255. quickhatch/gallery/dedupe.py +93 -0
  256. quickhatch/gallery/describe.py +204 -0
  257. quickhatch/gallery/extract.py +467 -0
  258. quickhatch/gallery/filter.py +492 -0
  259. quickhatch/gallery/library.py +35 -0
  260. quickhatch/gallery/llm_extract.py +495 -0
  261. quickhatch/gallery/scrape.py +278 -0
  262. quickhatch/gallery/verify.py +60 -0
  263. quickhatch/gui/__init__.py +1 -0
  264. quickhatch/gui/app.html +5212 -0
  265. quickhatch/gui/index.html +38 -0
  266. quickhatch/gui/server.py +8775 -0
  267. quickhatch/media/__init__.py +1 -0
  268. quickhatch/media/iso_download.py +94 -0
  269. quickhatch/media/usb_writer.py +184 -0
  270. quickhatch/scanners/__init__.py +1 -0
  271. quickhatch/scanners/apps.py +458 -0
  272. quickhatch/scanners/files.py +118 -0
  273. quickhatch/scanners/hardware.py +713 -0
  274. quickhatch/scanners/settings.py +286 -0
  275. quickhatch/secrets.py +86 -0
  276. quickhatch/setup/__init__.py +65 -0
  277. quickhatch/setup/attempts.py +199 -0
  278. quickhatch/setup/audit.py +1245 -0
  279. quickhatch/setup/capabilities.py +1617 -0
  280. quickhatch/setup/certify.py +527 -0
  281. quickhatch/setup/debugtools.py +139 -0
  282. quickhatch/setup/facts.py +646 -0
  283. quickhatch/setup/failure.py +137 -0
  284. quickhatch/setup/families/__init__.py +39 -0
  285. quickhatch/setup/families/arch.py +44 -0
  286. quickhatch/setup/families/base.py +362 -0
  287. quickhatch/setup/families/debian.py +80 -0
  288. quickhatch/setup/families/fedora.py +83 -0
  289. quickhatch/setup/families/ubuntu.py +54 -0
  290. quickhatch/setup/fuzz.py +478 -0
  291. quickhatch/setup/health.py +869 -0
  292. quickhatch/setup/operations.py +91 -0
  293. quickhatch/setup/orchestrator.py +11042 -0
  294. quickhatch/setup/planner.py +1470 -0
  295. quickhatch/setup/preflight.py +1479 -0
  296. quickhatch/setup/release_smoke.py +297 -0
  297. quickhatch/setup/replay.py +1126 -0
  298. quickhatch/setup/resolver.py +342 -0
  299. quickhatch/setup/runlog.py +281 -0
  300. quickhatch/setup/support_bundle.py +594 -0
  301. quickhatch/setup/types.py +281 -0
  302. quickhatch/wizard/__init__.py +1 -0
  303. quickhatch/wizard/additional.py +36 -0
  304. quickhatch/wizard/ai_setup.py +292 -0
  305. quickhatch/wizard/bridge.py +231 -0
  306. quickhatch/wizard/compatibility.py +99 -0
  307. quickhatch/wizard/distro.py +110 -0
  308. quickhatch/wizard/export.py +579 -0
  309. quickhatch/wizard/questionnaire.py +132 -0
  310. quickhatch/wizard/scan.py +75 -0
  311. quickhatch/wizard/ssh.py +172 -0
  312. quickhatch/wizard/usb.py +252 -0
  313. quickhatch/wizard/vm_verify.py +1473 -0
  314. quickhatch/wizard/welcome.py +43 -0
  315. quickhatch-0.1.0.dist-info/METADATA +295 -0
  316. quickhatch-0.1.0.dist-info/RECORD +319 -0
  317. quickhatch-0.1.0.dist-info/WHEEL +4 -0
  318. quickhatch-0.1.0.dist-info/entry_points.txt +2 -0
  319. 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,6 @@
1
+ """Entry point for python -m quickhatch."""
2
+
3
+ from quickhatch.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -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