diff --git a/.github/workflows/native-ci.yml b/.github/workflows/native-ci.yml index 687fda8..da3fc05 100644 --- a/.github/workflows/native-ci.yml +++ b/.github/workflows/native-ci.yml @@ -27,7 +27,7 @@ jobs: - name: Native build and credential-free smoke shell: pwsh run: | - $binary = if ($IsWindows) { './jev-ci.exe' } else { './jev-ci' } + $binary = if ($IsWindows) { './jevwise-ci.exe' } else { './jevwise-ci' } go build -o $binary ./cmd if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } foreach ($arguments in @(@('--help'), @('--version'), @('version'), @('skill', '--online'))) { @@ -42,5 +42,5 @@ jobs: GOARCH: arm64 CGO_ENABLED: '0' run: | - go build -o jev-windows-arm64.exe ./cmd + go build -o jevwise-windows-arm64.exe ./cmd if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } diff --git a/README.md b/README.md index e111c21..75140db 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Jevwise -A Go SDK for TypeSafe AI and the `jev` decision CLI. +A Go SDK for TypeSafe AI and the `jevwise` decision CLI. - SDK: `pkg/typesafe`, with typed questions, responses, raw metadata, and retries. - CLI: send a prompt and options to Jev, then print the service's probabilities as JSON. @@ -13,19 +13,21 @@ Use Go 1.27.2 or newer, matching `go.mod`. Run these commands from the checkout: ```sh git clone https://github.com/bitbrew-dev/jevwise.git cd jevwise -go build -o bin/jev ./cmd -bin/jev --help -bin/jev decide --help +go build -o bin/jevwise ./cmd +bin/jevwise --help +bin/jevwise decide --help ``` Help does not read credentials, configuration files, or contact the API. +The current checkout uses `jevwise`; releases through v1.2.0 used `jev`. There is no `jev` alias. Existing configuration and the `jev` provider remain unchanged. Verify ownership before removing an older binary; another project may own the `jev` command. + ```sh export TYPESAFE_API_KEY='' -bin/jev decide --prompt 'Which task should I tackle first?' \ +bin/jevwise decide --prompt 'Which task should I tackle first?' \ --option 'Fix the bug' --option 'Write documentation' -printf '%s' 'Which task should I tackle first?' | bin/jev decide --stdin \ +printf '%s' 'Which task should I tackle first?' | bin/jevwise decide --stdin \ --option 'Fix the bug' --option 'Write documentation' ``` @@ -41,8 +43,8 @@ printf '%s' 'Which task should I tackle first?' | bin/jev decide --stdin \ ## Version and platform builds ```sh -bin/jev version -bin/jev --version +bin/jevwise version +bin/jevwise --version make build-platform GOOS=windows GOARCH=amd64 make build-platform GOOS=windows GOARCH=arm64 ``` @@ -50,14 +52,14 @@ make build-platform GOOS=windows GOARCH=arm64 - Both version forms report version, commit, and build date without credentials, config loading, or network requests. - Plain `go build` reports `dev` with unknown metadata. Make injects metadata; only a clean exact tag is automatically a release version, otherwise `dev`. - Release linker symbols are `github.com/bitbrew-dev/jevwise/internal/buildinfo.Version`, `.Commit`, and `.Date`. -- Make produces `build/jev-windows-amd64.exe` or `build/jev-windows-arm64.exe`. Make helpers require Unix shell tools; native Windows PowerShell can use `go build -o jev.exe ./cmd`, then `.\jev.exe --version`. +- Make produces `build/jevwise-windows-amd64.exe` or `build/jevwise-windows-arm64.exe`. Make helpers require Unix shell tools; native Windows PowerShell can use `go build -o jevwise.exe ./cmd`, then `.\jevwise.exe --version`. ## Release checks and updates ```sh -bin/jev update --check -bin/jev update --check --timeout 20s -bin/jev update --timeout 60s # Install on supported standalone Linux/macOS builds +bin/jevwise update --check +bin/jevwise update --check --timeout 20s +bin/jevwise update --timeout 60s # Install on supported standalone Linux/macOS builds ``` - `--check` checks the latest stable GitHub release without downloading a binary or changing files. No background checks, API credentials, or decision configuration are used. @@ -66,7 +68,7 @@ bin/jev update --timeout 60s # Install on supported standalone Linux/macOS build - Accepts canonical stable `vX.Y.Z` tags only; prereleases/drafts and malformed or oversized responses are refused. - Without `--check`, downloads and verifies a strictly newer release before replacement. Equal/newer installed versions are not rewritten or downgraded. Development, unknown and unstamped builds require manual installation. - Automatic installation supports Linux/macOS amd64/arm64 only. Windows still supports version reporting and release checks, but updates are manual. Use your package manager for managed installations. -- After success, start a new invocation to use the new binary. An error saying the update was installed means publication succeeded but cancellation, cleanup or reporting failed: inspect `jev version` and any lock before retrying; no rollback occurs. +- After success, start a new invocation to use the new binary. An error saying the update was installed means publication succeeded but cancellation, cleanup or reporting failed: inspect `jevwise version` and any lock before retrying; no rollback occurs. ## Release binaries @@ -118,11 +120,11 @@ Precedence: **changed flags > nonblank environment > TOML > defaults**. ## Skill setup ```sh -bin/jev skill # Default: .agent/skills/typesafe-ai/SKILL.md -bin/jev skill --agent # Explicit default target -bin/jev skill --claude # .claude/skills/typesafe-ai/SKILL.md -bin/jev skill --force # Replace an existing regular skill file -bin/jev skill --online # Print the upstream GitHub URL only +bin/jevwise skill # Default: .agent/skills/typesafe-ai/SKILL.md +bin/jevwise skill --agent # Explicit default target +bin/jevwise skill --claude # .claude/skills/typesafe-ai/SKILL.md +bin/jevwise skill --force # Replace an existing regular skill file +bin/jevwise skill --online # Print the upstream GitHub URL only ``` - Downloads the latest [upstream skill](https://github.com/typesafe-ai/skills/blob/main/skills/typesafe-ai/SKILL.md) unchanged, relative to the current project directory. @@ -135,7 +137,7 @@ bin/jev skill --online # Print the upstream GitHub URL only ## Go SDK quickstart -Import the public SDK from `github.com/bitbrew-dev/jevwise/pkg/typesafe`. The CLI executable remains `jev`. +Import the public SDK from `github.com/bitbrew-dev/jevwise/pkg/typesafe`. The CLI executable is `jevwise`. Migration: replace previous `github.com/benbenbang/ts-jev-go-sdk/pkg/typesafe` imports with the new path. No old-module compatibility alias is provided; SDK APIs and configuration names are unchanged. @@ -208,34 +210,34 @@ Tests use injected services/transports and local HTTP fixtures, not paid API req ## Local MCP -MCP is included in v1.1.0. Diagnostic PID tracking is a newer, unreleased addition in the current checkout. +MCP is included in v1.1.0; diagnostic PID tracking and schema-2 state are included in v1.2.0. The executable rename is newer than those releases. -| Capability | v1.0.0 | v1.1.0 | Current checkout | -| --- | --- | --- | --- | -| Foreground and background MCP | Not included | Included | Included | -| Authenticated status and stop | Not included | Included | Included | -| Recorded child PID and schema-2 state | Not included | Not included | Included, unreleased | +| Capability | v1.0.0 | v1.1.0 | v1.2.0 | Current checkout | +| --- | --- | --- | --- | --- | +| Foreground and background MCP | Not included | Included | Included | Included | +| Authenticated status and stop | Not included | Included | Included | Included | +| Recorded child PID and schema-2 state | Not included | Not included | Included | Included | The examples below describe the current checkout, using synthetic paths and addresses. They are not services started by this documentation. ### Start and manage -Configure upstream credentials as for `jev decide`. Separately prepare a random agent bearer token in a protected, current-user-only file. Do not reuse your API key. +Configure upstream credentials as for `jevwise decide`. Separately prepare a random agent bearer token in a protected, current-user-only file. Do not reuse your API key. ```sh # Synthetic examples: supply your own protected token file before running. -bin/jev mcp --token-file /private/example/agent-token --listen 127.0.0.1:8080 -bin/jev mcp --background --token-file /private/example/agent-token -bin/jev mcp status -bin/jev mcp stop +bin/jevwise mcp --token-file /private/example/agent-token --listen 127.0.0.1:8080 +bin/jevwise mcp --background --token-file /private/example/agent-token +bin/jevwise mcp status +bin/jevwise mcp stop ``` | Command or flag | Behavior | | --- | --- | -| `jev mcp` | Foreground server; prints the public `/mcp` endpoint after startup. Ctrl+C stops it. | +| `jevwise mcp` | Foreground server; prints the public `/mcp` endpoint after startup. Ctrl+C stops it. | | `--background` | Starts a terminal-independent child after authenticated readiness and ownership checks. | -| `jev mcp status` | Authenticates the exact instance and reports its startup/running state. The post-v1.1.0 checkout also shows the recorded PID when available. | -| `jev mcp stop` | Authenticates the exact instance and requests shutdown; an acknowledgement is not a promise that cleanup has already finished. | +| `jevwise mcp status` | Authenticates the exact instance and reports its startup/running state. v1.2.0 and newer also show the recorded PID when available. | +| `jevwise mcp stop` | Authenticates the exact instance and requests shutdown; an acknowledgement is not a promise that cleanup has already finished. | | `--listen` | Literal loopback IP and nonzero port; default `127.0.0.1:8080`. | | `--runtime-dir` | Private background state directory; default `os.UserCacheDir()/jevwise-mcp`. Use the same override for start, status and stop. | | `--token-file` | Explicit protected agent-token file, overriding `JEV_MCP_TOKEN`; unreadable or unsafe files fail without fallback. | @@ -246,8 +248,8 @@ bin/jev mcp stop - Background mode does not install a login item, reboot service or automatic restart. On Windows, a restrictive parent Job Object can prevent detachment or terminate the child when its parent exits. - A repeated background start returns an endpoint only for an authenticated running instance. If it is still preparing or stopping, wait and check status instead. - Stop and restart after changing credentials, decision configuration or the executable. Updating a binary does not change an already-running process. -- Unreleased PID support (post-v1.1.0 checkout): background children save their own PID in private `/state.json`; authenticated status prints `PID: NUMBER (inspection only)`. Normal owned shutdown removes that state along with the key and lease. PID reuse means this number is not proof of ownership or liveness: use `jev mcp stop`, not a blind PID-based kill. -- Unreleased state format: updated children write schema-2 state. The updated CLI still manages schema-1 instances without displaying a PID; older CLIs, including v1.1.0, cannot read schema-2 state. Stop an old instance before upgrading/restarting, and use the matching or newer CLI to manage a new child. +- PID support (v1.2.0 and newer): background children save their own PID in private `/state.json`; authenticated status prints `PID: NUMBER (inspection only)`. Normal owned shutdown removes that state along with the key and lease. PID reuse means this number is not proof of ownership or liveness: use `jevwise mcp stop`, not a blind PID-based kill. +- State format (v1.2.0 and newer): updated children write schema-2 state. The updated CLI still manages schema-1 instances without displaying a PID; older CLIs, including v1.1.0, cannot read schema-2 state. Stop an old instance before upgrading/restarting, and use the matching or newer CLI to manage a new child. ### Agent connection and limits diff --git a/docs/internal/cli-rename.md b/docs/internal/cli-rename.md new file mode 100644 index 0000000..d40b855 --- /dev/null +++ b/docs/internal/cli-rename.md @@ -0,0 +1,17 @@ +# Executable rename: jev to jevwise + +## Plan and scope + +- [x] 25 - Rename command/help/version output, default Make and native-CI filenames, actionable MCP hints and public command examples. Test name/help/completion/version and Make output. Keep release tooling's native-version check compatible with existing tags. Parent: `cebb336`. +- [ ] 26 - Rename future release assets to `jevwise_...`; teach the updater to prefer the new exact name and use legacy names only when the new name is absent. Preserve size/checksum/redirect/version guards. Add offline publisher/downloader regression tests and migration notes. Depends on 25. +- Both scopes approved by the user. One PR at a time; feature branches stacked linearly on `virtual-main`, worktrees under `.agent/worktrees`; soft 300 / hard 450 aggregate changed lines including docs/tests. +- Keep SDK import paths, `cmd` entry-point path, Jev provider/model/hidden skill flag, `TS_JEV_*` / `TYPESAFE_*`, `ts-jev` config path, runtime state and update stamps unchanged. No `jev` alias, user-binary deletion, release/tag/visibility changes or Windows dependency upgrade. + +## Migration and QA + +- Existing published v1.0.0, v1.1.0 and v1.2.0 retain their `jev` command/artifact family. Never rewrite published binaries or manifests. New names apply to the next release and current source builds. +- Old clients know only `jev_...` assets: document manual installation under `jevwise` for the first renamed release. Never rename/remove another vendor's `jev` binary automatically; the updater replaces its current executable path, not its filename. +- Require root and independent QA: normal/race tests, tidy/vet/build/format/diff, offline Python fixtures, six builds and credential-free smoke. Installed-agent probes remain opt-in; no paid decisions or ordinary user configuration edits. +- Require all five exact-head CI successes before an approved merge, then verify merged tree/main/virtual-main and rerun checks. Preserve private spec and user artifacts; remove only merged task-owned worktrees/branches/temp files. + +- 25 local QA passed: root fresh normal/race/tidy/vet/build/format/diff, 11 Python fixtures and six Make builds; independent full tests plus focused race checks repeated three times. Command-only smoke is credential-free. CI/publication/merge pending. diff --git a/docs/mcp-agents.md b/docs/mcp-agents.md index a0f88f7..52e3b46 100644 --- a/docs/mcp-agents.md +++ b/docs/mcp-agents.md @@ -1,22 +1,24 @@ # Connect a local agent to Jevwise -Use Codex or Claude Code on the **same computer** as the Jevwise MCP server. Jevwise serves Streamable HTTP at a literal-loopback `/mcp` endpoint, not stdio or legacy `/sse`. Do not register `jev mcp` as a stdio command or expose it through a public tunnel. +Use Codex or Claude Code on the **same computer** as the Jevwise MCP server. Jevwise serves Streamable HTTP at a literal-loopback `/mcp` endpoint, not stdio or legacy `/sse`. Do not register `jevwise mcp` as a stdio command or expose it through a public tunnel. -MCP is available in v1.1.0. Recorded PID/schema-2 state requires the newer unreleased checkout; see [release availability](../README.md#local-mcp). +MCP is available in v1.1.0. Recorded PID/schema-2 state requires v1.2.0 or newer; see [release availability](../README.md#local-mcp). + +The commands below use the renamed checkout CLI, `jevwise`; published releases through v1.2.0 use `jev`. ## 1. Start Jevwise in the server terminal Prerequisites: -- Install/build `jev` and configure the Jev provider as in the [README](../README.md#build-and-run). +- Install/build `jevwise` and configure the Jev provider as in the [README](../README.md#build-and-run). - Prepare a random, separate agent bearer token in a protected current-user-only file. Follow the [token and runtime permissions](../README.md#credentials-state-and-safety), including Windows ACL requirements. - Replace the example token-file path below with your own existing protected file. Do not reuse your upstream API key or the runtime management key. ```sh -# macOS/Linux shell; use jev.exe on Windows. -jev mcp --background --token-file /absolute/path/to/protected/agent-token \ +# macOS/Linux shell; use jevwise.exe on Windows. +jevwise mcp --background --token-file /absolute/path/to/protected/agent-token \ --listen 127.0.0.1:8080 -jev mcp status +jevwise mcp status ``` Foreground mode omits `--background`. Keep its terminal open; Ctrl+C stops it. If you set `--runtime-dir`, use the same directory for start, status and stop. @@ -99,13 +101,13 @@ See [Claude Code's official MCP documentation](https://code.claude.com/docs/en/m | Check | What it establishes | | --- | --- | -| `jev mcp status` | The exact background instance authenticated its private management response. | +| `jevwise mcp status` | The exact background instance authenticated its private management response. | | Client connected status and `decide` discovery | The client can authenticate and discover the MCP tool. | | Calling `decide` | A real decision request, which can incur upstream API charges. Not needed for setup verification. | If you intentionally want a decision, supply a `prompt` and at least two distinct `options`. Keep tool approval enabled and review the call before sending it. -### Optional installed-client smoke test (unreleased checkout) +### Optional installed-client smoke test (source checkout) From the source checkout, with both `codex` and `claude` installed on PATH, run in a macOS/Linux shell: @@ -127,6 +129,6 @@ JEVWISE_MCP_AGENT_SMOKE=1 go test -mod=readonly -run '^TestRealAgentMCPDiscovery | Claude project server pending approval | Review the project and approve its `.mcp.json` entry in the client. | | Cannot verify background state | Inspect manually; do not delete state or kill a PID based only on metadata. | -Stop with `jev mcp stop` using the same runtime directory. Acknowledgement is not a guarantee cleanup has already finished. Stop/restart after executable or credential changes. Use the matching/newer CLI for schema-2 instances. +Stop with `jevwise mcp stop` using the same runtime directory. Acknowledgement is not a guarantee cleanup has already finished. Stop/restart after executable or credential changes. Use the matching/newer CLI for schema-2 instances. Remove only the client entry you added: `codex mcp remove jevwise`, or `claude mcp remove jevwise --scope local` for the local-scoped example. For project JSON, remove only that entry. Removing client configuration does not stop Jevwise. Clear `JEV_MCP_TOKEN` from the client terminal when finished. diff --git a/internal/buildinfo/info.go b/internal/buildinfo/info.go index f32773a..92c9e2e 100644 --- a/internal/buildinfo/info.go +++ b/internal/buildinfo/info.go @@ -6,6 +6,9 @@ import ( "runtime" ) +// CommandName is the public executable and root-command identity. +const CommandName = "jevwise" + // Update stamps survive stripped, path-trimmed release builds for passive inspection. const UpdateStampPrefix = "JEVWISE_UPDATE_V1[" const UpdateStampSuffix = "]JEVWISE_UPDATE_END" @@ -23,6 +26,6 @@ func String() string { if UpdateStamp != "" && UpdateStamp != UpdateStampPrefix+Version+UpdateStampSuffix { version = "unknown" } - return fmt.Sprintf("jev %s\ncommit: %s\nbuilt: %s\ngo: %s\nplatform: %s/%s\n", + return fmt.Sprintf(CommandName+" %s\ncommit: %s\nbuilt: %s\ngo: %s\nplatform: %s/%s\n", version, Commit, Date, runtime.Version(), runtime.GOOS, runtime.GOARCH) } diff --git a/internal/buildinfo/info_test.go b/internal/buildinfo/info_test.go index fa0d83e..db33fb3 100644 --- a/internal/buildinfo/info_test.go +++ b/internal/buildinfo/info_test.go @@ -13,7 +13,7 @@ func TestDefaultAndLinkedMetadataFormat(t *testing.T) { t.Fatal("incorrect default metadata") } Version, Commit, Date = "v1.2.3", "abc123", "2026-10-07T00:00:00Z" - want := "jev v1.2.3\ncommit: abc123\nbuilt: 2026-10-07T00:00:00Z\ngo: " + runtime.Version() + "\nplatform: " + runtime.GOOS + "/" + runtime.GOARCH + "\n" + want := "jevwise v1.2.3\ncommit: abc123\nbuilt: 2026-10-07T00:00:00Z\ngo: " + runtime.Version() + "\nplatform: " + runtime.GOOS + "/" + runtime.GOARCH + "\n" if String() != want || strings.Contains(String(), "unknown") { t.Fatal("metadata not used by formatter") } @@ -24,9 +24,9 @@ func TestUpdateStampConsistency(t *testing.T) { t.Cleanup(func() { Version, UpdateStamp = beforeVersion, beforeStamp }) Version = "v1.2.3" for _, tc := range []struct{ stamp, want string }{ - {"", "jev v1.2.3\n"}, - {UpdateStampPrefix + Version + UpdateStampSuffix, "jev v1.2.3\n"}, - {UpdateStampPrefix + "v2.0.0" + UpdateStampSuffix, "jev unknown\n"}, + {"", "jevwise v1.2.3\n"}, + {UpdateStampPrefix + Version + UpdateStampSuffix, "jevwise v1.2.3\n"}, + {UpdateStampPrefix + "v2.0.0" + UpdateStampSuffix, "jevwise unknown\n"}, } { UpdateStamp = tc.stamp if !strings.HasPrefix(String(), tc.want) { diff --git a/internal/buildinfo/make_test.go b/internal/buildinfo/make_test.go index 710704c..52fb17a 100644 --- a/internal/buildinfo/make_test.go +++ b/internal/buildinfo/make_test.go @@ -41,12 +41,12 @@ func TestMakeExactCleanTagAndWindowsOutput(t *testing.T) { t.Fatalf("make failed: %s", output) } text := string(output) - for _, want := range []string{"go build -trimpath", "mkdir -p \"custom-output\"", "jev-windows-arm64.exe", "github.com/bitbrew-dev/jevwise/internal/buildinfo.Version=" + tc.want, "buildinfo.Commit=abc123", "buildinfo.Date=stamp", "buildinfo.UpdateStamp=" + UpdateStampPrefix + tc.want + UpdateStampSuffix} { + for _, want := range []string{"go build -trimpath", "mkdir -p \"custom-output\"", "jevwise-windows-arm64.exe", "github.com/bitbrew-dev/jevwise/internal/buildinfo.Version=" + tc.want, "buildinfo.Commit=abc123", "buildinfo.Date=stamp", "buildinfo.UpdateStamp=" + UpdateStampPrefix + tc.want + UpdateStampSuffix} { if !strings.Contains(text, want) { t.Errorf("make output missing %q: %s", want, text) } } - if strings.Contains(text, "chmod") || strings.Contains(text, "/pkg/config") { + if strings.Contains(text, "jev-windows-arm64.exe") || strings.Contains(text, "chmod") || strings.Contains(text, "/pkg/config") { t.Fatal("obsolete or Windows-incompatible build recipe") } } diff --git a/internal/cli/mcp_background.go b/internal/cli/mcp_background.go index bd8172e..e658c4f 100644 --- a/internal/cli/mcp_background.go +++ b/internal/cli/mcp_background.go @@ -8,6 +8,7 @@ import ( "path/filepath" "time" + "github.com/bitbrew-dev/jevwise/internal/buildinfo" "github.com/bitbrew-dev/jevwise/internal/config" "github.com/bitbrew-dev/jevwise/internal/daemon" "github.com/bitbrew-dev/jevwise/internal/mcpserver" @@ -53,7 +54,7 @@ func runMCPBackground(cmd *cobra.Command, cfg config.Config, address, agentToken case "running": return writeBackgroundEndpoint(cmd, state.Address) case "prepared": - return errors.New("MCP background instance is preparing; wait and check jev mcp status") + return errors.New("MCP background instance is preparing; wait and check " + buildinfo.CommandName + " mcp status") case "stopping": return errors.New("MCP background instance is stopping; wait before starting again") default: @@ -112,7 +113,7 @@ func readBackgroundInstance(path string) (daemon.State, string, error) { } func backgroundUncertain(cause error) error { - return &decisionError{"MCP background may be running; use jev mcp status", cause} + return &decisionError{"MCP background may be running; use " + buildinfo.CommandName + " mcp status", cause} } func writeBackgroundEndpoint(cmd *cobra.Command, address string) error { diff --git a/internal/cli/mcp_background_test.go b/internal/cli/mcp_background_test.go index a9c0abe..93db717 100644 --- a/internal/cli/mcp_background_test.go +++ b/internal/cli/mcp_background_test.go @@ -175,7 +175,7 @@ func TestBackgroundExistingOwnedInstanceNeverLaunches(t *testing.T) { t.Fatal("unsafe existing instance was adopted") } if phase == "prepared" { - if err.Error() != "MCP background instance is preparing; wait and check jev mcp status" { + if err.Error() != "MCP background instance is preparing; wait and check jevwise mcp status" { t.Fatalf("prepared instance = %v", err) } controller, err := daemon.NewController(address, before.Instance, keyBefore) @@ -232,7 +232,7 @@ func TestBackgroundUncertainStartAndOutputFailures(t *testing.T) { cmd.SetArgs([]string{"mcp", "--background", "--runtime-dir", dir, "--api-key", "fixture-key"}) cmd.SetOut(versionWriter{short: true}) err := cmd.ExecuteContext(context.Background()) - if err == nil || strings.Contains(err.Error(), "private") || ((kind == "after" || kind == "shortwrite") && !strings.Contains(err.Error(), "may be running")) { + if err == nil || strings.Contains(err.Error(), "private") || ((kind == "after" || kind == "shortwrite") && !strings.Contains(err.Error(), "may be running; use jevwise mcp status")) { t.Fatal("unsafe or misleading startup failure", err) } if _, err := os.Lstat(dir); !errors.Is(err, os.ErrNotExist) { diff --git a/internal/cli/root.go b/internal/cli/root.go index badd505..d23cdd1 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -26,7 +26,7 @@ func NewRootWithFactory(factory ServiceFactory) *cobra.Command { } var showVersion bool cmd := &cobra.Command{ - Use: "jev", Short: "Make decisions with Jev", + Use: buildinfo.CommandName, Short: "Make decisions with Jev", SilenceUsage: true, SilenceErrors: true, Args: func(_ *cobra.Command, args []string) error { if len(args) != 0 { diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go index f738a8b..38755cb 100644 --- a/internal/cli/root_test.go +++ b/internal/cli/root_test.go @@ -9,6 +9,8 @@ import ( "strings" "testing" + "github.com/bitbrew-dev/jevwise/internal/config" + "github.com/bitbrew-dev/jevwise/internal/service" "github.com/spf13/cobra" ) @@ -129,3 +131,51 @@ func TestCommandReceivesExecutionContext(t *testing.T) { t.Fatalf("context not propagated: %v", err) } } + +func TestCommandIdentityHelpAndCompletion(t *testing.T) { + before := http.DefaultTransport + http.DefaultTransport = forbiddenTransport{t} + t.Cleanup(func() { http.DefaultTransport = before }) + fresh := func() *cobra.Command { + return NewRootWithFactory(func(config.Config) (service.DecisionService, func(), error) { + t.Fatal("help or completion created a service") + return nil, nil, nil + }) + } + root := fresh() + if root.Name() != "jevwise" || len(root.Aliases) != 0 { + t.Fatal("incorrect executable identity or legacy alias") + } + if _, _, err := execute(root, "--help"); err != nil { + t.Fatal(err) + } + var walk func(*cobra.Command) + walk = func(cmd *cobra.Command) { + args := append(strings.Fields(cmd.CommandPath())[1:], "--help") + out, stderr, err := execute(fresh(), args...) + if err != nil || stderr != "" || !strings.Contains(out, "\n "+cmd.CommandPath()) { + t.Fatalf("help does not use renamed command path %q", cmd.CommandPath()) + } + for _, alias := range cmd.Aliases { + if alias == "jev" { + t.Fatal("legacy command alias retained") + } + } + for _, child := range cmd.Commands() { + walk(child) + } + } + walk(root) + for shell, registration := range map[string]string{ + "bash": "__start_jevwise jevwise", "zsh": "#compdef jevwise", + "fish": "complete -c jevwise", "powershell": "-CommandName 'jevwise'", + } { + out, stderr, err := execute(fresh(), "completion", shell) + if err != nil || stderr != "" || !strings.Contains(out, registration) { + t.Fatalf("%s completion does not register jevwise", shell) + } + } + if _, _, err := execute(fresh(), "jev"); err == nil { + t.Fatal("legacy root command accepted") + } +} diff --git a/internal/cli/version_test.go b/internal/cli/version_test.go index 5bfc92e..25c3607 100644 --- a/internal/cli/version_test.go +++ b/internal/cli/version_test.go @@ -24,7 +24,7 @@ func TestVersionFormsAreIdenticalAndIndependent(t *testing.T) { return nil, nil, nil }) out, stderr, err := execute(cmd, form, "--config", "/missing", "--timeout", "invalid", "--provider", "invalid") - if err != nil || stderr != "" || out != buildinfo.String() || !strings.HasPrefix(out, "jev dev\ncommit: unknown\nbuilt: unknown\n") { + if err != nil || stderr != "" || out != buildinfo.String() || !strings.HasPrefix(out, "jevwise dev\ncommit: unknown\nbuilt: unknown\n") { t.Fatalf("version output incorrect: %q %q %v", out, stderr, err) } } diff --git a/internal/update/guard_test.go b/internal/update/guard_test.go index cfa4964..29bf55e 100644 --- a/internal/update/guard_test.go +++ b/internal/update/guard_test.go @@ -191,7 +191,7 @@ func TestMakeTrimmedExecutableGuard(t *testing.T) { if output, err := command.CombinedOutput(); err != nil { t.Fatalf("Make fixture build: %v\n%s", err, output) } - data, err := os.ReadFile(filepath.Join(directory, "jev-"+runtime.GOOS+"-"+runtime.GOARCH)) + data, err := os.ReadFile(filepath.Join(directory, "jevwise-"+runtime.GOOS+"-"+runtime.GOARCH)) if err != nil { t.Fatal(err) } diff --git a/makefile b/makefile index a7e3a60..eec58af 100644 --- a/makefile +++ b/makefile @@ -1,5 +1,5 @@ .EXPORT_ALL_VARIABLES: -NAME = jev +NAME = jevwise DirName ?= build PKG = github.com/bitbrew-dev/jevwise/internal/buildinfo ProjectUrl = "https://github.com/bitbrew-dev/jevwise" diff --git a/scripts/release_assets.py b/scripts/release_assets.py index b180787..a19197c 100644 --- a/scripts/release_assets.py +++ b/scripts/release_assets.py @@ -13,6 +13,7 @@ REPOSITORY = "bitbrew-dev/jevwise" PLATFORMS = [(system, arch) for system in ("darwin", "linux", "windows") for arch in ("amd64", "arm64")] BUILDINFO = "github.com/bitbrew-dev/jevwise/internal/buildinfo" +LEGACY_TAGS = {"v1.0.0", "v1.1.0", "v1.2.0"} def tag_valid(tag): @@ -133,7 +134,8 @@ def build(metadata, source, directory): run(["go", "build", "-mod=readonly", "-trimpath", "-ldflags", flags, "-o", str(directory / name), "./cmd"], cwd=source, env=env) host = (run(["go", "env", "GOHOSTOS"]), run(["go", "env", "GOHOSTARCH"])) native = directory / names[PLATFORMS.index(host)] - if not run([str(native), "version"]).startswith("jev " + metadata["tag"] + "\ncommit: " + metadata["commit"] + "\n"): + command = "jev" if metadata["tag"] in LEGACY_TAGS else "jevwise" + if not run([str(native), "version"]).startswith(command + " " + metadata["tag"] + "\ncommit: " + metadata["commit"] + "\n"): raise ValueError("linked release metadata mismatch") with (directory / "SHA256SUMS").open("w", encoding="ascii", newline="\n") as manifest: manifest.write(checksums(directory, names)) diff --git a/scripts/test_release_assets.py b/scripts/test_release_assets.py index 8a250a3..b071063 100644 --- a/scripts/test_release_assets.py +++ b/scripts/test_release_assets.py @@ -184,7 +184,7 @@ def build_run(arguments, cwd=None, env=None): Path(arguments[arguments.index("-o") + 1]).write_bytes((env["GOOS"] + env["GOARCH"]).encode()) return "" self.assertTrue(arguments[0].endswith("darwin_arm64")) - return f"jev {TAG}\ncommit: {COMMIT}\n" + return f"jevwise {TAG}\ncommit: {COMMIT}\n" with patch.object(release, "run", side_effect=build_run) as run: release.build(META, Path(temp), directory) self.assertEqual(sum(call.args[0][:2] == ["go", "build"] for call in run.call_args_list), 6) @@ -194,6 +194,23 @@ def build_run(arguments, cwd=None, env=None): with patch.dict(os.environ, {"GH_TOKEN": "secret"}), patch.object(release, "run", side_effect=[COMMIT, ""]), self.assertRaises(ValueError): release.build(META, Path(temp), Path(temp) / "blocked") + def test_legacy_tag_native_identity_remains_supported(self): + for tag in sorted(release.LEGACY_TAGS): + with self.subTest(tag=tag), tempfile.TemporaryDirectory() as temp, patch.dict(os.environ, {}, clear=True): + def build_run(arguments, cwd=None, env=None): + if arguments[:3] == ["git", "rev-parse", "HEAD"]: + return COMMIT + if arguments[0] == "git": + return "" + if arguments[:2] == ["go", "env"]: + return {"GOHOSTOS": "darwin", "GOHOSTARCH": "arm64"}[arguments[2]] + if arguments[:2] == ["go", "build"]: + Path(arguments[arguments.index("-o") + 1]).write_bytes(b"fixture") + return "" + return f"jev {tag}\ncommit: {COMMIT}\n" + with patch.object(release, "run", side_effect=build_run): + release.build(dict(META, tag=tag), Path(temp), Path(temp) / "assets") + def test_publication_preflight_identity_pagination_and_manifest_last(self): with tempfile.TemporaryDirectory() as temp: directory = Path(temp)