Something went wrong. Try again.
a simpler package manager for no-build websites
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168// This Source Code Form is subject to the terms of the Mozilla Public// License, v. 2.0. If a copy of the MPL was not distributed with this// file, You can obtain one at https://mozilla.org/MPL/2.0/.//// Copyright (c) 2026 Jake Lazaroff https://tangled.org/jakelazaroff.com/unpm
package cli
import ( "flag" "fmt" "io" "runtime/debug" "strings" "time"
"tangled.org/jakelazaroff.com/unpm/internal/cfg")
// stringSlice implements flag.Value so a flag can be repeated.type stringSlice []string
func (s *stringSlice) String() string { return strings.Join(*s, ", ") }func (s *stringSlice) Set(v string) error { *s = append(*s, v) return nil}
type App struct { Vendor func(*cfg.Config) ([]string, error) Check func(*cfg.Config) error Why func(*cfg.Config, string) error}
// Run parses args and executes the appropriate command, writing output to// stdout and stderr. It returns the exit code.func (a *App) Run(args []string, stdout, stderr io.Writer) int { if len(args) < 2 || args[1] == "help" { fmt.Fprintf(stderr, "usage: unpm <command> [flags]\n\ncommands:\n vendor download and vendor imports\n check warn about import map issues\n why explain why a file is vendored\n") return 0 }
cmd := args[1]
var config, out, root string var pin stringSlice var verbose bool fs := flag.NewFlagSet(cmd, flag.ContinueOnError) fs.SetOutput(stderr) fs.StringVar(&config, "config", "unpm.json", "path to config JSON file") fs.StringVar(&out, "out", "", "output directory") fs.StringVar(&root, "root", "", "root directory for import map paths") fs.Var(&pin, "pin", "pin a file relative to the output directory (can be repeated)") fs.BoolVar(&verbose, "verbose", false, "show detailed output")
// reorder args so flags can appear anywhere rawArgs := args[2:] var flagArgs, posArgs []string for i := 0; i < len(rawArgs); i++ { if strings.HasPrefix(rawArgs[i], "-") { flagArgs = append(flagArgs, rawArgs[i]) name := strings.TrimLeft(rawArgs[i], "-") if !strings.Contains(name, "=") && i+1 < len(rawArgs) && !strings.HasPrefix(rawArgs[i+1], "-") { if f := fs.Lookup(name); f != nil { i++ flagArgs = append(flagArgs, rawArgs[i]) } } } else { posArgs = append(posArgs, rawArgs[i]) } } if err := fs.Parse(append(flagArgs, posArgs...)); err != nil { return 1 }
if cmd == "version" { version := "unknown" if info, ok := debug.ReadBuildInfo(); ok { version = info.Main.Version }
fmt.Fprintln(stderr, version) return 0 }
switch cmd { case "vendor", "check", "why": default: fmt.Fprintf(stderr, "unknown command: %s\n", cmd) return 1 }
c, err := cfg.ReadConfig(config) if err != nil { fmt.Fprintf(stderr, "error: %v\n", err) return 1 }
if out != "" { c.Unpm.Out = out } if root != "" { c.Unpm.Root = root } c.Unpm.Pin = append(c.Unpm.Pin, pin...) c.Unpm.Verbose = verbose
switch cmd { case "vendor": var stop func() if !verbose { stop = spinner(stdout) } warnings, err := a.Vendor(c) if stop != nil { stop() fmt.Fprint(stdout, "\r\033[K") } for _, w := range warnings { fmt.Fprintf(stderr, "\033[33mwarning:\033[0m %s\n", w) } if err != nil { fmt.Fprintf(stderr, "error: %v\n", err) return 1 } fmt.Fprintln(stdout, "done.")
case "check": if err := a.Check(c); err != nil { fmt.Fprintf(stderr, "error: %v\n", err) return 1 } fmt.Fprintln(stdout, "done.")
case "why": if fs.NArg() < 1 { fmt.Fprintf(stderr, "usage: unpm why <file>\n") return 1 } if err := a.Why(c, fs.Arg(0)); err != nil { fmt.Fprintf(stderr, "error: %v\n", err) return 1 } }
return 0}
func spinner(w io.Writer) func() { frames := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} done := make(chan struct{}) go func() { i := 0 for { select { case <-done: return default: fmt.Fprintf(w, "\r%s", frames[i%len(frames)]) i++ time.Sleep(80 * time.Millisecond) } } }() return func() { close(done) }}