diff --git a/README.md b/README.md index 033f41c..ff16aa9 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ > _If you just want to use it, run_: > > ```sh -> curl -fsSL https://devmagic.run/install | bash +> curl --proto '=https' --tlsv1.2 -fsSL https://devmagic.run/install | bash > ``` > -> _(P.S.: It is always recommended to [see what you are running](https://devmagic.run/install) before doing so.)_ +> _(P.S.: It is always recommended to [see what you are running](https://devmagic.run/install) before doing so. The `--proto '=https' --tlsv1.2` flags refuse to silently downgrade to plain HTTP or an old TLS version — same ones [rustup](https://rustup.rs) and other installers use. Plain `curl -fsSL ... | bash` works too, just without that hardening.)_ ## TL;DR @@ -47,7 +47,7 @@ or use it to **develop DevMagic itself** (including the website hosted at devmag The easiest way to use DevMagic is to add it to your existing project: ```bash -curl -fsSL https://devmagic.run/install | bash +curl --proto '=https' --tlsv1.2 -fsSL https://devmagic.run/install | bash ``` This downloads the DevMagic templates (from [`templates/devcontainer/`](templates/devcontainer)), fills in your project name, and writes **ready-to-use files** into a `.devcontainer/` folder in the current directory: diff --git a/docs/adr/0006-generic-template-installer.md b/docs/adr/0006-generic-template-installer.md index 3f5bfd9..a986057 100644 --- a/docs/adr/0006-generic-template-installer.md +++ b/docs/adr/0006-generic-template-installer.md @@ -54,4 +54,4 @@ A single generic script that downloads and parses a manifest per group. Rejected ## Notes - Related: [ADR 0005](0005-generate-devcontainer-files-from-templates.md) (template folder and devcontainer generation). -- Future: refactor `setup/install-prettier.sh` to source its config files from `templates/prettier/` instead of heredocs; add more groups (VS Code settings, license, CI workflows) as needed. +- Future: refactor `setup/install-prettier.sh` to source its config files from `templates/prettier/` instead of heredocs; add more groups (VS Code settings, license, CI workflows) as needed. Tracked in [RFC 0001 - Unify Installer Implementation](../rfcs/0001-unify-installer-implementation.md). diff --git a/docs/adr/README.md b/docs/adr/README.md index 03e3cc7..f1aa431 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -10,6 +10,8 @@ An Architecture Decision Record (ADR) captures an important architectural decisi - What alternatives were considered - What the trade-offs and consequences are +ADRs are short and final — they record the _result_ of a decision, not the discussion that led to it. For the full discussion (options being weighed, open questions, an in-progress design that hasn't been decided yet), see [`../rfcs/`](../rfcs/README.md). + ## Format We use a simplified version of [Michael Nygard's ADR template](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions), which is the most popular format in open-source projects. diff --git a/docs/rfcs/0001-unify-installer-implementation.md b/docs/rfcs/0001-unify-installer-implementation.md new file mode 100644 index 0000000..6bd97cd --- /dev/null +++ b/docs/rfcs/0001-unify-installer-implementation.md @@ -0,0 +1,74 @@ +# 0001 - Unify Installer Implementation (Scripts vs. Generated Templates) + +**Status:** Exploring + +**Started:** 2026-07-14 + +**Participants:** Marcelo Almeida, AI pairing session (Claude Code) + +## Problem + +`www/app/install/[script]/route.ts` currently does two structurally different things depending on the id it's asked for, and the code that does the "template" side embeds a full bash script as a JS template literal with string interpolation. + +## Context + +[ADR 0006](../adr/0006-generic-template-installer.md) introduced the `templates:` registry entries and `generateTemplateInstaller()`, which builds a bash script on the fly (as a template string inside `route.ts`) that `curl`s each registered file to its destination. This sits alongside the pre-existing `scripts:` entries, which serve a bash file that already lives in the repo (`setup/*.sh`) more or less as-is (with a `PACKAGE_MANAGER` env var injected for the `prettier` script). + +So today there are two paths that both end up "putting files into a new project," implemented two different ways: + +1. **`scripts:`** — a real `.sh` file in `setup/`, containing arbitrary logic (e.g. `install-prettier.sh` edits `package.json`, installs Husky, configures lint-staged — not just file copies). +2. **`templates:`** — no file on disk; `route.ts` generates the installer text itself, string-building a bash script (heredoc-style JS template literal with `${...}` interpolation for paths, notes, the `REPO_URL` detection logic, etc). + +This PR's ADR already flagged the seams: + +> - Known duplication: `install-prettier.sh` still embeds the prettier config in heredocs; a future refactor can make it download from `templates/prettier/` instead. +> - `www/app/install/[script]/route.ts` ... Interpolated values are all registry-controlled. + +## Discussion Log + +## ~2026-07-14 (exact time not recorded; entry written up after the fact) + +Marcelo, reviewing the merged PR: + +> `[script]/route.ts` - this file seems to be doing a lot of stuff. Right now it is working, so let's merge as is: I still want to think more about it. Problems I currently have with it and you might have already mentioned in the ADR (do not fix them now, please): (a) The shell script embedded in it, with a bunch of string interpolation. Seems like something that should be in a template, using proper templating language and logic. (b) It might return a generated script that creates a file from a template OR a separate script (saved in the repo) that does the same thing, but differently, if I understood correctly. Edit: you mention this in the ADR already... I read the code before reading the ADR. I agree with you: next step could be unifying everything, moving shell scripts under 'setup' to 'templates'. + +Claude's read on it: agreed on both counts. (a) is the more clear-cut win — moving the generated-installer logic out of a JS string and into an actual template file (checked into `templates/`, rendered with a real templating step, even if minimal) makes it reviewable, diffable, and testable the same way the devcontainer templates already are. (b) is more of a design question than a bug: `setup/*.sh` scripts carry real logic (install order, conditionals, `package.json` mutation) that plain file-copy templates can't express, so "unify everything" probably doesn't mean "delete one path" — it likely means giving both paths the same _shape_: templates live under `templates/`, and the registry entry says whether the id needs (a) a plain file-copy installer (today's `templates:` behavior) or (b) a scripted installer with real logic (today's `scripts:` behavior, but sourced from `templates/` instead of `setup/`). That would remove the `setup/` vs `templates/` split entirely and leave `route.ts` as a dispatcher instead of a generator. + +## Options Considered + +### Option 1: Move the generated-installer template into a real file + +Keep the file-copy vs. scripted-logic distinction, but stop building the file-copy installer as a JS template literal in `route.ts`. Store it as an actual template file (e.g. `templates/_installer.sh.tmpl`) with `{{...}}` placeholders in the same style as the devcontainer templates, and have `route.ts` do simple substitution (or a minimal templating step) instead of string-building bash inline. + +- **Pro:** the installer becomes reviewable/diffable like any other template; keeps today's split between `scripts:` and `templates:` (smaller change). +- **Con:** doesn't address point (b) — two mechanisms still exist for conceptually the same job. + +### Option 2: Move `setup/*.sh` scripts under `templates/`, one registry entry type + +Relocate `setup/devmagic.sh`, `setup/install-prettier.sh`, `setup/devcontainer-setup.sh`, `setup/generate.sh` under `templates/` (e.g. `templates/prettier/install.sh`, `templates/devcontainer/install.sh`), so all installable things live in one place regardless of whether they're a plain file-copy or a scripted installer with logic. The registry entry would declare which kind it is; `route.ts` becomes a thin dispatcher (serve-as-is for scripted entries, generate-from-template for file-copy entries) instead of containing the generation logic itself. + +- **Pro:** one place (`templates/`) for everything a project can install; matches the `templates/` model already established by ADR 0005/0006; directly fixes the duplication ADR 0006 flagged (e.g. `install-prettier.sh` could pull its config from `templates/prettier/` instead of embedding a second copy in heredocs). +- **Con:** bigger change — touches every existing script path and reference (README, ADRs, the registry, `www/lib/install-scripts.ts`). + +### Option 3: Do nothing further + +Leave `route.ts` as-is; it works today and both paths are already registry-driven and reasonably well-tested. + +- **Pro:** zero effort, zero regression risk. +- **Con:** the duplication and embedded-bash-in-JS concerns don't go away, and every new scripted installer added under `setup/` widens the gap Option 2 would close. + +## Open Questions + +- Is a "real templating language" (Option 1) worth adding as a dependency, or is placeholder substitution (already used for the devcontainer templates) good enough for the installer script too? +- If `setup/` moves under `templates/` (Option 2), does the distinction between "file-copy template" and "scripted installer" become a `type:` field in the registry, or an implicit fact about the entry (has a `files:` list vs. a `scriptPath:`)? +- Worth doing before or after more template groups are added? More groups added under the current split means more to migrate later. + +## Decision + +Not yet — intentionally left open. Revisit before adding the next scripted (non-file-copy) template, since each new one under `setup/` widens the gap Option 2 would close. + +## Related + +- [ADR 0005 - Generate Devcontainer Files from Templates](../adr/0005-generate-devcontainer-files-from-templates.md) +- [ADR 0006 - Serve Generic Project Templates Through the Installer Registry](../adr/0006-generic-template-installer.md) +- [PR #83](https://github.com/marcelocra/devmagic/pull/83) (merged; this RFC captures follow-up discussion from its review) diff --git a/docs/rfcs/README.md b/docs/rfcs/README.md new file mode 100644 index 0000000..21a455d --- /dev/null +++ b/docs/rfcs/README.md @@ -0,0 +1,45 @@ +# RFCs (Design Discussions) + +This directory holds the **running discussion** for a feature or bug before (and often during and after) it gets built: the problem, the context, the options considered, the back-and-forth that led to a decision, and open questions. It is DevMagic's equivalent of what many projects call a "design doc" or "proposal." + +## RFC vs. ADR vs. `docs/archives/` + +DevMagic already has two other places decisions show up, and it's worth being precise about how they differ: + +- **RFC (`docs/rfcs/`, this directory)** — the _process_ of reaching a decision. Long-form, informal, evolving. Captures the problem, the context, every option considered (including ones we rejected), the discussion (including AI pairing sessions — paste the relevant exchange in, don't just summarize it away), and open questions that are still, well, open. An RFC can stay in "Draft" or "Exploring" status indefinitely — not everything needs to be resolved immediately, and writing it down is valuable even before a decision is made. +- **[ADR](../adr/) (`docs/adr/`)** — the _result_. Short, final, rarely edited after acceptance. Title, status, context (a paragraph, not a transcript), the decision, alternatives considered (a summary, not the debate), and consequences. If an RFC leads to a decision, the ADR is the two-paragraph summary of it — and should link back to the RFC for anyone who wants the full story. +- **`docs/archives/`** — informal planning notes and brainstorms that predate this convention (blog planning, an early refactor plan, etc.). Kept for historical reference; not maintained as a structured system. New planning work should use `docs/rfcs/` instead. + +Put differently: **RFC → (optional) ADR**. Many RFCs never need an ADR — a bug fix or a small feature doesn't need a decision record, just a place to think out loud and leave a trail. Reach for an ADR once there's a decision worth being able to cite later ("why did we do it this way?"). + +## When to write one + +Not everything needs an RFC. Use your judgement — the [OpenBMC design doc guidelines](https://github.com/openbmc/docs/blob/master/designs/design-template.md) put it well: if a change can be made in a single, reasonably small patchset with little impact, it doesn't need a design discussion. + +Write one when: + +- The problem or the right approach isn't obvious, and you want to think through it (or work through it with an AI pairing session) before touching code. +- There are multiple real options and you want a record of why the others were rejected. +- The discussion is likely to resurface later ("didn't we consider X already?"). + +## Format + +1. Copy `template.md` to a new file: `NNNN-short-title.md`, numbered sequentially like ADRs (shared numbering isn't required — RFC numbers and ADR numbers are independent sequences). +2. Start in whatever status fits — usually `Exploring` or `Draft`. +3. Keep appending to the Discussion Log as the conversation develops. Don't rewrite history — append, with dates. It's fine (encouraged, even) to paste in verbatim excerpts from a chat with an AI assistant or a conversation with a collaborator; that context is exactly what gets lost when people only keep the final decision. +4. When (if) a decision is reached, either fold the summary into the RFC's own "Decision" section, or — for decisions worth citing later — write an ADR and link to it from the RFC (and vice versa). +5. Update the status: `Decided`, `Implemented`, `Superseded`, or `Abandoned`. Don't delete abandoned RFCs — the discussion of why something _wasn't_ done is often as valuable as why something was. + +## Index + +- [0001 - Unify Installer Implementation (Scripts vs. Generated Templates)](0001-unify-installer-implementation.md) — **Exploring** + +## Further Reading + +The RFC vs. ADR distinction above (and this whole convention) is drawn from these: + +- [Engineering Planning with RFCs, Design Documents and ADRs](https://newsletter.pragmaticengineer.com/p/rfcs-and-design-docs) — the RFC/ADR workflow this directory follows: write the RFC, review async, decide, write a short ADR the same day. +- [Companies Using RFCs or Design Docs and Examples of These](https://blog.pragmaticengineer.com/rfcs-and-design-docs/) — real templates and examples from Airbnb, Amazon, and others, including Stedi's lightweight "Decision Records" format. +- [Notes on technical design documentation practices (RFCs, ADRs, decision logs)](https://gist.github.com/0xdevalias/7fbbed02d61190c617393e2e51372a11) — a broad, continuously-updated survey of the space. +- [Document your technical decisions using RFC and ADR](https://emanuelcasco.vercel.app/blog/document-with-rfc-and-adr) — a concise walkthrough of using both together. +- [OpenBMC design doc guidelines](https://github.com/openbmc/docs/blob/master/designs/design-template.md) — source of the "not everything needs one" guidance above, plus a full design doc template from an active open-source project. diff --git a/docs/rfcs/template.md b/docs/rfcs/template.md new file mode 100644 index 0000000..fac4f9a --- /dev/null +++ b/docs/rfcs/template.md @@ -0,0 +1,45 @@ +# NNNN - Title in Present Tense + +**Status:** Exploring + +**Started:** YYYY-MM-DD + +**Participants:** Who's involved (a person, "AI pairing session", both) + +## Problem + +What's broken, missing, or awkward? Who's affected, and how did it come up? + +## Context + +Constraints, prior art, related code, related RFCs/ADRs. Enough for someone with no memory of the conversation to understand why this matters. + +## Discussion Log + +Append-only. Each entry is a dated note, a paste from a conversation, or a summary of a call — whatever actually happened. Don't clean this up into something tidier after the fact; the messy version is the useful one. + +## YYYY-MM-DD HH:MM + +What was discussed, considered, or decided in this round. Paste verbatim exchanges when they carry the reasoning (a chat excerpt, a code review comment thread, a summary of a conversation). Include the time — long sessions produce several entries per day. Prefix approximate/backfilled times with `~`. + +## Options Considered + +### Option 1: Name + +What it is, what it costs, what it buys. + +### Option 2: Name + +... + +## Open Questions + +Things that are still unresolved. It's fine for this list to be the only content in an early RFC. + +## Decision + +Fill in once (if) something is decided. Link to an ADR if the decision is significant enough to want a permanent, short record (`../adr/`). + +## Related + +Links to PRs, issues, ADRs, other RFCs. diff --git a/www/app/install-scripts/page.tsx b/www/app/install-scripts/page.tsx index dc19f1b..1f6ff05 100644 --- a/www/app/install-scripts/page.tsx +++ b/www/app/install-scripts/page.tsx @@ -1,6 +1,22 @@ import { loadInstallScripts, loadInstallTemplates } from "@/lib/install-scripts"; import Link from "next/link"; +// Descriptions in the registry use backticks for inline code (e.g. `prettier`). +// This renders those as instead of literal backticks; kept intentionally +// tiny — no markdown parser, no nested formatting, just this one case. +function withInlineCode(text: string) { + const parts = text.split("`"); + return parts.map((part, i) => + i % 2 === 1 ? ( + + {part} + + ) : ( + part + ), + ); +} + export default function InstallScriptsPage() { const scripts = loadInstallScripts(); const templates = loadInstallTemplates(); @@ -35,9 +51,9 @@ export default function InstallScriptsPage() {
{scripts.map((script) => ( -
+

{script.name}

-

{script.description}

+

{withInlineCode(script.description)}

{script.requirements && script.requirements.length > 0 && (
@@ -118,9 +134,13 @@ curl -fsSL https://devmagic.run/install/${script.id}?pm=bun | bash`}
{templates.map((template) => ( -
+

{template.name}

-

{template.description}

+

{withInlineCode(template.description)}

Files it creates: diff --git a/www/app/page.tsx b/www/app/page.tsx index f2af389..18bb0f6 100644 --- a/www/app/page.tsx +++ b/www/app/page.tsx @@ -1,10 +1,12 @@ import { Button } from "@/components/button"; -import { CodeBlock } from "@/components/code-block"; +import { InstallCommand } from "@/components/install-command"; import Link from "next/link"; import { getTranslations } from "next-intl/server"; +import { loadInstallScripts, loadInstallTemplates } from "@/lib/install-scripts"; export default async function Home() { const t = await getTranslations("home"); + const catalog = [...loadInstallScripts(), ...loadInstallTemplates()]; return ( <> @@ -43,17 +45,10 @@ export default async function Home() {

- +

{t("inspectScript")}{" "} - + {t("inspectScriptLink")} {" "} {t("inspectScriptEnd")} @@ -218,6 +213,30 @@ export default async function Home() {

+ + {/* More Templates & Scripts */} +
+
+

{t("moreTemplates")}

+

{t("moreTemplatesSubtitle")}

+ +
+ {catalog.map((item) => ( + + {item.name} + + ))} +
+ + + {t("viewAllTemplates")} → + +
+
); } diff --git a/www/components/code-block.tsx b/www/components/code-block.tsx index e815c10..7cfebda 100644 --- a/www/components/code-block.tsx +++ b/www/components/code-block.tsx @@ -8,9 +8,21 @@ interface CodeBlockProps { showCopy?: boolean; className?: string; alternatives?: Array<{ label: string; code: string }>; + /** Replaces the alternatives buttons in the header row with custom content (e.g. unrelated tabs). */ + headerLeft?: React.ReactNode; + /** Where the alternatives switcher renders: the header row (default) or a right-aligned row below the code. */ + alternativesPosition?: "header" | "footer"; } -export function CodeBlock({ code, lang = "bash", showCopy = true, className = "", alternatives }: CodeBlockProps) { +export function CodeBlock({ + code, + lang = "bash", + showCopy = true, + className = "", + alternatives, + headerLeft, + alternativesPosition = "header", +}: CodeBlockProps) { const [copied, setCopied] = useState(false); const [selectedIndex, setSelectedIndex] = useState(0); @@ -27,25 +39,29 @@ export function CodeBlock({ code, lang = "bash", showCopy = true, className = "" }; return ( -
+
- {alternatives && alternatives.length > 1 && ( -
- {alternatives.map((alt, index) => ( - - ))} -
- )} + {headerLeft + ? headerLeft + : alternativesPosition === "header" && + alternatives && + alternatives.length > 1 && ( +
+ {alternatives.map((alt, index) => ( + + ))} +
+ )} {showCopy && (
+ {alternativesPosition === "footer" && alternatives && alternatives.length > 1 && ( +
+ {alternatives.map((alt, index) => ( + + ))} +
+ )}
); } diff --git a/www/components/footer.tsx b/www/components/footer.tsx index 5d24fc9..0ad85e3 100644 --- a/www/components/footer.tsx +++ b/www/components/footer.tsx @@ -48,6 +48,14 @@ export function Footer() { {t("features")} +
  • + + {t("templatesAndScripts")} + +
  • + {TARGETS.map((t, i) => ( + + ))} +
  • + ); + + return ( +
    + +
    + ); +} diff --git a/www/data/install-scripts.yml b/www/data/install-scripts.yml index 57c4ec7..9857ec0 100644 --- a/www/data/install-scripts.yml +++ b/www/data/install-scripts.yml @@ -1,15 +1,21 @@ # DevMagic Installer Registry # -# Two kinds of entries: +# Two kinds of entries, distinguished by what they install and how much logic +# that requires (see docs/rfcs/0001-unify-installer-implementation.md for the +# open question of whether/how to unify these): # -# scripts: bash installers served as-is from the repo. +# scripts: a real bash script, already in this repo (setup/*.sh), served +# as-is. Use this when installing needs actual logic beyond +# copying files verbatim — e.g. install-prettier.sh edits +# package.json, installs Husky, and configures lint-staged. # Run with: curl -fsSL https://devmagic.run/install/{id} | bash # Query parameters: # - pm: package manager (pnpm, npm, yarn, bun) - defaults to pnpm # -# templates: file groups from templates/. The endpoint generates a small -# installer on the fly that downloads each file to its -# destination in the current directory. +# templates: one or more plain files from templates/, with no custom logic. +# The endpoint generates a small installer on the fly that +# downloads each file (byte for byte, aside from the {{REPO_URL}} +# placeholder) to its destination in the current directory. # Run with: curl -fsSL https://devmagic.run/install/{id} | bash # Query parameters: # - force=1: overwrite existing files (default: skip them) @@ -49,7 +55,7 @@ scripts: templates: - id: editorconfig name: EditorConfig - description: Sensible .editorconfig defaults (2-space indent, LF, UTF-8, per-language tweaks for Python, shell, F#, Markdown and more) + description: Sensible .editorconfig defaults (2-space indent, LF, UTF-8) with per-language tweaks for Python/shell/nu, Markdown, F#, PHP/Laravel, Rust, Dockerfiles and more files: - src: templates/editorconfig/editorconfig dest: .editorconfig diff --git a/www/messages/en.json b/www/messages/en.json index b53fce0..700ee1a 100644 --- a/www/messages/en.json +++ b/www/messages/en.json @@ -23,8 +23,6 @@ "inspectScript": "(Always recommended to", "inspectScriptLink": "inspect the script", "inspectScriptEnd": "first)", - "curlLabel": "curl", - "wgetLabel": "wget", "getStarted": "Get Started", "viewOnGitHub": "View on GitHub", "whyDevMagic": "Why DevMagic?", @@ -44,7 +42,10 @@ "quickStartLabel": "Quick start:", "quickStartHint1": "install ", "quickStartHint2": " and ", - "quickStartHint3": ", run the command in your project folder, then choose “Reopen in Container”." + "quickStartHint3": ", run the command in your project folder, then choose “Reopen in Container”.", + "moreTemplates": "More Templates & Scripts", + "moreTemplatesSubtitle": "Beyond the dev container: drop in an .editorconfig, Prettier setup, .gitignore, changelog tooling, and more.", + "viewAllTemplates": "Browse the full catalog" }, "features": { "title": "Features", @@ -333,7 +334,8 @@ "about": "About", "legal": "Legal", "license": "Apache 2.0 License", - "copyright": "© {year} DevMagic. Made with ✨ for developers." + "copyright": "© {year} DevMagic. Made with ✨ for developers.", + "templatesAndScripts": "Templates & Scripts" }, "languageSwitcher": { "label": "Language", diff --git a/www/messages/pt-BR.json b/www/messages/pt-BR.json index 279c8be..d0ae3ff 100644 --- a/www/messages/pt-BR.json +++ b/www/messages/pt-BR.json @@ -23,8 +23,6 @@ "inspectScript": "(É recomendado", "inspectScriptLink": "inspecionar scripts", "inspectScriptEnd": "antes de rodar)", - "curlLabel": "curl", - "wgetLabel": "wget", "getStarted": "Começar Agora", "viewOnGitHub": "Ver no GitHub", "whyDevMagic": "Por Que DevMagic?", @@ -44,7 +42,10 @@ "quickStartLabel": "Início rápido:", "quickStartHint1": "instale o ", "quickStartHint2": " e o ", - "quickStartHint3": ", rode o comando na pasta do seu projeto e escolha “Reopen in Container”." + "quickStartHint3": ", rode o comando na pasta do seu projeto e escolha “Reopen in Container”.", + "moreTemplates": "Mais Templates e Scripts", + "moreTemplatesSubtitle": "Além do dev container: adicione um .editorconfig, configuração do Prettier, .gitignore, ferramentas de changelog e mais.", + "viewAllTemplates": "Ver catálogo completo" }, "features": { "title": "Recursos", @@ -333,7 +334,8 @@ "about": "Sobre", "legal": "Legal", "license": "Licença Apache 2.0", - "copyright": "© {year} DevMagic. Feito com ✨ para devs." + "copyright": "© {year} DevMagic. Feito com ✨ para devs.", + "templatesAndScripts": "Templates e Scripts" }, "languageSwitcher": { "label": "Idioma",