diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..449938c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,126 @@ +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + packages: write + issues: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.22' + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + if: env.DOCKER_USERNAME != '' + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + if: env.DOCKER_USERNAME != '' + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Run tests + run: | + go test -v ./... + ./scripts/ci.sh + + - name: Generate completions + run: | + go build -o cu ./cmd/cu + ./cu completion bash > completions/cu.bash + ./cu completion zsh > completions/_cu + ./cu completion fish > completions/cu.fish + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v5 + with: + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + GPG_FINGERPRINT: ${{ secrets.GPG_FINGERPRINT }} + + - name: Create Release Issue + uses: actions/github-script@v7 + if: success() + with: + script: | + const tag = context.ref.replace('refs/tags/', ''); + const issue = await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: `Release ${tag} - Post-Release Tasks`, + body: `## Release ${tag} has been published! 🎉 + + ### Automated Tasks Completed: + - [x] Built binaries for all platforms + - [x] Created GitHub release with artifacts + - [x] Generated checksums + - [x] Updated Homebrew tap (if token provided) + - [x] Published Docker images (if credentials provided) + + ### Manual Tasks Required: + - [ ] Verify Homebrew installation: \`brew install timimsms/clickup/clickup-cli\` + - [ ] Test Docker image: \`docker run clickup/cli:${tag} --version\` + - [ ] Update documentation if needed + - [ ] Announce release on social media + - [ ] Close this issue when all tasks are complete + + ### Release Notes: + https://github.com/${context.repo.owner}/${context.repo.repo}/releases/tag/${tag}`, + labels: ['release', 'documentation'] + }); + console.log(`Created issue #${issue.data.number}`); + + test-install: + needs: release + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Test direct download + shell: bash + run: | + # Determine OS and architecture + OS=$(echo "${{ runner.os }}" | tr '[:upper:]' '[:lower:]') + ARCH="x86_64" + if [ "$OS" = "macos" ]; then OS="darwin"; fi + if [ "$OS" = "windows" ]; then + OS="windows" + EXT=".zip" + else + EXT=".tar.gz" + fi + + # Download and extract + TAG="${{ github.ref_name }}" + URL="https://github.com/${{ github.repository }}/releases/download/${TAG}/cu_${OS}_${ARCH}${EXT}" + echo "Downloading from: $URL" + + if [ "$OS" = "windows" ]; then + curl -L -o cu.zip "$URL" + unzip cu.zip + ./cu.exe --version + else + curl -L "$URL" | tar xz + ./cu --version + fi \ No newline at end of file diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..949076d --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,159 @@ +# GoReleaser configuration for ClickUp CLI +# Documentation: https://goreleaser.com +version: 2 + +project_name: cu + +before: + hooks: + - go mod tidy + - go generate ./... + +builds: + - id: cu + main: ./cmd/cu + binary: cu + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + - windows + goarch: + - amd64 + - arm64 + - "386" + goarm: + - "7" + # Skip unsupported combinations + ignore: + - goos: darwin + goarch: "386" + - goos: windows + goarch: arm64 + # Build flags for smaller binaries + ldflags: + - -s -w + - -X github.com/tim/cu/internal/version.Version={{.Version}} + - -X github.com/tim/cu/internal/version.Commit={{.Commit}} + - -X github.com/tim/cu/internal/version.BuildDate={{.Date}} + - -X github.com/tim/cu/internal/version.BuiltBy=goreleaser + +archives: + - id: cu-archive + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + builds: + - cu + format: tar.gz + format_overrides: + - goos: windows + format: zip + files: + - README.md + - LICENSE + - CHANGELOG.md + - src: completions/* + dst: completions + +checksum: + name_template: 'checksums.txt' + algorithm: sha256 + +signs: + - artifacts: checksum + cmd: gpg + args: + - "--batch" + - "--local-user" + - "{{ .Env.GPG_FINGERPRINT }}" + - "--output" + - "${signature}" + - "--detach-sign" + - "${artifact}" + +sboms: + - artifacts: archive + documents: + - "{{ .ArtifactName }}.sbom.json" + +snapshot: + version_template: "{{ incpatch .Version }}-dev" + +changelog: + sort: asc + use: github + filters: + exclude: + - '^docs:' + - '^test:' + - '^chore:' + - '^ci:' + groups: + - title: 'New Features' + regexp: '^feat' + - title: 'Bug Fixes' + regexp: '^fix' + - title: 'Performance Improvements' + regexp: '^perf' + - title: 'Breaking Changes' + regexp: '^breaking' + +release: + github: + owner: timimsms + name: cu + draft: false + prerelease: auto + name_template: "{{.ProjectName}} v{{.Version}}" + header: | + ## ClickUp CLI v{{.Version}} + + **Full Changelog**: https://github.com/timimsms/cu/compare/{{ .PreviousTag }}...{{ .Tag }} + footer: | + ## Installation + + ### Homebrew (macOS/Linux) + ```bash + brew install timimsms/clickup/clickup-cli + ``` + + ### npm + ```bash + npm install -g @clickup/cli + ``` + + ### Docker + ```bash + docker run -it clickup/cli:{{ .Version }} + ``` + + ### Direct Download + See the assets below for platform-specific binaries. + +# Homebrew tap configuration (will create PR to separate tap repo) +# Requires HOMEBREW_TAP_GITHUB_TOKEN secret +# brews section is deprecated in v2, using separate action in workflow instead + +# Docker configuration +dockers: + - image_templates: + - "clickup/cli:{{ .Tag }}" + - "clickup/cli:v{{ .Major }}" + - "clickup/cli:v{{ .Major }}.{{ .Minor }}" + - "clickup/cli:latest" + dockerfile: Dockerfile.release + build_flag_templates: + - "--pull" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + - "--platform=linux/amd64,linux/arm64" + extra_files: + - completions/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1cdaeaa --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,35 @@ +# Changelog + +All notable changes to the ClickUp CLI will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- Initial release of ClickUp CLI +- Core task management commands (list, create, view, update, close, reopen, delete) +- Bulk operations for efficient task management +- Export functionality with CSV, JSON, and Markdown formats +- Interactive mode with fuzzy search +- Secure authentication using OS keychain +- Multi-format output support (table, JSON, YAML, CSV) +- Comprehensive caching layer for performance +- Shell completion for bash, zsh, and fish + +### Changed +- Nothing yet + +### Deprecated +- Nothing yet + +### Removed +- Nothing yet + +### Fixed +- Nothing yet + +### Security +- Implemented secure token storage using OS-specific keychains +- Added file path sanitization to prevent directory traversal attacks \ No newline at end of file diff --git a/Dockerfile.release b/Dockerfile.release new file mode 100644 index 0000000..126f570 --- /dev/null +++ b/Dockerfile.release @@ -0,0 +1,30 @@ +# Dockerfile for ClickUp CLI releases +# This is used by GoReleaser for Docker image builds + +FROM alpine:3.19 + +# Install ca-certificates for HTTPS requests +RUN apk --no-cache add ca-certificates + +# Create non-root user +RUN addgroup -g 1000 -S cu && \ + adduser -u 1000 -S cu -G cu + +# Copy binary from GoReleaser +COPY cu /usr/local/bin/cu + +# Copy shell completions +COPY completions /usr/share/cu/completions + +# Set up working directory +WORKDIR /workspace + +# Switch to non-root user +USER cu + +# Verify installation +RUN cu --version + +# Set entrypoint +ENTRYPOINT ["cu"] +CMD ["--help"] \ No newline at end of file diff --git a/completions/_cu b/completions/_cu new file mode 100644 index 0000000..75bcae0 --- /dev/null +++ b/completions/_cu @@ -0,0 +1,212 @@ +#compdef cu +compdef _cu cu + +# zsh completion for cu -*- shell-script -*- + +__cu_debug() +{ + local file="$BASH_COMP_DEBUG_FILE" + if [[ -n ${file} ]]; then + echo "$*" >> "${file}" + fi +} + +_cu() +{ + local shellCompDirectiveError=1 + local shellCompDirectiveNoSpace=2 + local shellCompDirectiveNoFileComp=4 + local shellCompDirectiveFilterFileExt=8 + local shellCompDirectiveFilterDirs=16 + local shellCompDirectiveKeepOrder=32 + + local lastParam lastChar flagPrefix requestComp out directive comp lastComp noSpace keepOrder + local -a completions + + __cu_debug "\n========= starting completion logic ==========" + __cu_debug "CURRENT: ${CURRENT}, words[*]: ${words[*]}" + + # The user could have moved the cursor backwards on the command-line. + # We need to trigger completion from the $CURRENT location, so we need + # to truncate the command-line ($words) up to the $CURRENT location. + # (We cannot use $CURSOR as its value does not work when a command is an alias.) + words=("${=words[1,CURRENT]}") + __cu_debug "Truncated words[*]: ${words[*]}," + + lastParam=${words[-1]} + lastChar=${lastParam[-1]} + __cu_debug "lastParam: ${lastParam}, lastChar: ${lastChar}" + + # For zsh, when completing a flag with an = (e.g., cu -n=) + # completions must be prefixed with the flag + setopt local_options BASH_REMATCH + if [[ "${lastParam}" =~ '-.*=' ]]; then + # We are dealing with a flag with an = + flagPrefix="-P ${BASH_REMATCH}" + fi + + # Prepare the command to obtain completions + requestComp="${words[1]} __complete ${words[2,-1]}" + if [ "${lastChar}" = "" ]; then + # If the last parameter is complete (there is a space following it) + # We add an extra empty parameter so we can indicate this to the go completion code. + __cu_debug "Adding extra empty parameter" + requestComp="${requestComp} \"\"" + fi + + __cu_debug "About to call: eval ${requestComp}" + + # Use eval to handle any environment variables and such + out=$(eval ${requestComp} 2>/dev/null) + __cu_debug "completion output: ${out}" + + # Extract the directive integer following a : from the last line + local lastLine + while IFS='\n' read -r line; do + lastLine=${line} + done < <(printf "%s\n" "${out[@]}") + __cu_debug "last line: ${lastLine}" + + if [ "${lastLine[1]}" = : ]; then + directive=${lastLine[2,-1]} + # Remove the directive including the : and the newline + local suffix + (( suffix=${#lastLine}+2)) + out=${out[1,-$suffix]} + else + # There is no directive specified. Leave $out as is. + __cu_debug "No directive found. Setting do default" + directive=0 + fi + + __cu_debug "directive: ${directive}" + __cu_debug "completions: ${out}" + __cu_debug "flagPrefix: ${flagPrefix}" + + if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then + __cu_debug "Completion received error. Ignoring completions." + return + fi + + local activeHelpMarker="_activeHelp_ " + local endIndex=${#activeHelpMarker} + local startIndex=$((${#activeHelpMarker}+1)) + local hasActiveHelp=0 + while IFS='\n' read -r comp; do + # Check if this is an activeHelp statement (i.e., prefixed with $activeHelpMarker) + if [ "${comp[1,$endIndex]}" = "$activeHelpMarker" ];then + __cu_debug "ActiveHelp found: $comp" + comp="${comp[$startIndex,-1]}" + if [ -n "$comp" ]; then + compadd -x "${comp}" + __cu_debug "ActiveHelp will need delimiter" + hasActiveHelp=1 + fi + + continue + fi + + if [ -n "$comp" ]; then + # If requested, completions are returned with a description. + # The description is preceded by a TAB character. + # For zsh's _describe, we need to use a : instead of a TAB. + # We first need to escape any : as part of the completion itself. + comp=${comp//:/\\:} + + local tab="$(printf '\t')" + comp=${comp//$tab/:} + + __cu_debug "Adding completion: ${comp}" + completions+=${comp} + lastComp=$comp + fi + done < <(printf "%s\n" "${out[@]}") + + # Add a delimiter after the activeHelp statements, but only if: + # - there are completions following the activeHelp statements, or + # - file completion will be performed (so there will be choices after the activeHelp) + if [ $hasActiveHelp -eq 1 ]; then + if [ ${#completions} -ne 0 ] || [ $((directive & shellCompDirectiveNoFileComp)) -eq 0 ]; then + __cu_debug "Adding activeHelp delimiter" + compadd -x "--" + hasActiveHelp=0 + fi + fi + + if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then + __cu_debug "Activating nospace." + noSpace="-S ''" + fi + + if [ $((directive & shellCompDirectiveKeepOrder)) -ne 0 ]; then + __cu_debug "Activating keep order." + keepOrder="-V" + fi + + if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then + # File extension filtering + local filteringCmd + filteringCmd='_files' + for filter in ${completions[@]}; do + if [ ${filter[1]} != '*' ]; then + # zsh requires a glob pattern to do file filtering + filter="\*.$filter" + fi + filteringCmd+=" -g $filter" + done + filteringCmd+=" ${flagPrefix}" + + __cu_debug "File filtering command: $filteringCmd" + _arguments '*:filename:'"$filteringCmd" + elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then + # File completion for directories only + local subdir + subdir="${completions[1]}" + if [ -n "$subdir" ]; then + __cu_debug "Listing directories in $subdir" + pushd "${subdir}" >/dev/null 2>&1 + else + __cu_debug "Listing directories in ." + fi + + local result + _arguments '*:dirname:_files -/'" ${flagPrefix}" + result=$? + if [ -n "$subdir" ]; then + popd >/dev/null 2>&1 + fi + return $result + else + __cu_debug "Calling _describe" + if eval _describe $keepOrder "completions" completions $flagPrefix $noSpace; then + __cu_debug "_describe found some completions" + + # Return the success of having called _describe + return 0 + else + __cu_debug "_describe did not find completions." + __cu_debug "Checking if we should do file completion." + if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then + __cu_debug "deactivating file completion" + + # We must return an error code here to let zsh know that there were no + # completions found by _describe; this is what will trigger other + # matching algorithms to attempt to find completions. + # For example zsh can match letters in the middle of words. + return 1 + else + # Perform file completion + __cu_debug "Activating file completion" + + # We must return the result of this command, so it must be the + # last command, or else we must store its result to return it. + _arguments '*:filename:_files'" ${flagPrefix}" + fi + fi + fi +} + +# don't run the completion function when being source-ed or eval-ed +if [ "$funcstack[1]" = "_cu" ]; then + _cu +fi diff --git a/completions/cu.bash b/completions/cu.bash new file mode 100644 index 0000000..74301a6 --- /dev/null +++ b/completions/cu.bash @@ -0,0 +1,1644 @@ +# bash completion for cu -*- shell-script -*- + +__cu_debug() +{ + if [[ -n ${BASH_COMP_DEBUG_FILE:-} ]]; then + echo "$*" >> "${BASH_COMP_DEBUG_FILE}" + fi +} + +# Homebrew on Macs have version 1.3 of bash-completion which doesn't include +# _init_completion. This is a very minimal version of that function. +__cu_init_completion() +{ + COMPREPLY=() + _get_comp_words_by_ref "$@" cur prev words cword +} + +__cu_index_of_word() +{ + local w word=$1 + shift + index=0 + for w in "$@"; do + [[ $w = "$word" ]] && return + index=$((index+1)) + done + index=-1 +} + +__cu_contains_word() +{ + local w word=$1; shift + for w in "$@"; do + [[ $w = "$word" ]] && return + done + return 1 +} + +__cu_handle_go_custom_completion() +{ + __cu_debug "${FUNCNAME[0]}: cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}" + + local shellCompDirectiveError=1 + local shellCompDirectiveNoSpace=2 + local shellCompDirectiveNoFileComp=4 + local shellCompDirectiveFilterFileExt=8 + local shellCompDirectiveFilterDirs=16 + + local out requestComp lastParam lastChar comp directive args + + # Prepare the command to request completions for the program. + # Calling ${words[0]} instead of directly cu allows handling aliases + args=("${words[@]:1}") + # Disable ActiveHelp which is not supported for bash completion v1 + requestComp="CU_ACTIVE_HELP=0 ${words[0]} __completeNoDesc ${args[*]}" + + lastParam=${words[$((${#words[@]}-1))]} + lastChar=${lastParam:$((${#lastParam}-1)):1} + __cu_debug "${FUNCNAME[0]}: lastParam ${lastParam}, lastChar ${lastChar}" + + if [ -z "${cur}" ] && [ "${lastChar}" != "=" ]; then + # If the last parameter is complete (there is a space following it) + # We add an extra empty parameter so we can indicate this to the go method. + __cu_debug "${FUNCNAME[0]}: Adding extra empty parameter" + requestComp="${requestComp} \"\"" + fi + + __cu_debug "${FUNCNAME[0]}: calling ${requestComp}" + # Use eval to handle any environment variables and such + out=$(eval "${requestComp}" 2>/dev/null) + + # Extract the directive integer at the very end of the output following a colon (:) + directive=${out##*:} + # Remove the directive + out=${out%:*} + if [ "${directive}" = "${out}" ]; then + # There is not directive specified + directive=0 + fi + __cu_debug "${FUNCNAME[0]}: the completion directive is: ${directive}" + __cu_debug "${FUNCNAME[0]}: the completions are: ${out}" + + if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then + # Error code. No completion. + __cu_debug "${FUNCNAME[0]}: received error from custom completion go code" + return + else + if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then + if [[ $(type -t compopt) = "builtin" ]]; then + __cu_debug "${FUNCNAME[0]}: activating no space" + compopt -o nospace + fi + fi + if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then + if [[ $(type -t compopt) = "builtin" ]]; then + __cu_debug "${FUNCNAME[0]}: activating no file completion" + compopt +o default + fi + fi + fi + + if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then + # File extension filtering + local fullFilter filter filteringCmd + # Do not use quotes around the $out variable or else newline + # characters will be kept. + for filter in ${out}; do + fullFilter+="$filter|" + done + + filteringCmd="_filedir $fullFilter" + __cu_debug "File filtering command: $filteringCmd" + $filteringCmd + elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then + # File completion for directories only + local subdir + # Use printf to strip any trailing newline + subdir=$(printf "%s" "${out}") + if [ -n "$subdir" ]; then + __cu_debug "Listing directories in $subdir" + __cu_handle_subdirs_in_dir_flag "$subdir" + else + __cu_debug "Listing directories in ." + _filedir -d + fi + else + while IFS='' read -r comp; do + COMPREPLY+=("$comp") + done < <(compgen -W "${out}" -- "$cur") + fi +} + +__cu_handle_reply() +{ + __cu_debug "${FUNCNAME[0]}" + local comp + case $cur in + -*) + if [[ $(type -t compopt) = "builtin" ]]; then + compopt -o nospace + fi + local allflags + if [ ${#must_have_one_flag[@]} -ne 0 ]; then + allflags=("${must_have_one_flag[@]}") + else + allflags=("${flags[*]} ${two_word_flags[*]}") + fi + while IFS='' read -r comp; do + COMPREPLY+=("$comp") + done < <(compgen -W "${allflags[*]}" -- "$cur") + if [[ $(type -t compopt) = "builtin" ]]; then + [[ "${COMPREPLY[0]}" == *= ]] || compopt +o nospace + fi + + # complete after --flag=abc + if [[ $cur == *=* ]]; then + if [[ $(type -t compopt) = "builtin" ]]; then + compopt +o nospace + fi + + local index flag + flag="${cur%=*}" + __cu_index_of_word "${flag}" "${flags_with_completion[@]}" + COMPREPLY=() + if [[ ${index} -ge 0 ]]; then + PREFIX="" + cur="${cur#*=}" + ${flags_completion[${index}]} + if [ -n "${ZSH_VERSION:-}" ]; then + # zsh completion needs --flag= prefix + eval "COMPREPLY=( \"\${COMPREPLY[@]/#/${flag}=}\" )" + fi + fi + fi + + if [[ -z "${flag_parsing_disabled}" ]]; then + # If flag parsing is enabled, we have completed the flags and can return. + # If flag parsing is disabled, we may not know all (or any) of the flags, so we fallthrough + # to possibly call handle_go_custom_completion. + return 0; + fi + ;; + esac + + # check if we are handling a flag with special work handling + local index + __cu_index_of_word "${prev}" "${flags_with_completion[@]}" + if [[ ${index} -ge 0 ]]; then + ${flags_completion[${index}]} + return + fi + + # we are parsing a flag and don't have a special handler, no completion + if [[ ${cur} != "${words[cword]}" ]]; then + return + fi + + local completions + completions=("${commands[@]}") + if [[ ${#must_have_one_noun[@]} -ne 0 ]]; then + completions+=("${must_have_one_noun[@]}") + elif [[ -n "${has_completion_function}" ]]; then + # if a go completion function is provided, defer to that function + __cu_handle_go_custom_completion + fi + if [[ ${#must_have_one_flag[@]} -ne 0 ]]; then + completions+=("${must_have_one_flag[@]}") + fi + while IFS='' read -r comp; do + COMPREPLY+=("$comp") + done < <(compgen -W "${completions[*]}" -- "$cur") + + if [[ ${#COMPREPLY[@]} -eq 0 && ${#noun_aliases[@]} -gt 0 && ${#must_have_one_noun[@]} -ne 0 ]]; then + while IFS='' read -r comp; do + COMPREPLY+=("$comp") + done < <(compgen -W "${noun_aliases[*]}" -- "$cur") + fi + + if [[ ${#COMPREPLY[@]} -eq 0 ]]; then + if declare -F __cu_custom_func >/dev/null; then + # try command name qualified custom func + __cu_custom_func + else + # otherwise fall back to unqualified for compatibility + declare -F __custom_func >/dev/null && __custom_func + fi + fi + + # available in bash-completion >= 2, not always present on macOS + if declare -F __ltrim_colon_completions >/dev/null; then + __ltrim_colon_completions "$cur" + fi + + # If there is only 1 completion and it is a flag with an = it will be completed + # but we don't want a space after the = + if [[ "${#COMPREPLY[@]}" -eq "1" ]] && [[ $(type -t compopt) = "builtin" ]] && [[ "${COMPREPLY[0]}" == --*= ]]; then + compopt -o nospace + fi +} + +# The arguments should be in the form "ext1|ext2|extn" +__cu_handle_filename_extension_flag() +{ + local ext="$1" + _filedir "@(${ext})" +} + +__cu_handle_subdirs_in_dir_flag() +{ + local dir="$1" + pushd "${dir}" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return +} + +__cu_handle_flag() +{ + __cu_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" + + # if a command required a flag, and we found it, unset must_have_one_flag() + local flagname=${words[c]} + local flagvalue="" + # if the word contained an = + if [[ ${words[c]} == *"="* ]]; then + flagvalue=${flagname#*=} # take in as flagvalue after the = + flagname=${flagname%=*} # strip everything after the = + flagname="${flagname}=" # but put the = back + fi + __cu_debug "${FUNCNAME[0]}: looking for ${flagname}" + if __cu_contains_word "${flagname}" "${must_have_one_flag[@]}"; then + must_have_one_flag=() + fi + + # if you set a flag which only applies to this command, don't show subcommands + if __cu_contains_word "${flagname}" "${local_nonpersistent_flags[@]}"; then + commands=() + fi + + # keep flag value with flagname as flaghash + # flaghash variable is an associative array which is only supported in bash > 3. + if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then + if [ -n "${flagvalue}" ] ; then + flaghash[${flagname}]=${flagvalue} + elif [ -n "${words[ $((c+1)) ]}" ] ; then + flaghash[${flagname}]=${words[ $((c+1)) ]} + else + flaghash[${flagname}]="true" # pad "true" for bool flag + fi + fi + + # skip the argument to a two word flag + if [[ ${words[c]} != *"="* ]] && __cu_contains_word "${words[c]}" "${two_word_flags[@]}"; then + __cu_debug "${FUNCNAME[0]}: found a flag ${words[c]}, skip the next argument" + c=$((c+1)) + # if we are looking for a flags value, don't show commands + if [[ $c -eq $cword ]]; then + commands=() + fi + fi + + c=$((c+1)) + +} + +__cu_handle_noun() +{ + __cu_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" + + if __cu_contains_word "${words[c]}" "${must_have_one_noun[@]}"; then + must_have_one_noun=() + elif __cu_contains_word "${words[c]}" "${noun_aliases[@]}"; then + must_have_one_noun=() + fi + + nouns+=("${words[c]}") + c=$((c+1)) +} + +__cu_handle_command() +{ + __cu_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" + + local next_command + if [[ -n ${last_command} ]]; then + next_command="_${last_command}_${words[c]//:/__}" + else + if [[ $c -eq 0 ]]; then + next_command="_cu_root_command" + else + next_command="_${words[c]//:/__}" + fi + fi + c=$((c+1)) + __cu_debug "${FUNCNAME[0]}: looking for ${next_command}" + declare -F "$next_command" >/dev/null && $next_command +} + +__cu_handle_word() +{ + if [[ $c -ge $cword ]]; then + __cu_handle_reply + return + fi + __cu_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" + if [[ "${words[c]}" == -* ]]; then + __cu_handle_flag + elif __cu_contains_word "${words[c]}" "${commands[@]}"; then + __cu_handle_command + elif [[ $c -eq 0 ]]; then + __cu_handle_command + elif __cu_contains_word "${words[c]}" "${command_aliases[@]}"; then + # aliashash variable is an associative array which is only supported in bash > 3. + if [[ -z "${BASH_VERSION:-}" || "${BASH_VERSINFO[0]:-}" -gt 3 ]]; then + words[c]=${aliashash[${words[c]}]} + __cu_handle_command + else + __cu_handle_noun + fi + else + __cu_handle_noun + fi + __cu_handle_word +} + +_cu_auth_login() +{ + last_command="cu_auth_login" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--token=") + two_word_flags+=("--token") + two_word_flags+=("-t") + local_nonpersistent_flags+=("--token") + local_nonpersistent_flags+=("--token=") + local_nonpersistent_flags+=("-t") + flags+=("--workspace=") + two_word_flags+=("--workspace") + two_word_flags+=("-w") + local_nonpersistent_flags+=("--workspace") + local_nonpersistent_flags+=("--workspace=") + local_nonpersistent_flags+=("-w") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_auth_logout() +{ + last_command="cu_auth_logout" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--workspace=") + two_word_flags+=("--workspace") + two_word_flags+=("-w") + local_nonpersistent_flags+=("--workspace") + local_nonpersistent_flags+=("--workspace=") + local_nonpersistent_flags+=("-w") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_auth_status() +{ + last_command="cu_auth_status" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_auth() +{ + last_command="cu_auth" + + command_aliases=() + + commands=() + commands+=("login") + commands+=("logout") + commands+=("status") + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_bulk_close() +{ + last_command="cu_bulk_close" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--yes") + flags+=("-y") + local_nonpersistent_flags+=("--yes") + local_nonpersistent_flags+=("-y") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_bulk_delete() +{ + last_command="cu_bulk_delete" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--yes") + flags+=("-y") + local_nonpersistent_flags+=("--yes") + local_nonpersistent_flags+=("-y") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_bulk_update() +{ + last_command="cu_bulk_update" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--add-assignee=") + two_word_flags+=("--add-assignee") + local_nonpersistent_flags+=("--add-assignee") + local_nonpersistent_flags+=("--add-assignee=") + flags+=("--dry-run") + local_nonpersistent_flags+=("--dry-run") + flags+=("--priority=") + two_word_flags+=("--priority") + two_word_flags+=("-p") + local_nonpersistent_flags+=("--priority") + local_nonpersistent_flags+=("--priority=") + local_nonpersistent_flags+=("-p") + flags+=("--remove-assignee=") + two_word_flags+=("--remove-assignee") + local_nonpersistent_flags+=("--remove-assignee") + local_nonpersistent_flags+=("--remove-assignee=") + flags+=("--status=") + two_word_flags+=("--status") + two_word_flags+=("-s") + local_nonpersistent_flags+=("--status") + local_nonpersistent_flags+=("--status=") + local_nonpersistent_flags+=("-s") + flags+=("--tag=") + two_word_flags+=("--tag") + local_nonpersistent_flags+=("--tag") + local_nonpersistent_flags+=("--tag=") + flags+=("--yes") + flags+=("-y") + local_nonpersistent_flags+=("--yes") + local_nonpersistent_flags+=("-y") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_bulk() +{ + last_command="cu_bulk" + + command_aliases=() + + commands=() + commands+=("close") + commands+=("delete") + commands+=("update") + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_completion() +{ + last_command="cu_completion" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--help") + flags+=("-h") + local_nonpersistent_flags+=("--help") + local_nonpersistent_flags+=("-h") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + must_have_one_noun+=("bash") + must_have_one_noun+=("fish") + must_have_one_noun+=("powershell") + must_have_one_noun+=("zsh") + noun_aliases=() +} + +_cu_config_get() +{ + last_command="cu_config_get" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_config_list() +{ + last_command="cu_config_list" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_config_set() +{ + last_command="cu_config_set" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_config() +{ + last_command="cu_config" + + command_aliases=() + + commands=() + commands+=("get") + commands+=("list") + commands+=("set") + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_export_tasks() +{ + last_command="cu_export_tasks" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--assignee=") + two_word_flags+=("--assignee") + local_nonpersistent_flags+=("--assignee") + local_nonpersistent_flags+=("--assignee=") + flags+=("--format=") + two_word_flags+=("--format") + two_word_flags+=("-f") + local_nonpersistent_flags+=("--format") + local_nonpersistent_flags+=("--format=") + local_nonpersistent_flags+=("-f") + flags+=("--list=") + two_word_flags+=("--list") + two_word_flags+=("-l") + local_nonpersistent_flags+=("--list") + local_nonpersistent_flags+=("--list=") + local_nonpersistent_flags+=("-l") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + local_nonpersistent_flags+=("--output") + local_nonpersistent_flags+=("--output=") + local_nonpersistent_flags+=("-o") + flags+=("--priority=") + two_word_flags+=("--priority") + local_nonpersistent_flags+=("--priority") + local_nonpersistent_flags+=("--priority=") + flags+=("--space=") + two_word_flags+=("--space") + two_word_flags+=("-s") + local_nonpersistent_flags+=("--space") + local_nonpersistent_flags+=("--space=") + local_nonpersistent_flags+=("-s") + flags+=("--status=") + two_word_flags+=("--status") + local_nonpersistent_flags+=("--status") + local_nonpersistent_flags+=("--status=") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_export() +{ + last_command="cu_export" + + command_aliases=() + + commands=() + commands+=("tasks") + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_help() +{ + last_command="cu_help" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + has_completion_function=1 + noun_aliases=() +} + +_cu_interactive() +{ + last_command="cu_interactive" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_list_default() +{ + last_command="cu_list_default" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_list_list() +{ + last_command="cu_list_list" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--archived") + local_nonpersistent_flags+=("--archived") + flags+=("--folder=") + two_word_flags+=("--folder") + two_word_flags+=("-f") + local_nonpersistent_flags+=("--folder") + local_nonpersistent_flags+=("--folder=") + local_nonpersistent_flags+=("-f") + flags+=("--space=") + two_word_flags+=("--space") + two_word_flags+=("-s") + local_nonpersistent_flags+=("--space") + local_nonpersistent_flags+=("--space=") + local_nonpersistent_flags+=("-s") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_list() +{ + last_command="cu_list" + + command_aliases=() + + commands=() + commands+=("default") + commands+=("list") + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_me() +{ + last_command="cu_me" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_space_list() +{ + last_command="cu_space_list" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_space() +{ + last_command="cu_space" + + command_aliases=() + + commands=() + commands+=("list") + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_task_close() +{ + last_command="cu_task_close" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_task_create() +{ + last_command="cu_task_create" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--assignee=") + two_word_flags+=("--assignee") + two_word_flags+=("-a") + local_nonpersistent_flags+=("--assignee") + local_nonpersistent_flags+=("--assignee=") + local_nonpersistent_flags+=("-a") + flags+=("--description=") + two_word_flags+=("--description") + two_word_flags+=("-d") + local_nonpersistent_flags+=("--description") + local_nonpersistent_flags+=("--description=") + local_nonpersistent_flags+=("-d") + flags+=("--due=") + two_word_flags+=("--due") + local_nonpersistent_flags+=("--due") + local_nonpersistent_flags+=("--due=") + flags+=("--list=") + two_word_flags+=("--list") + two_word_flags+=("-l") + local_nonpersistent_flags+=("--list") + local_nonpersistent_flags+=("--list=") + local_nonpersistent_flags+=("-l") + flags+=("--name=") + two_word_flags+=("--name") + two_word_flags+=("-n") + local_nonpersistent_flags+=("--name") + local_nonpersistent_flags+=("--name=") + local_nonpersistent_flags+=("-n") + flags+=("--priority=") + two_word_flags+=("--priority") + two_word_flags+=("-p") + local_nonpersistent_flags+=("--priority") + local_nonpersistent_flags+=("--priority=") + local_nonpersistent_flags+=("-p") + flags+=("--status=") + two_word_flags+=("--status") + two_word_flags+=("-s") + local_nonpersistent_flags+=("--status") + local_nonpersistent_flags+=("--status=") + local_nonpersistent_flags+=("-s") + flags+=("--tag=") + two_word_flags+=("--tag") + local_nonpersistent_flags+=("--tag") + local_nonpersistent_flags+=("--tag=") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_task_interactive() +{ + last_command="cu_task_interactive" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_task_list() +{ + last_command="cu_task_list" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--assignee=") + two_word_flags+=("--assignee") + local_nonpersistent_flags+=("--assignee") + local_nonpersistent_flags+=("--assignee=") + flags+=("--due=") + two_word_flags+=("--due") + local_nonpersistent_flags+=("--due") + local_nonpersistent_flags+=("--due=") + flags+=("--folder=") + two_word_flags+=("--folder") + two_word_flags+=("-f") + local_nonpersistent_flags+=("--folder") + local_nonpersistent_flags+=("--folder=") + local_nonpersistent_flags+=("-f") + flags+=("--limit=") + two_word_flags+=("--limit") + local_nonpersistent_flags+=("--limit") + local_nonpersistent_flags+=("--limit=") + flags+=("--list=") + two_word_flags+=("--list") + two_word_flags+=("-l") + local_nonpersistent_flags+=("--list") + local_nonpersistent_flags+=("--list=") + local_nonpersistent_flags+=("-l") + flags+=("--order=") + two_word_flags+=("--order") + local_nonpersistent_flags+=("--order") + local_nonpersistent_flags+=("--order=") + flags+=("--page=") + two_word_flags+=("--page") + local_nonpersistent_flags+=("--page") + local_nonpersistent_flags+=("--page=") + flags+=("--priority=") + two_word_flags+=("--priority") + local_nonpersistent_flags+=("--priority") + local_nonpersistent_flags+=("--priority=") + flags+=("--sort=") + two_word_flags+=("--sort") + local_nonpersistent_flags+=("--sort") + local_nonpersistent_flags+=("--sort=") + flags+=("--space=") + two_word_flags+=("--space") + two_word_flags+=("-s") + local_nonpersistent_flags+=("--space") + local_nonpersistent_flags+=("--space=") + local_nonpersistent_flags+=("-s") + flags+=("--status=") + two_word_flags+=("--status") + local_nonpersistent_flags+=("--status") + local_nonpersistent_flags+=("--status=") + flags+=("--tag=") + two_word_flags+=("--tag") + local_nonpersistent_flags+=("--tag") + local_nonpersistent_flags+=("--tag=") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_task_reopen() +{ + last_command="cu_task_reopen" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--status=") + two_word_flags+=("--status") + two_word_flags+=("-s") + local_nonpersistent_flags+=("--status") + local_nonpersistent_flags+=("--status=") + local_nonpersistent_flags+=("-s") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_task_search() +{ + last_command="cu_task_search" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--include-description") + local_nonpersistent_flags+=("--include-description") + flags+=("--limit=") + two_word_flags+=("--limit") + local_nonpersistent_flags+=("--limit") + local_nonpersistent_flags+=("--limit=") + flags+=("--list=") + two_word_flags+=("--list") + two_word_flags+=("-l") + local_nonpersistent_flags+=("--list") + local_nonpersistent_flags+=("--list=") + local_nonpersistent_flags+=("-l") + flags+=("--space=") + two_word_flags+=("--space") + two_word_flags+=("-s") + local_nonpersistent_flags+=("--space") + local_nonpersistent_flags+=("--space=") + local_nonpersistent_flags+=("-s") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_task_update() +{ + last_command="cu_task_update" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--add-assignee=") + two_word_flags+=("--add-assignee") + local_nonpersistent_flags+=("--add-assignee") + local_nonpersistent_flags+=("--add-assignee=") + flags+=("--description=") + two_word_flags+=("--description") + two_word_flags+=("-d") + local_nonpersistent_flags+=("--description") + local_nonpersistent_flags+=("--description=") + local_nonpersistent_flags+=("-d") + flags+=("--due=") + two_word_flags+=("--due") + local_nonpersistent_flags+=("--due") + local_nonpersistent_flags+=("--due=") + flags+=("--name=") + two_word_flags+=("--name") + two_word_flags+=("-n") + local_nonpersistent_flags+=("--name") + local_nonpersistent_flags+=("--name=") + local_nonpersistent_flags+=("-n") + flags+=("--priority=") + two_word_flags+=("--priority") + two_word_flags+=("-p") + local_nonpersistent_flags+=("--priority") + local_nonpersistent_flags+=("--priority=") + local_nonpersistent_flags+=("-p") + flags+=("--remove-assignee=") + two_word_flags+=("--remove-assignee") + local_nonpersistent_flags+=("--remove-assignee") + local_nonpersistent_flags+=("--remove-assignee=") + flags+=("--status=") + two_word_flags+=("--status") + two_word_flags+=("-s") + local_nonpersistent_flags+=("--status") + local_nonpersistent_flags+=("--status=") + local_nonpersistent_flags+=("-s") + flags+=("--tag=") + two_word_flags+=("--tag") + local_nonpersistent_flags+=("--tag") + local_nonpersistent_flags+=("--tag=") + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_task_view() +{ + last_command="cu_task_view" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_task() +{ + last_command="cu_task" + + command_aliases=() + + commands=() + commands+=("close") + commands+=("create") + commands+=("interactive") + commands+=("list") + commands+=("reopen") + commands+=("search") + commands+=("update") + commands+=("view") + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_user_list() +{ + last_command="cu_user_list" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_user() +{ + last_command="cu_user" + + command_aliases=() + + commands=() + commands+=("list") + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_version() +{ + last_command="cu_version" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +_cu_root_command() +{ + last_command="cu" + + command_aliases=() + + commands=() + commands+=("auth") + commands+=("bulk") + commands+=("completion") + commands+=("config") + commands+=("export") + commands+=("help") + commands+=("interactive") + commands+=("list") + commands+=("me") + commands+=("space") + commands+=("task") + commands+=("user") + commands+=("version") + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + flags+=("--debug") + flags+=("--output=") + two_word_flags+=("--output") + two_word_flags+=("-o") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + +__start_cu() +{ + local cur prev words cword split + declare -A flaghash 2>/dev/null || : + declare -A aliashash 2>/dev/null || : + if declare -F _init_completion >/dev/null 2>&1; then + _init_completion -s || return + else + __cu_init_completion -n "=" || return + fi + + local c=0 + local flag_parsing_disabled= + local flags=() + local two_word_flags=() + local local_nonpersistent_flags=() + local flags_with_completion=() + local flags_completion=() + local commands=("cu") + local command_aliases=() + local must_have_one_flag=() + local must_have_one_noun=() + local has_completion_function="" + local last_command="" + local nouns=() + local noun_aliases=() + + __cu_handle_word +} + +if [[ $(type -t compopt) = "builtin" ]]; then + complete -o default -F __start_cu cu +else + complete -o default -o nospace -F __start_cu cu +fi + +# ex: ts=4 sw=4 et filetype=sh diff --git a/completions/cu.fish b/completions/cu.fish new file mode 100644 index 0000000..e4cd15a --- /dev/null +++ b/completions/cu.fish @@ -0,0 +1,235 @@ +# fish completion for cu -*- shell-script -*- + +function __cu_debug + set -l file "$BASH_COMP_DEBUG_FILE" + if test -n "$file" + echo "$argv" >> $file + end +end + +function __cu_perform_completion + __cu_debug "Starting __cu_perform_completion" + + # Extract all args except the last one + set -l args (commandline -opc) + # Extract the last arg and escape it in case it is a space + set -l lastArg (string escape -- (commandline -ct)) + + __cu_debug "args: $args" + __cu_debug "last arg: $lastArg" + + # Disable ActiveHelp which is not supported for fish shell + set -l requestComp "CU_ACTIVE_HELP=0 $args[1] __complete $args[2..-1] $lastArg" + + __cu_debug "Calling $requestComp" + set -l results (eval $requestComp 2> /dev/null) + + # Some programs may output extra empty lines after the directive. + # Let's ignore them or else it will break completion. + # Ref: https://github.com/spf13/cobra/issues/1279 + for line in $results[-1..1] + if test (string trim -- $line) = "" + # Found an empty line, remove it + set results $results[1..-2] + else + # Found non-empty line, we have our proper output + break + end + end + + set -l comps $results[1..-2] + set -l directiveLine $results[-1] + + # For Fish, when completing a flag with an = (e.g., -n=) + # completions must be prefixed with the flag + set -l flagPrefix (string match -r -- '-.*=' "$lastArg") + + __cu_debug "Comps: $comps" + __cu_debug "DirectiveLine: $directiveLine" + __cu_debug "flagPrefix: $flagPrefix" + + for comp in $comps + printf "%s%s\n" "$flagPrefix" "$comp" + end + + printf "%s\n" "$directiveLine" +end + +# this function limits calls to __cu_perform_completion, by caching the result behind $__cu_perform_completion_once_result +function __cu_perform_completion_once + __cu_debug "Starting __cu_perform_completion_once" + + if test -n "$__cu_perform_completion_once_result" + __cu_debug "Seems like a valid result already exists, skipping __cu_perform_completion" + return 0 + end + + set --global __cu_perform_completion_once_result (__cu_perform_completion) + if test -z "$__cu_perform_completion_once_result" + __cu_debug "No completions, probably due to a failure" + return 1 + end + + __cu_debug "Performed completions and set __cu_perform_completion_once_result" + return 0 +end + +# this function is used to clear the $__cu_perform_completion_once_result variable after completions are run +function __cu_clear_perform_completion_once_result + __cu_debug "" + __cu_debug "========= clearing previously set __cu_perform_completion_once_result variable ==========" + set --erase __cu_perform_completion_once_result + __cu_debug "Successfully erased the variable __cu_perform_completion_once_result" +end + +function __cu_requires_order_preservation + __cu_debug "" + __cu_debug "========= checking if order preservation is required ==========" + + __cu_perform_completion_once + if test -z "$__cu_perform_completion_once_result" + __cu_debug "Error determining if order preservation is required" + return 1 + end + + set -l directive (string sub --start 2 $__cu_perform_completion_once_result[-1]) + __cu_debug "Directive is: $directive" + + set -l shellCompDirectiveKeepOrder 32 + set -l keeporder (math (math --scale 0 $directive / $shellCompDirectiveKeepOrder) % 2) + __cu_debug "Keeporder is: $keeporder" + + if test $keeporder -ne 0 + __cu_debug "This does require order preservation" + return 0 + end + + __cu_debug "This doesn't require order preservation" + return 1 +end + + +# This function does two things: +# - Obtain the completions and store them in the global __cu_comp_results +# - Return false if file completion should be performed +function __cu_prepare_completions + __cu_debug "" + __cu_debug "========= starting completion logic ==========" + + # Start fresh + set --erase __cu_comp_results + + __cu_perform_completion_once + __cu_debug "Completion results: $__cu_perform_completion_once_result" + + if test -z "$__cu_perform_completion_once_result" + __cu_debug "No completion, probably due to a failure" + # Might as well do file completion, in case it helps + return 1 + end + + set -l directive (string sub --start 2 $__cu_perform_completion_once_result[-1]) + set --global __cu_comp_results $__cu_perform_completion_once_result[1..-2] + + __cu_debug "Completions are: $__cu_comp_results" + __cu_debug "Directive is: $directive" + + set -l shellCompDirectiveError 1 + set -l shellCompDirectiveNoSpace 2 + set -l shellCompDirectiveNoFileComp 4 + set -l shellCompDirectiveFilterFileExt 8 + set -l shellCompDirectiveFilterDirs 16 + + if test -z "$directive" + set directive 0 + end + + set -l compErr (math (math --scale 0 $directive / $shellCompDirectiveError) % 2) + if test $compErr -eq 1 + __cu_debug "Received error directive: aborting." + # Might as well do file completion, in case it helps + return 1 + end + + set -l filefilter (math (math --scale 0 $directive / $shellCompDirectiveFilterFileExt) % 2) + set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) % 2) + if test $filefilter -eq 1; or test $dirfilter -eq 1 + __cu_debug "File extension filtering or directory filtering not supported" + # Do full file completion instead + return 1 + end + + set -l nospace (math (math --scale 0 $directive / $shellCompDirectiveNoSpace) % 2) + set -l nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) % 2) + + __cu_debug "nospace: $nospace, nofiles: $nofiles" + + # If we want to prevent a space, or if file completion is NOT disabled, + # we need to count the number of valid completions. + # To do so, we will filter on prefix as the completions we have received + # may not already be filtered so as to allow fish to match on different + # criteria than the prefix. + if test $nospace -ne 0; or test $nofiles -eq 0 + set -l prefix (commandline -t | string escape --style=regex) + __cu_debug "prefix: $prefix" + + set -l completions (string match -r -- "^$prefix.*" $__cu_comp_results) + set --global __cu_comp_results $completions + __cu_debug "Filtered completions are: $__cu_comp_results" + + # Important not to quote the variable for count to work + set -l numComps (count $__cu_comp_results) + __cu_debug "numComps: $numComps" + + if test $numComps -eq 1; and test $nospace -ne 0 + # We must first split on \t to get rid of the descriptions to be + # able to check what the actual completion will be. + # We don't need descriptions anyway since there is only a single + # real completion which the shell will expand immediately. + set -l split (string split --max 1 \t $__cu_comp_results[1]) + + # Fish won't add a space if the completion ends with any + # of the following characters: @=/:., + set -l lastChar (string sub -s -1 -- $split) + if not string match -r -q "[@=/:.,]" -- "$lastChar" + # In other cases, to support the "nospace" directive we trick the shell + # by outputting an extra, longer completion. + __cu_debug "Adding second completion to perform nospace directive" + set --global __cu_comp_results $split[1] $split[1]. + __cu_debug "Completions are now: $__cu_comp_results" + end + end + + if test $numComps -eq 0; and test $nofiles -eq 0 + # To be consistent with bash and zsh, we only trigger file + # completion when there are no other completions + __cu_debug "Requesting file completion" + return 1 + end + end + + return 0 +end + +# Since Fish completions are only loaded once the user triggers them, we trigger them ourselves +# so we can properly delete any completions provided by another script. +# Only do this if the program can be found, or else fish may print some errors; besides, +# the existing completions will only be loaded if the program can be found. +if type -q "cu" + # The space after the program name is essential to trigger completion for the program + # and not completion of the program name itself. + # Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish. + complete --do-complete "cu " > /dev/null 2>&1 +end + +# Remove any pre-existing completions for the program since we will be handling all of them. +complete -c cu -e + +# this will get called after the two calls below and clear the $__cu_perform_completion_once_result global +complete -c cu -n '__cu_clear_perform_completion_once_result' +# The call to __cu_prepare_completions will setup __cu_comp_results +# which provides the program's completion choices. +# If this doesn't require order preservation, we don't use the -k flag +complete -c cu -n 'not __cu_requires_order_preservation && __cu_prepare_completions' -f -a '$__cu_comp_results' +# otherwise we use the -k flag +complete -k -c cu -n '__cu_requires_order_preservation && __cu_prepare_completions' -f -a '$__cu_comp_results' diff --git a/docs/RELEASE_SETUP.md b/docs/RELEASE_SETUP.md new file mode 100644 index 0000000..ae567c9 --- /dev/null +++ b/docs/RELEASE_SETUP.md @@ -0,0 +1,128 @@ +# Release Setup Guide + +This document outlines the manual steps required to complete the release automation setup for the ClickUp CLI. + +## Prerequisites + +Before creating your first release, you need to: + +### 1. GitHub Secrets + +Add the following secrets to your GitHub repository (Settings → Secrets and variables → Actions): + +- **`HOMEBREW_TAP_GITHUB_TOKEN`** (Required for Homebrew) + - Create a Personal Access Token with `repo` scope + - This token needs access to create PRs in your homebrew tap repository + +- **`DOCKER_USERNAME`** and **`DOCKER_TOKEN`** (Optional for Docker Hub) + - Your Docker Hub username + - Docker Hub access token (not password) + - Only needed if you want to publish Docker images + +- **`GPG_FINGERPRINT`** (Optional for signing) + - Your GPG key fingerprint for signing releases + - Only needed if you want to sign release artifacts + +### 2. Homebrew Tap Repository + +Create a separate repository for your Homebrew tap: + +1. Create a new repository named `homebrew-clickup` in your GitHub account +2. Initialize it with a README.md: + ```markdown + # Homebrew Tap for ClickUp CLI + + ## Installation + + ```bash + brew install timimsms/clickup/clickup-cli + ``` + ``` + +3. Create a `Formula` directory in the repository +4. The GoReleaser workflow will automatically create and update the formula file + +### 3. Docker Hub Setup (Optional) + +If you want to publish Docker images: + +1. Create a Docker Hub account if you don't have one +2. Create a repository named `clickup/cli` (or your preferred name) +3. Generate an access token: Account Settings → Security → Access Tokens +4. Add the token and username as GitHub secrets (see above) + +### 4. npm Package Setup (Future) + +npm distribution will be set up in a separate PR. For now, the release notes will mention it as "coming soon". + +## Creating a Release + +### Automatic Method (Recommended) + +1. Use the release script: + ```bash + ./scripts/release.sh patch # for bug fixes + ./scripts/release.sh minor # for new features + ./scripts/release.sh major # for breaking changes + ``` + +2. The script will: + - Check for uncommitted changes + - Create and push a version tag + - Trigger the GitHub Actions workflow + +### Manual Method + +1. Create and push a tag: + ```bash + git tag -a v1.0.0 -m "Release v1.0.0" + git push origin v1.0.0 + ``` + +2. The GitHub Actions workflow will automatically: + - Build binaries for all platforms + - Create a GitHub release with artifacts + - Update your Homebrew tap + - Build and push Docker images + - Create a post-release issue with verification steps + +## Verifying a Release + +After a release, verify everything worked: + +1. **GitHub Release**: Check https://github.com/timimsms/cu/releases +2. **Homebrew**: `brew install timimsms/clickup/clickup-cli` +3. **Docker**: `docker run clickup/cli:latest --version` +4. **Direct Download**: Test the installation script when implemented + +## Troubleshooting + +### GoReleaser Fails + +- Check the GitHub Actions logs for detailed error messages +- Ensure all required secrets are set +- Verify the `.goreleaser.yml` syntax with `goreleaser check` + +### Homebrew Tap Not Updated + +- Ensure the `HOMEBREW_TAP_GITHUB_TOKEN` has the correct permissions +- Check that the tap repository exists and has a `Formula` directory +- Look for PR creation errors in the GitHub Actions logs + +### Docker Push Fails + +- Verify Docker Hub credentials are correct +- Ensure the Docker repository exists +- Check Docker Hub rate limits + +## Local Testing + +To test the release process locally without publishing: + +```bash +# Snapshot build (doesn't publish) +goreleaser release --snapshot --clean + +# Check the dist/ directory for artifacts +ls -la dist/ +``` \ No newline at end of file diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..9941225 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# Release script for ClickUp CLI +# Usage: ./scripts/release.sh [major|minor|patch] + +set -euo pipefail + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Get the release type +RELEASE_TYPE="${1:-patch}" + +# Validate release type +if [[ ! "$RELEASE_TYPE" =~ ^(major|minor|patch)$ ]]; then + echo -e "${RED}Error: Invalid release type. Use major, minor, or patch${NC}" + exit 1 +fi + +# Get current version +CURRENT_VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") +CURRENT_VERSION="${CURRENT_VERSION#v}" + +# Parse version components +IFS='.' read -ra VERSION_PARTS <<< "$CURRENT_VERSION" +MAJOR="${VERSION_PARTS[0]:-0}" +MINOR="${VERSION_PARTS[1]:-0}" +PATCH="${VERSION_PARTS[2]:-0}" + +# Calculate new version +case "$RELEASE_TYPE" in + major) + NEW_VERSION="$((MAJOR + 1)).0.0" + ;; + minor) + NEW_VERSION="${MAJOR}.$((MINOR + 1)).0" + ;; + patch) + NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))" + ;; +esac + +NEW_TAG="v${NEW_VERSION}" + +echo -e "${YELLOW}Current version: v${CURRENT_VERSION}${NC}" +echo -e "${GREEN}New version: ${NEW_TAG}${NC}" +echo "" + +# Check for uncommitted changes +if ! git diff-index --quiet HEAD --; then + echo -e "${RED}Error: You have uncommitted changes${NC}" + echo "Please commit or stash your changes before releasing" + exit 1 +fi + +# Confirm release +read -p "Are you sure you want to create release ${NEW_TAG}? (y/N) " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Release cancelled" + exit 0 +fi + +# Update version in version.go if it exists +VERSION_FILE="internal/version/version.go" +if [ -f "$VERSION_FILE" ]; then + echo "Updating version in ${VERSION_FILE}..." + # This is a placeholder - adjust based on your version.go structure + # sed -i.bak "s/Version = \".*\"/Version = \"${NEW_VERSION}\"/" "$VERSION_FILE" + # git add "$VERSION_FILE" + # git commit -m "chore: bump version to ${NEW_VERSION}" +fi + +# Create and push tag +echo -e "${YELLOW}Creating tag ${NEW_TAG}...${NC}" +git tag -a "${NEW_TAG}" -m "Release ${NEW_TAG}" + +echo -e "${YELLOW}Pushing tag to origin...${NC}" +git push origin "${NEW_TAG}" + +echo -e "${GREEN}✅ Release ${NEW_TAG} created successfully!${NC}" +echo "" +echo "The GitHub Actions workflow will now:" +echo " - Build binaries for all platforms" +echo " - Create a GitHub release" +echo " - Update the Homebrew tap" +echo " - Build and push Docker images" +echo "" +echo "Monitor the release progress at:" +echo "https://github.com/timimsms/cu/actions" \ No newline at end of file