@objctp/opencode-shell-routines 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +114 -0
  3. package/agents/shell-architect.md +88 -0
  4. package/agents/shell-expert.md +60 -0
  5. package/commands/shell-audit.md +47 -0
  6. package/commands/shell-batch-exec.md +48 -0
  7. package/commands/shell-new.md +57 -0
  8. package/commands/shell-routines-setup.md +66 -0
  9. package/commands/shell-test-run.md +46 -0
  10. package/opencode.json +19 -0
  11. package/package.json +34 -0
  12. package/plugins/shell-hooks.ts +150 -0
  13. package/scripts/lib-batch.sh +297 -0
  14. package/scripts/lib-common.sh +332 -0
  15. package/skills/shell-batch-operations/SKILL.md +97 -0
  16. package/skills/shell-batch-operations/assets/batch-template.sh +124 -0
  17. package/skills/shell-batch-operations/examples/data-pipeline.sh +157 -0
  18. package/skills/shell-batch-operations/examples/file-batch.sh +140 -0
  19. package/skills/shell-batch-operations/references/decision-tree.md +53 -0
  20. package/skills/shell-best-practices/SKILL.md +313 -0
  21. package/skills/shell-best-practices/assets/library.sh +142 -0
  22. package/skills/shell-best-practices/assets/minimal.sh +54 -0
  23. package/skills/shell-best-practices/assets/posix.sh +180 -0
  24. package/skills/shell-best-practices/assets/standard.sh +203 -0
  25. package/skills/shell-best-practices/references/patterns.md +386 -0
  26. package/skills/shell-best-practices/references/security.md +195 -0
  27. package/skills/shell-debugging/SKILL.md +115 -0
  28. package/skills/shell-debugging/examples/debug-session.md +165 -0
  29. package/skills/shell-debugging/references/debugging-guide.md +336 -0
  30. package/skills/shell-profiling/SKILL.md +154 -0
  31. package/skills/shell-profiling/examples/profile-session.md +225 -0
  32. package/skills/shell-profiling/references/optimisation-patterns.md +373 -0
  33. package/skills/shell-profiling/references/profiling-tools.md +318 -0
  34. package/skills/shell-profiling/scripts/bench.sh +82 -0
  35. package/skills/shell-profiling/scripts/trace-aggregate.sh +34 -0
  36. package/skills/shell-review/SKILL.md +61 -0
  37. package/skills/shell-review/examples/sample-review.md +42 -0
  38. package/skills/shell-review/references/guidelines.md +48 -0
  39. package/skills/shell-review/references/review-template.md +56 -0
  40. package/skills/shell-security/SKILL.md +128 -0
  41. package/skills/shell-security/examples/dangerous-command-review.md +231 -0
  42. package/skills/shell-security/examples/secure-script-example.sh +317 -0
  43. package/skills/shell-security/references/dangerous-commands.md +561 -0
  44. package/skills/shell-security/references/security-patterns.md +30 -0
  45. package/skills/shell-security/references/sensitive-files.md +525 -0
  46. package/skills/shell-security/scripts/security-audit.sh +208 -0
  47. package/skills/shell-test/SKILL.md +237 -0
  48. package/skills/shell-test/examples/test-example.md +74 -0
  49. package/skills/shell-test/references/advanced-patterns.md +52 -0
  50. package/skills/shell-test/references/assertions.md +184 -0
  51. package/skills/shell-test/references/test-template.md +60 -0
  52. package/skills/shell-test/scripts/public-coverage.sh +93 -0
@@ -0,0 +1,184 @@
1
+ # Bashunit Assertions Reference
2
+
3
+ Complete list of bashunit assertions for test cases.
4
+
5
+ > **Verify against your installed version.** This is a static summary — the authoritative list, including exact signatures, is `bashunit doc assert` (filter with `bashunit doc <term>`, e.g. `bashunit doc exit`). Run it before relying on an assertion: versions differ (e.g. `assert_success` was removed; `assert_exec` was added; the exit-code family takes **no** command argument — see Exit Code Assertions below).
6
+
7
+ ## Core Assertions
8
+
9
+ | Assertion | Use Case | Example |
10
+ |-----------|----------|---------|
11
+ | `assert_equals "expected" "$actual"` | String comparison (ignores ANSI colours) | `assert_equals "hello" "$output"` |
12
+ | `assert_same "expected" "$actual"` | Exact comparison (including special chars) | `assert_same "hello" "hello"` |
13
+ | `assert_not_same "a" "b"` | Exact inequality | `assert_not_same "hello" "world"` |
14
+ | `assert_matches "regex" "$string"` | Regex pattern matching | `assert_matches "^[0-9]+$" "$var"` |
15
+ | `assert_not_matches "regex" "$string"` | Negative regex match | `assert_not_matches "^[a-z]+$" "123"` |
16
+ | `assert_contains "needle" "$haystack"` | Substring check | `assert_contains "error" "$log"` |
17
+ | `assert_not_contains "needle" "$haystack"` | Negative substring check | `assert_not_contains "ok" "$log"` |
18
+ | `assert_contains_ignore_case "NEEDLE" "$haystack"` | Case-insensitive substring | `assert_contains_ignore_case "hello" "Hello World"` |
19
+ | `assert_empty "$var"` | Assert value is empty | `assert_empty ""` |
20
+ | `assert_not_empty "$var"` | Assert value is non-empty | `assert_not_empty "content"` |
21
+ | `assert_string_starts_with "prefix" "$string"` | Prefix check | `assert_string_starts_with "Hello" "Hello World"` |
22
+ | `assert_string_ends_with "suffix" "$string"` | Suffix check | `assert_string_ends_with "World" "Hello World"` |
23
+ | `assert_string_matches_format "%d items" "$string"` | Format matching with placeholders | `assert_string_matches_format "%d items at %f each" "42 items at 9.99 each"` |
24
+ | `assert_line_count N "$multiline"` | Assert number of lines | `assert_line_count 3 "$output"` |
25
+
26
+ ## Exit Code Assertions
27
+
28
+ These read `$?` from the command run immediately before the assertion — they take **no** command argument. To run a command string and check its exit in one call, use `assert_exec`.
29
+
30
+ | Assertion | Use Case | Example |
31
+ |-----------|----------|---------|
32
+ | `assert_exit_code "N"` | Last command exited with code N | `cmd; assert_exit_code 2` |
33
+ | `assert_successful_code` | Last command exited 0 | `ok_cmd; assert_successful_code` |
34
+ | `assert_unsuccessful_code` | Last command exited non-zero | `bad_cmd; assert_unsuccessful_code` |
35
+ | `assert_general_error` | Last command exited 1 | `bad_input; assert_general_error` |
36
+ | `assert_command_not_found` | Last command exited 127 | `missing; assert_command_not_found` |
37
+ | `assert_exec "cmd" --exit N` | Run a command string, assert its exit | `assert_exec "curl -s http://x" --exit 0` |
38
+
39
+ ## File Assertions
40
+
41
+ | Assertion | Use Case | Example |
42
+ |-----------|----------|---------|
43
+ | `assert_file_exists "$path"` | File exists | `assert_file_exists "/tmp/output.txt"` |
44
+ | `assert_file_contains "needle" "$path"` | File contains substring | `assert_file_contains "error" "/var/log/app.log"` |
45
+ | `assert_file_not_exists "$path"` | File does not exist | `assert_file_not_exists "/tmp/old"` |
46
+ | `assert_directory_exists "$path"` | Directory exists | `assert_directory_exists "/var/log"` |
47
+ | `assert_directory_not_exists "$path"` | Directory does not exist | `assert_directory_not_exists "/tmp/missing"` |
48
+
49
+ ## Array Assertions
50
+
51
+ | Assertion | Use Case | Example |
52
+ |-----------|----------|---------|
53
+ | `assert_array_contains "needle" "${arr[@]}"` | Array contains value | `assert_array_contains "apple" "${fruits[@]}"` |
54
+ | `assert_array_not_contains "needle" "${arr[@]}"` | Array does not contain value | `assert_array_not_contains "pear" "${fruits[@]}"` |
55
+
56
+ ## Usage Patterns
57
+
58
+ ### Testing Function Output
59
+ ```bash
60
+ function test_function_returns_expected() {
61
+ my_function "input" > "$TEMP_FILE" # main-shell call so coverage records the body
62
+ assert_equals "expected_value" "$(<"$TEMP_FILE")"
63
+ }
64
+ ```
65
+
66
+ ### Testing Exit Codes
67
+ ```bash
68
+ function test_function_succeeds() {
69
+ my_function "valid_input"
70
+ assert_successful_code
71
+ }
72
+
73
+ function test_function_fails_on_invalid_input() {
74
+ my_function "invalid_input"
75
+ assert_general_error
76
+ }
77
+ ```
78
+
79
+ ### Testing File Operations
80
+ ```bash
81
+ function test_creates_output_file() {
82
+ my_function "$TEMP_FILE"
83
+ assert_file_exists "$TEMP_FILE"
84
+ }
85
+
86
+ function test_output_contains_expected_content() {
87
+ my_function "$TEMP_FILE"
88
+ assert_file_contains "expected text" "$TEMP_FILE"
89
+ }
90
+ ```
91
+
92
+ ### Testing String Content
93
+ ```bash
94
+ function test_output_contains_error_message() {
95
+ local result
96
+ result=$(my_function)
97
+ assert_contains "Error:" "$result"
98
+ }
99
+ ```
100
+
101
+ ### Testing Pattern Matching
102
+ ```bash
103
+ function test_returns_numeric_id() {
104
+ local result
105
+ result=$(get_id)
106
+ assert_matches "^[0-9]+$" "$result"
107
+ }
108
+ ```
109
+
110
+ ### Mocking External Commands
111
+ ```bash
112
+ # Simple mock with fixed output
113
+ function test_fetch_returns_data() {
114
+ bashunit::mock curl <<< '{"status":"ok"}'
115
+ local result
116
+ result=$(fetch_data "http://example.com")
117
+ assert_contains "ok" "$result"
118
+ }
119
+
120
+ # Conditional mock based on arguments
121
+ function test_deploy_selects_region() {
122
+ mockAws() {
123
+ if [[ "$1" == "region" ]]; then echo "eu-west-1"; fi
124
+ }
125
+ bashunit::mock aws mockAws
126
+ local result
127
+ result=$(deploy_function)
128
+ assert_equals "eu-west-1" "$result"
129
+ }
130
+
131
+ # Multi-line mock with heredoc
132
+ function test_ps_lists_processes() {
133
+ bashunit::mock ps <<EOF
134
+ PID TTY TIME CMD
135
+ 1234 pts/0 00:00:01 bash
136
+ EOF
137
+ local result
138
+ result=$(list_processes)
139
+ assert_line_count 2 "$result"
140
+ }
141
+ ```
142
+
143
+ ## Edge Case Testing
144
+
145
+ | Edge Case | Test Pattern |
146
+ |-----------|--------------|
147
+ | Empty input | `function_name ""; assert_general_error` |
148
+ | Whitespace input | `function_name " "; assert_equals "trimmed" "$result"` |
149
+ | Special characters | `function_name '$!*@#'; assert_successful_code` |
150
+ | Large input | `function_name "$(seq 1 10000)"; assert_successful_code` |
151
+ | Null/zero values | `function_name 0; assert_equals "0" "$result"` |
152
+
153
+ ## Coverage
154
+
155
+ bashunit tracks **whole-file** line-level code coverage natively via the `--coverage` flag. This includes main blocks, private helpers, and untestable external calls, so it can under-state how well public functions are tested or be unreachable on some scripts. To measure **public-function** coverage directly (scoped to `<namespace>::` functions, excluding private `_` helpers and non-function code), run `scripts/public-coverage.sh`.
156
+
157
+ ### CLI Flags
158
+
159
+ | Flag | Purpose | Example |
160
+ |------|---------|---------|
161
+ | `--coverage` | Enable coverage tracking | `bashunit tests/ --coverage` |
162
+ | `--coverage-min N` | Fail if coverage below N% | `bashunit tests/ --coverage-min 80` |
163
+ | `--coverage-paths` | Source paths to track | `--coverage-paths "src/,lib/"` |
164
+ | `--coverage-exclude` | Exclude glob patterns | `--coverage-exclude "vendor/*,*_mock.sh"` |
165
+ | `--coverage-report-html` | Generate HTML report | `--coverage-report-html coverage/html` |
166
+ | `--no-coverage-report` | Console output only (skip LCOV) | `--coverage --no-coverage-report` |
167
+
168
+ ### Enforcement
169
+
170
+ - **Default threshold**: 80% (set via `--coverage-min 80`)
171
+ - **Behaviour**: bashunit exits with code 1 if coverage is below the threshold -- suitable for CI pipelines
172
+ - **Override**: pass a different threshold to `/shell-test-run`
173
+
174
+ ```bash
175
+ # Standard run with default 80% threshold
176
+ bashunit tests/ --coverage --coverage-paths src/ --coverage-min 80
177
+
178
+ # Lower threshold for scripts with untestable external calls
179
+ bashunit tests/ --coverage --coverage-min 50
180
+
181
+ # Example failure output
182
+ # Coverage: 75.5% (below minimum 80%)
183
+ # Exit code: 1
184
+ ```
@@ -0,0 +1,60 @@
1
+ # Bashunit Test Template
2
+
3
+ Standard test file structure for bashunit tests.
4
+
5
+ ```bash
6
+ #!/usr/bin/env bash
7
+ # tests/[script-name]-test.sh
8
+
9
+ # Runs once before all tests in this file.
10
+ # Source with strict-mode save/restore: scripts under test open with
11
+ # `set -euo pipefail`, which would abort tests before assertions can inspect $?
12
+ function set_up_before_script() {
13
+ local _opts
14
+ _opts=$(shopt -po errexit nounset pipefail 2>/dev/null || true)
15
+ source path/to/[script_name].sh
16
+ eval "$_opts"
17
+ }
18
+
19
+ # Runs before each test
20
+ function set_up() {
21
+ TEMP_FILE=$(bashunit::temp_file)
22
+ }
23
+
24
+ # Runs after each test
25
+ function tear_down() {
26
+ rm -f "$TEMP_FILE"
27
+ }
28
+
29
+ function test_[function_name]_does_something() {
30
+ [function_name] "input" > "$TEMP_FILE" # main-shell call (see Coverage and Subshells)
31
+ assert_equals "expected" "$(<"$TEMP_FILE")"
32
+ }
33
+
34
+ function test_[function_name]_handles_empty_input() {
35
+ [function_name] ""
36
+ assert_general_error
37
+ }
38
+
39
+ # Runs once after all tests in this file
40
+ function tear_down_after_script() {
41
+ # Cleanup after all tests
42
+ }
43
+ ```
44
+
45
+ ## Template Components
46
+
47
+ | Component | Scope | Purpose |
48
+ |-----------|-------|---------|
49
+ | `set_up_before_script()` | Once before all tests | Source the script under test, start services |
50
+ | `set_up()` | Before each test | Create temp files, set environment variables |
51
+ | `tear_down()` | After each test | Remove temp files, unset variables |
52
+ | `test_[name]()` | Per test case | Individual test case -- name describes what is being tested |
53
+ | `tear_down_after_script()` | Once after all tests | Stop services, final cleanup |
54
+
55
+ ## Naming Conventions
56
+
57
+ - **Test file:** `[script-name]-test.sh` in `tests/` directory
58
+ - **Test function:** `test_[function_name]_[scenario]()`
59
+ - **Custom title:** Use `set_test_title "Description"` inside test functions for descriptive reporting
60
+ - **Assertions:** Use descriptive assertions that clearly show expected vs actual
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env bash
2
+ # public-coverage.sh -- Measure PUBLIC-function line coverage (plugin convention).
3
+ #
4
+ # Public functions are <namespace>::function_name (e.g. shroutines::add, myapp::run).
5
+ # Private functions are _function_name (leading underscore). This script reports line
6
+ # coverage scoped to PUBLIC functions only -- excluding private helpers and all
7
+ # non-function code (main blocks, top-level statements, constants).
8
+ #
9
+ # Why: bashunit's --coverage / --coverage-min measure WHOLE-FILE line coverage, which
10
+ # includes untestable code (main blocks, external calls) and can be unreachable or
11
+ # meaningless. The ~80% target in the shell-test skill refers to public-function
12
+ # coverage; this script measures that figure directly.
13
+ #
14
+ # Requires: bashunit >= 0.36 (BASHUNIT_COVERAGE_SHOW_FUNCTIONS / per-function LCOV).
15
+ #
16
+ # Usage:
17
+ # public-coverage.sh [--min N] [bashunit args...]
18
+ # --min N Minimum public-function coverage percent (default 80)
19
+ # Examples:
20
+ # public-coverage.sh tests/ --coverage-paths src/
21
+ # public-coverage.sh --min 90 tests/ --coverage-paths src/,lib/
22
+ #
23
+ # Exit status: 0 if public coverage >= threshold; 1 if below or no public functions.
24
+
25
+ set -euo pipefail
26
+
27
+ MIN=80
28
+ ARGS=()
29
+ while (($# > 0)); do
30
+ case "$1" in
31
+ --min)
32
+ MIN="${2:?--min requires a value}"
33
+ shift 2
34
+ ;;
35
+ --)
36
+ shift
37
+ ARGS=("$@")
38
+ break
39
+ ;;
40
+ *)
41
+ ARGS+=("$1")
42
+ shift
43
+ ;;
44
+ esac
45
+ done
46
+
47
+ # Run bashunit with per-function output forced on. Swallow its exit code: a test
48
+ # failure or a whole-file --coverage-min miss must not abort the public-coverage check.
49
+ RAW=$(BASHUNIT_COVERAGE_SHOW_FUNCTIONS=true bashunit "${ARGS[@]}" --coverage 2>&1) || true
50
+
51
+ # From the "Functions" section, extract each entry as "<name> <hit> <total>".
52
+ FUNCS=$(printf '%s\n' "$RAW" |
53
+ sed $'s/\x1b\\[[0-9;]*m//g' |
54
+ awk '/^Functions$/{f=1;next} /^Coverage report written/{f=0} f && /lines \(/' |
55
+ sed -nE 's/^ +([^ ]+) +([0-9]+)\/ *([0-9]+) lines.*/\1 \2 \3/p') || true
56
+
57
+ echo "Public-function coverage (<namespace>::name; private _ helpers excluded)"
58
+ echo "------------------------------------------------------------------------"
59
+
60
+ PUB_HIT=0
61
+ PUB_TOTAL=0
62
+ while IFS=' ' read -r name hit total; do
63
+ [[ -z "${name:-}" ]] && continue
64
+ # public = namespaced with :: and not private (_-prefixed)
65
+ if [[ "$name" == *::* && "$name" != _* ]]; then
66
+ [[ "${total:-0}" =~ ^[0-9]+$ ]] || continue
67
+ ((total == 0)) && continue
68
+ pct=$((hit * 100 / total))
69
+ printf ' %-36s %s/%s (%d%%)\n' "$name" "$hit" "$total" "$pct"
70
+ PUB_HIT=$((PUB_HIT + hit))
71
+ PUB_TOTAL=$((PUB_TOTAL + total))
72
+ fi
73
+ done <<<"${FUNCS}"
74
+
75
+ echo "------------------------------------------------------------------------"
76
+ if ((PUB_TOTAL == 0)); then
77
+ echo "No public (<namespace>::name) functions found in the coverage report." >&2
78
+ echo "Namespace your public functions (e.g. myapp::run) per the plugin convention," >&2
79
+ echo "or fall back to whole-file coverage: bashunit tests/ --coverage --coverage-min N." >&2
80
+ exit 1
81
+ fi
82
+
83
+ PUB_PCT=$((PUB_HIT * 100 / PUB_TOTAL))
84
+ printf 'Public functions: %d/%d lines (%d%%) -- threshold %d%%\n' \
85
+ "$PUB_HIT" "$PUB_TOTAL" "$PUB_PCT" "$MIN"
86
+
87
+ if ((PUB_PCT >= MIN)); then
88
+ echo "PASS"
89
+ exit 0
90
+ else
91
+ echo "FAIL: public-function coverage below threshold" >&2
92
+ exit 1
93
+ fi