Skip to content

Custom remote templates in "Start from scratch" + allow subfolders in home#473

Merged
cooper (czxtm) merged 8 commits into
mainfrom
jp/custom-remote-templates
Jul 5, 2026
Merged

Custom remote templates in "Start from scratch" + allow subfolders in home#473
cooper (czxtm) merged 8 commits into
mainfrom
jp/custom-remote-templates

Conversation

@arximboldi

@arximboldi Juanpe Bolívar (arximboldi) commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Summary

#470 changed ?dir= imports to clone the whole repo and point the config dir at the subdirectory — right for imports (preserves .git/origin/sync), but it removed the workflow of using github:user/repo?dir=<path-to-template> as a template (copy the files, no history). As discussed, that use-case belongs in "Start from scratch", not import: importing adopts a repo's identity; a template only seeds a new one.

What

"Start from scratch" gains a Custom template card accepting the same repo-ref syntax as imports (owner/repo, github:owner/repo, git URLs, ?ref=, ?dir=), with template semantics:

  • the repo is cloned into a tempdir (GitHub App token → private repos work) and the referenced directory — which must contain a flake.nix — is copied into a fresh config dir;
  • placeholders are rendered exactly like bundled templates (HOSTNAME_PLACEHOLDER etc. in .nix contents, {{hostname}}/placeholders in all file names);
  • git history is not inherited: fresh repo, single initial commit tagged nixmac-base-<hash>, flake.lock finalized non-fatally (a shipped lock is kept — the author's pins);
  • wrong/missing ?dir= errors list the flake locations actually found in the repo.

Additionally, this allows when importing a template to specify a subfolder in home of arbitrary depths (e.g. ~/configs/nix-darwin) which before was not allowed.

Design notes

  • Atomic command (config.createFromTemplate): clone + validation happen before the config dir is selected, so failures surface inline on the still-mounted create screen instead of advancing onboarding to a broken state (the bundled prepareNewDirbootstrap split would set configDir before the slow clone).
  • Host attribute: when the template consumes the hostname placeholder (contents or file names) it is host-parameterized per the conventions, so the chosen machine name is adopted directly — same skip-Choose-Machine UX as bundled templates. Templates without the placeholder leave it empty and route through the Choose Machine host picker. Either way, no nix eval probe runs inside the create spinner; the backend owns the host attribute for template creates end to end.
  • Hardening (also covers bundled templates): template copy now skips .git/.DS_Store and symlinks (a hostile template could previously loop forever via ln -s . self or copy out-of-tree file contents into a repo the user may later push), and the hostname is validated at the scaffold boundary — it is spliced into file names through Path::join, so a crafted value via the raw flake.bootstrapDefault endpoint could escape the destination.
  • github:owner/repo is now accepted as sugar by both ref parsers (imports too); github:o/r/<ref> errors with a pointer to ?ref=.

Follow-ups (out of scope)

  • Preserving validated in-tree symlinks in templates, if a real template ever needs it
  • Shallow clone / progress UI for huge repos (limitation shared with imports)

Docs

  • Docs updated (companion PR in darkmatter/nixmac-web: #___)
  • No docs update needed

Nix users reflexively type nix-style refs. Both parse_repo_ref and its
frontend mirror parseFlakeRef now strip a leading 'github:' as sugar
for the plain shorthand, including query params. The path-segment ref
form (github:owner/repo/<ref>) stays rejected, now with an error
pointing at ?ref=<ref>; attribute suffixes (#attr) stay rejected too.

The sugar deliberately applies everywhere the parser is used — imports
today, and the upcoming create-from-template flow.
scaffold_template owns the whole write path — destination-safety check,
template copy with placeholder substitution, git init, tagged initial
commit — with no AppHandle, so a remote-template caller (and unit
tests) can drive it directly. bootstrap_with_template delegates;
behavior-neutral for bundled templates.

copy_template_dir is hardened for untrusted sources ahead of remote
templates: .git and .DS_Store are never copied, and symlinks are
skipped (with a warning) instead of followed — following them would let
a hostile template recurse forever or pull out-of-tree file contents
into a config the user may later publish. The hostname is validated at
the scaffold boundary since placeholder substitution splices it into
file names that pass through Path::join; a crafted value from the raw
bootstrapDefault endpoint could previously escape the destination.

finalize_flake_lock treats 'nothing to commit' as success: a template
may ship a complete flake.lock that nix flake lock leaves untouched.
New config.createFromTemplate route: clones the referenced repository
(e.g. github:owner/repo?dir=templates/mac) into a temporary directory
and scaffolds the referenced template directory into a fresh config dir
— placeholders rendered, fresh git history. Unlike imports, the
template's own git history is deliberately not inherited; that is the
point of a template.

The command is atomic, modeled on config_import_github: the target is
prepared, the clone and flake.nix validation happen against the
tempdir, and the config dir is only selected on success (a failed
clone or validation cleans the target and never advances onboarding).
Missing or wrong ?dir= errors list the flake locations actually found
in the repository. The GitHub App clone token applies, so private
template repos work. flake.lock is finalized non-fatally afterwards,
mirroring the bundled bootstrap; a template may ship its own lock.

No host attribute is set: third-party templates may not use
HOSTNAME_PLACEHOLDER, so the Choose Machine step resolves the real
hosts instead.
Pure move of the ref input + live parse-hint block out of
flake-ref-source into controls/repo-ref-input, so the upcoming
custom-template create flow can reuse it without diverging validation
UX.
Start from scratch gains a fourth choice: a Custom template card that
accepts the same repository-ref syntax as imports (owner/repo,
github:owner/repo, ?ref=, ?dir=) via the shared RepoRefInput, and
scaffolds through the atomic config.createFromTemplate — files copied,
placeholders rendered, fresh git history, no inheritance of the
template's own history. This restores the pre-non-root-flake workflow
of using a repo subdirectory as a starting point, in the path that
matches the intent.

Because the command only selects the config dir on success, clone and
validation errors render inline on the still-mounted create screen —
unlike the bundled prepareNewDir+bootstrap split, which advances the
step machine before scaffolding. The create button stays gated on a
valid ref while the custom card is selected; the bundled default
selection and the create-default-config-button testid are unchanged
for the wdio flow.
What template authors can rely on: placeholder substitution rules
(contents vs file names), fresh-history semantics vs imports, symlink
and VCS-metadata handling, shipped flake.lock behavior, and the
supported reference syntax including the github: sugar.
@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor
Warnings
⚠️

PR description is missing a ## Test Plan (or ## Testing Instructions) section. Add one describing how a reviewer can verify your change, or check No test plan needed if no testing is needed.

⚠️

No Linear issue ID found in this PR's title, description, or branch name (expected something like ENG-123). Add one so this work is traceable in Linear, or add #no-linear to the PR description to acknowledge it's intentionally untracked.

⚠️

New UI components were added without a Storybook story. Consider adding a sibling *.stories.tsx file:

  • apps/native/src/components/widget/controls/repo-ref-input.tsx
Messages
📖 No docs update needed — acknowledged.

📋 PR Overview

Lines changed 1153 (+1033 / -120)
Files 2 added, 14 modified, 0 deleted
Draft / WIP no
Has Test Plan no
Linear issue no
No Test Plan Needed no
New UI components yes (1)
New Storybook stories no
New Rust modules no
New TS source files yes (1)
New tests yes (1)
package.json touched no
Cargo.toml touched no
Infra / CI touched no

🔬 Coverage

Report Lines Statements Functions Branches
apps/native/coverage/coverage-summary.json 34.1% 33.5% 30.4% 28.0%

Generated by 🚫 dangerJS against 5eed294

@darkmatter

darkmatter Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

🎨 Storybook preview

Open Storybook preview

Updated for 5eed294


⚠️ Detected UI changes (5)

These stories' HTML snapshots changed. I've added screenshots + links to the changed stories below. Review them carefully then accept the changes to regenerate baselines and include them in this PR:

UI/Tooltip › Open

UI/Tooltip › Open

Widget/Controls/FlakeDirChooser › Multiple Candidates

Widget/Controls/FlakeDirChooser › Multiple Candidates

Widget/Controls/FlakeDirChooser › Applying

Widget/Controls/FlakeDirChooser › Applying

Widget/Drift/DriftReview › Unsummarized

Widget/Drift/DriftReview › Unsummarized

Widget/Drift/DriftReview › Summarized

Widget/Drift/DriftReview › Summarized


Accept UI changes

  • Click here to accept these changes

Alternatively, you can run bun run test:update-snapshots locally to re-generate the baselines and then push the changes to this PR.

What does this do?

The screenshots above show UI changes detected by the Storybook
snapshot tests run on this PR. Each image is the rendered output of
a Storybook story from the code in this PR branch; the snapshot
test compared it against the committed baseline in
__snapshots__/ and flagged the difference.

Checking the box tells the darkmatter[bot] to regenerate the
baselines from this PR's current code and commit them directly to
this branch. The new baselines become the source of truth for
future runs — only accept after confirming the visual changes are
intentional.

Comparison baseline: the committed __snapshots__/ files on this
PR branch (carried forward from develop). Accept updates them in
place on this branch.

…ized

A remote template that consumes the hostname placeholder (in .nix
contents or file names) follows the template conventions, so
darwinConfigurations.<chosen-name> exists after rendering — adopt the
chosen name as the host attribute directly, giving convention-following
templates the same skip-Choose-Machine UX as bundled ones. Templates
that skip the placeholder still leave the host attribute empty and
route through the host picker; no nix eval is needed either way.

The scaffold copy now reports placeholder consumption
(ScaffoldOutcome), and the backend owns the host attribute for template
creates end to end — the frontend no longer resets it after
createFromTemplate, which would have wiped the adoption.
Destinations for imports and new configurations were restricted to
direct children of the home directory, so '~/tmp/nix-darwin' failed
with 'Use a directory name, not a path' — a message written when the
input really was just a folder name. The UIs now take full paths, so
validate_new_dir_location accepts any path inside home (missing
parents are created; the canonical /etc/nix-darwin stays allowed).
Parent-dir segments are still rejected since the comparison is lexical.

The destination fields now say that ~ paths work and that missing
folders are created.
@arximboldi Juanpe Bolívar (arximboldi) changed the title Custom remote templates in "Start from scratch" Custom remote templates in "Start from scratch" + allow subfolders in home Jul 5, 2026
@czxtm cooper (czxtm) enabled auto-merge July 5, 2026 10:38
@czxtm cooper (czxtm) added this pull request to the merge queue Jul 5, 2026
Merged via the queue into main with commit c025dc1 Jul 5, 2026
12 of 14 checks passed
@czxtm cooper (czxtm) deleted the jp/custom-remote-templates branch July 5, 2026 10:51
Juanpe Bolívar (arximboldi) added a commit that referenced this pull request Jul 5, 2026
… rule

Main (#473) relaxed validate_new_dir_location to allow nested config
dirs under home, which the PR merge build combines with this branch —
asserting that a nested home path is not wipeable now fails there. The
guard test keeps only the invariants that are its own: paths outside
home are never wipeable, and the active config is never deleted from
under itself. How deep an import target may nest is
validate_new_dir_location's contract, tested where it lives.
Juanpe Bolívar (arximboldi) added a commit that referenced this pull request Jul 6, 2026
… rule

Main (#473) relaxed validate_new_dir_location to allow nested config
dirs under home, which the PR merge build combines with this branch —
asserting that a nested home path is not wipeable now fails there. The
guard test keeps only the invariants that are its own: paths outside
home are never wipeable, and the active config is never deleted from
under itself. How deep an import target may nest is
validate_new_dir_location's contract, tested where it lives.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants