@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,150 @@
1
+ import type { Hooks, Plugin, PluginInput } from "@opencode-ai/plugin";
2
+
3
+ const SHELL_EXTENSIONS = new Set(["sh", "bash", "zsh", "ksh"]);
4
+ const SHEBANG_PATTERN = /^#!.*\b(bash|sh|zsh|ksh)\b/;
5
+ const DASH_PATTERN = /#!.*\bdash\b/;
6
+ const SH_ONLY_PATTERN = /#!.*\bsh\b/;
7
+ const BASH_FAMILY_PATTERN = /#!.*\b(bash|zsh|ksh)\b/;
8
+
9
+ async function hasCmd($: PluginInput["$"], cmd: string): Promise<boolean> {
10
+ const r = await $`command -v ${cmd}`.nothrow();
11
+ return r.exitCode === 0;
12
+ }
13
+
14
+ export const ShellHooksPlugin: Plugin = async (
15
+ { $ }: PluginInput,
16
+ ): Promise<Hooks> => {
17
+ const hasShellcheck = await hasCmd($, "shellcheck");
18
+ const hasCheckbashisms = await hasCmd($, "checkbashisms");
19
+ return {
20
+ "tool.execute.after": async (
21
+ input: {
22
+ tool: string;
23
+ sessionID: string;
24
+ callID: string;
25
+ args: { file_path?: string; filePath?: string; [key: string]: unknown };
26
+ },
27
+ output: { title: string; output: string; metadata: unknown },
28
+ ) => {
29
+ if (input.tool !== "write" && input.tool !== "edit") return;
30
+
31
+ const filePath: string | undefined = input.args?.file_path ??
32
+ input.args?.filePath;
33
+ if (!filePath) return;
34
+
35
+ // Canonicalise and verify file exists
36
+ let resolved: string;
37
+ try {
38
+ resolved = await $`realpath ${filePath}`.nothrow().text();
39
+ resolved = resolved.trim();
40
+ } catch {
41
+ return;
42
+ }
43
+ if (!resolved) return;
44
+
45
+ const exists = await $`test -f ${resolved}`.nothrow();
46
+ if (exists.exitCode !== 0) return;
47
+
48
+ const ext = resolved.split(".").pop()?.toLowerCase() ?? "";
49
+
50
+ // Read first line once — needed for shebang check and dialect detection
51
+ let firstLine: string;
52
+ try {
53
+ firstLine = await $`head -1 ${resolved}`.nothrow().text();
54
+ } catch {
55
+ return;
56
+ }
57
+
58
+ if (!SHELL_EXTENSIONS.has(ext) && !SHEBANG_PATTERN.test(firstLine)) {
59
+ return;
60
+ }
61
+
62
+ let dialect = "bash";
63
+ let isPosix = false;
64
+ if (DASH_PATTERN.test(firstLine)) {
65
+ dialect = "dash";
66
+ isPosix = true;
67
+ } else if (
68
+ SH_ONLY_PATTERN.test(firstLine) && !BASH_FAMILY_PATTERN.test(firstLine)
69
+ ) {
70
+ dialect = "sh";
71
+ isPosix = true;
72
+ }
73
+
74
+ const findings: string[] = [];
75
+
76
+ // ShellCheck — findings on stdout, exits non-zero on issues
77
+ if (hasShellcheck) {
78
+ try {
79
+ const sc = await $`shellcheck -s ${dialect} ${resolved} 2>&1`
80
+ .nothrow().text();
81
+ if (sc.trim()) {
82
+ findings.push(
83
+ `ShellCheck findings in ${resolved} (shell=${dialect}):\n${sc.trim()}`,
84
+ );
85
+ }
86
+ // deno-lint-ignore no-empty
87
+ } catch {}
88
+ }
89
+
90
+ if (!isPosix) {
91
+ try {
92
+ const syntax = await $`bash -n ${resolved} 2>&1`.nothrow().text();
93
+ if (syntax.trim()) {
94
+ findings.push(`Syntax error in ${resolved}: ${syntax.trim()}`);
95
+ }
96
+ // deno-lint-ignore no-empty
97
+ } catch {}
98
+ }
99
+
100
+ if (isPosix && hasCheckbashisms) {
101
+ try {
102
+ const bashisms = await $`checkbashisms ${resolved} 2>&1`.nothrow()
103
+ .text();
104
+ if (bashisms.trim()) {
105
+ findings.push(
106
+ `POSIX compatibility issue in ${resolved} — bashisms detected:\n${bashisms.trim()}\n` +
107
+ "Note: /bin/sh is dash on Ubuntu/Debian. These will fail at runtime.",
108
+ );
109
+ }
110
+ // deno-lint-ignore no-empty
111
+ } catch {}
112
+ }
113
+
114
+ // TODO/FIXME/HACK/XXX/BUG markers
115
+ try {
116
+ const todos =
117
+ await $`grep -n -E '(^|[^[:alnum:]_])(TODO|FIXME|HACK|XXX|BUG):' ${resolved}`
118
+ .nothrow()
119
+ .text();
120
+ if (todos.trim()) {
121
+ findings.push(`Unresolved markers in ${resolved}:\n${todos.trim()}`);
122
+ }
123
+ // deno-lint-ignore no-empty
124
+ } catch {}
125
+
126
+ // Batch script pattern validation
127
+ try {
128
+ const content = await $`cat ${resolved}`.nothrow().text();
129
+ if (content.includes("lib-batch.sh")) {
130
+ if (!content.includes("batch_output")) {
131
+ findings.push(
132
+ `Batch script detected in ${resolved}: ensure batch_output() is called to return JSON results`,
133
+ );
134
+ }
135
+ if (!content.includes("declare -A RESULTS")) {
136
+ findings.push(
137
+ `Batch script detected in ${resolved}: declare RESULTS array with: declare -A RESULTS`,
138
+ );
139
+ }
140
+ }
141
+ // deno-lint-ignore no-empty
142
+ } catch {}
143
+
144
+ if (findings.length > 0) {
145
+ output.output += "\n\n---\n**Shell quality checks:**\n" +
146
+ findings.join("\n\n");
147
+ }
148
+ },
149
+ };
150
+ };
@@ -0,0 +1,297 @@
1
+ #!/usr/bin/env bash
2
+ # shellcheck disable=SC2178
3
+ #
4
+ # Batch operations library for shell-routines plugin
5
+ # Source this file in scripts that perform multiple operations and return structured JSON
6
+ #
7
+ # Functions:
8
+ # batch_add_result - Add a key-value pair to results
9
+ # batch_add_metadata - Add metadata entry
10
+ # batch_add_error - Add error entry
11
+ # batch_output - Output JSON result to stdout
12
+ # batch_progress - Log progress to stderr
13
+ #
14
+
15
+ # Guard against direct execution
16
+ [[ "${BASH_SOURCE[0]}" == "${0}" ]] && {
17
+ echo "Error: This file should be sourced, not executed" >&2
18
+ exit 2
19
+ }
20
+
21
+ # Version tracking
22
+ # shellcheck disable=SC2034
23
+ readonly LIB_BATCH_VERSION="1.0.0"
24
+
25
+ ###
26
+ ### :::: Result Collection :::: #######
27
+ ###
28
+
29
+ # Add a key-value pair to results array
30
+ # Usage: batch_add_result RESULTS "key" "value"
31
+ # Results are stored as: key=value
32
+ function batch_add_result() {
33
+ local -n results_ref="$1"
34
+ local key="$2"
35
+ local value="$3"
36
+
37
+ results_ref["${key}"]="${value}"
38
+ }
39
+
40
+ # Add a numbered result (for arrays/lists)
41
+ # Usage: batch_add_result_item RESULTS "item"
42
+ function batch_add_result_item() {
43
+ local -n results_ref="$1"
44
+ local item="$2"
45
+
46
+ local index="${#results_ref[@]}"
47
+ results_ref["${index}"]="${item}"
48
+ }
49
+
50
+ # Add metadata entry
51
+ # Usage: batch_add_metadata METADATA "key" "value"
52
+ function batch_add_metadata() {
53
+ local -n metadata_ref="$1"
54
+ local key="$2"
55
+ local value="$3"
56
+
57
+ metadata_ref+=("${key}=${value}")
58
+ }
59
+
60
+ # Add error entry
61
+ # Usage: batch_add_error ERRORS "error message"
62
+ function batch_add_error() {
63
+ local -n errors_ref="$1"
64
+ local error_msg="$2"
65
+
66
+ errors_ref+=("${error_msg}")
67
+ }
68
+
69
+ ###
70
+ ### :::: Progress Logging :::: ########
71
+ ###
72
+
73
+ # Log progress message to stderr (doesn't pollute JSON output)
74
+ # Usage: batch_progress "Processing file: $file"
75
+ function batch_progress() {
76
+ printf '[%(%Y-%m-%d %H:%M:%S)T] [BATCH] %s\n' -1 "$*" >&2
77
+ }
78
+
79
+ # Log step with percentage
80
+ # Usage: batch_step "Processing files" 5 100
81
+ function batch_step() {
82
+ local message="$1"
83
+ local current="$2"
84
+ local total="$3"
85
+
86
+ local percentage=0
87
+ if ((total > 0)); then
88
+ percentage=$((current * 100 / total))
89
+ fi
90
+
91
+ batch_progress "${message} (${current}/${total} - ${percentage}%)"
92
+ }
93
+
94
+ ###
95
+ ### :::: JSON Output :::: #############
96
+ ###
97
+
98
+ # Fallback for optional ERRORS argument
99
+ declare -a _EMPTY_ERRORS=()
100
+
101
+ # Build JSON from associative array (results)
102
+ # Usage: _build_results_json RESULTS
103
+ # Returns: JSON object string
104
+ function _build_results_json() {
105
+ local -n _brj_ref="$1"
106
+ local json="{"
107
+ local first=true
108
+
109
+ for key in "${!_brj_ref[@]}"; do
110
+ if [[ "$first" == "true" ]]; then
111
+ first=false
112
+ else
113
+ json+=","
114
+ fi
115
+
116
+ # Inline JSON escaping (no subprocess)
117
+ local value="${_brj_ref[$key]}"
118
+ value="${value//\\/\\\\}"
119
+ value="${value//\"/\\\"}"
120
+ value="${value//$'\n'/\\n}"
121
+ value="${value//$'\r'/\\r}"
122
+ value="${value//$'\t'/\\t}"
123
+
124
+ # Check if value is numeric (integer or float) or boolean
125
+ if [[ "$value" =~ ^-?[0-9]+$ ]] || [[ "$value" =~ ^-?[0-9]+\.[0-9]+$ ]] || [[ "$value" =~ ^(true|false)$ ]]; then
126
+ json+="\"${key}\":${value}"
127
+ else
128
+ json+="\"${key}\":\"${value}\""
129
+ fi
130
+ done
131
+
132
+ json+="}"
133
+ printf '%s' "$json"
134
+ }
135
+
136
+ # Build JSON from metadata array
137
+ # Usage: _build_metadata_json METADATA
138
+ # Returns: JSON object string
139
+ function _build_metadata_json() {
140
+ local -n _bmj_ref="$1"
141
+ local json="{"
142
+ local first=true
143
+
144
+ for entry in "${_bmj_ref[@]}"; do
145
+ if [[ "$entry" =~ ^([^=]+)=(.*)$ ]]; then
146
+ local key="${BASH_REMATCH[1]}"
147
+ # Inline JSON escaping (no subprocess)
148
+ local value="${BASH_REMATCH[2]}"
149
+ value="${value//\\/\\\\}"
150
+ value="${value//\"/\\\"}"
151
+ value="${value//$'\n'/\\n}"
152
+ value="${value//$'\r'/\\r}"
153
+ value="${value//$'\t'/\\t}"
154
+
155
+ if [[ "$first" == "true" ]]; then
156
+ first=false
157
+ else
158
+ json+=","
159
+ fi
160
+
161
+ json+="\"${key}\":\"${value}\""
162
+ fi
163
+ done
164
+
165
+ json+="}"
166
+ printf '%s' "$json"
167
+ }
168
+
169
+ # Build JSON from errors array
170
+ # Usage: _build_errors_json ERRORS
171
+ # Returns: JSON array string
172
+ function _build_errors_json() {
173
+ local -n _bej_ref="$1"
174
+ local json="["
175
+
176
+ for i in "${!_bej_ref[@]}"; do
177
+ if ((i > 0)); then
178
+ json+=","
179
+ fi
180
+
181
+ # Inline JSON escaping (no subprocess)
182
+ local escaped="${_bej_ref[$i]}"
183
+ escaped="${escaped//\\/\\\\}"
184
+ escaped="${escaped//\"/\\\"}"
185
+ escaped="${escaped//$'\n'/\\n}"
186
+ escaped="${escaped//$'\r'/\\r}"
187
+ escaped="${escaped//$'\t'/\\t}"
188
+ json+="\"${escaped}\""
189
+ done
190
+
191
+ json+="]"
192
+ printf '%s' "$json"
193
+ }
194
+
195
+ # Output final JSON result to stdout
196
+ # Usage: batch_output RESULTS METADATA [ERRORS]
197
+ # Results: JSON object with results, metadata, and optional errors
198
+ function batch_output() {
199
+ local -n results_ref="$1"
200
+ local -n metadata_ref="$2"
201
+ local -n errors_ref="${3:-_EMPTY_ERRORS}"
202
+
203
+ local json="{"
204
+
205
+ # Add results
206
+ json+="\"results\":$(_build_results_json results_ref),"
207
+
208
+ # Add metadata
209
+ json+="\"metadata\":$(_build_metadata_json metadata_ref)"
210
+
211
+ # Add errors if any
212
+ if [[ ${#errors_ref[@]} -gt 0 ]]; then
213
+ json+=",\"errors\":$(_build_errors_json errors_ref)"
214
+ fi
215
+
216
+ json+="}"
217
+ printf '%s\n' "$json"
218
+ }
219
+
220
+ ###
221
+ ### :::: Batch Processing Helpers :::: ###
222
+ ###
223
+
224
+ # Process files with a callback function
225
+ # Usage: batch_process_files RESULTS METADATA ERRORS "glob_pattern" callback_function
226
+ # Example: batch_process_files RESULTS METADATA ERRORS "*.txt" process_txt_file
227
+ function batch_process_files() {
228
+ local -n results_ref="$1"
229
+ local -n metadata_ref="$2"
230
+ local -n errors_ref="$3"
231
+ local pattern="$4"
232
+ local callback="$5"
233
+
234
+ local files=()
235
+ local processed=0
236
+ local failed=0
237
+
238
+ # Collect files
239
+ while IFS= read -r -d '' file; do
240
+ files+=("$file")
241
+ done < <(find . -name "$pattern" -print0 2>/dev/null)
242
+
243
+ local total="${#files[@]}"
244
+ batch_progress "Found ${total} files matching '${pattern}'"
245
+
246
+ # Process each file
247
+ local i=0
248
+ for file in "${files[@]}"; do
249
+ i=$((i + 1))
250
+ batch_step "Processing" "$i" "$total"
251
+
252
+ if "$callback" "$file"; then
253
+ processed=$((processed + 1))
254
+ else
255
+ failed=$((failed + 1))
256
+ batch_add_error errors_ref "Failed to process: ${file}"
257
+ fi
258
+ done
259
+
260
+ # Add summary
261
+ batch_add_result results_ref "total" "$total"
262
+ batch_add_result results_ref "processed" "$processed"
263
+ batch_add_result results_ref "failed" "$failed"
264
+
265
+ return 0
266
+ }
267
+
268
+ # Run command and capture output
269
+ # Usage: batch_run_command RESULTS "key" command [args...]
270
+ # Returns: Exit code of command
271
+ function batch_run_command() {
272
+ local -n results_ref="$1"
273
+ local key="$2"
274
+ shift 2
275
+
276
+ local output
277
+ local exit_code
278
+
279
+ output=$("$@" 2>&1)
280
+ exit_code=$?
281
+
282
+ batch_add_result results_ref "${key}_exit" "$exit_code"
283
+ batch_add_result results_ref "${key}_output" "$output"
284
+
285
+ return "$exit_code"
286
+ }
287
+
288
+ ###
289
+ ### :::: Export Functions :::: ########
290
+ ###
291
+
292
+ export -f batch_add_result batch_add_result_item
293
+ export -f batch_add_metadata batch_add_error
294
+ export -f batch_progress batch_step
295
+ export -f batch_output
296
+ export -f batch_process_files batch_run_command
297
+ # Internal functions (_build_*, _EMPTY_ERRORS) are not exported