Tool Deployment Template — raw code → published + supported

Tool Deployment Template — raw code → published + supported

A reusable, tool-agnostic playbook for taking ANY internal/proprietary desktop tool from a working source repo to a publicly downloadable, supported product. Derived from the Echo pilot (Plans 1–3).

Placeholders: <Tool> = display name (accents OK) · <tool> = ASCII identifier · <company-site> = static company site repo.

Worked example — Echo. <Tool> = Echo, <tool> = Echo, <company-site> = jct-www (jacksoncreektech.ca). Local Windows dictation tool: Ctrl+Space → faster-whisper (local) → auto-paste. Source repo private: C:\Projects\superwhisper-clone.


Stage 1 — Brand pass

  • Separate two name spaces. Pick a DISPLAY name (user-visible, accents/spaces allowed) and a distinct FILESYSTEM/registry IDENTIFIER (ASCII, no spaces, safe for paths/keys/exe names).

    Echo: display Echo; identifier Echo. Old brand was display Chuchoteur / id Chuchoteur, plus the working title SuperWhisper Clone.

  • Rename display strings (window titles, tray text, about box, site copy) → <Tool>.
  • Rename filesystem/registry identifiers (exe, folders, AppData dir, autostart registry key, launchers) → <tool>.
  • Add a one-time data migration so existing users keep their data. Move %APPDATA%\<OldId>%APPDATA%\<tool> on first run; preserve everything (models, history, config). Cover it with a TDD test.

    Echo: migrate.py moves %APPDATA%\Chuchoteur → %APPDATA%\Echo, preserving downloaded models + history. Test-first.

  • Rename launchers to <Tool>.bat / <Tool>.vbs.
  • Grep gate — confirm no old brand survives in user-visible code/strings, except deliberate internal sentinels (e.g. the migration source path).
    # expect ZERO hits outside migrate.py / changelog
    rg -i "chuchoteur|superwhisper" --glob '!migrate.py'
    

Stage 2 — Package

  • PyInstaller one-folder spec. Use one-folder (not one-file) for faster startup and easier native-extension bundling. Keep a checked-in <tool>.spec.

  • collect_all for native-extension deps. Whisper/ML stacks ship compiled binaries + data that PyInstaller misses by default. Add collect_all for each.

    from PyInstaller.utils.hooks import collect_all
    datas, binaries, hiddenimports = [], [], []
    for pkg in ("ctranslate2", "av", "onnxruntime"):
        d, b, h = collect_all(pkg)
        datas += d; binaries += b; hiddenimports += h
    
  • Gotcha — interpreter version must match. The Python that runs PyInstaller must be the same version you built/tested against; mismatched minor versions produce broken or missing-DLL builds.

    Echo built with Python 3.13 (app targets 3.13/3.14). Output: dist\Echo\Echo.exe (~27 MB exe, ~374 MB folder).

  • GATE — single-instance + visible hotkey failure (background-app tools). Any tool that grabs a global hotkey MUST: (a) guard against a second instance (named mutex / lock file) — two copies silently fight over the hotkey; (b) surface a registration failure to the user (tray notification), because a windowed .exe has no stdout, so a print() is invisible. Without both, a stale/duplicate instance makes a fresh build look dead.

    Echo: discovered 2026-06-01 when a source instance from 2 days prior held Ctrl+Space and the new Echo.exe failed RegisterHotKey silently. Tracked as JCT-17 (single-instance) + JCT-18 (surface failure).

  • GATE — user data/config in a writable location, never next to the code. Use Path(__file__).parent for the BUNDLED default only; the live config + user data must live under %APPDATA%\<Tool>\ (seeded from the bundled default on first run). In a packaged build __file__ is inside read-only Program Files, so writing config there fails (EPERM) and Settings silently don't persist.

    Echo: discovered 2026-06-01 — config.py wrote to Program Files, changing the model needed elevation. Fixed to %APPDATA%\Echo\config.yaml. Tracked as JCT-19.

  • GATE — hardware-aware model default + honest latency expectation (local-inference tools). Detect the accelerator (CUDA vs CPU) at first run and set the default model accordingly — but know the hard limit: good multilingual ASR is encoder-bound and there is NO model/param that is both fast on CPU and accurate at bilingual. On CPU, a medium-class model is the quality floor and runs at RTF ~0.6–0.7x (a 30s clip ≈ 20s); smaller models trade away quality, and large-v3-turbo is actually SLOWER than medium on CPU (bigger encoder). beam-size / word-timestamps tuning saves only ~10%. The real speedup is a CUDA GPU (RTF ~0.05–0.1x). So: default medium for multilingual, auto-bump to large only on CUDA, and where the tool needs to be fast, run it on an NVIDIA box — don't expect CPU tuning to save it. Pair with a streaming partial-text UI so the unavoidable CPU wait shows visible progress.

    Echo: AMD iGPU (no CUDA, unusable by ctranslate2) + medium = ~20s on long clips; small fast but failed bilingual; turbo slower than medium; params negligible. Measured on the AMD 780M. Tracked as JCT-20.

  • GATE — installer creates Start Menu (+ Desktop) shortcut. A bare .exe in Program Files with no shortcut leaves users unable to launch it. Ship an installer or an install-shortcut.ps1.

    Echo: discovered 2026-06-01 — no shortcut after install. Tracked as JCT-21.

  • Ship unsigned now, leave a signing hook for later. Add the signing step (commented) in <tool>.spec / BUILD.md so it can be enabled without rework.

  • Code-signing options (CAD), when ready:

    Option Cost (CAD) SmartScreen reputation
    Azure Trusted Signing ~$13–14/mo (cheapest) Builds over time
    EV code-signing cert ~$400–700/yr Instant (no warm-up)
  • SECURITY GATE — scan bundled config/data for secrets before producing any public artifact. Open every bundled config.yaml/.env/data file and confirm no API keys, tokens, or credentials are present.

    Echo: verified bundled config.yaml had an EMPTY api key. No public artifact ships until this passes.

Stage 3 — Distribute

  • Size decision rule. If the artifact is > 25 MB, do NOT use Cloudflare Pages (25 MB/file limit). Use GitHub Releases.

    Echo zip = 152 MB → GitHub Releases.

  • Dedicated PUBLIC releases repo; source stays private. Create <owner>/<tool>-releases (public) for binaries only. The source repo remains private.

    Echo: public bockbr/echo-releases; source superwhisper-clone stays private.

  • Cut the release & wire the download button to the asset URL.
    gh release create v0.1.0 --repo <owner>/<tool>-releases <Tool>-windows-x64.zip
    # asset URL pattern:
    # https://github.com/<owner>/<tool>-releases/releases/download/v0.1.0/<Tool>-windows-x64.zip
    

    Echo asset: https://github.com/bockbr/echo-releases/releases/download/v0.1.0/Echo-windows-x64.zip

  • Extend the company static site with /tools/<tool>/. Add a /tools/ index + a product page: pitch, CSS-only how-it-works capsules, download CTA → release URL, install steps, changelog, feedback link.

    Echo: added /tools/ index + /tools/echo/ to jct-www (jacksoncreektech.ca).

Stage 4 — Feedback

  • Create issues with a per-tool LABEL (not a component). A label needs no admin/project setup and gives identical filtering.
    project = <KEY> AND labels = <tool>
    

    Echo: 8 backlog items → JSM project JCT as JCT-7..JCT-14, label echo. Filter: project = JCT AND labels = echo.

  • Tokenless feedback link. Point the site feedback button at the JSM customer portal URL. No API token/secret in the static site.
  • Migrate any existing backlog file into Jira and make the file point to Jira (single source of truth = the tracker).

Stage 5 — Document & close

  • Create the production-line folder production-lines/<tool>/ with: pipeline.md, deployment-template.md (this), how-it-works.md, internal-runbook.md.
  • Write a how-it-works doc describing runtime flow + key modules.
  • Close the tracking order (request/work-order) once the page is live and feedback loop is verified.

What to decide per tool

Decision Field to fill Echo value
Display name <Tool> (accents OK) Echo
Identifier <tool> (ASCII, paths/exe) Echo
Releases repo <owner>/<tool>-releases (public) bockbr/echo-releases
Jira label <tool> (in project <KEY>) echo (JCT)
Signing unsigned / Azure Trusted Signing / EV unsigned (hook ready)