Skip to content

feat: shell completion - #4

Merged
a3chron merged 8 commits into
mainfrom
feat/shell-completion
Aug 26, 2026
Merged

feat: shell completion#4
a3chron merged 8 commits into
mainfrom
feat/shell-completion

Conversation

@a3chron

@a3chron a3chron commented Aug 26, 2026

Copy link
Copy Markdown
Owner

No description provided.

a3chron and others added 8 commits August 26, 2026 14:49
Whitespace only, no behavioural change. These two files were left
unformatted by earlier commits and `gofmt -l` flagged them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Completes `author/slug@version` in three stages (author, slug, version)
for apply, preview, info and remove, sourced from the local theme cache
so TAB is instant and works offline. Set STELLAR_COMPLETION_ONLINE=1 to
additionally query the hub, via a 2s-timeout client that degrades
silently to local-only results when the hub is slow or unreachable.

Supporting changes:

- internal/cache: ListAuthors, ListAuthorThemes and ListThemeVersions,
  all returning (nil, nil) for a missing directory so completion never
  fails on a cache that doesn't exist yet.
- internal/api: NewCompletionClient (2s instead of 30s) and
  SearchThemesByAuthorName over GET /api/themes?authorName=.
- internal/theme: IsValidSegment and IsValidVersion, derived from the
  same character class and grammar as ParseIdentifier, so a name that
  apply would reject is never suggested. Every candidate is gated on
  them, local ones included: the cache is a plain directory that a
  synced dotfiles checkout can put anything in, and candidates are
  printed straight into the user's terminal.
- Hub candidates must prefix-match the typed word case-sensitively.
  bash's compgen and zsh's compadd filter that way themselves, so
  emitting the hub's canonical casing worked in fish and PowerShell
  only.
- No-argument commands get cobra.NoFileCompletions; without a
  ValidArgsFunction cobra returns ShellCompDirectiveDefault and the
  shell offers the user's filenames after e.g. `stellar list`.
- The root PersistentPreRunE is skipped for completion requests. It ran
  a directory cleanup and MkdirAll on every keystroke, and an error
  there suppresses the directive line entirely - which every shell
  reads as "offer filenames", silently breaking completion on a
  read-only HOME.
- internal/testutil: mock GET /api/themes, per-path request counters, a
  distinct opaque author id (the hub's id and name are unrelated, and
  routes resolve authors by name), limit/offset handling, and a
  snapshot-under-mutex fix for a data race in the theme handlers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`((passed++))` evaluates to the pre-increment value, so the arithmetic
command exits 1 the first time it runs with passed=0 - and under
`set -e` that killed the script right after printing the first PASS.
The suite itself was fine; only the runner bailed out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The release workflow built and published binaries without ever running
the test suite, so a tag was the first thing to find out whether the
code worked. goreleaser now needs a test job as well as lint.

Linux runs with -race and is blocking. Windows runs too, since it's the
platform whose copy-vs-symlink path most needs native coverage, but is
marked continue-on-error: the suite has never been run on a Windows
runner, and a release is the wrong moment to discover that. Drop that
line once it's been green once.

Also switched all three jobs from a pinned go-version: 1.24 to
go-version-file: go.mod. The module requires 1.25.5, so the pin made
every job download a newer toolchain before doing any work, and it can
now no longer drift from the module.

Contract tests stay out of CI - they're behind the `contract` build tag
and hit the live hub.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tests only ran on a tag push, so anything broken on main stayed
invisible until release time - which is exactly when you don't want to
find out.

lint, test and the go.mod checks move into a reusable checks.yaml that
both ci.yaml (pull requests, and pushes to main) and release.yaml call.
Defining them once means the checks gating a release are by construction
the same ones the PR had to pass, rather than two lists that drift.

The module checks used to live inside the goreleaser job, where they
only ever ran during a release; they now run on every PR as well.

A PR from a branch in this repo fires only the pull_request event, since
its push doesn't match main, so this doesn't double up. In-flight runs
are cancelled when a PR gets a new push.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first native Windows CI run failed in ~25 places. All of it is the
tests assuming a Unix filesystem - product code is unaffected, and
nothing here changes behaviour for Windows users.

- testutil.CreateConfig: call sites build config.json by concatenating a
  path into a JSON string, and "C:\Users\..." makes that invalid JSON -
  \U is not a valid escape, so every affected test failed with "invalid
  character 'U' in string escape code". This was the bulk of the
  failures (rollback, current, remove, clean). Backslashes are now
  escaped in the helper rather than at all ~30 call sites; on Unix it's
  a no-op. Config writing in internal/config already used
  json.MarshalIndent and was never affected.
- e2e apply: asserted the config file contains the raw theme path, but
  JSON stores it with escaped separators. Compares the encoded form now.
- theme parser: two expectations built paths with "/" instead of
  filepath.Join, so they could only ever match on Unix.
- completion: the hostile-cache-entry test staged a directory with an
  ANSI escape in its name. Windows rejects control characters in
  filenames outright, so that case is now Unix-only - it's also the only
  platform where the threat exists.
- update: staging a leftover next to the *running* test binary hits a
  Windows sharing violation, since every executed command runs
  cleanupUpdateLeftovers and a pending delete blocks re-creating the
  name. It skips with the reason instead of failing; the removal logic
  is covered against a temp dir by TestRemoveUpdateLeftovers.

Verified on Linux only - the Windows leg is still continue-on-error, so
the next run reports without blocking.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Last Windows holdout. previousPath was assembled with "/" and then
compared against the symlink target stellar writes, which uses the
platform separator - so the assertion could only ever match on Unix.

Swept the rest of the suite for the same shape: the other occurrences
are paths handed to file APIs (Windows accepts "/" there) or fed into
config JSON for reading back, and every other assert.Equal against a
symlink target already uses a CreateThemeFile path, which is
filepath.Join-built. This was the only comparison that broke.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It's been green on a native runner, which is what the advisory
continue-on-error was waiting for. Both legs now gate pull requests and
releases, so the copy-vs-symlink path can't regress unnoticed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@a3chron
a3chron merged commit 8e0b4b2 into main Aug 26, 2026
4 checks passed
@a3chron
a3chron deleted the feat/shell-completion branch August 26, 2026 14:55
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.

1 participant