Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@
> curl -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. For a hardened `curl` invocation — one that refuses to silently downgrade to plain HTTP or an old TLS version — add `--proto '=https' --tlsv1.2`, the same flags [rustup](https://rustup.rs) and other installers use:_
>
> ```sh
> curl --proto '=https' --tlsv1.2 -fsSL https://devmagic.run/install | bash
> ```
>
> _)_

## TL;DR

Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0006-generic-template-installer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
2 changes: 2 additions & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
74 changes: 74 additions & 0 deletions docs/rfcs/0001-unify-installer-implementation.md
Original file line number Diff line number Diff line change
@@ -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

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)
45 changes: 45 additions & 0 deletions docs/rfcs/README.md
Original file line number Diff line number Diff line change
@@ -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.
45 changes: 45 additions & 0 deletions docs/rfcs/template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# NNNN - Title in Present Tense

**Status:** Exploring <!-- Exploring | Draft | Decided | Implemented | Superseded | Abandoned -->

**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

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).

## 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.
28 changes: 24 additions & 4 deletions www/app/install-scripts/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <code> 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 ? (
<code key={i} className="bg-muted px-1 py-0.5 rounded text-sm">
{part}
</code>
) : (
part
),
);
}

export default function InstallScriptsPage() {
const scripts = loadInstallScripts();
const templates = loadInstallTemplates();
Expand Down Expand Up @@ -35,9 +51,9 @@ export default function InstallScriptsPage() {

<div className="space-y-6">
{scripts.map((script) => (
<div key={script.id} className="bg-card border border-border rounded-lg p-6">
<div key={script.id} id={script.id} className="bg-card border border-border rounded-lg p-6 scroll-mt-24">
<h3 className="text-2xl font-semibold mb-2">{script.name}</h3>
<p className="text-muted-foreground mb-4">{script.description}</p>
<p className="text-muted-foreground mb-4">{withInlineCode(script.description)}</p>

{script.requirements && script.requirements.length > 0 && (
<div className="bg-muted/50 border-l-4 border-warning rounded p-4 mb-4">
Expand Down Expand Up @@ -118,9 +134,13 @@ curl -fsSL https://devmagic.run/install/${script.id}?pm=bun | bash`}

<div className="space-y-6">
{templates.map((template) => (
<div key={template.id} className="bg-card border border-border rounded-lg p-6">
<div
key={template.id}
id={template.id}
className="bg-card border border-border rounded-lg p-6 scroll-mt-24"
>
<h3 className="text-2xl font-semibold mb-2">{template.name}</h3>
<p className="text-muted-foreground mb-4">{template.description}</p>
<p className="text-muted-foreground mb-4">{withInlineCode(template.description)}</p>

<div className="mb-4">
<strong className="block mb-2">Files it creates:</strong>
Expand Down
26 changes: 26 additions & 0 deletions www/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { Button } from "@/components/button";
import { CodeBlock } from "@/components/code-block";
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 (
<>
Expand Down Expand Up @@ -218,6 +220,30 @@ export default async function Home() {
</div>
</div>
</section>

{/* More Templates & Scripts */}
<section className="container mx-auto px-4 py-16 border-t border-border/50">
<div className="max-w-4xl mx-auto text-center">
<h2 className="text-2xl md:text-3xl font-bold mb-3">{t("moreTemplates")}</h2>
<p className="text-muted-foreground mb-8">{t("moreTemplatesSubtitle")}</p>

<div className="flex flex-wrap justify-center gap-3 mb-8">
{catalog.map((item) => (
<Link
key={item.id}
href={`/install-scripts#${item.id}`}
className="px-4 py-2 rounded-full border border-border bg-card hover:border-primary hover:text-primary transition-colors text-sm font-medium"
>
{item.name}
</Link>
))}
</div>

<Link href="/install-scripts" className="text-primary hover:underline font-medium">
{t("viewAllTemplates")}
</Link>
</div>
</section>
</>
);
}
8 changes: 8 additions & 0 deletions www/components/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ export function Footer() {
{t("features")}
</Link>
</li>
<li>
<Link
href="/install-scripts"
className="text-muted-foreground hover:text-foreground transition-colors hover:translate-x-1 inline-block"
>
{t("templatesAndScripts")}
</Link>
</li>
<li>
<Link
href="/docs"
Expand Down
Loading