@glrs-dev/harness-plugin-opencode 0.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.
- package/CHANGELOG.md +720 -0
- package/LICENSE +21 -0
- package/README.md +353 -0
- package/SECURITY.md +89 -0
- package/dist/agents/prompts/agents-md-writer.md +89 -0
- package/dist/agents/prompts/architecture-advisor.md +46 -0
- package/dist/agents/prompts/build.md +93 -0
- package/dist/agents/prompts/code-searcher.md +54 -0
- package/dist/agents/prompts/docs-maintainer.md +128 -0
- package/dist/agents/prompts/gap-analyzer.md +44 -0
- package/dist/agents/prompts/lib-reader.md +39 -0
- package/dist/agents/prompts/pilot-builder.md +107 -0
- package/dist/agents/prompts/pilot-planner.md +153 -0
- package/dist/agents/prompts/plan-reviewer.md +49 -0
- package/dist/agents/prompts/plan.md +144 -0
- package/dist/agents/prompts/prime.md +374 -0
- package/dist/agents/prompts/qa-reviewer.md +68 -0
- package/dist/agents/prompts/qa-thorough.md +63 -0
- package/dist/agents/prompts/research.md +138 -0
- package/dist/agents/shared/index.ts +26 -0
- package/dist/agents/shared/workflow-mechanics.md +32 -0
- package/dist/bin/memory-mcp-launcher.sh +145 -0
- package/dist/bin/plan-check.sh +255 -0
- package/dist/chunk-VJUETC6A.js +205 -0
- package/dist/chunk-VVMP6QWS.js +731 -0
- package/dist/chunk-XCZ3NOXR.js +703 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +5096 -0
- package/dist/commands/prompts/autopilot.md +96 -0
- package/dist/commands/prompts/costs.md +94 -0
- package/dist/commands/prompts/fresh.md +382 -0
- package/dist/commands/prompts/init-deep.md +196 -0
- package/dist/commands/prompts/research.md +27 -0
- package/dist/commands/prompts/review.md +96 -0
- package/dist/commands/prompts/ship.md +104 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +2092 -0
- package/dist/install-4EYR56OR.js +9 -0
- package/dist/skills/agent-estimation/SKILL.md +159 -0
- package/dist/skills/paths.ts +18 -0
- package/dist/skills/pilot-planning/SKILL.md +49 -0
- package/dist/skills/pilot-planning/rules/dag-shape.md +47 -0
- package/dist/skills/pilot-planning/rules/decomposition.md +36 -0
- package/dist/skills/pilot-planning/rules/first-principles.md +29 -0
- package/dist/skills/pilot-planning/rules/milestones.md +57 -0
- package/dist/skills/pilot-planning/rules/self-review.md +46 -0
- package/dist/skills/pilot-planning/rules/task-context.md +47 -0
- package/dist/skills/pilot-planning/rules/touches-scope.md +47 -0
- package/dist/skills/pilot-planning/rules/verify-design.md +53 -0
- package/dist/skills/research/SKILL.md +350 -0
- package/dist/skills/research-auto/SKILL.md +283 -0
- package/dist/skills/research-local/SKILL.md +268 -0
- package/dist/skills/research-web/SKILL.md +119 -0
- package/dist/skills/review-plan/SKILL.md +32 -0
- package/dist/skills/vercel-composition-patterns/AGENTS.md +946 -0
- package/dist/skills/vercel-composition-patterns/README.md +60 -0
- package/dist/skills/vercel-composition-patterns/SKILL.md +89 -0
- package/dist/skills/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md +100 -0
- package/dist/skills/vercel-composition-patterns/rules/architecture-compound-components.md +112 -0
- package/dist/skills/vercel-composition-patterns/rules/patterns-children-over-render-props.md +87 -0
- package/dist/skills/vercel-composition-patterns/rules/patterns-explicit-variants.md +100 -0
- package/dist/skills/vercel-composition-patterns/rules/react19-no-forwardref.md +42 -0
- package/dist/skills/vercel-composition-patterns/rules/state-context-interface.md +191 -0
- package/dist/skills/vercel-composition-patterns/rules/state-decouple-implementation.md +113 -0
- package/dist/skills/vercel-composition-patterns/rules/state-lift-state.md +125 -0
- package/dist/skills/vercel-react-best-practices/AGENTS.md +2975 -0
- package/dist/skills/vercel-react-best-practices/README.md +123 -0
- package/dist/skills/vercel-react-best-practices/SKILL.md +137 -0
- package/dist/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/dist/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
- package/dist/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
- package/dist/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
- package/dist/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
- package/dist/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
- package/dist/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
- package/dist/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/dist/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/dist/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
- package/dist/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/dist/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/dist/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
- package/dist/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
- package/dist/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/dist/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/dist/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
- package/dist/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
- package/dist/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
- package/dist/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
- package/dist/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
- package/dist/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
- package/dist/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
- package/dist/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/dist/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
- package/dist/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
- package/dist/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
- package/dist/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/dist/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/dist/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
- package/dist/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/dist/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/dist/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/dist/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/dist/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/dist/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
- package/dist/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/dist/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
- package/dist/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/dist/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
- package/dist/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/dist/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
- package/dist/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/dist/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/dist/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
- package/dist/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
- package/dist/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/dist/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/dist/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
- package/dist/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/dist/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/dist/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
- package/dist/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
- package/dist/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
- package/dist/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
- package/dist/skills/vercel-react-best-practices/rules/server-hoist-static-io.md +142 -0
- package/dist/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/dist/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
- package/dist/skills/web-design-guidelines/SKILL.md +39 -0
- package/package.json +70 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# plan-check.sh — parse a plan file's plan-state fence and report on it.
|
|
3
|
+
#
|
|
4
|
+
# Modes:
|
|
5
|
+
# plan-check.sh <path> Prints a summary line then one line per item:
|
|
6
|
+
# `total=N done=M pending=K invalid=I`
|
|
7
|
+
# `STATUS ID VERIFY` (one per item)
|
|
8
|
+
#
|
|
9
|
+
# plan-check.sh --run <path> Prints the verify command of each PENDING
|
|
10
|
+
# item on stdout, one per line, raw. The
|
|
11
|
+
# caller is responsible for executing them.
|
|
12
|
+
# This script NEVER executes verify commands
|
|
13
|
+
# itself — that would bypass the caller's
|
|
14
|
+
# bash-permission scope.
|
|
15
|
+
#
|
|
16
|
+
# plan-check.sh --check <path>
|
|
17
|
+
# Structural validation only. Exits 1 if any
|
|
18
|
+
# fence item is missing a required field.
|
|
19
|
+
#
|
|
20
|
+
# Fence format, inside `## Acceptance criteria`:
|
|
21
|
+
#
|
|
22
|
+
# ```plan-state
|
|
23
|
+
# - [ ] id: a1
|
|
24
|
+
# intent: Prose description of business intent (one line).
|
|
25
|
+
# tests:
|
|
26
|
+
# - path/to/test.sh::"some test name"
|
|
27
|
+
# - path/to/other.ts::"another test"
|
|
28
|
+
# verify: bash path/to/test.sh
|
|
29
|
+
#
|
|
30
|
+
# - [x] id: a2
|
|
31
|
+
# ...
|
|
32
|
+
# ```
|
|
33
|
+
#
|
|
34
|
+
# Backward compat: a plan without a ```plan-state fence emits the line
|
|
35
|
+
# `legacy` and exits 0 — callers treat it as "old format, fall back".
|
|
36
|
+
#
|
|
37
|
+
# Portability: POSIX bash + awk + grep only. No sed -i.
|
|
38
|
+
|
|
39
|
+
set -eu
|
|
40
|
+
|
|
41
|
+
MODE=""
|
|
42
|
+
PLAN_PATH=""
|
|
43
|
+
|
|
44
|
+
case "${1:-}" in
|
|
45
|
+
--run) MODE=run; PLAN_PATH="${2:-}" ;;
|
|
46
|
+
--check) MODE=check; PLAN_PATH="${2:-}" ;;
|
|
47
|
+
-h|--help|"")
|
|
48
|
+
sed -n '2,34p' "$0"
|
|
49
|
+
exit 0
|
|
50
|
+
;;
|
|
51
|
+
*) MODE=summary; PLAN_PATH="${1:-}" ;;
|
|
52
|
+
esac
|
|
53
|
+
|
|
54
|
+
if [[ -z "${PLAN_PATH:-}" ]]; then
|
|
55
|
+
echo "plan-check.sh: missing plan path" >&2
|
|
56
|
+
exit 2
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
if [[ ! -f "$PLAN_PATH" ]]; then
|
|
60
|
+
echo "plan-check.sh: file not found: $PLAN_PATH" >&2
|
|
61
|
+
exit 2
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# Extract the plan-state fence body into a temp file. awk state machine:
|
|
65
|
+
# enter `## Acceptance criteria`, enter ``` plan-state, exit on next ```.
|
|
66
|
+
FENCE_BODY="$(awk '
|
|
67
|
+
/^## Acceptance criteria/ { in_ac = 1; next }
|
|
68
|
+
/^## / && in_ac && !in_fence { in_ac = 0 }
|
|
69
|
+
in_ac && /^```plan-state[[:space:]]*$/ { in_fence = 1; next }
|
|
70
|
+
in_fence && /^```[[:space:]]*$/ { in_fence = 0; next }
|
|
71
|
+
in_fence { print }
|
|
72
|
+
' "$PLAN_PATH")"
|
|
73
|
+
|
|
74
|
+
if [[ -z "$FENCE_BODY" ]]; then
|
|
75
|
+
# No fence found — legacy plan. Report and exit cleanly.
|
|
76
|
+
if [[ "$MODE" == "summary" ]]; then
|
|
77
|
+
echo "legacy (no plan-state fence)"
|
|
78
|
+
fi
|
|
79
|
+
# --run on a legacy plan emits nothing (no commands to run).
|
|
80
|
+
# --check on a legacy plan succeeds (we're accepting legacy plans).
|
|
81
|
+
exit 0
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# Parse items. awk state machine:
|
|
85
|
+
# - A line `- [ ] id: ID` or `- [x] id: ID` starts a new item.
|
|
86
|
+
# - While inside an item, indented keys `intent:`, `tests:`, `verify:` set
|
|
87
|
+
# fields. Under `tests:`, subsequent ` - ...` lines extend the list
|
|
88
|
+
# until the next key or the next item.
|
|
89
|
+
# - Items are separated by one or more blank lines OR by the next `- [`.
|
|
90
|
+
#
|
|
91
|
+
# We emit a tab-delimited record per item:
|
|
92
|
+
# STATUS<TAB>ID<TAB>INTENT<TAB>TESTS<TAB>VERIFY
|
|
93
|
+
# TESTS is a `|`-delimited list. Missing fields are the empty string.
|
|
94
|
+
PARSED="$(echo "$FENCE_BODY" | awk '
|
|
95
|
+
function flush() {
|
|
96
|
+
if (cur_id != "") {
|
|
97
|
+
# Trim trailing/leading whitespace on each field
|
|
98
|
+
gsub(/^[[:space:]]+|[[:space:]]+$/, "", cur_intent)
|
|
99
|
+
gsub(/^[[:space:]]+|[[:space:]]+$/, "", cur_verify)
|
|
100
|
+
gsub(/^\||\|$/, "", cur_tests)
|
|
101
|
+
printf "%s\t%s\t%s\t%s\t%s\n", cur_status, cur_id, cur_intent, cur_tests, cur_verify
|
|
102
|
+
}
|
|
103
|
+
cur_status = ""; cur_id = ""; cur_intent = ""; cur_tests = ""; cur_verify = ""
|
|
104
|
+
in_tests = 0
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/^-[[:space:]]+\[[[:space:]xX[:space:]]\][[:space:]]+id:/ {
|
|
108
|
+
flush()
|
|
109
|
+
status = $0
|
|
110
|
+
sub(/^-[[:space:]]+\[[[:space:]]*/, "", status)
|
|
111
|
+
sub(/\].*$/, "", status)
|
|
112
|
+
# status is either empty/space (" ") -> pending, or "x"/"X" -> done
|
|
113
|
+
if (status ~ /[xX]/) cur_status = "done"; else cur_status = "pending"
|
|
114
|
+
# Capture id
|
|
115
|
+
id_part = $0
|
|
116
|
+
sub(/^.*id:[[:space:]]*/, "", id_part)
|
|
117
|
+
cur_id = id_part
|
|
118
|
+
next
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/^[[:space:]]*intent:/ {
|
|
122
|
+
field = $0
|
|
123
|
+
sub(/^[[:space:]]*intent:[[:space:]]*/, "", field)
|
|
124
|
+
cur_intent = field
|
|
125
|
+
in_tests = 0
|
|
126
|
+
next
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/^[[:space:]]*intent\b/ {
|
|
130
|
+
# already handled
|
|
131
|
+
next
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/^[[:space:]]*tests:/ {
|
|
135
|
+
in_tests = 1
|
|
136
|
+
next
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/^[[:space:]]*verify:/ {
|
|
140
|
+
field = $0
|
|
141
|
+
sub(/^[[:space:]]*verify:[[:space:]]*/, "", field)
|
|
142
|
+
cur_verify = field
|
|
143
|
+
in_tests = 0
|
|
144
|
+
next
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# Continuation lines inside tests: list
|
|
148
|
+
in_tests && /^[[:space:]]+-[[:space:]]/ {
|
|
149
|
+
line = $0
|
|
150
|
+
sub(/^[[:space:]]+-[[:space:]]+/, "", line)
|
|
151
|
+
if (cur_tests == "") cur_tests = line
|
|
152
|
+
else cur_tests = cur_tests "|" line
|
|
153
|
+
next
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# Continuation line for intent (indented without `-`, after intent is set
|
|
157
|
+
# and before another key). Append with a space separator.
|
|
158
|
+
!in_tests && /^[[:space:]]{4,}[^-[:space:]]/ && cur_id != "" && cur_intent != "" && cur_verify == "" {
|
|
159
|
+
line = $0
|
|
160
|
+
sub(/^[[:space:]]+/, "", line)
|
|
161
|
+
cur_intent = cur_intent " " line
|
|
162
|
+
next
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
END { flush() }
|
|
166
|
+
' 2>&1)"
|
|
167
|
+
|
|
168
|
+
# If PARSED contains awk errors, surface them as invalid.
|
|
169
|
+
if echo "$PARSED" | grep -q '^awk:'; then
|
|
170
|
+
echo "plan-check.sh: parser error" >&2
|
|
171
|
+
echo "$PARSED" >&2
|
|
172
|
+
exit 3
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
# Count totals.
|
|
176
|
+
total=0
|
|
177
|
+
done_count=0
|
|
178
|
+
pending_count=0
|
|
179
|
+
invalid_count=0
|
|
180
|
+
invalid_reasons=()
|
|
181
|
+
|
|
182
|
+
while IFS=$'\t' read -r status id intent tests verify; do
|
|
183
|
+
[[ -z "$status" ]] && continue
|
|
184
|
+
total=$((total + 1))
|
|
185
|
+
if [[ -z "$id" ]]; then
|
|
186
|
+
invalid_count=$((invalid_count + 1))
|
|
187
|
+
invalid_reasons+=("missing id")
|
|
188
|
+
continue
|
|
189
|
+
fi
|
|
190
|
+
if [[ -z "$intent" ]]; then
|
|
191
|
+
invalid_count=$((invalid_count + 1))
|
|
192
|
+
invalid_reasons+=("$id: missing intent")
|
|
193
|
+
continue
|
|
194
|
+
fi
|
|
195
|
+
if [[ -z "$tests" ]]; then
|
|
196
|
+
invalid_count=$((invalid_count + 1))
|
|
197
|
+
invalid_reasons+=("$id: missing tests")
|
|
198
|
+
continue
|
|
199
|
+
fi
|
|
200
|
+
if [[ -z "$verify" ]]; then
|
|
201
|
+
invalid_count=$((invalid_count + 1))
|
|
202
|
+
invalid_reasons+=("$id: missing verify")
|
|
203
|
+
continue
|
|
204
|
+
fi
|
|
205
|
+
if [[ "$status" == "done" ]]; then
|
|
206
|
+
done_count=$((done_count + 1))
|
|
207
|
+
else
|
|
208
|
+
pending_count=$((pending_count + 1))
|
|
209
|
+
fi
|
|
210
|
+
done <<< "$PARSED"
|
|
211
|
+
|
|
212
|
+
case "$MODE" in
|
|
213
|
+
summary)
|
|
214
|
+
printf 'total=%d done=%d pending=%d invalid=%d\n' \
|
|
215
|
+
"$total" "$done_count" "$pending_count" "$invalid_count"
|
|
216
|
+
while IFS=$'\t' read -r status id intent tests verify; do
|
|
217
|
+
[[ -z "$status" ]] && continue
|
|
218
|
+
# For the summary-per-item line, prefer displaying the verify
|
|
219
|
+
# command (truncated) so the reader sees what gates each item.
|
|
220
|
+
v="${verify:0:60}"
|
|
221
|
+
if [[ -n "$verify" && ${#verify} -gt 60 ]]; then v="${v}…"; fi
|
|
222
|
+
printf '%s %s %s\n' "$status" "$id" "$v"
|
|
223
|
+
done <<< "$PARSED"
|
|
224
|
+
if [[ "$invalid_count" -gt 0 ]]; then
|
|
225
|
+
echo "invalid:"
|
|
226
|
+
for r in "${invalid_reasons[@]}"; do
|
|
227
|
+
echo " $r"
|
|
228
|
+
done
|
|
229
|
+
fi
|
|
230
|
+
;;
|
|
231
|
+
|
|
232
|
+
run)
|
|
233
|
+
# Emit verify command per PENDING item, one per line. Skip done items,
|
|
234
|
+
# skip invalid items. Caller executes via their own bash permission.
|
|
235
|
+
while IFS=$'\t' read -r status id intent tests verify; do
|
|
236
|
+
[[ -z "$status" ]] && continue
|
|
237
|
+
[[ "$status" == "done" ]] && continue
|
|
238
|
+
[[ -z "$verify" ]] && continue
|
|
239
|
+
[[ -z "$intent" || -z "$tests" ]] && continue
|
|
240
|
+
echo "$verify"
|
|
241
|
+
done <<< "$PARSED"
|
|
242
|
+
;;
|
|
243
|
+
|
|
244
|
+
check)
|
|
245
|
+
# Structural validation. Exit 1 if anything invalid.
|
|
246
|
+
if [[ "$invalid_count" -gt 0 ]]; then
|
|
247
|
+
echo "plan-check: $invalid_count invalid item(s):" >&2
|
|
248
|
+
for r in "${invalid_reasons[@]}"; do
|
|
249
|
+
echo " $r" >&2
|
|
250
|
+
done
|
|
251
|
+
exit 1
|
|
252
|
+
fi
|
|
253
|
+
printf 'ok: %d item(s) pass structural validation\n' "$total"
|
|
254
|
+
;;
|
|
255
|
+
esac
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// src/auto-update.ts
|
|
2
|
+
import * as fs from "fs/promises";
|
|
3
|
+
import * as fsSync from "fs";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import * as os from "os";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { execFile } from "child_process";
|
|
8
|
+
var PACKAGE_NAME = "@glrs-dev/harness-plugin-opencode";
|
|
9
|
+
function getOpenCodeCachePackageDir() {
|
|
10
|
+
const cacheHome = process.env["XDG_CACHE_HOME"] ?? path.join(os.homedir(), ".cache");
|
|
11
|
+
return path.join(
|
|
12
|
+
cacheHome,
|
|
13
|
+
"opencode",
|
|
14
|
+
"packages",
|
|
15
|
+
"@glrs-dev",
|
|
16
|
+
"harness-opencode@latest"
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
async function inspectCachePin(cacheDir) {
|
|
20
|
+
const pkgPath = path.join(cacheDir, "package.json");
|
|
21
|
+
let raw;
|
|
22
|
+
try {
|
|
23
|
+
raw = await fs.readFile(pkgPath, "utf8");
|
|
24
|
+
} catch {
|
|
25
|
+
return { kind: "missing" };
|
|
26
|
+
}
|
|
27
|
+
let parsed;
|
|
28
|
+
try {
|
|
29
|
+
parsed = JSON.parse(raw);
|
|
30
|
+
} catch {
|
|
31
|
+
return { kind: "missing" };
|
|
32
|
+
}
|
|
33
|
+
const deps = parsed.dependencies ?? {};
|
|
34
|
+
const spec = deps[PACKAGE_NAME];
|
|
35
|
+
if (typeof spec !== "string") {
|
|
36
|
+
return { kind: "not-our-package", name: parsed.name ?? "(unknown)" };
|
|
37
|
+
}
|
|
38
|
+
if (/^\d+\.\d+\.\d+(-[0-9a-zA-Z.-]+)?(\+[0-9a-zA-Z.-]+)?$/.test(spec)) {
|
|
39
|
+
return { kind: "exact", version: spec };
|
|
40
|
+
}
|
|
41
|
+
return { kind: "non-exact", spec };
|
|
42
|
+
}
|
|
43
|
+
async function atomicWriteJson(targetPath, value) {
|
|
44
|
+
const serialized = JSON.stringify(value, null, 2) + "\n";
|
|
45
|
+
const tmpPath = `${targetPath}.tmp-${process.pid}-${Date.now()}`;
|
|
46
|
+
await fs.writeFile(tmpPath, serialized, "utf8");
|
|
47
|
+
await fs.rename(tmpPath, targetPath);
|
|
48
|
+
}
|
|
49
|
+
async function refreshPluginCache(currentVersion, latestVersion, ctx = {}) {
|
|
50
|
+
if (process.env["HARNESS_OPENCODE_AUTO_UPDATE"] === "0") {
|
|
51
|
+
return {
|
|
52
|
+
outcome: "disabled",
|
|
53
|
+
message: "HARNESS_OPENCODE_AUTO_UPDATE=0 \u2014 cache rewrite skipped"
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const cacheDir = ctx.cacheDir ?? getOpenCodeCachePackageDir();
|
|
57
|
+
if (currentVersion === latestVersion) {
|
|
58
|
+
return {
|
|
59
|
+
outcome: "already-current",
|
|
60
|
+
message: `running ${currentVersion}, latest is ${latestVersion}`,
|
|
61
|
+
fromVersion: currentVersion,
|
|
62
|
+
toVersion: latestVersion
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const pin = await inspectCachePin(cacheDir);
|
|
66
|
+
switch (pin.kind) {
|
|
67
|
+
case "missing":
|
|
68
|
+
return {
|
|
69
|
+
outcome: "cache-missing",
|
|
70
|
+
message: `no cache pin at ${cacheDir} \u2014 nothing to rewrite`
|
|
71
|
+
};
|
|
72
|
+
case "not-our-package":
|
|
73
|
+
return {
|
|
74
|
+
outcome: "not-our-package",
|
|
75
|
+
message: `cache dir exists but doesn't pin ${PACKAGE_NAME} (name=${pin.name})`
|
|
76
|
+
};
|
|
77
|
+
case "non-exact":
|
|
78
|
+
return {
|
|
79
|
+
outcome: "non-exact-pin",
|
|
80
|
+
message: `cache pin is "${pin.spec}" (not exact) \u2014 user-managed, leaving alone`
|
|
81
|
+
};
|
|
82
|
+
case "exact": {
|
|
83
|
+
if (pin.version === latestVersion) {
|
|
84
|
+
return {
|
|
85
|
+
outcome: "already-current",
|
|
86
|
+
message: `cache already pinned to ${latestVersion}`,
|
|
87
|
+
fromVersion: pin.version,
|
|
88
|
+
toVersion: latestVersion
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const fromVersion = pin.version;
|
|
95
|
+
if (ctx.dryRun) {
|
|
96
|
+
return {
|
|
97
|
+
outcome: "refreshed",
|
|
98
|
+
message: `[dry-run] would rewrite ${cacheDir} from ${fromVersion} to ${latestVersion}`,
|
|
99
|
+
fromVersion,
|
|
100
|
+
toVersion: latestVersion
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const pkgPath = path.join(cacheDir, "package.json");
|
|
105
|
+
const pkgRaw = await fs.readFile(pkgPath, "utf8");
|
|
106
|
+
const pkg = JSON.parse(pkgRaw);
|
|
107
|
+
const deps = { ...pkg.dependencies ?? {} };
|
|
108
|
+
deps[PACKAGE_NAME] = latestVersion;
|
|
109
|
+
const newPkg = { ...pkg, dependencies: deps };
|
|
110
|
+
await atomicWriteJson(pkgPath, newPkg);
|
|
111
|
+
const lockPath = path.join(cacheDir, "package-lock.json");
|
|
112
|
+
try {
|
|
113
|
+
const lockRaw = await fs.readFile(lockPath, "utf8");
|
|
114
|
+
const lock = JSON.parse(lockRaw);
|
|
115
|
+
const packages = { ...lock.packages ?? {} };
|
|
116
|
+
if (packages[""] && packages[""].dependencies) {
|
|
117
|
+
packages[""] = {
|
|
118
|
+
...packages[""],
|
|
119
|
+
dependencies: {
|
|
120
|
+
...packages[""].dependencies,
|
|
121
|
+
[PACKAGE_NAME]: latestVersion
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const nmKey = `node_modules/${PACKAGE_NAME}`;
|
|
126
|
+
if (packages[nmKey]) {
|
|
127
|
+
packages[nmKey] = {
|
|
128
|
+
...packages[nmKey],
|
|
129
|
+
version: latestVersion
|
|
130
|
+
};
|
|
131
|
+
delete packages[nmKey]["resolved"];
|
|
132
|
+
delete packages[nmKey]["integrity"];
|
|
133
|
+
}
|
|
134
|
+
const newLock = { ...lock, packages };
|
|
135
|
+
await atomicWriteJson(lockPath, newLock);
|
|
136
|
+
} catch {
|
|
137
|
+
try {
|
|
138
|
+
await fs.rm(lockPath, { force: true });
|
|
139
|
+
} catch {
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const nmPath = path.join(cacheDir, "node_modules");
|
|
143
|
+
try {
|
|
144
|
+
await fs.rm(nmPath, { recursive: true, force: true });
|
|
145
|
+
} catch {
|
|
146
|
+
}
|
|
147
|
+
if (!ctx.skipInstall) {
|
|
148
|
+
try {
|
|
149
|
+
await new Promise((resolve, reject) => {
|
|
150
|
+
const child = execFile(
|
|
151
|
+
"npm",
|
|
152
|
+
["install", "--no-audit", "--no-fund"],
|
|
153
|
+
{ cwd: cacheDir, timeout: 3e4 },
|
|
154
|
+
(err) => err ? reject(err) : resolve()
|
|
155
|
+
);
|
|
156
|
+
child.unref?.();
|
|
157
|
+
});
|
|
158
|
+
} catch {
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
outcome: "refreshed",
|
|
163
|
+
message: `rewrote cache pin ${fromVersion} \u2192 ${latestVersion}; next OpenCode restart will reinstall`,
|
|
164
|
+
fromVersion,
|
|
165
|
+
toVersion: latestVersion
|
|
166
|
+
};
|
|
167
|
+
} catch (err) {
|
|
168
|
+
return {
|
|
169
|
+
outcome: "error",
|
|
170
|
+
message: `cache rewrite failed: ${err.message}`,
|
|
171
|
+
fromVersion,
|
|
172
|
+
toVersion: latestVersion
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function readOurPackageVersion(fromFileUrl) {
|
|
177
|
+
const here = path.dirname(fileURLToPath(fromFileUrl));
|
|
178
|
+
const candidates = [
|
|
179
|
+
path.join(here, "..", "package.json"),
|
|
180
|
+
// dist/index.js → dist/../package.json
|
|
181
|
+
path.join(here, "..", "..", "package.json"),
|
|
182
|
+
// src/index.ts → src/../../package.json
|
|
183
|
+
path.join(here, "package.json")
|
|
184
|
+
// safety net
|
|
185
|
+
];
|
|
186
|
+
for (const candidate of candidates) {
|
|
187
|
+
try {
|
|
188
|
+
const raw = fsSync.readFileSync(candidate, "utf8");
|
|
189
|
+
const parsed = JSON.parse(raw);
|
|
190
|
+
if (parsed.name === PACKAGE_NAME && typeof parsed.version === "string") {
|
|
191
|
+
return parsed.version;
|
|
192
|
+
}
|
|
193
|
+
} catch {
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return "0.0.0";
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export {
|
|
200
|
+
PACKAGE_NAME,
|
|
201
|
+
getOpenCodeCachePackageDir,
|
|
202
|
+
inspectCachePin,
|
|
203
|
+
refreshPluginCache,
|
|
204
|
+
readOurPackageVersion
|
|
205
|
+
};
|