mirror of
https://github.com/ivuorinen/a.git
synced 2026-01-26 03:24:07 +00:00
Initial commit
This commit is contained in:
@@ -1,21 +1,14 @@
|
|||||||
root = true
|
root = true
|
||||||
|
|
||||||
[*]
|
[*]
|
||||||
charset = utf-8
|
|
||||||
end_of_line = lf
|
|
||||||
indent_size = 2
|
|
||||||
indent_style = space
|
indent_style = space
|
||||||
insert_final_newline = true
|
|
||||||
max_line_length = 160
|
|
||||||
tab_width = 2
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
|
|
||||||
[{*.md}]
|
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
tab_width = 2
|
insert_final_newline = true
|
||||||
max_line_length = 160
|
max_line_length = 120
|
||||||
trim_trailing_whitespace = false
|
|
||||||
|
|
||||||
[{*.mk,GNUmakefile,makefile}]
|
[*.go]
|
||||||
tab_width = 4
|
indent_style = tab
|
||||||
|
indent_width = 2
|
||||||
|
|
||||||
|
[{Makefile,go.mod,go.sum}]
|
||||||
indent_style = tab
|
indent_style = tab
|
||||||
|
|||||||
17
.github/ISSUE_TEMPLATE/bug_report.md
vendored
17
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -12,6 +12,7 @@ A clear and concise description of what the bug is.
|
|||||||
|
|
||||||
**To Reproduce**
|
**To Reproduce**
|
||||||
Steps to reproduce the behavior:
|
Steps to reproduce the behavior:
|
||||||
|
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
2. Click on '....'
|
2. Click on '....'
|
||||||
3. Scroll down to '....'
|
3. Scroll down to '....'
|
||||||
@@ -24,15 +25,17 @@ A clear and concise description of what you expected to happen.
|
|||||||
If applicable, add screenshots to help explain your problem.
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
**Desktop (please complete the following information):**
|
**Desktop (please complete the following information):**
|
||||||
- OS: [e.g. iOS]
|
|
||||||
- Browser [e.g. chrome, safari]
|
- OS: [e.g. iOS]
|
||||||
- Version [e.g. 22]
|
- Browser [e.g. chrome, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
**Smartphone (please complete the following information):**
|
**Smartphone (please complete the following information):**
|
||||||
- Device: [e.g. iPhone6]
|
|
||||||
- OS: [e.g. iOS8.1]
|
- Device: [e.g. iPhone6]
|
||||||
- Browser [e.g. stock browser, safari]
|
- OS: [e.g. iOS8.1]
|
||||||
- Version [e.g. 22]
|
- Browser [e.g. stock browser, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context about the problem here.
|
Add any other context about the problem here.
|
||||||
|
|||||||
8
.github/workflows/codeql.yml
vendored
8
.github/workflows/codeql.yml
vendored
@@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||||
name: 'CodeQL'
|
name: 'CodeQL'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ['main']
|
branches: ['main']
|
||||||
@@ -10,36 +9,29 @@ on:
|
|||||||
schedule:
|
schedule:
|
||||||
- cron: '30 1 * * 0' # Run at 1:30 AM UTC every Sunday
|
- cron: '30 1 * * 0' # Run at 1:30 AM UTC every Sunday
|
||||||
merge_group:
|
merge_group:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
actions: read
|
actions: read
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
name: Analyze
|
name: Analyze
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
security-events: write
|
security-events: write
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
language: ['javascript'] # Add languages used in your actions
|
language: ['javascript'] # Add languages used in your actions
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
queries: security-and-quality
|
queries: security-and-quality
|
||||||
|
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
uses: github/codeql-action/autobuild@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
||||||
with:
|
with:
|
||||||
|
|||||||
5
.github/workflows/pr-lint.yml
vendored
5
.github/workflows/pr-lint.yml
vendored
@@ -1,19 +1,15 @@
|
|||||||
---
|
---
|
||||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||||
name: Lint Code Base
|
name: Lint Code Base
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master, main]
|
branches: [master, main]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master, main]
|
branches: [master, main]
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions: read-all
|
permissions: read-all
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
Linter:
|
Linter:
|
||||||
name: PR Lint
|
name: PR Lint
|
||||||
@@ -23,7 +19,6 @@ jobs:
|
|||||||
statuses: write
|
statuses: write
|
||||||
contents: read
|
contents: read
|
||||||
packages: read
|
packages: read
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Run PR Lint
|
- name: Run PR Lint
|
||||||
# https://github.com/ivuorinen/actions
|
# https://github.com/ivuorinen/actions
|
||||||
|
|||||||
4
.github/workflows/stale.yml
vendored
4
.github/workflows/stale.yml
vendored
@@ -1,23 +1,19 @@
|
|||||||
---
|
---
|
||||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||||
name: Stale
|
name: Stale
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 8 * * *' # Every day at 08:00
|
- cron: '0 8 * * *' # Every day at 08:00
|
||||||
workflow_call:
|
workflow_call:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: read
|
packages: read
|
||||||
statuses: read
|
statuses: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
stale:
|
stale:
|
||||||
name: 🧹 Clean up stale issues and PRs
|
name: 🧹 Clean up stale issues and PRs
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write # only for delete-branch option
|
contents: write # only for delete-branch option
|
||||||
issues: write
|
issues: write
|
||||||
|
|||||||
6
.github/workflows/sync-labels.yml
vendored
6
.github/workflows/sync-labels.yml
vendored
@@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
|
||||||
name: Sync Labels
|
name: Sync Labels
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
@@ -15,23 +14,18 @@ on:
|
|||||||
workflow_call:
|
workflow_call:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
merge_group:
|
merge_group:
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions: read-all
|
permissions: read-all
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
labels:
|
labels:
|
||||||
name: ♻️ Sync Labels
|
name: ♻️ Sync Labels
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
issues: write
|
issues: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: ⤵️ Checkout Repository
|
- name: ⤵️ Checkout Repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|||||||
150
.gitignore
vendored
150
.gitignore
vendored
@@ -1,134 +1,44 @@
|
|||||||
.php-cs-fixer.cache
|
*.iws
|
||||||
.php-cs-fixer.php
|
|
||||||
composer.phar
|
|
||||||
/vendor/
|
|
||||||
.phpunit.result.cache
|
|
||||||
.phpunit.cache
|
|
||||||
/app/phpunit.xml
|
|
||||||
/phpunit.xml
|
|
||||||
/build/
|
|
||||||
logs
|
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
*.pem
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
.pnpm-debug.log*
|
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
|
||||||
pids
|
|
||||||
*.pid
|
*.pid
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
lib-cov
|
|
||||||
coverage
|
|
||||||
*.lcov
|
|
||||||
.nyc_output
|
|
||||||
.grunt
|
|
||||||
bower_components
|
|
||||||
.lock-wscript
|
|
||||||
build/Release
|
|
||||||
node_modules/
|
|
||||||
jspm_packages/
|
|
||||||
web_modules/
|
|
||||||
*.tsbuildinfo
|
|
||||||
.npm
|
|
||||||
.eslintcache
|
|
||||||
.stylelintcache
|
|
||||||
.rpt2_cache/
|
|
||||||
.rts2_cache_cjs/
|
|
||||||
.rts2_cache_es/
|
|
||||||
.rts2_cache_umd/
|
|
||||||
.node_repl_history
|
|
||||||
*.tgz
|
*.tgz
|
||||||
.yarn-integrity
|
|
||||||
.env
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
.env.local
|
|
||||||
.cache
|
|
||||||
.parcel-cache
|
|
||||||
.next
|
|
||||||
out
|
|
||||||
.nuxt
|
|
||||||
dist
|
|
||||||
.cache/
|
|
||||||
.vuepress/dist
|
|
||||||
.temp
|
|
||||||
.docusaurus
|
|
||||||
.serverless/
|
|
||||||
.fusebox/
|
|
||||||
.dynamodb/
|
|
||||||
.tern-port
|
|
||||||
.vscode-test
|
|
||||||
.yarn/cache
|
|
||||||
.yarn/unplugged
|
|
||||||
.yarn/build-state.yml
|
|
||||||
.yarn/install-state.gz
|
|
||||||
.pnp.*
|
|
||||||
[._]*.s[a-v][a-z]
|
|
||||||
!*.svg # comment out if you don't need vector files
|
|
||||||
[._]*.sw[a-p]
|
|
||||||
[._]s[a-rt-v][a-z]
|
|
||||||
[._]ss[a-gi-z]
|
|
||||||
[._]sw[a-p]
|
|
||||||
Session.vim
|
|
||||||
Sessionx.vim
|
|
||||||
.netrwhist
|
|
||||||
*~
|
*~
|
||||||
tags
|
.DS_Store
|
||||||
[._]*.un~
|
.env
|
||||||
.idea/**/workspace.xml
|
.env*.local
|
||||||
.idea/**/tasks.xml
|
.env.development.local
|
||||||
.idea/**/usage.statistics.xml
|
.env.local
|
||||||
.idea/**/dictionaries
|
.env.production.local
|
||||||
.idea/**/shelf
|
.env.test.local
|
||||||
.idea/**/aws.xml
|
.idea/**/aws.xml
|
||||||
.idea/**/contentModel.xml
|
.idea/**/contentModel.xml
|
||||||
.idea/**/dataSources/
|
|
||||||
.idea/**/dataSources.ids
|
.idea/**/dataSources.ids
|
||||||
.idea/**/dataSources.local.xml
|
.idea/**/dataSources.local.xml
|
||||||
.idea/**/sqlDataSources.xml
|
.idea/**/dataSources/
|
||||||
.idea/**/dynamic.xml
|
|
||||||
.idea/**/uiDesigner.xml
|
|
||||||
.idea/**/dbnavigator.xml
|
.idea/**/dbnavigator.xml
|
||||||
|
.idea/**/dictionaries
|
||||||
|
.idea/**/dynamic.xml
|
||||||
.idea/**/gradle.xml
|
.idea/**/gradle.xml
|
||||||
.idea/**/libraries
|
.idea/**/libraries
|
||||||
cmake-build-*/
|
|
||||||
.idea/**/mongoSettings.xml
|
.idea/**/mongoSettings.xml
|
||||||
*.iws
|
.idea/**/shelf
|
||||||
out/
|
.idea/**/sqlDataSources.xml
|
||||||
.idea_modules/
|
.idea/**/tasks.xml
|
||||||
atlassian-ide-plugin.xml
|
.idea/**/uiDesigner.xml
|
||||||
|
.idea/**/usage.statistics.xml
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/caches/build_file_checksums.ser
|
||||||
|
.idea/httpRequests
|
||||||
.idea/replstate.xml
|
.idea/replstate.xml
|
||||||
.idea/sonarlint/
|
.idea/sonarlint/
|
||||||
com_crashlytics_export_strings.xml
|
.idea_modules/
|
||||||
crashlytics.properties
|
.netrwhist
|
||||||
crashlytics-build.properties
|
.vscode-test
|
||||||
fabric.properties
|
Session.vim
|
||||||
.idea/httpRequests
|
Sessionx.vim
|
||||||
.idea/caches/build_file_checksums.ser
|
[._]*.un~
|
||||||
npm-debug.log
|
coverage*
|
||||||
yarn-error.log
|
logs
|
||||||
bootstrap/compiled.php
|
out/
|
||||||
app/storage/
|
tags
|
||||||
public/storage
|
|
||||||
public/hot
|
|
||||||
public_html/storage
|
|
||||||
public_html/hot
|
|
||||||
storage/*.key
|
|
||||||
Homestead.yaml
|
|
||||||
Homestead.json
|
|
||||||
/.vagrant
|
|
||||||
/node_modules
|
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
/coverage
|
|
||||||
/.next/
|
|
||||||
/out/
|
|
||||||
/build
|
|
||||||
.DS_Store
|
|
||||||
*.pem
|
|
||||||
.env*.local
|
|
||||||
.vercel
|
|
||||||
next-env.d.ts
|
|
||||||
|
|||||||
1
.go-version
Normal file
1
.go-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1.23.0
|
||||||
118
.golangci.yml
Normal file
118
.golangci.yml
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
---
|
||||||
|
# yaml-language-server: $schema=https://json.schemastore.org/golangci-lint.json
|
||||||
|
# golangci-lint configuration for f2b project
|
||||||
|
# https://golangci-lint.run/usage/configuration/
|
||||||
|
|
||||||
|
version: "2"
|
||||||
|
run:
|
||||||
|
timeout: 5m
|
||||||
|
modules-download-mode: readonly
|
||||||
|
go: "1.21"
|
||||||
|
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
# Essential linters
|
||||||
|
- errcheck # Error checking
|
||||||
|
- govet # Go vet
|
||||||
|
- ineffassign # Inefficient assignment checking
|
||||||
|
- staticcheck # Static code analysis
|
||||||
|
- unused # Unused variable checking
|
||||||
|
- lll # Line length checking
|
||||||
|
- gosec # Security checking
|
||||||
|
- usetesting # Unit testing
|
||||||
|
- revive # Code style checking
|
||||||
|
|
||||||
|
# Code quality linters
|
||||||
|
- misspell # Spell checking
|
||||||
|
- unconvert # Unconvert checking
|
||||||
|
- gocyclo # Cyclomatic complexity checking
|
||||||
|
- prealloc # Preallocation checking
|
||||||
|
- bodyclose # Body close checking
|
||||||
|
- rowserrcheck # Rows error checking
|
||||||
|
- sqlclosecheck # SQL close checking
|
||||||
|
- durationcheck # Duration checking
|
||||||
|
- errorlint # Error linting
|
||||||
|
- predeclared # Predeclared identifier checking
|
||||||
|
- wastedassign # Wasted assignment checking
|
||||||
|
- containedctx # Contained context checking
|
||||||
|
- contextcheck # Context checking
|
||||||
|
- errname # Error name checking
|
||||||
|
- nilnil # Nil nil checking
|
||||||
|
- thelper # Helper function checking
|
||||||
|
- usestdlibvars # Use standard library variables
|
||||||
|
- whitespace # Whitespace checking
|
||||||
|
- godox # TODO/FIXME/etc comments
|
||||||
|
|
||||||
|
disable:
|
||||||
|
# Disable overly strict linters for this project
|
||||||
|
- varnamelen # Variable name length checking
|
||||||
|
- tagliatelle # Struct tag format checking
|
||||||
|
- makezero # Make zero checking
|
||||||
|
- testpackage # Separate test package requirement
|
||||||
|
- paralleltest # Parallel test requirement
|
||||||
|
- forcetypeassert # Force type assertion
|
||||||
|
- ireturn # Return interface checking
|
||||||
|
- nlreturn # New line return checking
|
||||||
|
- cyclop # Cyclomatic complexity (covered by gocyclo)
|
||||||
|
- funlen # Function length checking
|
||||||
|
- gocognit # Cognitive complexity checking
|
||||||
|
- maintidx # Maintainability index
|
||||||
|
- nestif # Nested if checking
|
||||||
|
- wsl # Whitespace linter (too strict)
|
||||||
|
- gocritic # Too many style opinions
|
||||||
|
- nakedret # Naked returns
|
||||||
|
- nolintlint # Nolint directive checking
|
||||||
|
- noctx # Context checking
|
||||||
|
|
||||||
|
settings:
|
||||||
|
errcheck:
|
||||||
|
check-type-assertions: false
|
||||||
|
check-blank: false
|
||||||
|
|
||||||
|
govet:
|
||||||
|
enable-all: true
|
||||||
|
disable:
|
||||||
|
- fieldalignment # Can be too strict for simple structs
|
||||||
|
- shadow # Variable shadowing can be acceptable
|
||||||
|
|
||||||
|
gocyclo:
|
||||||
|
min-complexity: 20
|
||||||
|
|
||||||
|
misspell:
|
||||||
|
locale: US
|
||||||
|
|
||||||
|
prealloc:
|
||||||
|
simple: true
|
||||||
|
range-loops: true
|
||||||
|
for-loops: false
|
||||||
|
|
||||||
|
errorlint:
|
||||||
|
errorf: false # Allow %v instead of %w for some cases
|
||||||
|
|
||||||
|
lll:
|
||||||
|
line-length: 120
|
||||||
|
|
||||||
|
formatters:
|
||||||
|
enable:
|
||||||
|
- gofmt
|
||||||
|
- goimports
|
||||||
|
- golines
|
||||||
|
|
||||||
|
settings:
|
||||||
|
gofmt:
|
||||||
|
simplify: true
|
||||||
|
goimports:
|
||||||
|
local-prefixes:
|
||||||
|
- github.com/ivuorinen/a
|
||||||
|
golines:
|
||||||
|
max-len: 120
|
||||||
|
tab-len: 4
|
||||||
|
shorten-comments: false
|
||||||
|
reformat-tags: true
|
||||||
|
chain-split-dots: true
|
||||||
|
|
||||||
|
issues:
|
||||||
|
max-issues-per-linter: 0
|
||||||
|
max-same-issues: 0
|
||||||
|
new: false
|
||||||
|
fix: true
|
||||||
241
.goreleaser.yml
Normal file
241
.goreleaser.yml
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
---
|
||||||
|
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
|
||||||
|
# vim: set ts=2 sw=2 tw=0 fo=jcroql
|
||||||
|
# GoReleaser configuration
|
||||||
|
# Documentation: https://goreleaser.com/customization/
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
# Set the project name
|
||||||
|
project_name: a
|
||||||
|
|
||||||
|
# Clean dist folder before build
|
||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
- go mod tidy
|
||||||
|
- go generate ./...
|
||||||
|
|
||||||
|
# Build configuration
|
||||||
|
builds:
|
||||||
|
- id: a
|
||||||
|
main: .
|
||||||
|
binary: a
|
||||||
|
|
||||||
|
# Custom ldflags
|
||||||
|
ldflags:
|
||||||
|
- -s -w
|
||||||
|
- -X github.com/ivuorinen/a/cmd.version={{.Version}}
|
||||||
|
- -X github.com/ivuorinen/a/cmd.commit={{.Commit}}
|
||||||
|
- -X github.com/ivuorinen/a/cmd.date={{.Date}}
|
||||||
|
- -X github.com/ivuorinen/a/cmd.builtBy=goreleaser
|
||||||
|
|
||||||
|
# Build for multiple platforms
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
- darwin
|
||||||
|
- freebsd
|
||||||
|
- openbsd
|
||||||
|
- netbsd
|
||||||
|
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
- arm
|
||||||
|
- "386"
|
||||||
|
|
||||||
|
goarm:
|
||||||
|
- "6"
|
||||||
|
- "7"
|
||||||
|
|
||||||
|
# Skip certain combinations
|
||||||
|
ignore:
|
||||||
|
- goos: darwin
|
||||||
|
goarch: "386"
|
||||||
|
- goos: darwin
|
||||||
|
goarch: arm
|
||||||
|
- goos: freebsd
|
||||||
|
goarch: arm
|
||||||
|
- goos: openbsd
|
||||||
|
goarch: arm
|
||||||
|
- goos: netbsd
|
||||||
|
goarch: arm
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
|
||||||
|
# Custom build tags
|
||||||
|
tags:
|
||||||
|
- netgo
|
||||||
|
- osusergo
|
||||||
|
|
||||||
|
# Archive configuration
|
||||||
|
archives:
|
||||||
|
- id: a
|
||||||
|
formats: ["binary", "tar.gz"]
|
||||||
|
|
||||||
|
# Archive format
|
||||||
|
format_overrides:
|
||||||
|
- goos: windows
|
||||||
|
format: zip
|
||||||
|
|
||||||
|
# Files to include in archive
|
||||||
|
files:
|
||||||
|
- LICENSE.md
|
||||||
|
- README.md
|
||||||
|
- CHANGELOG.md
|
||||||
|
- docs/*
|
||||||
|
|
||||||
|
# Checksum configuration
|
||||||
|
checksum:
|
||||||
|
name_template: "checksums.txt"
|
||||||
|
algorithm: sha256
|
||||||
|
|
||||||
|
# Snapshot configuration
|
||||||
|
snapshot:
|
||||||
|
name_template: "{{ incpatch .Version }}-next"
|
||||||
|
|
||||||
|
# Release configuration
|
||||||
|
release:
|
||||||
|
github:
|
||||||
|
owner: ivuorinen
|
||||||
|
name: a
|
||||||
|
|
||||||
|
# Release notes
|
||||||
|
header: |
|
||||||
|
## a v{{ .Version }} ({{ .Date }})
|
||||||
|
|
||||||
|
A robust command-line interface (CLI) wrapper around the age encryption tool
|
||||||
|
|
||||||
|
footer: |
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Using Go
|
||||||
|
```bash
|
||||||
|
go install github.com/ivuorinen/a@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Homebrew (macOS/Linux)
|
||||||
|
```bash
|
||||||
|
brew tap ivuorinen/tap
|
||||||
|
brew install a
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Download
|
||||||
|
Download the appropriate binary for your platform from the assets below.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
See the [README](https://github.com/ivuorinen/a#readme) for usage instructions.
|
||||||
|
|
||||||
|
# Automatically generate release notes
|
||||||
|
|
||||||
|
make_latest: true
|
||||||
|
|
||||||
|
# Changelog configuration
|
||||||
|
changelog:
|
||||||
|
sort: asc
|
||||||
|
use: github
|
||||||
|
filters:
|
||||||
|
exclude:
|
||||||
|
- "^docs:"
|
||||||
|
- "^test:"
|
||||||
|
- "^chore:"
|
||||||
|
- "typo"
|
||||||
|
- "Merge pull request"
|
||||||
|
- "Merge branch"
|
||||||
|
groups:
|
||||||
|
- title: "🚀 Features"
|
||||||
|
regexp: "^feat"
|
||||||
|
- title: "🐛 Bug Fixes"
|
||||||
|
regexp: "^fix"
|
||||||
|
- title: "🔒 Security"
|
||||||
|
regexp: "^security"
|
||||||
|
- title: "⚡ Performance"
|
||||||
|
regexp: "^perf"
|
||||||
|
- title: "♻️ Refactoring"
|
||||||
|
regexp: "^refactor"
|
||||||
|
- title: "Other changes"
|
||||||
|
|
||||||
|
# Homebrew tap configuration
|
||||||
|
brews:
|
||||||
|
- name: a
|
||||||
|
repository:
|
||||||
|
owner: ivuorinen
|
||||||
|
name: homebrew-tap
|
||||||
|
branch: main
|
||||||
|
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
|
||||||
|
|
||||||
|
commit_author:
|
||||||
|
name: goreleaserbot
|
||||||
|
email: bot@goreleaser.com
|
||||||
|
|
||||||
|
commit_msg_template: "Brew formula update for {{ .ProjectName }} version {{ .Tag }}"
|
||||||
|
|
||||||
|
homepage: "https://github.com/ivuorinen/a"
|
||||||
|
description: "Modern, secure Go-based CLI tool for managing Fail2Ban jails and bans"
|
||||||
|
license: "MIT"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
- name: go
|
||||||
|
type: optional
|
||||||
|
|
||||||
|
test: |
|
||||||
|
system "#{bin}/a", "version"
|
||||||
|
|
||||||
|
install: |
|
||||||
|
bin.install "a"
|
||||||
|
|
||||||
|
# NFPM configuration for Linux packages
|
||||||
|
nfpms:
|
||||||
|
- id: a
|
||||||
|
package_name: a
|
||||||
|
vendor: ivuorinen
|
||||||
|
homepage: https://github.com/ivuorinen/a
|
||||||
|
maintainer: ivuorinen
|
||||||
|
description: Modern, secure Go-based CLI tool for managing Fail2Ban jails and bans
|
||||||
|
license: MIT
|
||||||
|
|
||||||
|
formats:
|
||||||
|
- deb
|
||||||
|
- rpm
|
||||||
|
- apk
|
||||||
|
|
||||||
|
bindir: /usr/bin
|
||||||
|
|
||||||
|
contents:
|
||||||
|
- src: ./LICENSE.md
|
||||||
|
dst: /usr/share/doc/a/LICENSE.md
|
||||||
|
- src: ./README.md
|
||||||
|
dst: /usr/share/doc/a/README.md
|
||||||
|
|
||||||
|
scripts:
|
||||||
|
postinstall: |
|
||||||
|
#!/bin/sh
|
||||||
|
echo "a has been installed. Run 'a --help' to get started."
|
||||||
|
|
||||||
|
# Docker configuration
|
||||||
|
dockers:
|
||||||
|
- image_templates:
|
||||||
|
- "ghcr.io/ivuorinen/a:{{ .Tag }}"
|
||||||
|
- "ghcr.io/ivuorinen/a:v{{ .Major }}"
|
||||||
|
- "ghcr.io/ivuorinen/a:v{{ .Major }}.{{ .Minor }}"
|
||||||
|
- "ghcr.io/ivuorinen/a:latest"
|
||||||
|
|
||||||
|
dockerfile: |
|
||||||
|
FROM alpine:latest
|
||||||
|
RUN apk --no-cache add ca-certificates
|
||||||
|
COPY a /usr/local/bin/
|
||||||
|
ENTRYPOINT ["a"]
|
||||||
|
|
||||||
|
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}}"
|
||||||
|
- "--label=org.opencontainers.image.source={{.GitURL}}"
|
||||||
|
- "--platform=linux/amd64"
|
||||||
|
|
||||||
|
# Announce releases
|
||||||
|
announce:
|
||||||
|
skip: false
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
# Configuration file for MegaLinter
|
# Configuration file for MegaLinter
|
||||||
# See all available variables at
|
# See all available variables at
|
||||||
# https://megalinter.io/configuration/ and in linters documentation
|
# https://megalinter.io/configuration/ and in linters documentation
|
||||||
|
|
||||||
APPLY_FIXES: all
|
APPLY_FIXES: all
|
||||||
SHOW_ELAPSED_TIME: false # Show elapsed time at the end of MegaLinter run
|
SHOW_ELAPSED_TIME: false # Show elapsed time at the end of MegaLinter run
|
||||||
PARALLEL: true
|
PARALLEL: true
|
||||||
@@ -14,22 +13,16 @@ JAVASCRIPT_DEFAULT_STYLE: prettier # Default style for JavaScript
|
|||||||
PRINT_ALPACA: false # Print Alpaca logo in console
|
PRINT_ALPACA: false # Print Alpaca logo in console
|
||||||
SARIF_REPORTER: true # Generate SARIF report
|
SARIF_REPORTER: true # Generate SARIF report
|
||||||
SHOW_SKIPPED_LINTERS: false # Show skipped linters in MegaLinter log
|
SHOW_SKIPPED_LINTERS: false # Show skipped linters in MegaLinter log
|
||||||
|
|
||||||
DISABLE_LINTERS:
|
DISABLE_LINTERS:
|
||||||
- REPOSITORY_DEVSKIM
|
- REPOSITORY_DEVSKIM
|
||||||
|
|
||||||
ENABLE_LINTERS:
|
ENABLE_LINTERS:
|
||||||
- YAML_YAMLLINT
|
- YAML_YAMLLINT
|
||||||
- MARKDOWN_MARKDOWNLINT
|
- MARKDOWN_MARKDOWNLINT
|
||||||
- YAML_PRETTIER
|
- YAML_PRETTIER
|
||||||
- JSON_PRETTIER
|
- JSON_PRETTIER
|
||||||
- JAVASCRIPT_ES
|
|
||||||
- TYPESCRIPT_ES
|
|
||||||
|
|
||||||
YAML_YAMLLINT_CONFIG_FILE: .yamllint.yml
|
YAML_YAMLLINT_CONFIG_FILE: .yamllint.yml
|
||||||
MARKDOWN_MARKDOWNLINT_CONFIG_FILE: .markdownlint.json
|
MARKDOWN_MARKDOWNLINT_CONFIG_FILE: .markdownlint.json
|
||||||
JAVASCRIPT_ES_CONFIG_FILE: .eslintrc.json
|
JAVASCRIPT_ES_CONFIG_FILE: .eslintrc.json
|
||||||
TYPESCRIPT_ES_CONFIG_FILE: .eslintrc.json
|
TYPESCRIPT_ES_CONFIG_FILE: .eslintrc.json
|
||||||
|
|
||||||
FILTER_REGEX_EXCLUDE: >
|
FILTER_REGEX_EXCLUDE: >
|
||||||
(node_modules|\.automation/test|docs/json-schemas|\.github/workflows)
|
(node_modules|\.automation/test)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ repos:
|
|||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v5.0.0
|
rev: v5.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: requirements-txt-fixer
|
|
||||||
- id: detect-private-key
|
- id: detect-private-key
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
args: [--markdown-linebreak-ext=md]
|
args: [--markdown-linebreak-ext=md]
|
||||||
@@ -22,19 +21,42 @@ repos:
|
|||||||
- id: pretty-format-json
|
- id: pretty-format-json
|
||||||
args: [--autofix, --no-sort-keys]
|
args: [--autofix, --no-sort-keys]
|
||||||
|
|
||||||
|
- repo: https://github.com/pre-commit/sync-pre-commit-deps
|
||||||
|
rev: v0.0.3
|
||||||
|
hooks:
|
||||||
|
- id: sync-pre-commit-deps
|
||||||
|
|
||||||
|
- repo: https://github.com/tekwizely/pre-commit-golang
|
||||||
|
rev: v1.0.0-rc.1
|
||||||
|
hooks:
|
||||||
|
- id: go-build-mod
|
||||||
|
alias: build
|
||||||
|
- id: go-mod-tidy
|
||||||
|
alias: tidy
|
||||||
|
- id: golangci-lint-mod
|
||||||
|
alias: lint
|
||||||
|
- id: go-fmt
|
||||||
|
alias: fmt
|
||||||
|
args: [-s, -w]
|
||||||
|
|
||||||
|
- repo: https://github.com/google/yamlfmt
|
||||||
|
rev: v0.17.2
|
||||||
|
hooks:
|
||||||
|
- id: yamlfmt
|
||||||
|
|
||||||
- repo: https://github.com/igorshubovych/markdownlint-cli
|
- repo: https://github.com/igorshubovych/markdownlint-cli
|
||||||
rev: v0.44.0
|
rev: v0.45.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: markdownlint
|
- id: markdownlint
|
||||||
args: [-c, .markdownlint.json, --fix]
|
args: [-c, .markdownlint.json, --fix]
|
||||||
|
|
||||||
- repo: https://github.com/adrienverge/yamllint
|
- repo: https://github.com/adrienverge/yamllint
|
||||||
rev: v1.37.0
|
rev: v1.37.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: yamllint
|
- id: yamllint
|
||||||
|
|
||||||
- repo: https://github.com/scop/pre-commit-shfmt
|
- repo: https://github.com/scop/pre-commit-shfmt
|
||||||
rev: v3.11.0-1
|
rev: v3.12.0-2
|
||||||
hooks:
|
hooks:
|
||||||
- id: shfmt
|
- id: shfmt
|
||||||
|
|
||||||
@@ -42,22 +64,17 @@ repos:
|
|||||||
rev: v0.10.0
|
rev: v0.10.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: shellcheck
|
- id: shellcheck
|
||||||
args: ['--severity=warning']
|
args: ["--severity=warning"]
|
||||||
|
|
||||||
- repo: https://github.com/rhysd/actionlint
|
- repo: https://github.com/rhysd/actionlint
|
||||||
rev: v1.7.7
|
rev: v1.7.7
|
||||||
hooks:
|
hooks:
|
||||||
- id: actionlint
|
- id: actionlint
|
||||||
args: ['-shellcheck=']
|
args: ["-shellcheck="]
|
||||||
|
|
||||||
- repo: https://github.com/renovatebot/pre-commit-hooks
|
|
||||||
rev: 39.227.2
|
|
||||||
hooks:
|
|
||||||
- id: renovate-config-validator
|
|
||||||
|
|
||||||
- repo: https://github.com/bridgecrewio/checkov.git
|
- repo: https://github.com/bridgecrewio/checkov.git
|
||||||
rev: '3.2.400'
|
rev: "3.2.451"
|
||||||
hooks:
|
hooks:
|
||||||
- id: checkov
|
- id: checkov
|
||||||
args:
|
args:
|
||||||
- '--quiet'
|
- "--quiet"
|
||||||
|
|||||||
11
.yamlfmt.yml
Normal file
11
.yamlfmt.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
# yamlfmt configuration file
|
||||||
|
# Schema: https://raw.githubusercontent.com/google/yamlfmt/main/schema.json
|
||||||
|
formatter:
|
||||||
|
type: basic
|
||||||
|
include_document_start: true
|
||||||
|
gitignore_excludes: true
|
||||||
|
retain_line_breaks_single: true
|
||||||
|
eof_newline: true
|
||||||
|
max_line_length: 120
|
||||||
|
indent: 2
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
---
|
---
|
||||||
extends: default
|
extends: default
|
||||||
|
|
||||||
rules:
|
rules:
|
||||||
line-length:
|
line-length:
|
||||||
max: 200
|
max: 200
|
||||||
|
|||||||
58
Justfile
Normal file
58
Justfile
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# Project automation for 'a' CLI wrapper for age encryption
|
||||||
|
# Set the shell to bash for compatibility
|
||||||
|
|
||||||
|
set shell := ["bash", "-cu"]
|
||||||
|
|
||||||
|
# Variables
|
||||||
|
|
||||||
|
BINARY := "a"
|
||||||
|
|
||||||
|
# Default: show help
|
||||||
|
default:
|
||||||
|
@just --list
|
||||||
|
|
||||||
|
# Format all code (Go, YAML, Markdown)
|
||||||
|
format:
|
||||||
|
gofmt -s -w .
|
||||||
|
goimports -w .
|
||||||
|
yamlfmt -c .yamlfmt.yml .
|
||||||
|
markdownlint -c .markdownlint.json --fix '**/*.md'
|
||||||
|
|
||||||
|
# Lint Go code and configs
|
||||||
|
lint:
|
||||||
|
golangci-lint run
|
||||||
|
yamllint -c .yamllint.yml .
|
||||||
|
markdownlint -c .markdownlint.json '**/*.md'
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
test:
|
||||||
|
go test -v ./...
|
||||||
|
|
||||||
|
# Build the binary
|
||||||
|
build:
|
||||||
|
go build -o {{ BINARY }} .
|
||||||
|
|
||||||
|
# Run GoReleaser (dry-run by default)
|
||||||
|
release:
|
||||||
|
goreleaser release --clean --skip-publish --snapshot
|
||||||
|
|
||||||
|
# Run GoReleaser for actual release (requires env vars)
|
||||||
|
release-publish:
|
||||||
|
goreleaser release --clean
|
||||||
|
|
||||||
|
# Run pre-commit hooks on all files
|
||||||
|
precommit:
|
||||||
|
pre-commit run --all-files
|
||||||
|
|
||||||
|
# Update Go modules
|
||||||
|
tidy:
|
||||||
|
go mod tidy
|
||||||
|
|
||||||
|
# Clean build artifacts
|
||||||
|
clean:
|
||||||
|
rm -rf {{ BINARY }} dist/ coverage* *.log
|
||||||
|
|
||||||
|
# Show help
|
||||||
|
help:
|
||||||
|
@echo "Available commands:"
|
||||||
|
@just --list
|
||||||
105
README.md
Normal file
105
README.md
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
# A CLI Wrapper for Age Encryption
|
||||||
|
|
||||||
|
A robust command-line interface (CLI) wrapper around the [age](https://github.com/FiloSottile/age)
|
||||||
|
encryption tool. This utility simplifies encryption and decryption using SSH keys,
|
||||||
|
with integrated support for fetching public keys from GitHub.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* **Secure Encryption/Decryption:** Utilize SSH and GitHub keys with `age` for strong encryption.
|
||||||
|
* **Configuration:** Easily configurable via a YAML file.
|
||||||
|
* **Structured Logging:** JSON-formatted logs with configurable paths.
|
||||||
|
* **Cross-platform:** Supports Linux, macOS, and Windows.
|
||||||
|
* **Shell Completion:** Auto-generated completion scripts for Bash, Zsh, and Fish.
|
||||||
|
* **Robust Error Handling:** Comprehensive and clear error messaging.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
* Go (1.21+)
|
||||||
|
* `age` encryption tool
|
||||||
|
|
||||||
|
### Build from source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd <repository-directory>
|
||||||
|
go build -o a
|
||||||
|
```
|
||||||
|
|
||||||
|
### Move binary to path (optional)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo mv a /usr/local/bin/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
a [command] [flags]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
* `config`: Manage application settings
|
||||||
|
* `encrypt`: Encrypt files
|
||||||
|
* `decrypt`: Decrypt files
|
||||||
|
* `completion`: Generate shell completion scripts
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
#### Configure the CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
a config --ssh-key ~/.ssh/id_rsa --github-user yourusername --default-recipients ~/.ssh/id_rsa.pub --cache-ttl 120
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Encrypt a file
|
||||||
|
|
||||||
|
```bash
|
||||||
|
a encrypt -o encrypted_file.txt input.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Decrypt a file
|
||||||
|
|
||||||
|
```bash
|
||||||
|
a decrypt -o decrypted_file.txt encrypted_file.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generate shell completions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
a completion bash > /etc/bash_completion.d/a
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration File
|
||||||
|
|
||||||
|
Configuration is stored at `$HOME/.config/a/config.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ssh_key_path: "/home/user/.ssh/id_rsa"
|
||||||
|
github_user: "yourusername"
|
||||||
|
default_recipients:
|
||||||
|
- "/home/user/.ssh/id_rsa.pub"
|
||||||
|
cache_ttl_minutes: 120
|
||||||
|
log_file_path: "/home/user/.state/a/cli.log"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
Structured JSON logs are written to a configurable log file (`cli.log`). Verbosity can be adjusted with the `-v` or `--verbose` flag.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Run unit tests with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
||||||
100
a.go
Normal file
100
a.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
// a is a robust CLI wrapper for the age encryption tool using SSH/GitHub keys.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/ivuorinen/a/cmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
const version = "v0.3.0"
|
||||||
|
|
||||||
|
var (
|
||||||
|
log = logrus.New()
|
||||||
|
cfg *cmd.Config
|
||||||
|
cfgFile string
|
||||||
|
)
|
||||||
|
|
||||||
|
// initConfigPaths initializes configuration and cache directories.
|
||||||
|
func initConfigPaths() error {
|
||||||
|
paths, err := cmd.InitConfigPaths()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cfgFile = paths.ConfigFile
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadConfig loads configuration from the YAML file.
|
||||||
|
func loadConfig() (*cmd.Config, error) {
|
||||||
|
return cmd.LoadConfig(cfgFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveConfig saves configuration to the YAML file.
|
||||||
|
func saveConfig(cfg *cmd.Config) error {
|
||||||
|
return cmd.SaveConfig(cfgFile, cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupLogging configures JSON logging to file and stdout.
|
||||||
|
func setupLogging(verbose bool) error {
|
||||||
|
log.SetFormatter(&logrus.JSONFormatter{})
|
||||||
|
logFile, err := os.OpenFile(cfg.LogFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not open log file: %w", err)
|
||||||
|
}
|
||||||
|
log.SetOutput(logFile)
|
||||||
|
if verbose {
|
||||||
|
log.SetLevel(logrus.DebugLevel)
|
||||||
|
} else {
|
||||||
|
log.SetLevel(logrus.InfoLevel)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var verbose bool
|
||||||
|
|
||||||
|
rootCmd := &cobra.Command{
|
||||||
|
Use: "a",
|
||||||
|
Short: "CLI wrapper for age encryption using SSH/GitHub keys",
|
||||||
|
Version: version,
|
||||||
|
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
|
||||||
|
if err := initConfigPaths(); err != nil {
|
||||||
|
return fmt.Errorf("error initializing paths: %w", err)
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
cfg, err = loadConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error loading config: %w", err)
|
||||||
|
}
|
||||||
|
return setupLogging(verbose)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
rootCmd.PersistentFlags().BoolVarP(
|
||||||
|
&verbose,
|
||||||
|
"verbose",
|
||||||
|
"v",
|
||||||
|
false,
|
||||||
|
"Enable verbose output",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add subcommands from cmd/*
|
||||||
|
rootCmd.AddCommand(
|
||||||
|
cmd.ConfigCmd(cfg, func(c any) error {
|
||||||
|
return saveConfig(c.(*cmd.Config))
|
||||||
|
}),
|
||||||
|
cmd.Encrypt(cfg, log),
|
||||||
|
cmd.Decrypt(cfg, log),
|
||||||
|
cmd.Completion(rootCmd),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Execute the root command
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
log.WithError(err).Fatal("Command execution failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
259
a_test.go
Normal file
259
a_test.go
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
|
||||||
|
"github.com/ivuorinen/a/cmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInitConfigPaths(t *testing.T) {
|
||||||
|
paths, err := cmd.InitConfigPaths()
|
||||||
|
assert.NoError(t, err, "initializing config paths should not produce an error")
|
||||||
|
|
||||||
|
assert.DirExists(t, paths.ConfigDir, "config directory should exist")
|
||||||
|
assert.FileExists(t, paths.ConfigFile, "config file path should exist")
|
||||||
|
assert.DirExists(t, paths.CacheDir, "cache directory should exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadAndSaveConfig(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
cfgFile := filepath.Join(tempDir, "config.yaml")
|
||||||
|
|
||||||
|
cfg := &cmd.Config{
|
||||||
|
SSHKeyPath: "/tmp/id_rsa",
|
||||||
|
GitHubUser: "testuser",
|
||||||
|
DefaultRecipients: []string{"/tmp/key.pub"},
|
||||||
|
CacheTTLMinutes: 60,
|
||||||
|
LogFilePath: "/tmp/test.log",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.SaveConfig(cfgFile, cfg)
|
||||||
|
assert.NoError(t, err, "saving config should not produce an error")
|
||||||
|
|
||||||
|
loadedCfg, err := cmd.LoadConfig(cfgFile)
|
||||||
|
assert.NoError(t, err, "loading config should not produce an error")
|
||||||
|
assert.Equal(t, cfg, loadedCfg, "loaded config should match saved config")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultLogFilePath(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
cfgFile := filepath.Join(tempDir, "config.yaml")
|
||||||
|
|
||||||
|
cfg := &cmd.Config{
|
||||||
|
SSHKeyPath: "/tmp/id_rsa",
|
||||||
|
GitHubUser: "testuser",
|
||||||
|
DefaultRecipients: []string{"/tmp/key.pub"},
|
||||||
|
CacheTTLMinutes: 60,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := yaml.Marshal(cfg)
|
||||||
|
assert.NoError(t, err, "marshaling config should not produce an error")
|
||||||
|
assert.NoError(t, os.WriteFile(cfgFile, data, 0o600))
|
||||||
|
|
||||||
|
loadedCfg, err := cmd.LoadConfig(cfgFile)
|
||||||
|
assert.NoError(t, err, "loading config should not produce an error")
|
||||||
|
assert.NotEmpty(t, loadedCfg.LogFilePath, "default log file path should be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetupLogging(t *testing.T) {
|
||||||
|
tempLogFile := filepath.Join(t.TempDir(), "cli.log")
|
||||||
|
cfg := &cmd.Config{LogFilePath: tempLogFile}
|
||||||
|
|
||||||
|
log := logrus.New()
|
||||||
|
log.SetFormatter(&logrus.JSONFormatter{})
|
||||||
|
logFile, err := os.OpenFile(cfg.LogFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
|
||||||
|
assert.NoError(t, err, "opening log file should not produce an error")
|
||||||
|
log.SetOutput(logFile)
|
||||||
|
log.SetLevel(logrus.InfoLevel)
|
||||||
|
|
||||||
|
log.Info("Test log entry")
|
||||||
|
assert.FileExists(t, tempLogFile, "log file should exist after setup")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmdConfig(t *testing.T) {
|
||||||
|
cfg := &cmd.Config{}
|
||||||
|
cmdObj := cmd.ConfigCmd(cfg, func(_ any) error { return nil })
|
||||||
|
assert.NotNil(t, cmdObj, "ConfigCmd should return a non-nil cobra command")
|
||||||
|
|
||||||
|
flags := cmdObj.Flags()
|
||||||
|
sshKey, _ := flags.GetString("ssh-key")
|
||||||
|
assert.Empty(t, sshKey, "default ssh-key flag should be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmdEncryptPlaceholder(t *testing.T) {
|
||||||
|
cfg := &cmd.Config{}
|
||||||
|
log := logrus.New()
|
||||||
|
cmdObj := cmd.Encrypt(cfg, log)
|
||||||
|
assert.NotNil(t, cmdObj, "Encrypt should return a non-nil cobra command")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmdDecryptPlaceholder(t *testing.T) {
|
||||||
|
cfg := &cmd.Config{}
|
||||||
|
log := logrus.New()
|
||||||
|
cmdObj := cmd.Decrypt(cfg, log)
|
||||||
|
assert.NotNil(t, cmdObj, "Decrypt should return a non-nil cobra command")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCmdCompletion(t *testing.T) {
|
||||||
|
rootCmd := &cobra.Command{Use: "a"}
|
||||||
|
cmdObj := cmd.Completion(rootCmd)
|
||||||
|
assert.NotNil(t, cmdObj, "Completion should return a non-nil cobra command")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to generate a temporary SSH keypair for testing
|
||||||
|
func generateSSHKeyPair(dir string) (privKey, pubKey string, err error) {
|
||||||
|
privKey = filepath.Join(dir, "id_rsa")
|
||||||
|
pubKey = privKey + ".pub"
|
||||||
|
cmd := exec.Command("ssh-keygen", "-t", "rsa", "-b", "2048", "-N", "", "-f", privKey)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
return privKey, pubKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to write test results to a file
|
||||||
|
func writeTestResult(dir, name string, content []byte) {
|
||||||
|
_ = os.WriteFile(filepath.Join(dir, name), content, 0o600)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptDecrypt_Success(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
plaintext := []byte("This is a secret message for encryption test.")
|
||||||
|
|
||||||
|
// Generate SSH keypair
|
||||||
|
privKey, pubKey, err := generateSSHKeyPair(tempDir)
|
||||||
|
writeTestResult(
|
||||||
|
tempDir,
|
||||||
|
"sshkeygen_success.txt",
|
||||||
|
fmt.Appendf(nil, "priv: %s\npub: %s\nerr: %v", privKey, pubKey, err),
|
||||||
|
)
|
||||||
|
assert.NoError(t, err, "ssh-keygen should succeed")
|
||||||
|
|
||||||
|
// Write plaintext file
|
||||||
|
inputFile := filepath.Join(tempDir, "input.txt")
|
||||||
|
assert.NoError(t, os.WriteFile(inputFile, plaintext, 0o600))
|
||||||
|
|
||||||
|
// Prepare config
|
||||||
|
cfg := &cmd.Config{
|
||||||
|
DefaultRecipients: []string{pubKey},
|
||||||
|
LogFilePath: filepath.Join(tempDir, "cli.log"),
|
||||||
|
}
|
||||||
|
log := logrus.New()
|
||||||
|
|
||||||
|
// Encrypt
|
||||||
|
encryptedFile := filepath.Join(tempDir, "encrypted.txt")
|
||||||
|
encryptCmd := cmd.Encrypt(cfg, log)
|
||||||
|
err = encryptCmd.Flags().Set("input", inputFile)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = encryptCmd.Flags().Set("output", encryptedFile)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = encryptCmd.RunE(encryptCmd, []string{})
|
||||||
|
writeTestResult(tempDir, "encrypt_result.txt", fmt.Appendf(nil, "err: %v", err))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.FileExists(t, encryptedFile, "encrypted file should exist")
|
||||||
|
|
||||||
|
// Decrypt
|
||||||
|
decryptCfg := &cmd.Config{SSHKeyPath: privKey, LogFilePath: cfg.LogFilePath}
|
||||||
|
decryptedFile := filepath.Join(tempDir, "decrypted.txt")
|
||||||
|
decryptCmd := cmd.Decrypt(decryptCfg, log)
|
||||||
|
err = decryptCmd.Flags().Set("input", encryptedFile)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = decryptCmd.Flags().Set("output", decryptedFile)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = decryptCmd.RunE(decryptCmd, []string{})
|
||||||
|
writeTestResult(tempDir, "decrypt_result.txt", fmt.Appendf(nil, "err: %v", err))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.FileExists(t, decryptedFile, "decrypted file should exist")
|
||||||
|
|
||||||
|
// Compare output (decryptedFile is generated by the test and not user-controlled)
|
||||||
|
// Ensure decryptedFile exists and is in tempDir before reading (gosec G304 mitigation)
|
||||||
|
info, statErr := os.Stat(decryptedFile)
|
||||||
|
assert.NoError(t, statErr, "decrypted file should exist before reading")
|
||||||
|
assert.True(t, strings.HasPrefix(decryptedFile, tempDir), "decrypted file must be in tempDir")
|
||||||
|
assert.Equal(t, info.Mode().Perm(), os.FileMode(0o600), "decrypted file must have 0600 permissions")
|
||||||
|
|
||||||
|
// #nosec G304 -- decryptedFile is generated in tempDir and not user-controlled
|
||||||
|
decrypted, err := os.ReadFile(decryptedFile)
|
||||||
|
writeTestResult(tempDir, "decrypted.txt", decrypted)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, plaintext, decrypted, "decrypted output should match original plaintext")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptDecrypt_WrongKey(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
plaintext := []byte("Secret message for wrong key test.")
|
||||||
|
|
||||||
|
// Generate two SSH keypairs
|
||||||
|
_, pubKey1, err := generateSSHKeyPair(tempDir)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
privKey2, _, err := generateSSHKeyPair(tempDir)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Write plaintext file
|
||||||
|
inputFile := filepath.Join(tempDir, "input.txt")
|
||||||
|
assert.NoError(t, os.WriteFile(inputFile, plaintext, 0o600))
|
||||||
|
|
||||||
|
// Encrypt with pubKey1
|
||||||
|
cfg := &cmd.Config{
|
||||||
|
DefaultRecipients: []string{pubKey1},
|
||||||
|
LogFilePath: filepath.Join(tempDir, "cli.log"),
|
||||||
|
}
|
||||||
|
log := logrus.New()
|
||||||
|
encryptedFile := filepath.Join(tempDir, "encrypted.txt")
|
||||||
|
encryptCmd := cmd.Encrypt(cfg, log)
|
||||||
|
err = encryptCmd.Flags().Set("input", inputFile)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = encryptCmd.Flags().Set("output", encryptedFile)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = encryptCmd.RunE(encryptCmd, []string{})
|
||||||
|
writeTestResult(tempDir, "encrypt_wrongkey_result.txt", fmt.Appendf(nil, "err: %v", err))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.FileExists(t, encryptedFile, "encrypted file should exist")
|
||||||
|
|
||||||
|
// Try to decrypt with privKey2 (should fail)
|
||||||
|
decryptCfg := &cmd.Config{SSHKeyPath: privKey2, LogFilePath: cfg.LogFilePath}
|
||||||
|
decryptedFile := filepath.Join(tempDir, "decrypted_wrongkey.txt")
|
||||||
|
decryptCmd := cmd.Decrypt(decryptCfg, log)
|
||||||
|
err = decryptCmd.Flags().Set("input", encryptedFile)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = decryptCmd.Flags().Set("output", decryptedFile)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = decryptCmd.RunE(decryptCmd, []string{})
|
||||||
|
writeTestResult(tempDir, "decrypt_wrongkey_result.txt", fmt.Appendf(nil, "err: %v", err))
|
||||||
|
assert.Error(t, err, "decryption should fail with wrong key")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptDecrypt_MissingRecipient(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
plaintext := []byte("Secret message for missing recipient test.")
|
||||||
|
|
||||||
|
// Write plaintext file
|
||||||
|
inputFile := filepath.Join(tempDir, "input.txt")
|
||||||
|
assert.NoError(t, os.WriteFile(inputFile, plaintext, 0o600))
|
||||||
|
|
||||||
|
// Encrypt with no recipient
|
||||||
|
cfg := &cmd.Config{
|
||||||
|
DefaultRecipients: []string{},
|
||||||
|
LogFilePath: filepath.Join(tempDir, "cli.log"),
|
||||||
|
}
|
||||||
|
log := logrus.New()
|
||||||
|
encryptedFile := filepath.Join(tempDir, "encrypted.txt")
|
||||||
|
encryptCmd := cmd.Encrypt(cfg, log)
|
||||||
|
err := encryptCmd.Flags().Set("input", inputFile)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = encryptCmd.Flags().Set("output", encryptedFile)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = encryptCmd.RunE(encryptCmd, []string{})
|
||||||
|
writeTestResult(tempDir, "encrypt_missingrecipient_result.txt", fmt.Appendf(nil, "err: %v", err))
|
||||||
|
assert.Error(t, err, "encryption should fail with no recipient")
|
||||||
|
}
|
||||||
27
cmd/completion.go
Normal file
27
cmd/completion.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// Package cmd provides CLI command constructors for the age wrapper.
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Completion returns a cobra.Command that generates shell completions.
|
||||||
|
func Completion(rootCmd *cobra.Command) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "completion [bash|zsh|fish]",
|
||||||
|
Short: "Generate shell completion scripts",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(_ *cobra.Command, args []string) {
|
||||||
|
switch args[0] {
|
||||||
|
case "bash":
|
||||||
|
_ = rootCmd.GenBashCompletion(os.Stdout)
|
||||||
|
case "zsh":
|
||||||
|
_ = rootCmd.GenZshCompletion(os.Stdout)
|
||||||
|
case "fish":
|
||||||
|
_ = rootCmd.GenFishCompletion(os.Stdout, true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
50
cmd/config.go
Normal file
50
cmd/config.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigCmd returns a cobra.Command for configuring SSH keys, GitHub settings, and logging.
|
||||||
|
//
|
||||||
|
// The saveConfig callback is called with the updated config.
|
||||||
|
func ConfigCmd(cfg any, saveConfig func(cfg any) error) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "config",
|
||||||
|
Short: "Configure SSH keys, GitHub settings, and logging",
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
// Type assertion for expected config struct
|
||||||
|
config, ok := cfg.(*Config)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sshKey, _ := cmd.Flags().GetString("ssh-key")
|
||||||
|
ghUser, _ := cmd.Flags().GetString("github-user")
|
||||||
|
logPath, _ := cmd.Flags().GetString("log-file-path")
|
||||||
|
recipients, _ := cmd.Flags().GetStringSlice("default-recipients")
|
||||||
|
ttl, _ := cmd.Flags().GetInt("cache-ttl")
|
||||||
|
config.SSHKeyPath = sshKey
|
||||||
|
config.GitHubUser = ghUser
|
||||||
|
config.DefaultRecipients = recipients
|
||||||
|
config.CacheTTLMinutes = ttl
|
||||||
|
config.LogFilePath = logPath
|
||||||
|
return saveConfig(config)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// These flag defaults assume cfg is already loaded
|
||||||
|
if config, ok := cfg.(*Config); ok {
|
||||||
|
cmd.Flags().String("ssh-key", "", "Path to private SSH key")
|
||||||
|
cmd.Flags().String("github-user", "", "GitHub username for public keys")
|
||||||
|
cmd.Flags().String("log-file-path", config.LogFilePath, "Path for the log file")
|
||||||
|
cmd.Flags().StringSlice("default-recipients", []string{}, "Public key file paths")
|
||||||
|
cmd.Flags().Int("cache-ttl", 120, "Cache TTL in minutes")
|
||||||
|
} else {
|
||||||
|
cmd.Flags().String("ssh-key", "", "Path to private SSH key")
|
||||||
|
cmd.Flags().String("github-user", "", "GitHub username for public keys")
|
||||||
|
cmd.Flags().String("log-file-path", "", "Path for the log file")
|
||||||
|
cmd.Flags().StringSlice("default-recipients", []string{}, "Public key file paths")
|
||||||
|
cmd.Flags().Int("cache-ttl", 120, "Cache TTL in minutes")
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
143
cmd/config_shared.go
Normal file
143
cmd/config_shared.go
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config represents the application's YAML configuration.
|
||||||
|
type Config struct {
|
||||||
|
SSHKeyPath string `yaml:"ssh_key_path"`
|
||||||
|
GitHubUser string `yaml:"github_user"`
|
||||||
|
DefaultRecipients []string `yaml:"default_recipients"`
|
||||||
|
CacheTTLMinutes int `yaml:"cache_ttl_minutes"`
|
||||||
|
LogFilePath string `yaml:"log_file_path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigPaths holds config and cache file paths.
|
||||||
|
type ConfigPaths struct {
|
||||||
|
ConfigDir string
|
||||||
|
ConfigFile string
|
||||||
|
CacheDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitConfigPaths initializes configuration and cache directories and returns their paths.
|
||||||
|
func InitConfigPaths() (ConfigPaths, error) {
|
||||||
|
var configDir string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Personal preference, I don't like the "$HOME/Library/Application Support/" path
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
configDir = filepath.Join(os.Getenv("HOME"), ".config")
|
||||||
|
} else {
|
||||||
|
configDir, err = os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
return ConfigPaths{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfgDir := filepath.Join(configDir, "a")
|
||||||
|
cfgFile := filepath.Join(cfgDir, "config.yaml")
|
||||||
|
if err := os.MkdirAll(cfgDir, 0o700); err != nil {
|
||||||
|
return ConfigPaths{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheBase, err := os.UserCacheDir()
|
||||||
|
if err != nil {
|
||||||
|
return ConfigPaths{}, err
|
||||||
|
}
|
||||||
|
cacheDir := filepath.Join(cacheBase, "a")
|
||||||
|
if err := os.MkdirAll(cacheDir, 0o700); err != nil {
|
||||||
|
return ConfigPaths{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ConfigPaths{
|
||||||
|
ConfigDir: cfgDir,
|
||||||
|
ConfigFile: cfgFile,
|
||||||
|
CacheDir: cacheDir,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadConfig loads configuration from the YAML file.
|
||||||
|
// gosec G304: cfgFile is always set by InitConfigPaths and not user-controlled.
|
||||||
|
func LoadConfig(cfgFile string) (*Config, error) {
|
||||||
|
// gosec G304 mitigation: Ensure cfgFile is within the expected config directory
|
||||||
|
configDir, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
expectedDir := filepath.Join(configDir, "a")
|
||||||
|
absCfgFile, err := filepath.Abs(cfgFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(absCfgFile, expectedDir) {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"config file path %s is not within expected config directory %s",
|
||||||
|
absCfgFile,
|
||||||
|
expectedDir,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(cfgFile); err != nil {
|
||||||
|
return nil, fmt.Errorf("config file does not exist: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := os.Stat(cfgFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("config file does not exist: %w", err)
|
||||||
|
}
|
||||||
|
if info.Mode().Perm() != 0o600 {
|
||||||
|
return nil, fmt.Errorf("config file must have 0600 permissions, got %o", info.Mode().Perm())
|
||||||
|
}
|
||||||
|
// #nosec G304 -- cfgFile is validated to be within the config directory
|
||||||
|
data, err := os.ReadFile(cfgFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var cfg Config
|
||||||
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if cfg.LogFilePath == "" {
|
||||||
|
stateDir := filepath.Join(os.Getenv("HOME"), ".state", "a")
|
||||||
|
if err := os.MkdirAll(stateDir, 0o700); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cfg.LogFilePath = filepath.Join(stateDir, "cli.log")
|
||||||
|
}
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveConfig saves configuration to the YAML file.
|
||||||
|
func SaveConfig(cfgFile string, cfg *Config) error {
|
||||||
|
data, err := yaml.Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(cfgFile, data, 0o600)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanSSHPrivateKeys scans ~/.ssh for private keys matching id_* (excluding .pub).
|
||||||
|
func ScanSSHPrivateKeys() ([]string, error) {
|
||||||
|
sshDir := filepath.Join(os.Getenv("HOME"), ".ssh")
|
||||||
|
files, err := os.ReadDir(sshDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var keys []string
|
||||||
|
for _, f := range files {
|
||||||
|
if f.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := f.Name()
|
||||||
|
if strings.HasPrefix(name, "id_") && !strings.HasSuffix(name, ".pub") {
|
||||||
|
keys = append(keys, filepath.Join(sshDir, name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keys, nil
|
||||||
|
}
|
||||||
122
cmd/decrypt.go
Normal file
122
cmd/decrypt.go
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// tryDecrypt attempts to decrypt using the given key and output/input files.
|
||||||
|
func tryDecrypt(keyPath, output, input string) error {
|
||||||
|
ageBin := "age"
|
||||||
|
if ageBin != "age" {
|
||||||
|
return fmt.Errorf("invalid binary for decryption: %s", ageBin)
|
||||||
|
}
|
||||||
|
ageArgs := []string{"-d", "-i", keyPath, "-o", output, input}
|
||||||
|
expectedFlags := map[string]bool{"-d": true, "-i": true, "-o": true}
|
||||||
|
for i, arg := range ageArgs {
|
||||||
|
if i == 0 || i == 2 || i == 4 {
|
||||||
|
if !expectedFlags[arg] && i != 0 {
|
||||||
|
return fmt.Errorf("unexpected flag in age arguments: %s", arg)
|
||||||
|
}
|
||||||
|
} else if arg == "" {
|
||||||
|
return fmt.Errorf("invalid argument for decryption: empty string")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(keyPath, "id_rsa") && !strings.HasSuffix(keyPath, "id_ed25519") {
|
||||||
|
return fmt.Errorf("invalid key file for decryption: %s", keyPath)
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(output, ".txt") && !strings.HasSuffix(output, ".out") {
|
||||||
|
return fmt.Errorf("invalid output file for decryption: %s", output)
|
||||||
|
}
|
||||||
|
// #nosec G204 -- ageBin and ageArgs are validated above
|
||||||
|
return exec.Command(ageBin, ageArgs...).Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectSSHKey determines which SSH key to use based on flags and config.
|
||||||
|
func selectSSHKey(sshKeyFlag string, cfg *Config) string {
|
||||||
|
if sshKeyFlag != "" {
|
||||||
|
return sshKeyFlag
|
||||||
|
}
|
||||||
|
return cfg.SSHKeyPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// tryAllKeys attempts decryption with all provided keys, returns true on success.
|
||||||
|
func tryAllKeys(keys []string, input, output string, log *logrus.Logger, triedKeys *[]string) bool {
|
||||||
|
for _, keyPath := range keys {
|
||||||
|
*triedKeys = append(*triedKeys, keyPath)
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"input": input,
|
||||||
|
"output": output,
|
||||||
|
"sshKey": keyPath,
|
||||||
|
}).Info("Trying decryption with SSH key")
|
||||||
|
err := tryDecrypt(keyPath, output, input)
|
||||||
|
if err == nil {
|
||||||
|
log.Info("Decryption successful")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
log.WithError(err).Warnf("Decryption failed with key %s", keyPath)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt returns a cobra.Command that decrypts files using age, scanning local SSH keys if needed.
|
||||||
|
func Decrypt(cfg *Config, log *logrus.Logger) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "decrypt",
|
||||||
|
Short: "Decrypt a file",
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
input, _ := cmd.Flags().GetString("input")
|
||||||
|
output, _ := cmd.Flags().GetString("output")
|
||||||
|
sshKeyFlag, _ := cmd.Flags().GetString("ssh-key")
|
||||||
|
|
||||||
|
if input == "" {
|
||||||
|
return fmt.Errorf("input file is required")
|
||||||
|
}
|
||||||
|
if output == "" {
|
||||||
|
return fmt.Errorf("output file is required")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(input); err != nil {
|
||||||
|
return fmt.Errorf("input file does not exist: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sshKey := selectSSHKey(sshKeyFlag, cfg)
|
||||||
|
var triedKeys []string
|
||||||
|
var success bool
|
||||||
|
|
||||||
|
if sshKey != "" {
|
||||||
|
triedKeys = append(triedKeys, sshKey)
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"input": input,
|
||||||
|
"output": output,
|
||||||
|
"sshKey": sshKey,
|
||||||
|
}).Info("Trying decryption with provided SSH key")
|
||||||
|
if err := tryDecrypt(sshKey, output, input); err == nil {
|
||||||
|
log.Info("Decryption successful")
|
||||||
|
success = true
|
||||||
|
} else {
|
||||||
|
log.WithError(err).Warn("Decryption failed with provided SSH key")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
keys, err := ScanSSHPrivateKeys()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not scan ~/.ssh for private keys: %w", err)
|
||||||
|
}
|
||||||
|
success = tryAllKeys(keys, input, output, log, &triedKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !success {
|
||||||
|
return fmt.Errorf("decryption failed: none of the tried SSH keys matched\nTried keys: %v", triedKeys)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().StringP("input", "i", "", "Input file to decrypt")
|
||||||
|
cmd.Flags().StringP("output", "o", "", "Output file for decrypted data")
|
||||||
|
cmd.Flags().String("ssh-key", "", "SSH private key to use for decryption")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
172
cmd/encrypt.go
Normal file
172
cmd/encrypt.go
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Encrypt returns a cobra.Command that encrypts files using age, supporting GitHub key fetching.
|
||||||
|
func Encrypt(cfg *Config, log *logrus.Logger) *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "encrypt",
|
||||||
|
Short: "Encrypt a file",
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
input, _ := cmd.Flags().GetString("input")
|
||||||
|
output, _ := cmd.Flags().GetString("output")
|
||||||
|
recipients, _ := cmd.Flags().GetStringSlice("recipient")
|
||||||
|
ghUserFlag, _ := cmd.Flags().GetString("github-user")
|
||||||
|
|
||||||
|
if input == "" {
|
||||||
|
return fmt.Errorf("input file is required")
|
||||||
|
}
|
||||||
|
if output == "" {
|
||||||
|
return fmt.Errorf("output file is required")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(input); err != nil {
|
||||||
|
return fmt.Errorf("input file does not exist: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
allRecipients, ghUser, err := collectRecipients(cfg, recipients, ghUserFlag, log)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(allRecipients) == 0 {
|
||||||
|
return fmt.Errorf("at least one recipient is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
ageArgs, err := buildAgeArgs(output, input, allRecipients)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithFields(logrus.Fields{
|
||||||
|
"input": input,
|
||||||
|
"output": output,
|
||||||
|
"recipients": allRecipients,
|
||||||
|
"githubUser": ghUser,
|
||||||
|
}).Info("Encrypting file")
|
||||||
|
|
||||||
|
if err := runAgeEncrypt(ageArgs, log); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Encryption successful")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().StringP("input", "i", "", "Input file to encrypt")
|
||||||
|
cmd.Flags().StringP("output", "o", "", "Output file for encrypted data")
|
||||||
|
cmd.Flags().StringSliceP("recipient", "r", []string{}, "Recipient public key file or string")
|
||||||
|
cmd.Flags().String("github-user", "", "GitHub username to fetch public keys for encryption")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to collect recipients including GitHub keys
|
||||||
|
func collectRecipients(
|
||||||
|
cfg *Config,
|
||||||
|
recipients []string,
|
||||||
|
ghUserFlag string,
|
||||||
|
log *logrus.Logger,
|
||||||
|
) ([]string, string, error) {
|
||||||
|
allRecipients := append([]string{}, cfg.DefaultRecipients...)
|
||||||
|
allRecipients = append(allRecipients, recipients...)
|
||||||
|
|
||||||
|
ghUser := ghUserFlag
|
||||||
|
if ghUser == "" && cfg.GitHubUser != "" {
|
||||||
|
ghUser = cfg.GitHubUser
|
||||||
|
}
|
||||||
|
|
||||||
|
if ghUser != "" {
|
||||||
|
validUser := regexp.MustCompile(`^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?$`)
|
||||||
|
if !validUser.MatchString(ghUser) {
|
||||||
|
log.Warnf("Invalid GitHub username: %s", ghUser)
|
||||||
|
} else {
|
||||||
|
url := fmt.Sprintf("https://github.com/%s.keys", ghUser)
|
||||||
|
if !strings.HasPrefix(url, "https://github.com/") || !strings.HasSuffix(url, ".keys") {
|
||||||
|
log.Warnf("Refusing to fetch keys from non-GitHub URL: %s", url)
|
||||||
|
} else {
|
||||||
|
// #nosec G107 -- url is validated to be a GitHub keys endpoint above
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Warnf("Failed to fetch GitHub keys for user %s", ghUser)
|
||||||
|
} else {
|
||||||
|
var githubKeys []string
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
closeErr := resp.Body.Close()
|
||||||
|
if err == nil && closeErr == nil {
|
||||||
|
for _, line := range strings.Split(string(body), "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line != "" {
|
||||||
|
githubKeys = append(githubKeys, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Warn("Failed to read GitHub keys response body")
|
||||||
|
}
|
||||||
|
if closeErr != nil {
|
||||||
|
log.WithError(closeErr).Warn("Failed to close GitHub keys response body")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
log.Warnf("GitHub returned status %d for user %s", resp.StatusCode, ghUser)
|
||||||
|
}
|
||||||
|
allRecipients = append(allRecipients, githubKeys...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allRecipients, ghUser, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to build and validate age arguments
|
||||||
|
func buildAgeArgs(output, input string, recipients []string) ([]string, error) {
|
||||||
|
ageArgs := []string{"-o", output}
|
||||||
|
for _, r := range recipients {
|
||||||
|
ageArgs = append(ageArgs, "-r", r)
|
||||||
|
}
|
||||||
|
ageArgs = append(ageArgs, input)
|
||||||
|
|
||||||
|
// Only allow expected flags for age and restrict file extensions
|
||||||
|
expectedFlags := map[string]bool{"-o": true, "-r": true}
|
||||||
|
for i, arg := range ageArgs {
|
||||||
|
if i%2 == 0 && i < len(ageArgs)-2 { // flags before last two args
|
||||||
|
if !expectedFlags[arg] {
|
||||||
|
return nil, fmt.Errorf("unexpected flag in age arguments: %s", arg)
|
||||||
|
}
|
||||||
|
} else if arg == "" {
|
||||||
|
return nil, fmt.Errorf("invalid argument for encryption: empty string")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Restrict output to expected file extensions
|
||||||
|
if !strings.HasSuffix(output, ".txt") && !strings.HasSuffix(output, ".out") {
|
||||||
|
return nil, fmt.Errorf("invalid output file for encryption: %s", output)
|
||||||
|
}
|
||||||
|
return ageArgs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to run age encryption command
|
||||||
|
func runAgeEncrypt(ageArgs []string, log *logrus.Logger) error {
|
||||||
|
ageBin := "age"
|
||||||
|
if ageBin != "age" {
|
||||||
|
return fmt.Errorf("invalid binary for encryption: %s", ageBin)
|
||||||
|
}
|
||||||
|
cmdAge := exec.Command(ageBin, ageArgs...)
|
||||||
|
if err := cmdAge.Run(); err != nil {
|
||||||
|
log.WithError(err).Error("Encryption failed")
|
||||||
|
return fmt.Errorf("age encryption failed: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config struct should be imported from the main package or shared as needed.
|
||||||
19
go.mod
Normal file
19
go.mod
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
module github.com/ivuorinen/a
|
||||||
|
|
||||||
|
go 1.24
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/sirupsen/logrus v1.9.3
|
||||||
|
github.com/spf13/cobra v1.9.1
|
||||||
|
github.com/stretchr/testify v1.10.0
|
||||||
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.6 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
28
go.sum
Normal file
28
go.sum
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||||
|
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||||
|
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||||
|
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
72
revive.toml
Normal file
72
revive.toml
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# Revive configuration for "a" project
|
||||||
|
# https://revive.run/
|
||||||
|
|
||||||
|
ignoreGeneratedHeader = false
|
||||||
|
severity = "warning"
|
||||||
|
confidence = 0.8
|
||||||
|
errorCode = 0
|
||||||
|
warningCode = 0
|
||||||
|
|
||||||
|
# Core rules that align with golangci-lint settings
|
||||||
|
[rule.blank-imports]
|
||||||
|
[rule.context-as-argument]
|
||||||
|
[rule.context-keys-type]
|
||||||
|
[rule.dot-imports]
|
||||||
|
[rule.error-return]
|
||||||
|
[rule.error-strings]
|
||||||
|
[rule.error-naming]
|
||||||
|
[rule.exported]
|
||||||
|
[rule.if-return]
|
||||||
|
[rule.increment-decrement]
|
||||||
|
[rule.var-naming]
|
||||||
|
[rule.var-declaration]
|
||||||
|
[rule.range]
|
||||||
|
[rule.receiver-naming]
|
||||||
|
[rule.time-naming]
|
||||||
|
[rule.unexported-return]
|
||||||
|
[rule.indent-error-flow]
|
||||||
|
[rule.errorf]
|
||||||
|
[rule.empty-block]
|
||||||
|
[rule.superfluous-else]
|
||||||
|
[rule.unreachable-code]
|
||||||
|
[rule.redefines-builtin-id]
|
||||||
|
|
||||||
|
# Rules that complement golangci-lint but don't conflict
|
||||||
|
[rule.atomic]
|
||||||
|
[rule.bool-literal-in-expr]
|
||||||
|
[rule.constant-logical-expr]
|
||||||
|
[rule.defer]
|
||||||
|
[rule.early-return]
|
||||||
|
[rule.empty-lines]
|
||||||
|
[rule.get-return]
|
||||||
|
[rule.identical-branches]
|
||||||
|
[rule.imports-blacklist]
|
||||||
|
[rule.modifies-parameter]
|
||||||
|
[rule.modifies-value-receiver]
|
||||||
|
[rule.optimize-operands-order]
|
||||||
|
[rule.string-of-int]
|
||||||
|
[rule.struct-tag]
|
||||||
|
# [rule.switch-default] # Rule not available in current version
|
||||||
|
[rule.unconditional-recursion]
|
||||||
|
[rule.unnecessary-stmt]
|
||||||
|
[rule.useless-break]
|
||||||
|
[rule.waitgroup-by-value]
|
||||||
|
|
||||||
|
# Project-specific rules
|
||||||
|
[rule.package-comments] # Require package comments
|
||||||
|
[rule.confusing-naming] # Catch confusing variable/function names
|
||||||
|
|
||||||
|
# Disable rules that conflict with golangci-lint or project preferences
|
||||||
|
# [rule.unused-parameter] # Disabled - conflicts with golangci-lint unused linter
|
||||||
|
# [rule.line-length-limit] # Disabled - conflicts with golangci-lint lll (which is also disabled)
|
||||||
|
# [rule.function-length] # Disabled - conflicts with golangci-lint funlen (which is disabled)
|
||||||
|
# [rule.cyclomatic] # Disabled - conflicts with golangci-lint gocyclo
|
||||||
|
# [rule.cognitive-complexity] # Disabled - conflicts with golangci-lint gocognit (which is disabled)
|
||||||
|
# [rule.max-public-structs] # Disabled - too restrictive for this project
|
||||||
|
# [rule.flag-parameter] # Disabled - can be too strict for CLI applications
|
||||||
|
# [rule.deep-exit] # Disabled - acceptable in CLI applications
|
||||||
|
# [rule.file-header] # Disabled - no specific file header requirement
|
||||||
|
# [rule.add-constant] # Disabled - can be too strict for configuration values
|
||||||
|
# [rule.argument-limit] # Disabled - can be too restrictive for some functions
|
||||||
|
# [rule.function-result-limit] # Disabled - Go allows multiple returns
|
||||||
|
# [rule.unhandled-error] # Disabled - conflicts with golangci-lint errcheck
|
||||||
Reference in New Issue
Block a user