Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ coverage/
/.cache
/.tmp
.claude/
.playwright-mcp/
1 change: 1 addition & 0 deletions .structlint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ file_naming_pattern:
- "*.png"
- "*.jpg"
- "*.svg"
- "*.gif"
- "README*"
- "LICENSE*"
- "CHANGELOG*"
Expand Down
1 change: 1 addition & 0 deletions COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ Use `serve` when you want:
- file-level placeholder trees
- evaluated transform previews
- idle refresh while editing values
- edit the delimiter pair in the browser and rescan instantly, no restart
- saved presets per repo/template in browser IndexedDB
- JSON import/export for presets

Expand Down
2 changes: 2 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ Open `http://127.0.0.1:17817`, then use:

The workbench shows a file tree for each placeholder and evaluated transform previews such as `APP_NAME:toUpperCase -> TEMPLATETESTER`.

If the repo uses a different delimiter pair, you don't need to restart the server: edit the `[[` `]]` pair shown next to the directory path and click **Set** to rescan with the new pair. It applies to Local, Clone, and Generate alike.

## Clone with SSH auth

```sh
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# YankRun

<div align="center">
<img src="doc/banner.png" alt="YankRun" width="400">
<img src="doc/yankrun-logo.png" alt="YankRun" width="400">
<p>
<img src="https://img.shields.io/badge/Go-1.25%2B-00ADD8?style=flat-square&logo=go" alt="Go Version">
<img src="https://img.shields.io/badge/OS-Linux%20%7C%20macOS%20%7C%20Windows-darkblue?style=flat-square&logo=windows" alt="OS Support">
Expand All @@ -11,6 +11,8 @@

**Template smarter**: Clone repos, replace tokens, or template existing projects — safely, with custom delimiters that won't clash with Helm, Jinja, or any other template language.

![yankrun serve workbench: scan, fill placeholders, preview, apply](doc/serve-demo.gif)

## TL;DR

```sh
Expand Down Expand Up @@ -47,7 +49,7 @@ yankrun template --dir ./project --input values.yaml --startDelim "<%" --endDeli
- **Transformation functions** (`toUpperCase`, `toLowerCase`, `gsub`)
- **Template file processing** (`.tpl` files processed and renamed)
- **Caching** for `generate` - caches GitHub repos and template variables in `~/.yankrun/cache.yaml`
- **Interactive workbench** (`serve`) for local/clone/generate workflows with file trees, evaluated transform previews, saved presets, and JSON import/export
- **Interactive workbench** (`serve`) for local/clone/generate workflows with file trees, evaluated transform previews, saved presets, JSON import/export, and in-browser custom delimiters
- **Safe terminal workflow** (`tui`) for preview-first directory templating

## Documentation
Expand Down Expand Up @@ -301,12 +303,15 @@ yankrun serve --dir ./my-project --addr 127.0.0.1:19090
yankrun serve --dir ./my-project --input values.yaml --dryRun
```

See it in action in the demo GIF at the top of this README.

The workbench supports:

- local scan, preview, and apply using the same replacement logic as `template`
- clone from SSH or HTTPS repositories using the existing cloner/auth behavior
- generate from configured templates and GitHub discovery
- file-level placeholder trees and evaluated transform previews
- **custom delimiters from the browser** — edit the `[[` `]]` pair next to the directory path and click **Set** to rescan with a new pair, no restart required (still `--startDelim`/`--endDelim` to set the starting pair)
- local saved presets in IndexedDB, with JSON import/export

**Flags:**
Expand Down
Binary file removed doc/banner.png
Binary file not shown.
Binary file added doc/serve-demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/yankrun-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion docs/AI/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ User Command → main.go → actions/*.go → internal/workflow → services/*.g
| `services/cloner.go` | Git clone operations | `CloneRepository()`, `CloneRepositoryBranch()` |
| `services/configio.go` | Config file management | `Load()`, `Save()`, `Reset()` |
| `internal/workflow/workflow.go` | Shared workflow used by CLI/TUI/web | `ScanDir()`, `ApplyDir()`, `CloneAndApply()` |
| `internal/web/server.go` | Embedded local workbench API | `Scan()`, `Apply()`, `Clone()`, `Generate()` |
| `internal/web/server.go` | Embedded local workbench API | `Scan()`, `Apply()`, `Clone()`, `Generate()`, `SetDelimiters()`, `ValidateDelimiters()` |
| `internal/tui/tui.go` | Preview-first terminal workflow | `Run()` |
| `actions/clone.go` | Clone command handler | `Execute()` |
| `actions/template.go` | Template command handler | `Execute()` |
Expand All @@ -92,6 +92,7 @@ User Command → main.go → actions/*.go → internal/workflow → services/*.g
- `serve` embeds `internal/web/templates` and `internal/web/static` into the single binary.
- The web UI supports local scan/apply, direct clone, and generate from configured templates.
- Preview responses include file-level placeholder trees and evaluated transform previews.
- `POST /api/delimiters` lets the browser change the active start/end delimiter pair at runtime (`Server.SetDelimiters`); it validates with `ValidateDelimiters` (rejects empty, equal, or mutually-containing pairs — an empty pair would otherwise hang the literal scan in `services/replacer.go`), updates `Server.startDelim`/`endDelim` under `Server.mu`, and returns a fresh scan. The new pair applies to Local, Clone, and Generate alike since they all read it from the same `Server.settings()`.
- Browser IndexedDB stores saved presets locally; JSON import/export is client-side only.
- `tui` uses the same workflow engine for local directory scan/apply and remains preview-first.

Expand Down
10 changes: 7 additions & 3 deletions docs/user/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ The workbench provides:
- file-level placeholder trees so you can see what will change
- evaluated transform previews, for example `APP_NAME:toUpperCase -> MYAPP`
- idle refresh after value edits so previews stay current
- editable delimiters in the browser — change the `[[` `]]` pair shown next to the directory path and click **Set** to rescan the whole workbench (Local, Clone, and Generate) with the new pair, no restart needed
- saved presets stored in browser IndexedDB, searchable by repo/template/branch/output/value keys
- preset JSON export/import for moving saved runs between browsers

Expand All @@ -372,9 +373,12 @@ Typical flow:

1. Start the server with an optional local directory and values file.
2. Use **Local**, **Clone**, or **Generate** mode.
3. Click **Preview** before applying.
4. Edit values; evaluated previews refresh after a short idle delay.
5. Restore prior work from the left preset rail when repeating a repo/template.
3. If your files use a different delimiter pair, edit it next to the directory path and click **Set** to rescan — no restart needed.
4. Click **Preview** before applying.
5. Edit values; evaluated previews refresh after a short idle delay.
6. Restore prior work from the left preset rail when repeating a repo/template.

Delimiters set from the browser apply to every mode (Local, Clone, Generate) for the rest of the session; a rejected change (empty, identical, or overlapping delimiters) leaves the previous pair active and shows the reason in the notice banner.

For clone mode:

Expand Down
78 changes: 75 additions & 3 deletions internal/web/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ type GenerateRequest struct {
DryRun bool `json:"dryRun"`
}

type DelimitersRequest struct {
StartDelim string `json:"startDelim"`
EndDelim string `json:"endDelim"`
}

func New(opts Options) (*Server, error) {
page, err := loadTemplate()
if err != nil {
Expand Down Expand Up @@ -191,18 +196,22 @@ func (s *Server) routes() {
s.mux.HandleFunc("/api/templates", s.handleTemplates)
s.mux.HandleFunc("/api/clone", s.handleClone)
s.mux.HandleFunc("/api/generate", s.handleGenerate)
s.mux.HandleFunc("/api/delimiters", s.handleSetDelimiters)
}

func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
s.mu.Lock()
dir, startDelim, endDelim := s.dir, s.startDelim, s.endDelim
s.mu.Unlock()
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_ = s.page.Execute(w, map[string]any{
"Dir": s.dir,
"StartDelim": s.startDelim,
"EndDelim": s.endDelim,
"Dir": dir,
"StartDelim": startDelim,
"EndDelim": endDelim,
"ForceDryRun": s.forceDryRun,
})
}
Expand Down Expand Up @@ -259,6 +268,67 @@ func (s *Server) handleEvaluate(w http.ResponseWriter, r *http.Request) {
writeJSON(w, summary, nil)
}

func (s *Server) handleSetDelimiters(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
defer r.Body.Close()
var req DelimitersRequest
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<10)).Decode(&req); err != nil {
http.Error(w, "invalid JSON body", http.StatusBadRequest)
return
}
summary, err := s.SetDelimiters(req.StartDelim, req.EndDelim)
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
_ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
writeJSON(w, summary, nil)
}

// SetDelimiters swaps the active start/end delimiters and returns a fresh scan
// of the current directory using them.
func (s *Server) SetDelimiters(start, end string) (PlaceholderSummary, error) {
start, end, err := ValidateDelimiters(start, end)
if err != nil {
return PlaceholderSummary{}, err
}
s.mu.Lock()
s.startDelim = start
s.endDelim = end
s.mu.Unlock()
return s.Scan()
}

// ValidateDelimiters rejects delimiter pairs that would make scanning hang or
// silently corrupt results, and returns the trimmed start/end pair to use.
//
// The literal scan (walkAndAnalyzeFiles in services/replacer.go) finds each
// delimiter with strings.Index, which returns 0 without consuming any input
// for an empty needle. If both delimiters were empty the scan loop would spin
// forever on any non-empty file, hanging the request goroutine indefinitely -
// so empty (or whitespace-only) delimiters are rejected outright. Requiring
// start != end and that neither contains the other rules out the remaining
// cases where the scan and regex-based replace paths would silently disagree
// on where a placeholder begins and ends.
func ValidateDelimiters(start, end string) (string, string, error) {
start = strings.TrimSpace(start)
end = strings.TrimSpace(end)
if start == "" || end == "" {
return "", "", fmt.Errorf("start and end delimiters are required")
}
if start == end {
return "", "", fmt.Errorf("start and end delimiters must be different")
}
if strings.Contains(start, end) || strings.Contains(end, start) {
return "", "", fmt.Errorf("start and end delimiters must not contain each other")
}
return start, end, nil
}

func (s *Server) Scan() (PlaceholderSummary, error) {
s.mu.Lock()
dir := s.dir
Expand Down Expand Up @@ -422,6 +492,8 @@ func writeJSON(w http.ResponseWriter, payload any, err error) {
}

func (s *Server) settings() workflow.TemplateSettings {
s.mu.Lock()
defer s.mu.Unlock()
return workflow.TemplateSettings{
StartDelim: s.startDelim,
EndDelim: s.endDelim,
Expand Down
Loading
Loading