oh-my-opencode 4.5.12 → 4.7.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.
- package/.agents/skills/opencode-qa/SKILL.md +194 -0
- package/.agents/skills/opencode-qa/references/cli-commands.md +188 -0
- package/.agents/skills/opencode-qa/references/db-investigation.md +197 -0
- package/.agents/skills/opencode-qa/references/events-hooks.md +110 -0
- package/.agents/skills/opencode-qa/references/sdk.md +96 -0
- package/.agents/skills/opencode-qa/references/server-api.md +200 -0
- package/.agents/skills/opencode-qa/references/testing-harness.md +218 -0
- package/.agents/skills/opencode-qa/references/tui-tmux.md +52 -0
- package/.agents/skills/opencode-qa/scripts/db-session-by-id.sh +53 -0
- package/.agents/skills/opencode-qa/scripts/db-session-by-name.sh +57 -0
- package/.agents/skills/opencode-qa/scripts/db-session-by-text.sh +158 -0
- package/.agents/skills/opencode-qa/scripts/export-roundtrip.sh +57 -0
- package/.agents/skills/opencode-qa/scripts/lib/common.sh +216 -0
- package/.agents/skills/opencode-qa/scripts/server-smoke.sh +64 -0
- package/.agents/skills/opencode-qa/scripts/sse-hook-probe.sh +106 -0
- package/.agents/skills/opencode-qa/scripts/tui-smoke.sh +89 -0
- package/README.ja.md +13 -3
- package/README.ko.md +13 -3
- package/README.md +24 -14
- package/README.ru.md +13 -3
- package/README.zh-cn.md +13 -3
- package/bin/oh-my-opencode.js +4 -3
- package/bin/oh-my-opencode.test.ts +35 -7
- package/bin/platform.d.ts +1 -1
- package/bin/platform.js +4 -4
- package/bin/platform.test.ts +31 -9
- package/bin/version-mismatch.js +47 -0
- package/bin/version-mismatch.test.ts +120 -0
- package/dist/cli/cleanup-command.d.ts +4 -0
- package/dist/cli/cleanup.d.ts +11 -0
- package/dist/cli/cli-program.d.ts +2 -1
- package/dist/cli/codex-ulw-loop.d.ts +12 -0
- package/dist/cli/doctor/checks/tui-plugin-config.d.ts +2 -0
- package/dist/cli/index.js +2189 -529
- package/dist/cli/install-codex/codex-cache.d.ts +1 -0
- package/dist/cli/install-codex/codex-cleanup-config.d.ts +6 -0
- package/dist/cli/install-codex/codex-cleanup.d.ts +21 -0
- package/dist/cli/install-codex/codex-config-permissions.d.ts +1 -0
- package/dist/cli/install-codex/codex-config-reasoning.d.ts +2 -0
- package/dist/cli/install-codex/codex-config-toml.d.ts +2 -1
- package/dist/cli/install-codex/codex-installation-detection.d.ts +36 -0
- package/dist/cli/install-codex/codex-model-catalog.d.ts +13 -0
- package/dist/cli/install-codex/codex-package-layout.d.ts +1 -0
- package/dist/cli/install-codex/codex-project-local-cleanup-best-effort.d.ts +7 -0
- package/dist/cli/install-codex/codex-project-local-cleanup.d.ts +35 -0
- package/dist/cli/install-codex/git-bash.d.ts +35 -0
- package/dist/cli/install-codex/index.d.ts +4 -0
- package/dist/cli/install-codex/toml-section-editor.d.ts +2 -0
- package/dist/cli/install-codex/types.d.ts +20 -0
- package/dist/cli/run/event-state.d.ts +1 -0
- package/dist/cli/run/poll-for-completion.d.ts +1 -0
- package/dist/cli/run/prompt-start.d.ts +7 -0
- package/dist/cli/star-request.d.ts +9 -0
- package/dist/config/schema/hooks.d.ts +0 -1
- package/dist/create-hooks.d.ts +0 -1
- package/dist/features/background-agent/concurrency.d.ts +1 -0
- package/dist/features/background-agent/process-cleanup.d.ts +6 -0
- package/dist/features/builtin-skills/skills/debugging.d.ts +2 -0
- package/dist/features/builtin-skills/skills/index.d.ts +1 -0
- package/dist/features/claude-code-session-state/state.d.ts +1 -0
- package/dist/features/opencode-skill-loader/index.d.ts +1 -0
- package/dist/features/opencode-skill-loader/opencode-config-skills-reader.d.ts +5 -0
- package/dist/features/tmux-subagent/attachable-session-status.d.ts +1 -1
- package/dist/features/tmux-subagent/session-status-parser.d.ts +1 -0
- package/dist/hooks/comment-checker/cli.d.ts +1 -0
- package/dist/hooks/index.d.ts +0 -1
- package/dist/hooks/tasks-todowrite-disabler/constants.d.ts +1 -1
- package/dist/index.js +1077 -563
- package/dist/plugin/hooks/create-core-hooks.d.ts +0 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts +1 -2
- package/dist/plugin/messages-transform.d.ts +8 -1
- package/dist/plugin/user-abort-interrupted-recovery-guard.d.ts +6 -0
- package/dist/shared/command-executor/execute-hook-command.d.ts +2 -0
- package/dist/shared/prompt-async-gate/recent-dispatches.d.ts +14 -0
- package/dist/shared/prompt-async-gate/semantic-dedupe.d.ts +7 -0
- package/dist/shared/prompt-async-gate/session-idle-dispatch.d.ts +1 -0
- package/dist/shared/prompt-async-gate/timing.d.ts +1 -0
- package/dist/shared/prompt-async-gate/types.d.ts +2 -0
- package/dist/shared/prompt-async-gate.d.ts +1 -1
- package/dist/tools/skill/description-formatter.d.ts +5 -1
- package/dist/tools/skill/types.d.ts +1 -0
- package/package.json +22 -18
- package/packages/ast-grep-mcp/dist/cli.js +53 -9
- package/packages/git-bash-mcp/dist/cli.js +367 -0
- package/packages/lsp-tools-mcp/dist/lsp/process.js +1 -1
- package/packages/omo-codex/plugin/.mcp.json +11 -0
- package/packages/omo-codex/plugin/components/comment-checker/README.md +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +29 -0
- package/packages/omo-codex/plugin/components/git-bash/package.json +23 -0
- package/packages/omo-codex/plugin/components/git-bash/src/cli.ts +33 -0
- package/packages/omo-codex/plugin/components/git-bash/src/codex-hook.ts +180 -0
- package/packages/omo-codex/plugin/components/git-bash/src/index.ts +10 -0
- package/packages/omo-codex/plugin/components/git-bash/test/codex-hook.test.ts +195 -0
- package/packages/omo-codex/plugin/components/git-bash/tsconfig.build.json +13 -0
- package/packages/omo-codex/plugin/components/git-bash/tsconfig.json +25 -0
- package/packages/omo-codex/plugin/components/lsp/README.md +1 -1
- package/packages/omo-codex/plugin/components/lsp/src/cli.ts +5 -5
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +33 -0
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +19 -27
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook-cli.test.ts +28 -0
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook-errors.test.ts +55 -0
- package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +7 -5
- package/packages/omo-codex/plugin/components/rules/README.md +1 -1
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus.md +6 -4
- package/packages/omo-codex/plugin/components/rules/bundled-rules/windows-git-bash.md +10 -0
- package/packages/omo-codex/plugin/components/rules/src/post-compact-budget.ts +0 -2
- package/packages/omo-codex/plugin/components/rules/test/package-smoke.test.ts +3 -1
- package/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts +97 -0
- package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +6 -5
- package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +22 -0
- package/packages/omo-codex/plugin/components/ultrawork/CHANGELOG.md +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/README.md +3 -3
- package/packages/omo-codex/plugin/components/ultrawork/agents/codex-ultrawork-reviewer.toml +4 -1
- package/packages/omo-codex/plugin/components/ultrawork/agents/librarian.toml +8 -7
- package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +9 -8
- package/packages/omo-codex/plugin/components/ultrawork/directive.md +32 -6
- package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +27 -4
- package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +25 -0
- package/packages/omo-codex/plugin/components/ulw-loop/README.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +28 -205
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +231 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +12 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint.test.ts +19 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +102 -5
- package/packages/omo-codex/plugin/hooks/hooks.json +35 -2
- package/packages/omo-codex/plugin/model-catalog.json +49 -0
- package/packages/omo-codex/plugin/package-lock.json +19 -0
- package/packages/omo-codex/plugin/package.json +3 -1
- package/packages/omo-codex/plugin/scripts/auto-update.mjs +159 -0
- package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +16 -1
- package/packages/omo-codex/plugin/scripts/build-components.mjs +2 -1
- package/packages/omo-codex/plugin/scripts/migrate-codex-config.mjs +269 -0
- package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +89 -0
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +6 -6
- package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +6 -6
- package/packages/omo-codex/plugin/skills/lcx-report-bug/SKILL.md +127 -0
- package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +9 -0
- package/packages/omo-codex/plugin/skills/refactor/SKILL.md +6 -6
- package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +6 -6
- package/packages/omo-codex/plugin/skills/review-work/SKILL.md +33 -8
- package/packages/omo-codex/plugin/skills/start-work/SKILL.md +25 -5
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +28 -205
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +231 -0
- package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +17 -17
- package/packages/omo-codex/plugin/test/aggregate.test.mjs +188 -20
- package/packages/omo-codex/plugin/test/auto-update.test.mjs +129 -0
- package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +58 -11
- package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +34 -0
- package/packages/omo-codex/plugin/test/mcp-research-servers.test.mjs +21 -0
- package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +146 -0
- package/packages/omo-codex/plugin/test/node-install-surface.test.mjs +48 -0
- package/packages/omo-codex/plugin/test/subagent-guidance.test.mjs +76 -0
- package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +67 -0
- package/packages/omo-codex/plugin/test/sync-skills.test.mjs +54 -2
- package/packages/omo-codex/scripts/install/cache.mjs +5 -3
- package/packages/omo-codex/scripts/install/cli-args.mjs +112 -0
- package/packages/omo-codex/scripts/install/config.mjs +23 -1
- package/packages/omo-codex/scripts/install/delegated-command.mjs +25 -0
- package/packages/omo-codex/scripts/install/git-bash.mjs +99 -0
- package/packages/omo-codex/scripts/install/git-bash.test.mjs +174 -0
- package/packages/omo-codex/scripts/install/legacy-bins.mjs +1 -0
- package/packages/omo-codex/scripts/install/mcp-runtime-cache.mjs +5 -1
- package/packages/omo-codex/scripts/install/model-catalog.mjs +66 -0
- package/packages/omo-codex/scripts/install/multi-agent-v2-config.mjs +7 -1
- package/packages/omo-codex/scripts/install/permissions.d.mts +1 -0
- package/packages/omo-codex/scripts/install/permissions.mjs +26 -0
- package/packages/omo-codex/scripts/install/project-local-cleanup.mjs +229 -0
- package/packages/omo-codex/scripts/install/reasoning-config.mjs +72 -0
- package/packages/omo-codex/scripts/install/source-package-build.mjs +20 -0
- package/packages/omo-codex/scripts/install/toml-editor.mjs +19 -2
- package/packages/omo-codex/scripts/install-bin-links.test.mjs +23 -0
- package/packages/omo-codex/scripts/install-cli-args.test.mjs +146 -0
- package/packages/omo-codex/scripts/install-config-autonomous.test.mjs +48 -0
- package/packages/omo-codex/scripts/install-config-reasoning.test.mjs +141 -0
- package/packages/omo-codex/scripts/install-config.test.mjs +205 -0
- package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +157 -0
- package/packages/omo-codex/scripts/install-local-git-bash-preflight.test.mjs +145 -0
- package/packages/omo-codex/scripts/install-local.mjs +91 -8
- package/packages/omo-codex/scripts/install-local.test.mjs +15 -0
- package/packages/omo-codex/scripts/install-mcp-runtime.test.mjs +60 -0
- package/packages/omo-codex/scripts/install-packaged-local.test.mjs +67 -0
- package/packages/omo-codex/scripts/install-project-local-cleanup.test.mjs +277 -0
- package/packages/shared-skills/skills/lcx-report-bug/SKILL.md +127 -0
- package/packages/shared-skills/skills/lcx-report-bug/agents/openai.yaml +9 -0
- package/packages/shared-skills/skills/review-work/SKILL.md +33 -8
- package/packages/shared-skills/skills/start-work/SKILL.md +25 -5
- package/packages/shared-skills/skills/ulw-plan/SKILL.md +11 -11
- package/postinstall.mjs +36 -3
- package/dist/hooks/context-window-monitor.d.ts +0 -19
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# db-session-by-text.sh - find opencode message TEXT by content.
|
|
3
|
+
#
|
|
4
|
+
# Message text lives inside the `part` table as JSON blobs
|
|
5
|
+
# (json_extract(data,'$.text') for text parts). The `part` table holds the bulk
|
|
6
|
+
# of the DB (tens of GB of tool output), so an UNBOUNDED text scan is refused.
|
|
7
|
+
# You MUST scope the search to a bounded, recent set of sessions:
|
|
8
|
+
# --session ses_... one session (indexed, instant)
|
|
9
|
+
# --recent N the N most-recent sessions (default 25)
|
|
10
|
+
# --since "<window>" sessions created within a window (e.g. "7 days"),
|
|
11
|
+
# capped at the 200 most-recent in that window
|
|
12
|
+
#
|
|
13
|
+
# All bounded modes use `part.session_id IN (SELECT id FROM session ORDER BY
|
|
14
|
+
# time_created DESC LIMIT ...)`, which drives the part_session_idx on exactly
|
|
15
|
+
# the newest sessions. (A naive JOIN with `WHERE session.time_created >= X`
|
|
16
|
+
# scans oldest-first and can take ~50s; the IN-subquery form returns in ~20ms.)
|
|
17
|
+
#
|
|
18
|
+
# Usage:
|
|
19
|
+
# db-session-by-text.sh --session ses_3a4e... "ULTRAWORK"
|
|
20
|
+
# db-session-by-text.sh --recent 50 "permission denied"
|
|
21
|
+
# db-session-by-text.sh --since "7 days" --limit 50 "TODO"
|
|
22
|
+
# db-session-by-text.sh --self-test
|
|
23
|
+
#
|
|
24
|
+
# Output: JSON array of {session_id, part_id, snippet} (snippet = first 120
|
|
25
|
+
# chars of the matching text part).
|
|
26
|
+
|
|
27
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
28
|
+
. "$SCRIPT_DIR/lib/common.sh"
|
|
29
|
+
|
|
30
|
+
oqa_text_scoped() {
|
|
31
|
+
local sid needle limit esc_id esc_txt
|
|
32
|
+
sid="$1"; needle="$2"; limit="${3:-50}"
|
|
33
|
+
esc_id="$(oqa_sql_escape "$sid")"
|
|
34
|
+
esc_txt="$(oqa_sql_escape "$needle")"
|
|
35
|
+
oqa_db_query "SELECT
|
|
36
|
+
p.session_id AS session_id,
|
|
37
|
+
p.id AS part_id,
|
|
38
|
+
substr(json_extract(p.data,'\$.text'),1,120) AS snippet
|
|
39
|
+
FROM part p
|
|
40
|
+
WHERE p.session_id='$esc_id'
|
|
41
|
+
AND json_extract(p.data,'\$.type')='text'
|
|
42
|
+
AND json_extract(p.data,'\$.text') LIKE '%$esc_txt%'
|
|
43
|
+
LIMIT $limit"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
oqa_text_recent() {
|
|
47
|
+
local n needle limit esc_txt
|
|
48
|
+
n="$1"; needle="$2"; limit="${3:-50}"
|
|
49
|
+
case "$n" in (*[!0-9]*|"") n=25 ;; esac
|
|
50
|
+
esc_txt="$(oqa_sql_escape "$needle")"
|
|
51
|
+
oqa_db_query "SELECT
|
|
52
|
+
p.session_id AS session_id,
|
|
53
|
+
p.id AS part_id,
|
|
54
|
+
substr(json_extract(p.data,'\$.text'),1,120) AS snippet
|
|
55
|
+
FROM part p
|
|
56
|
+
WHERE p.session_id IN (SELECT id FROM session ORDER BY time_created DESC LIMIT $n)
|
|
57
|
+
AND json_extract(p.data,'\$.type')='text'
|
|
58
|
+
AND json_extract(p.data,'\$.text') LIKE '%$esc_txt%'
|
|
59
|
+
LIMIT $limit"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
oqa_text_since() {
|
|
63
|
+
local window needle limit esc_win esc_txt
|
|
64
|
+
window="$1"; needle="$2"; limit="${3:-50}"
|
|
65
|
+
esc_win="$(oqa_sql_escape "$window")"
|
|
66
|
+
esc_txt="$(oqa_sql_escape "$needle")"
|
|
67
|
+
# Cap at the 200 most-recent sessions inside the window and drive
|
|
68
|
+
# part_session_idx via an IN-subquery (newest-first) to stay fast.
|
|
69
|
+
oqa_db_query "SELECT
|
|
70
|
+
p.session_id AS session_id,
|
|
71
|
+
p.id AS part_id,
|
|
72
|
+
substr(json_extract(p.data,'\$.text'),1,120) AS snippet
|
|
73
|
+
FROM part p
|
|
74
|
+
WHERE p.session_id IN (
|
|
75
|
+
SELECT id FROM session
|
|
76
|
+
WHERE time_created >= (strftime('%s','now','-$esc_win') * 1000)
|
|
77
|
+
ORDER BY time_created DESC LIMIT 200)
|
|
78
|
+
AND json_extract(p.data,'\$.type')='text'
|
|
79
|
+
AND json_extract(p.data,'\$.text') LIKE '%$esc_txt%'
|
|
80
|
+
LIMIT $limit"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
oqa_text_main() {
|
|
84
|
+
local sid="" window="" recent="" limit=50 needle=""
|
|
85
|
+
while [ $# -gt 0 ]; do
|
|
86
|
+
case "$1" in
|
|
87
|
+
--session) sid="$2"; shift 2 ;;
|
|
88
|
+
--recent) recent="$2"; shift 2 ;;
|
|
89
|
+
--since) window="$2"; shift 2 ;;
|
|
90
|
+
--limit) limit="$2"; shift 2 ;;
|
|
91
|
+
*) needle="$1"; shift ;;
|
|
92
|
+
esac
|
|
93
|
+
done
|
|
94
|
+
if [ -z "$needle" ]; then
|
|
95
|
+
oqa_log "error: missing search text"; return 2
|
|
96
|
+
fi
|
|
97
|
+
if [ -n "$sid" ]; then
|
|
98
|
+
oqa_text_scoped "$sid" "$needle" "$limit"; return 0
|
|
99
|
+
fi
|
|
100
|
+
if [ -n "$recent" ]; then
|
|
101
|
+
oqa_text_recent "$recent" "$needle" "$limit"; return 0
|
|
102
|
+
fi
|
|
103
|
+
if [ -n "$window" ]; then
|
|
104
|
+
oqa_text_since "$window" "$needle" "$limit"; return 0
|
|
105
|
+
fi
|
|
106
|
+
oqa_log "error: refusing an unbounded global text scan over the multi-GB part table."
|
|
107
|
+
oqa_log " scope it with --session <ses_id>, --recent <N>, or --since \"<N days|hours>\"."
|
|
108
|
+
return 2
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
oqa_self_test() {
|
|
112
|
+
oqa_require opencode jq || return 1
|
|
113
|
+
local fails=0
|
|
114
|
+
|
|
115
|
+
# 1) scoped: find a recent session with text parts, derive a needle from one.
|
|
116
|
+
local sid needle out n
|
|
117
|
+
sid="$(oqa_db_query "SELECT session_id AS s FROM part WHERE json_extract(data,'\$.type')='text' ORDER BY rowid DESC LIMIT 1" | jq -r '.[0].s // empty')"
|
|
118
|
+
if [ -z "$sid" ]; then oqa_log "FAIL: no text parts found"; return 1; fi
|
|
119
|
+
needle="$(oqa_db_query "SELECT substr(json_extract(data,'\$.text'),1,8) AS t FROM part WHERE session_id='$(oqa_sql_escape "$sid")' AND json_extract(data,'\$.type')='text' AND length(json_extract(data,'\$.text'))>=8 LIMIT 1" | jq -r '.[0].t // empty')"
|
|
120
|
+
if [ -z "$needle" ]; then oqa_log "FAIL: could not derive a text needle"; return 1; fi
|
|
121
|
+
out="$(oqa_text_main --session "$sid" "$needle")"
|
|
122
|
+
n="$(printf '%s' "$out" | jq 'length')"
|
|
123
|
+
if [ "${n:-0}" -ge 1 ]; then oqa_pass "scoped text search found $n row(s) in $sid"; else oqa_log "FAIL: scoped search empty for '$needle' in $sid"; fails=$((fails+1)); fi
|
|
124
|
+
|
|
125
|
+
# 2) unbounded refusal: no --session/--since must exit 2.
|
|
126
|
+
oqa_text_main "$needle" >/dev/null 2>&1
|
|
127
|
+
if [ "$?" -eq 2 ]; then oqa_pass "unbounded global scan refused (exit 2)"; else oqa_log "FAIL: unbounded scan was not refused"; fails=$((fails+1)); fi
|
|
128
|
+
|
|
129
|
+
# 3) bounded --recent search completes well under a hard 30s budget, even in
|
|
130
|
+
# the worst case (no early match) because the IN-subquery caps the scan to
|
|
131
|
+
# the newest N sessions and drives part_session_idx.
|
|
132
|
+
local t0 t1
|
|
133
|
+
t0=$(date +%s)
|
|
134
|
+
oqa_text_main --recent 25 --limit 5 "oqa_no_match_$(date +%s)_zzz" >/dev/null 2>&1
|
|
135
|
+
t1=$(date +%s)
|
|
136
|
+
if [ $((t1 - t0)) -le 30 ]; then
|
|
137
|
+
oqa_pass "bounded --recent 25 worst-case search completed in $((t1-t0))s (<=30s)"
|
|
138
|
+
else
|
|
139
|
+
oqa_log "FAIL: bounded --recent search took $((t1-t0))s (>30s)"; fails=$((fails+1))
|
|
140
|
+
fi
|
|
141
|
+
# also prove --recent returns real matches for the derived needle
|
|
142
|
+
out="$(oqa_text_main --recent 25 --limit 5 "$needle" 2>/dev/null)"
|
|
143
|
+
n="$(printf '%s' "$out" | jq 'length')"
|
|
144
|
+
if [ "${n:-0}" -ge 1 ]; then oqa_pass "bounded --recent 25 found $n row(s) for '$needle'"; else oqa_log "FAIL: --recent found no rows for '$needle'"; fails=$((fails+1)); fi
|
|
145
|
+
|
|
146
|
+
[ "$fails" -eq 0 ] && { oqa_pass "db-session-by-text"; return 0; }
|
|
147
|
+
oqa_log "db-session-by-text had $fails failure(s)"; return 1
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
case "${1:-}" in
|
|
151
|
+
--self-test) oqa_self_test; exit $? ;;
|
|
152
|
+
-h|--help|"")
|
|
153
|
+
sed -n '2,24p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
|
154
|
+
[ -z "${1:-}" ] && exit 2 || exit 0 ;;
|
|
155
|
+
*)
|
|
156
|
+
oqa_require opencode jq || exit 1
|
|
157
|
+
oqa_text_main "$@" ;;
|
|
158
|
+
esac
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# export-roundtrip.sh - export a session as clean JSON and verify it round-trips.
|
|
3
|
+
#
|
|
4
|
+
# `opencode export <id>` prints a human line ("Exporting session: ...") to
|
|
5
|
+
# STDERR and the JSON document to STDOUT, so suppress stderr before piping to jq.
|
|
6
|
+
# The JSON shape is { info: {id, slug, projectID, directory, title, tokens,
|
|
7
|
+
# time, ...}, messages: [...] }.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# export-roundtrip.sh ses_3a4e22ad5ffebMKLt0tL7exPjZ # prints clean JSON
|
|
11
|
+
# export-roundtrip.sh --self-test
|
|
12
|
+
#
|
|
13
|
+
# Tip: redirect to a file for archival: export-roundtrip.sh <id> > session.json
|
|
14
|
+
|
|
15
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
. "$SCRIPT_DIR/lib/common.sh"
|
|
17
|
+
|
|
18
|
+
oqa_export() {
|
|
19
|
+
# stderr carries the "Exporting session:" banner; drop it for clean JSON.
|
|
20
|
+
opencode export "$1" 2>/dev/null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
oqa_self_test() {
|
|
24
|
+
oqa_require opencode jq || return 1
|
|
25
|
+
local id out got title msgtype
|
|
26
|
+
id="$(oqa_db_query "SELECT id FROM session ORDER BY time_created DESC LIMIT 1" | jq -r '.[0].id // empty')"
|
|
27
|
+
if [ -z "$id" ]; then oqa_log "FAIL: no sessions to export"; return 1; fi
|
|
28
|
+
|
|
29
|
+
out="$(oqa_export "$id")"
|
|
30
|
+
# 1) stdout must be valid JSON (stderr banner excluded).
|
|
31
|
+
if ! printf '%s' "$out" | jq -e . >/dev/null 2>&1; then
|
|
32
|
+
oqa_log "FAIL: export stdout is not valid JSON for $id"; return 1
|
|
33
|
+
fi
|
|
34
|
+
# 2) the info.id must round-trip.
|
|
35
|
+
got="$(printf '%s' "$out" | jq -r '.info.id // empty')"
|
|
36
|
+
if [ "$got" != "$id" ]; then
|
|
37
|
+
oqa_log "FAIL: export .info.id '$got' != '$id'"; return 1
|
|
38
|
+
fi
|
|
39
|
+
# 3) info.title is a string and messages is an array.
|
|
40
|
+
title="$(printf '%s' "$out" | jq -r '.info.title|type' 2>/dev/null)"
|
|
41
|
+
msgtype="$(printf '%s' "$out" | jq -r '.messages|type' 2>/dev/null)"
|
|
42
|
+
if [ "$title" = "string" ] && { [ "$msgtype" = "array" ] || [ "$msgtype" = "null" ]; }; then
|
|
43
|
+
oqa_pass "export round-trips $id (info.id matches, valid JSON)"
|
|
44
|
+
return 0
|
|
45
|
+
fi
|
|
46
|
+
oqa_log "FAIL: unexpected shape (title=$title messages=$msgtype)"; return 1
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
case "${1:-}" in
|
|
50
|
+
--self-test) oqa_self_test; exit $? ;;
|
|
51
|
+
-h|--help|"")
|
|
52
|
+
sed -n '2,16p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
|
53
|
+
[ -z "${1:-}" ] && exit 2 || exit 0 ;;
|
|
54
|
+
*)
|
|
55
|
+
oqa_require opencode jq || exit 1
|
|
56
|
+
oqa_export "$1" ;;
|
|
57
|
+
esac
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# common.sh - shared helpers for opencode-qa scripts.
|
|
3
|
+
#
|
|
4
|
+
# Source it from a sibling script:
|
|
5
|
+
# SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
|
+
# . "$SCRIPT_DIR/lib/common.sh"
|
|
7
|
+
#
|
|
8
|
+
# SAFETY MODEL (read this):
|
|
9
|
+
# - DB-read helpers (oqa_db_path / oqa_db_query) hit the LIVE opencode DB
|
|
10
|
+
# READ-ONLY. That is safe and intended for session investigation.
|
|
11
|
+
# - Anything that SPAWNS opencode (serve / run / tui) must run under an
|
|
12
|
+
# ISOLATED XDG sandbox (oqa_mk_isolated_xdg) so QA never writes junk
|
|
13
|
+
# sessions into the real ~/.local/share/opencode DB.
|
|
14
|
+
# - oqa_cleanup runs on EXIT and tears down servers, tmux sessions, curl
|
|
15
|
+
# watchers, and every temp dir created via the helpers.
|
|
16
|
+
|
|
17
|
+
# No `set -e`: these scripts deliberately probe failure paths (401, refused).
|
|
18
|
+
set -uo pipefail
|
|
19
|
+
|
|
20
|
+
OQA_TMPDIRS=()
|
|
21
|
+
OQA_TMUX_SESSIONS=()
|
|
22
|
+
OQA_CURL_PIDS=()
|
|
23
|
+
OQA_SERVER_PID=""
|
|
24
|
+
|
|
25
|
+
oqa_log() { printf '%s\n' "$*" >&2; }
|
|
26
|
+
oqa_pass() { printf 'PASS: %s\n' "$*"; }
|
|
27
|
+
oqa_fail() { printf 'FAIL: %s\n' "$*" >&2; return 1; }
|
|
28
|
+
|
|
29
|
+
# oqa_require <bin>... -> 0 if all present, else 1 (names the missing ones).
|
|
30
|
+
oqa_require() {
|
|
31
|
+
local missing=0 b
|
|
32
|
+
for b in "$@"; do
|
|
33
|
+
if ! command -v "$b" >/dev/null 2>&1; then
|
|
34
|
+
oqa_log "missing dependency: $b"
|
|
35
|
+
missing=1
|
|
36
|
+
fi
|
|
37
|
+
done
|
|
38
|
+
return "$missing"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Absolute path of the active opencode DB (resolves channel / OPENCODE_DB).
|
|
42
|
+
oqa_db_path() {
|
|
43
|
+
opencode db path 2>/dev/null | head -1
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Escape a value for safe embedding inside a single-quoted SQL literal.
|
|
47
|
+
oqa_sql_escape() {
|
|
48
|
+
local s="${1//\'/\'\'}"
|
|
49
|
+
printf '%s' "$s"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Run a read-only SQL query against the active DB; emit JSON rows.
|
|
53
|
+
# Usage: oqa_db_query "SELECT ... LIMIT 5"
|
|
54
|
+
oqa_db_query() {
|
|
55
|
+
opencode db "$1" --format json 2>/dev/null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Create an isolated XDG sandbox so a spawned opencode never touches the real
|
|
59
|
+
# DB. Sets globals OQA_XDG_ROOT + OQA_PROJ and exports XDG_*; registers the
|
|
60
|
+
# root for cleanup.
|
|
61
|
+
#
|
|
62
|
+
# IMPORTANT: call this DIRECTLY, never via $(...). Command substitution runs in
|
|
63
|
+
# a subshell, which would discard the exports and the cleanup registration.
|
|
64
|
+
# oqa_mk_isolated_xdg # good
|
|
65
|
+
# root="$OQA_XDG_ROOT" # read the global afterwards
|
|
66
|
+
oqa_mk_isolated_xdg() {
|
|
67
|
+
local root
|
|
68
|
+
root="$(mktemp -d -t oqa-xdg.XXXXXX)" || return 1
|
|
69
|
+
OQA_TMPDIRS+=("$root")
|
|
70
|
+
mkdir -p "$root/data" "$root/config" "$root/cache" "$root/state" "$root/proj"
|
|
71
|
+
export OQA_XDG_ROOT="$root"
|
|
72
|
+
export XDG_DATA_HOME="$root/data"
|
|
73
|
+
export XDG_CONFIG_HOME="$root/config"
|
|
74
|
+
export XDG_CACHE_HOME="$root/cache"
|
|
75
|
+
export XDG_STATE_HOME="$root/state"
|
|
76
|
+
export OQA_PROJ="$root/proj"
|
|
77
|
+
# keep the sandbox offline + fast
|
|
78
|
+
export OPENCODE_DISABLE_AUTOUPDATE=1
|
|
79
|
+
export OPENCODE_DISABLE_MODELS_FETCH=1
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# Print a free TCP port on 127.0.0.1.
|
|
83
|
+
oqa_free_port() {
|
|
84
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
85
|
+
python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()'
|
|
86
|
+
elif command -v bun >/dev/null 2>&1; then
|
|
87
|
+
bun -e 'const s=Bun.listen({hostname:"127.0.0.1",port:0,socket:{data(){}}});console.log(s.port);s.stop()'
|
|
88
|
+
else
|
|
89
|
+
# last resort: a high random port (small race window)
|
|
90
|
+
printf '%s' "$(( (RANDOM % 20000) + 40000 ))"
|
|
91
|
+
fi
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Poll an HTTP url until it accepts a connection (any status) or times out.
|
|
95
|
+
# Usage: oqa_wait_http <url> [user:pass] [timeout_s]
|
|
96
|
+
oqa_wait_http() {
|
|
97
|
+
local url="$1" auth="${2:-}" timeout="${3:-25}" deadline
|
|
98
|
+
deadline=$(( $(date +%s) + timeout ))
|
|
99
|
+
while [ "$(date +%s)" -lt "$deadline" ]; do
|
|
100
|
+
if [ -n "$auth" ]; then
|
|
101
|
+
curl -s -o /dev/null -u "$auth" "$url" && return 0
|
|
102
|
+
else
|
|
103
|
+
curl -s -o /dev/null "$url" && return 0
|
|
104
|
+
fi
|
|
105
|
+
sleep 0.2
|
|
106
|
+
done
|
|
107
|
+
return 1
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Start an isolated, password-protected headless server.
|
|
111
|
+
# Sets globals OQA_SERVER_URL / OQA_SERVER_PASS / OQA_SERVER_PORT / OQA_SERVER_PID.
|
|
112
|
+
# Returns 1 if it never becomes ready.
|
|
113
|
+
#
|
|
114
|
+
# IMPORTANT: call this DIRECTLY, never via $(...). The PID + exports must land
|
|
115
|
+
# in the caller's shell so oqa_cleanup can kill the server on exit.
|
|
116
|
+
# oqa_start_server || { oqa_log "no server"; exit 1; }
|
|
117
|
+
# url="$OQA_SERVER_URL"
|
|
118
|
+
oqa_start_server() {
|
|
119
|
+
oqa_mk_isolated_xdg || return 1
|
|
120
|
+
local port pass
|
|
121
|
+
port="$(oqa_free_port)"
|
|
122
|
+
pass="oqa-${RANDOM}${RANDOM}"
|
|
123
|
+
OPENCODE_SERVER_PASSWORD="$pass" opencode serve --port "$port" --hostname 127.0.0.1 \
|
|
124
|
+
>"$XDG_STATE_HOME/serve.log" 2>&1 &
|
|
125
|
+
OQA_SERVER_PID=$!
|
|
126
|
+
export OQA_SERVER_PORT="$port"
|
|
127
|
+
export OQA_SERVER_PASS="$pass"
|
|
128
|
+
export OQA_SERVER_URL="http://127.0.0.1:$port"
|
|
129
|
+
if ! oqa_wait_http "$OQA_SERVER_URL/global/health" "opencode:$pass" 30; then
|
|
130
|
+
oqa_log "server failed to start; log follows:"
|
|
131
|
+
cat "$XDG_STATE_HOME/serve.log" >&2 2>/dev/null || true
|
|
132
|
+
return 1
|
|
133
|
+
fi
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# Teardown everything the helpers created. Safe to call multiple times.
|
|
137
|
+
oqa_cleanup() {
|
|
138
|
+
if [ -n "${OQA_SERVER_PID:-}" ]; then
|
|
139
|
+
kill "$OQA_SERVER_PID" 2>/dev/null || true
|
|
140
|
+
sleep 0.3
|
|
141
|
+
kill -9 "$OQA_SERVER_PID" 2>/dev/null || true
|
|
142
|
+
OQA_SERVER_PID=""
|
|
143
|
+
fi
|
|
144
|
+
local s p d
|
|
145
|
+
for s in "${OQA_TMUX_SESSIONS[@]:-}"; do
|
|
146
|
+
[ -n "$s" ] && tmux kill-session -t "$s" 2>/dev/null || true
|
|
147
|
+
done
|
|
148
|
+
for p in "${OQA_CURL_PIDS[@]:-}"; do
|
|
149
|
+
[ -n "$p" ] && kill "$p" 2>/dev/null || true
|
|
150
|
+
done
|
|
151
|
+
for d in "${OQA_TMPDIRS[@]:-}"; do
|
|
152
|
+
[ -n "$d" ] && rm -rf "$d" 2>/dev/null || true
|
|
153
|
+
done
|
|
154
|
+
OQA_TMPDIRS=()
|
|
155
|
+
OQA_TMUX_SESSIONS=()
|
|
156
|
+
OQA_CURL_PIDS=()
|
|
157
|
+
}
|
|
158
|
+
trap oqa_cleanup EXIT
|
|
159
|
+
|
|
160
|
+
# ---- self-check ------------------------------------------------------------
|
|
161
|
+
# Run: bash scripts/lib/common.sh --self-check
|
|
162
|
+
oqa__self_check() {
|
|
163
|
+
local fails=0
|
|
164
|
+
|
|
165
|
+
if oqa_require opencode sqlite3 curl jq tmux; then
|
|
166
|
+
oqa_pass "dependencies present (opencode sqlite3 curl jq tmux)"
|
|
167
|
+
else
|
|
168
|
+
oqa_log "FAIL: missing dependencies"; fails=$((fails+1))
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
local dbp; dbp="$(oqa_db_path)"
|
|
172
|
+
if [ -n "$dbp" ] && [ -f "$dbp" ]; then
|
|
173
|
+
oqa_pass "oqa_db_path -> $dbp"
|
|
174
|
+
else
|
|
175
|
+
oqa_log "FAIL: oqa_db_path returned '$dbp'"; fails=$((fails+1))
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
local esc; esc="$(oqa_sql_escape "a'b'c")"
|
|
179
|
+
if [ "$esc" = "a''b''c" ]; then
|
|
180
|
+
oqa_pass "oqa_sql_escape quotes single quotes"
|
|
181
|
+
else
|
|
182
|
+
oqa_log "FAIL: oqa_sql_escape -> '$esc'"; fails=$((fails+1))
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
local port; port="$(oqa_free_port)"
|
|
186
|
+
if [ "$port" -gt 0 ] 2>/dev/null; then
|
|
187
|
+
oqa_pass "oqa_free_port -> $port"
|
|
188
|
+
else
|
|
189
|
+
oqa_log "FAIL: oqa_free_port -> '$port'"; fails=$((fails+1))
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
# isolation + trap teardown: an inner shell creates a sandbox (calling the
|
|
193
|
+
# helper DIRECTLY so the cleanup registration survives) and exits; the EXIT
|
|
194
|
+
# trap must remove it. We pass the sandbox path out via a marker file.
|
|
195
|
+
local marker isodir
|
|
196
|
+
marker="$(mktemp -t oqa-marker.XXXXXX)"
|
|
197
|
+
bash -c '. "'"${BASH_SOURCE[0]}"'"; oqa_mk_isolated_xdg; printf "%s" "$OQA_XDG_ROOT" > "'"$marker"'"'
|
|
198
|
+
isodir="$(cat "$marker" 2>/dev/null)"; rm -f "$marker"
|
|
199
|
+
if [ -n "$isodir" ] && [ ! -d "$isodir" ]; then
|
|
200
|
+
oqa_pass "isolated XDG sandbox auto-removed on exit ($isodir)"
|
|
201
|
+
else
|
|
202
|
+
oqa_log "FAIL: sandbox not cleaned: '$isodir' (exists=$([ -d "$isodir" ] && echo yes || echo no))"; fails=$((fails+1))
|
|
203
|
+
fi
|
|
204
|
+
|
|
205
|
+
if [ "$fails" -eq 0 ]; then
|
|
206
|
+
oqa_pass "common.sh self-check"
|
|
207
|
+
return 0
|
|
208
|
+
fi
|
|
209
|
+
oqa_log "common.sh self-check had $fails failure(s)"
|
|
210
|
+
return 1
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if [ "${1:-}" = "--self-check" ]; then
|
|
214
|
+
oqa__self_check
|
|
215
|
+
exit $?
|
|
216
|
+
fi
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# server-smoke.sh - boot an ISOLATED opencode HTTP server and verify the core
|
|
3
|
+
# API surface end to end. Uses an isolated XDG sandbox + a random password, so
|
|
4
|
+
# it never touches the real ~/.local/share/opencode DB, and tears the server
|
|
5
|
+
# down on exit.
|
|
6
|
+
#
|
|
7
|
+
# Checks:
|
|
8
|
+
# 1. GET /global/health -> {"healthy":true,"version":...}
|
|
9
|
+
# 2. GET /doc -> OpenAPI spec with >=100 paths
|
|
10
|
+
# 3. GET /session (no credentials) -> HTTP 401 (auth is enforced)
|
|
11
|
+
#
|
|
12
|
+
# Usage:
|
|
13
|
+
# server-smoke.sh # run the smoke test
|
|
14
|
+
# server-smoke.sh --self-test # same thing (alias for the QA sweep)
|
|
15
|
+
|
|
16
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
17
|
+
. "$SCRIPT_DIR/lib/common.sh"
|
|
18
|
+
|
|
19
|
+
oqa_server_smoke() {
|
|
20
|
+
oqa_require opencode curl jq || return 1
|
|
21
|
+
if ! oqa_start_server; then
|
|
22
|
+
oqa_log "FAIL: server did not become ready"; return 1
|
|
23
|
+
fi
|
|
24
|
+
local url="$OQA_SERVER_URL" auth="opencode:$OQA_SERVER_PASS" fails=0
|
|
25
|
+
|
|
26
|
+
local healthy version
|
|
27
|
+
healthy="$(curl -s -u "$auth" "$url/global/health" | jq -r '.healthy // false')"
|
|
28
|
+
version="$(curl -s -u "$auth" "$url/global/health" | jq -r '.version // "?"')"
|
|
29
|
+
if [ "$healthy" = "true" ]; then
|
|
30
|
+
oqa_pass "GET /global/health healthy=true version=$version ($url)"
|
|
31
|
+
else
|
|
32
|
+
oqa_log "FAIL: /global/health healthy=$healthy"; fails=$((fails+1))
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
local npaths
|
|
36
|
+
npaths="$(curl -s -u "$auth" "$url/doc" | jq '.paths | length' 2>/dev/null)"
|
|
37
|
+
if [ "${npaths:-0}" -ge 100 ]; then
|
|
38
|
+
oqa_pass "GET /doc lists $npaths documented paths (>=100)"
|
|
39
|
+
else
|
|
40
|
+
oqa_log "FAIL: /doc path count=$npaths"; fails=$((fails+1))
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
local code
|
|
44
|
+
code="$(curl -s -o /dev/null -w '%{http_code}' "$url/session?directory=$OQA_PROJ")"
|
|
45
|
+
if [ "$code" = "401" ]; then
|
|
46
|
+
oqa_pass "unauthenticated GET /session rejected with HTTP 401"
|
|
47
|
+
else
|
|
48
|
+
oqa_log "FAIL: unauthenticated /session returned $code (expected 401)"; fails=$((fails+1))
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
if [ "$fails" -eq 0 ]; then
|
|
52
|
+
oqa_pass "server-smoke"
|
|
53
|
+
return 0
|
|
54
|
+
fi
|
|
55
|
+
oqa_log "server-smoke had $fails failure(s)"; return 1
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
case "${1:-}" in
|
|
59
|
+
-h|--help)
|
|
60
|
+
sed -n '2,18p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
|
61
|
+
exit 0 ;;
|
|
62
|
+
*)
|
|
63
|
+
oqa_server_smoke; exit $? ;;
|
|
64
|
+
esac
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# sse-hook-probe.sh - QA opencode's event stream (the plumbing behind hooks).
|
|
3
|
+
#
|
|
4
|
+
# opencode publishes lifecycle events over Server-Sent Events at GET /event
|
|
5
|
+
# (per-instance) and GET /global/event. Plugins observe the same events via the
|
|
6
|
+
# `event` hook, so confirming an event on the wire is how you prove a hook
|
|
7
|
+
# would have fired.
|
|
8
|
+
#
|
|
9
|
+
# Two modes:
|
|
10
|
+
# (default / --self-test) Spawn an ISOLATED server and assert the stream
|
|
11
|
+
# opens with a `server.connected` event. No real DB
|
|
12
|
+
# is touched. This proves the SSE plumbing works.
|
|
13
|
+
# --attach <url> Watch an ALREADY-RUNNING server's /event stream for
|
|
14
|
+
# a specific event type (default: server.connected).
|
|
15
|
+
# Use this against your real server to verify a hook
|
|
16
|
+
# or action. Pair it with a prompt in another shell:
|
|
17
|
+
# curl -X POST -u opencode:$PASS \
|
|
18
|
+
# -H 'Content-Type: application/json' \
|
|
19
|
+
# -d '{"parts":[{"type":"text","text":"hi"}]}' \
|
|
20
|
+
# "<url>/session/<ses_id>/prompt_async?directory=<dir>"
|
|
21
|
+
# then watch for e.g. message.part.updated.
|
|
22
|
+
#
|
|
23
|
+
# --attach options:
|
|
24
|
+
# --password <p> server password (user defaults to "opencode")
|
|
25
|
+
# --user <u> server username (default: opencode)
|
|
26
|
+
# --directory <d> instance directory (default: $PWD)
|
|
27
|
+
# --event <type> event type to wait for (default: server.connected)
|
|
28
|
+
# --timeout <s> seconds to wait (default: 15)
|
|
29
|
+
#
|
|
30
|
+
# Usage:
|
|
31
|
+
# sse-hook-probe.sh --self-test
|
|
32
|
+
# sse-hook-probe.sh --attach http://127.0.0.1:4096 --password secret \
|
|
33
|
+
# --directory "$PWD" --event message.part.updated --timeout 30
|
|
34
|
+
|
|
35
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
36
|
+
. "$SCRIPT_DIR/lib/common.sh"
|
|
37
|
+
|
|
38
|
+
# Watch an SSE stream for an event type. Args: url auth directory event timeout
|
|
39
|
+
# Returns 0 if seen, 1 otherwise. Always kills its curl watcher.
|
|
40
|
+
oqa_sse_watch() {
|
|
41
|
+
local url="$1" auth="$2" dir="$3" want="$4" timeout="${5:-15}"
|
|
42
|
+
local out cpid found="" deadline
|
|
43
|
+
out="$(mktemp -t oqa-sse.XXXXXX)"; OQA_TMPDIRS+=("$out")
|
|
44
|
+
if [ -n "$auth" ]; then
|
|
45
|
+
curl -sN -u "$auth" "$url/event?directory=$dir" >"$out" 2>/dev/null &
|
|
46
|
+
else
|
|
47
|
+
curl -sN "$url/event?directory=$dir" >"$out" 2>/dev/null &
|
|
48
|
+
fi
|
|
49
|
+
cpid=$!; OQA_CURL_PIDS+=("$cpid")
|
|
50
|
+
deadline=$(( $(date +%s) + timeout ))
|
|
51
|
+
while [ "$(date +%s)" -lt "$deadline" ]; do
|
|
52
|
+
if grep -q "\"$want\"" "$out" 2>/dev/null; then found=1; break; fi
|
|
53
|
+
kill -0 "$cpid" 2>/dev/null || break
|
|
54
|
+
sleep 0.2
|
|
55
|
+
done
|
|
56
|
+
kill "$cpid" 2>/dev/null || true
|
|
57
|
+
if [ -n "$found" ]; then
|
|
58
|
+
printf 'first matching event: '
|
|
59
|
+
grep -m1 "\"$want\"" "$out" | sed 's/^data: //' | jq -c '{type: .type}' 2>/dev/null || true
|
|
60
|
+
return 0
|
|
61
|
+
fi
|
|
62
|
+
oqa_log "stream head (no '$want' within ${timeout}s):"; head -5 "$out" >&2
|
|
63
|
+
return 1
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
oqa_self_test() {
|
|
67
|
+
oqa_require opencode curl jq || return 1
|
|
68
|
+
if ! oqa_start_server; then oqa_log "FAIL: server did not start"; return 1; fi
|
|
69
|
+
if oqa_sse_watch "$OQA_SERVER_URL" "opencode:$OQA_SERVER_PASS" "$OQA_PROJ" "server.connected" 15; then
|
|
70
|
+
oqa_pass "SSE /event opened and delivered server.connected"
|
|
71
|
+
return 0
|
|
72
|
+
fi
|
|
73
|
+
oqa_log "FAIL: did not observe server.connected"; return 1
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
oqa_attach_mode() {
|
|
77
|
+
local url="" user="opencode" pass="" dir="$PWD" event="server.connected" timeout=15
|
|
78
|
+
shift # drop --attach
|
|
79
|
+
url="$1"; shift || true
|
|
80
|
+
while [ $# -gt 0 ]; do
|
|
81
|
+
case "$1" in
|
|
82
|
+
--password) pass="$2"; shift 2 ;;
|
|
83
|
+
--user) user="$2"; shift 2 ;;
|
|
84
|
+
--directory) dir="$2"; shift 2 ;;
|
|
85
|
+
--event) event="$2"; shift 2 ;;
|
|
86
|
+
--timeout) timeout="$2"; shift 2 ;;
|
|
87
|
+
*) oqa_log "unknown option: $1"; shift ;;
|
|
88
|
+
esac
|
|
89
|
+
done
|
|
90
|
+
[ -n "$url" ] || { oqa_log "error: --attach requires a URL"; return 2; }
|
|
91
|
+
local auth=""; [ -n "$pass" ] && auth="$user:$pass"
|
|
92
|
+
oqa_log "watching $url/event?directory=$dir for '$event' (<=${timeout}s)"
|
|
93
|
+
if oqa_sse_watch "$url" "$auth" "$dir" "$event" "$timeout"; then
|
|
94
|
+
oqa_pass "observed '$event' on $url"
|
|
95
|
+
return 0
|
|
96
|
+
fi
|
|
97
|
+
oqa_log "FAIL: '$event' not observed"; return 1
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
case "${1:-}" in
|
|
101
|
+
--attach) oqa_attach_mode "$@"; exit $? ;;
|
|
102
|
+
-h|--help)
|
|
103
|
+
sed -n '2,34p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
|
104
|
+
exit 0 ;;
|
|
105
|
+
*) oqa_self_test; exit $? ;;
|
|
106
|
+
esac
|