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
6 changes: 4 additions & 2 deletions .github/workflows/_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,12 @@ jobs:
done
- name: Add shell completion loaders
# Generated by prepack during npm pack.
run: cp completions/seam.bash completions/seam.fish completions/seam.zsh release/
run: |
version=$(jq --raw-output '.version' package.json)
tar -czf "release/seam-completions-v${version}.tar.gz" -C completions seam.bash seam.fish seam.zsh
- name: Generate checksums
working-directory: release
run: sha256sum seam* > checksums.txt
run: sha256sum seam-* > checksums.txt
- name: Upload artifact
uses: actions/upload-artifact@v7
with:
Expand Down
14 changes: 6 additions & 8 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
prerelease: ${{ contains(github.ref_name, '-') }}
files: |
*.tgz
release/seam*
release/seam-*
release/checksums.txt
body_path: ${{ github.workspace }}/${{ steps.changelog.outputs.outfile }}
npm:
Expand Down Expand Up @@ -113,20 +113,18 @@ jobs:
conflicts=('seam')
options=('!strip' '!debug')
source=("\${url}/raw/v\${pkgver}/LICENSE.txt"
"seam-\${pkgver}.bash::\${url}/releases/download/v\${pkgver}/seam.bash"
"seam-\${pkgver}.fish::\${url}/releases/download/v\${pkgver}/seam.fish"
"seam-\${pkgver}.zsh::\${url}/releases/download/v\${pkgver}/seam.zsh")
"\${url}/releases/download/v\${pkgver}/seam-completions-v\${pkgver}.tar.gz")
source_x86_64=("\${pkgname}-\${pkgver}-x86_64::\${url}/releases/download/v\${pkgver}/seam-v\${pkgver}-linux-x64")
source_aarch64=("\${pkgname}-\${pkgver}-aarch64::\${url}/releases/download/v\${pkgver}/seam-v\${pkgver}-linux-arm64")
sha256sums=('SKIP' 'SKIP' 'SKIP' 'SKIP')
sha256sums=('SKIP' 'SKIP')
sha256sums_x86_64=('SKIP')
sha256sums_aarch64=('SKIP')

package() {
install -Dm755 "\${pkgname}-\${pkgver}-\${CARCH}" "\${pkgdir}/usr/bin/seam"
install -Dm644 "seam-\${pkgver}.bash" "\${pkgdir}/usr/share/bash-completion/completions/seam"
install -Dm644 "seam-\${pkgver}.fish" "\${pkgdir}/usr/share/fish/vendor_completions.d/seam.fish"
install -Dm644 "seam-\${pkgver}.zsh" "\${pkgdir}/usr/share/zsh/site-functions/_seam"
install -Dm644 seam.bash "\${pkgdir}/usr/share/bash-completion/completions/seam"
install -Dm644 seam.fish "\${pkgdir}/usr/share/fish/vendor_completions.d/seam.fish"
install -Dm644 seam.zsh "\${pkgdir}/usr/share/zsh/site-functions/_seam"
install -Dm644 LICENSE.txt "\${pkgdir}/usr/share/licenses/\${pkgname}/LICENSE"
}
PKGBUILD
Expand Down
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,17 +202,28 @@ seam completion zsh > "${fpath[1]}/_seam"
```

System packages install completion loaders instead: small scripts packaged
under `completions/` in the published package and attached to each
[GitHub release]. A loader runs `seam completion` the first time the shell
completes a seam command, so installed completions always match the CLI's
current Seam API definitions and never go stale between package updates. The
`seam-bin` AUR package installs the loaders for all three shells.
under `completions/` in the published package, and released as
`seam-completions-v<version>.tar.gz` on each [GitHub release]. A loader runs
`seam completion` the first time the shell completes a seam command, so
installed completions always match the CLI's current Seam API definitions and
never go stale between package updates. The `seam-bin` AUR package installs
the loaders for all three shells.

Completions are generated from the cached Seam API definitions, so they may
briefly lag a newly released API. Pass `--update` to refresh the cache first,
e.g., `seam completion bash --update`. They do not reflect definitions served
by another Seam API server when `seam config use-remote-api-defs` is enabled.

If completions do not appear after installing them system wide:

- Bash reads them via the [bash-completion] package,
so it must be installed and sourced by the shell.
- Zsh caches the completion functions it found at startup: after installing,
rebuild the cache with `rm -f ~/.zcompdump*` and start a new shell.
This applies to frameworks that call `compinit -C`, e.g., oh-my-zsh.
- Fish needs nothing extra: completions load on demand in new sessions.

[bash-completion]: https://github.com/scop/bash-completion
[GitHub release]: https://github.com/seamapi/cli/releases/latest

## Development and Testing
Expand Down
14 changes: 14 additions & 0 deletions src/lib/completion/completion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { expect, test } from 'vitest'
import { testBlueprint } from '../../../test/fixtures/blueprint.js'
import { describeForShell } from './describe.js'
import {
completionScriptSentinels,
completionShells,
isCompletionShell,
renderCompletion,
Expand Down Expand Up @@ -76,6 +77,19 @@ test.each(completionShells)(
},
)

test.each(completionShells)(
'%s completion stub: evaluates only what the script generator produces',
(shell) => {
const sentinel = completionScriptSentinels[shell]
// The stub requires the sentinel, and the generated script provides it
// as its exact first line, so the two cannot drift apart.
expect(renderCompletionStub(shell)).toContain(sentinel)
expect(
renderCompletion(shell, testBlueprint).startsWith(`${sentinel}\n`),
).toBe(true)
},
)

test('zsh completion stub: is an autoloadable completion function', () => {
expect(renderCompletionStub('zsh').startsWith('#compdef seam\n')).toBe(true)
})
Expand Down
42 changes: 35 additions & 7 deletions src/lib/completion/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,24 @@ export const renderCompletion = (
* at first completion, never at shell startup.
*
* The loader degrades to no completions when the seam command is missing or
* cannot produce a script, e.g., offline before the definitions are cached.
* does not produce a completion script: a script is only evaluated when it
* starts with the exact first line 'seam completion' generates, so nothing
* else the CLI may print, e.g., 'Not logged in' from a version without the
* completion command, is ever evaluated as shell code.
*/
export const renderCompletionStub = (shell: CompletionShell): string =>
stubs[shell]

/**
* First line of each generated completion script, which the loaders require
* before evaluating one. Must match the output of {@link renderCompletion}.
*/
export const completionScriptSentinels: Record<CompletionShell, string> = {
bash: '# bash completion for the seam command.',
fish: '# fish completion for the seam command.',
zsh: '#compdef seam',
}

const stubHeader = (shell: CompletionShell): string =>
`# ${shell} completion loader for the seam command.
#
Expand All @@ -58,25 +71,40 @@ const stubs: Record<CompletionShell, string> = {
# Install to /usr/share/bash-completion/completions/seam

if command -v seam > /dev/null 2>&1; then
eval "$(seam completion bash 2> /dev/null)"
__seam_completion_script="$(seam completion bash 2> /dev/null)"
# Evaluate only a completion script, never anything else the CLI printed.
case "$__seam_completion_script" in
'${completionScriptSentinels.bash}'*) eval "$__seam_completion_script" ;;
esac
unset __seam_completion_script
fi
`,
fish: `${stubHeader('fish')}
#
# Install to /usr/share/fish/vendor_completions.d/seam.fish

if command --query seam
seam completion fish 2> /dev/null | source
set -l __seam_completion_script (seam completion fish 2> /dev/null | string collect)
# Source only a completion script, never anything else the CLI printed.
if string match --quiet '${completionScriptSentinels.fish}*' -- $__seam_completion_script
printf '%s\\n' $__seam_completion_script | source
end
end
`,
zsh: `#compdef seam
${stubHeader('zsh')}
#
# Install to a directory in fpath as _seam

# The generated script ends by dispatching on funcstack, so evaluating it
# while this autoloaded _seam runs both redefines _seam and completes the
# in-flight request.
eval "$(seam completion zsh 2> /dev/null)"
local __seam_completion_script
__seam_completion_script="$(seam completion zsh 2> /dev/null)"

# Evaluate only a completion script, never anything else the CLI printed.
# The script ends by dispatching on funcstack, so evaluating it while this
# autoloaded _seam runs both redefines _seam and completes the in-flight
# request.
if [[ "$__seam_completion_script" == '${completionScriptSentinels.zsh}'* ]]; then
eval "$__seam_completion_script"
fi
`,
}
Loading