core/plugin/version.go
Alex Dunmow 2a76b30c51 feat(cli): scope subcommand, interactive scope prompt, bump+version helpers
Pre-existing CLI improvements ahead of the tarball-publish refactor:
- New top-level `ninja scope` command (create, list, set-default).
- `init` accepts no --scope: prompts from ListMyScopes or uses creds default.
- Plugin name prompted if not provided.
- `plugin bump <major|minor|patch>` writes the bumped version into plugin.mod.
- `plugin version` prints the current plugin.mod version.
- `login` prints a URL with ?user_code= so the link is one click.
- creds: HostCreds gains optional default_scope.
- plugin/version: ParseBaseSemver + BumpVersion helpers, with tests.
2026-06-03 01:18:11 +08:00

90 lines
2.2 KiB
Go

package plugin
import (
"bufio"
"bytes"
"fmt"
"strconv"
"strings"
"golang.org/x/mod/semver"
)
// ParseModVersion extracts the version string from an embedded plugin.mod file.
// Returns "0.0.0" if parsing fails or version is not found.
func ParseModVersion(data []byte) string {
scanner := bufio.NewScanner(bytes.NewReader(data))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "version") {
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
val := strings.TrimSpace(parts[1])
val = strings.Trim(val, `"`)
if val != "" {
return val
}
}
}
return "0.0.0"
}
// CompareVersions compares two semver strings.
// Returns -1 if v1 < v2, 0 if equal, +1 if v1 > v2.
// Returns 0 if either version is invalid.
func CompareVersions(v1, v2 string) int {
if !strings.HasPrefix(v1, "v") {
v1 = "v" + v1
}
if !strings.HasPrefix(v2, "v") {
v2 = "v" + v2
}
if !semver.IsValid(v1) || !semver.IsValid(v2) {
return 0
}
return semver.Compare(v1, v2)
}
// ParseBaseSemver parses a plain MAJOR.MINOR.PATCH version into integers.
// Rejects pre-release suffixes and build metadata.
func ParseBaseSemver(s string) (major, minor, patch int, err error) {
parts := strings.Split(s, ".")
if len(parts) != 3 {
return 0, 0, 0, fmt.Errorf("invalid version %q: expected MAJOR.MINOR.PATCH", s)
}
out := [3]int{}
for i, p := range parts {
n, perr := strconv.Atoi(p)
if perr != nil || n < 0 {
return 0, 0, 0, fmt.Errorf("invalid version %q: each part must be a non-negative integer", s)
}
out[i] = n
}
return out[0], out[1], out[2], nil
}
// BumpVersion returns current bumped at the given level ("major", "minor", or "patch").
// Bumping major resets minor and patch to 0; bumping minor resets patch to 0.
func BumpVersion(current, level string) (string, error) {
major, minor, patch, err := ParseBaseSemver(current)
if err != nil {
return "", err
}
switch level {
case "major":
major++
minor = 0
patch = 0
case "minor":
minor++
patch = 0
case "patch":
patch++
default:
return "", fmt.Errorf("unknown bump level %q (want major|minor|patch)", level)
}
return fmt.Sprintf("%d.%d.%d", major, minor, patch), nil
}