shipwright-cli 2.4.0 → 3.1.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/README.md +16 -11
- package/completions/_shipwright +248 -94
- package/completions/shipwright.bash +68 -19
- package/completions/shipwright.fish +310 -42
- package/config/decision-tiers.json +55 -0
- package/config/defaults.json +111 -0
- package/config/event-schema.json +218 -0
- package/config/policy.json +21 -18
- package/dashboard/coverage/coverage-summary.json +14 -0
- package/dashboard/public/index.html +1 -1
- package/dashboard/server.ts +306 -17
- package/dashboard/src/components/charts/bar.test.ts +79 -0
- package/dashboard/src/components/charts/donut.test.ts +68 -0
- package/dashboard/src/components/charts/pipeline-rail.test.ts +117 -0
- package/dashboard/src/components/charts/sparkline.test.ts +125 -0
- package/dashboard/src/core/api.test.ts +309 -0
- package/dashboard/src/core/helpers.test.ts +301 -0
- package/dashboard/src/core/router.test.ts +307 -0
- package/dashboard/src/core/router.ts +7 -0
- package/dashboard/src/core/sse.test.ts +144 -0
- package/dashboard/src/views/metrics.test.ts +186 -0
- package/dashboard/src/views/overview.test.ts +173 -0
- package/dashboard/src/views/pipelines.test.ts +183 -0
- package/dashboard/src/views/team.test.ts +253 -0
- package/dashboard/vitest.config.ts +14 -5
- package/docs/TIPS.md +1 -1
- package/docs/patterns/README.md +1 -1
- package/package.json +7 -9
- package/scripts/adapters/docker-deploy.sh +1 -1
- package/scripts/adapters/tmux-adapter.sh +11 -1
- package/scripts/adapters/wezterm-adapter.sh +1 -1
- package/scripts/check-version-consistency.sh +1 -1
- package/scripts/lib/architecture.sh +127 -0
- package/scripts/lib/bootstrap.sh +75 -0
- package/scripts/lib/compat.sh +89 -6
- package/scripts/lib/config.sh +91 -0
- package/scripts/lib/daemon-adaptive.sh +3 -3
- package/scripts/lib/daemon-dispatch.sh +63 -17
- package/scripts/lib/daemon-failure.sh +0 -0
- package/scripts/lib/daemon-health.sh +1 -1
- package/scripts/lib/daemon-patrol.sh +64 -17
- package/scripts/lib/daemon-poll.sh +54 -25
- package/scripts/lib/daemon-state.sh +125 -23
- package/scripts/lib/daemon-triage.sh +31 -9
- package/scripts/lib/decide-autonomy.sh +295 -0
- package/scripts/lib/decide-scoring.sh +228 -0
- package/scripts/lib/decide-signals.sh +462 -0
- package/scripts/lib/fleet-failover.sh +63 -0
- package/scripts/lib/helpers.sh +29 -6
- package/scripts/lib/pipeline-detection.sh +2 -2
- package/scripts/lib/pipeline-github.sh +9 -9
- package/scripts/lib/pipeline-intelligence.sh +105 -38
- package/scripts/lib/pipeline-quality-checks.sh +17 -16
- package/scripts/lib/pipeline-quality.sh +1 -1
- package/scripts/lib/pipeline-stages.sh +440 -59
- package/scripts/lib/pipeline-state.sh +54 -4
- package/scripts/lib/policy.sh +0 -0
- package/scripts/lib/test-helpers.sh +247 -0
- package/scripts/postinstall.mjs +78 -12
- package/scripts/signals/example-collector.sh +36 -0
- package/scripts/sw +17 -7
- package/scripts/sw-activity.sh +1 -11
- package/scripts/sw-adaptive.sh +109 -85
- package/scripts/sw-adversarial.sh +4 -14
- package/scripts/sw-architecture-enforcer.sh +1 -11
- package/scripts/sw-auth.sh +8 -17
- package/scripts/sw-autonomous.sh +111 -49
- package/scripts/sw-changelog.sh +1 -11
- package/scripts/sw-checkpoint.sh +144 -20
- package/scripts/sw-ci.sh +2 -12
- package/scripts/sw-cleanup.sh +13 -17
- package/scripts/sw-code-review.sh +16 -36
- package/scripts/sw-connect.sh +5 -12
- package/scripts/sw-context.sh +9 -26
- package/scripts/sw-cost.sh +17 -18
- package/scripts/sw-daemon.sh +76 -71
- package/scripts/sw-dashboard.sh +57 -17
- package/scripts/sw-db.sh +524 -26
- package/scripts/sw-decide.sh +685 -0
- package/scripts/sw-decompose.sh +1 -11
- package/scripts/sw-deps.sh +15 -25
- package/scripts/sw-developer-simulation.sh +1 -11
- package/scripts/sw-discovery.sh +138 -30
- package/scripts/sw-doc-fleet.sh +7 -17
- package/scripts/sw-docs-agent.sh +6 -16
- package/scripts/sw-docs.sh +4 -12
- package/scripts/sw-doctor.sh +134 -43
- package/scripts/sw-dora.sh +11 -19
- package/scripts/sw-durable.sh +35 -52
- package/scripts/sw-e2e-orchestrator.sh +11 -27
- package/scripts/sw-eventbus.sh +115 -115
- package/scripts/sw-evidence.sh +114 -30
- package/scripts/sw-feedback.sh +3 -13
- package/scripts/sw-fix.sh +2 -20
- package/scripts/sw-fleet-discover.sh +1 -11
- package/scripts/sw-fleet-viz.sh +10 -18
- package/scripts/sw-fleet.sh +13 -17
- package/scripts/sw-github-app.sh +6 -16
- package/scripts/sw-github-checks.sh +1 -11
- package/scripts/sw-github-deploy.sh +1 -11
- package/scripts/sw-github-graphql.sh +2 -12
- package/scripts/sw-guild.sh +1 -11
- package/scripts/sw-heartbeat.sh +49 -12
- package/scripts/sw-hygiene.sh +45 -43
- package/scripts/sw-incident.sh +48 -74
- package/scripts/sw-init.sh +35 -37
- package/scripts/sw-instrument.sh +1 -11
- package/scripts/sw-intelligence.sh +368 -53
- package/scripts/sw-jira.sh +5 -14
- package/scripts/sw-launchd.sh +2 -12
- package/scripts/sw-linear.sh +8 -17
- package/scripts/sw-logs.sh +4 -12
- package/scripts/sw-loop.sh +905 -104
- package/scripts/sw-memory.sh +263 -20
- package/scripts/sw-mission-control.sh +2 -12
- package/scripts/sw-model-router.sh +73 -34
- package/scripts/sw-otel.sh +15 -23
- package/scripts/sw-oversight.sh +1 -11
- package/scripts/sw-patrol-meta.sh +5 -11
- package/scripts/sw-pipeline-composer.sh +7 -17
- package/scripts/sw-pipeline-vitals.sh +1 -11
- package/scripts/sw-pipeline.sh +550 -122
- package/scripts/sw-pm.sh +2 -12
- package/scripts/sw-pr-lifecycle.sh +33 -28
- package/scripts/sw-predictive.sh +16 -22
- package/scripts/sw-prep.sh +6 -16
- package/scripts/sw-ps.sh +1 -11
- package/scripts/sw-public-dashboard.sh +2 -12
- package/scripts/sw-quality.sh +85 -14
- package/scripts/sw-reaper.sh +1 -11
- package/scripts/sw-recruit.sh +15 -25
- package/scripts/sw-regression.sh +11 -21
- package/scripts/sw-release-manager.sh +19 -28
- package/scripts/sw-release.sh +8 -16
- package/scripts/sw-remote.sh +1 -11
- package/scripts/sw-replay.sh +48 -44
- package/scripts/sw-retro.sh +70 -92
- package/scripts/sw-review-rerun.sh +1 -1
- package/scripts/sw-scale.sh +174 -41
- package/scripts/sw-security-audit.sh +12 -22
- package/scripts/sw-self-optimize.sh +239 -23
- package/scripts/sw-session.sh +5 -15
- package/scripts/sw-setup.sh +8 -18
- package/scripts/sw-standup.sh +5 -15
- package/scripts/sw-status.sh +32 -23
- package/scripts/sw-strategic.sh +129 -13
- package/scripts/sw-stream.sh +1 -11
- package/scripts/sw-swarm.sh +76 -36
- package/scripts/sw-team-stages.sh +10 -20
- package/scripts/sw-templates.sh +4 -14
- package/scripts/sw-testgen.sh +3 -13
- package/scripts/sw-tmux-pipeline.sh +1 -19
- package/scripts/sw-tmux-role-color.sh +0 -10
- package/scripts/sw-tmux-status.sh +3 -11
- package/scripts/sw-tmux.sh +2 -20
- package/scripts/sw-trace.sh +1 -19
- package/scripts/sw-tracker-github.sh +0 -10
- package/scripts/sw-tracker-jira.sh +1 -11
- package/scripts/sw-tracker-linear.sh +1 -11
- package/scripts/sw-tracker.sh +7 -24
- package/scripts/sw-triage.sh +29 -39
- package/scripts/sw-upgrade.sh +5 -23
- package/scripts/sw-ux.sh +1 -19
- package/scripts/sw-webhook.sh +18 -32
- package/scripts/sw-widgets.sh +3 -21
- package/scripts/sw-worktree.sh +11 -27
- package/scripts/update-homebrew-sha.sh +73 -0
- package/templates/pipelines/tdd.json +72 -0
- package/scripts/sw-pipeline.sh.mock +0 -7
package/scripts/sw-decompose.sh
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
set -euo pipefail
|
|
7
7
|
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
8
8
|
|
|
9
|
-
VERSION="
|
|
9
|
+
VERSION="3.1.0"
|
|
10
10
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
11
|
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
12
12
|
|
|
@@ -34,16 +34,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
|
|
|
34
34
|
echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
|
|
35
35
|
}
|
|
36
36
|
fi
|
|
37
|
-
CYAN="${CYAN:-\033[38;2;0;212;255m}"
|
|
38
|
-
PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
|
|
39
|
-
BLUE="${BLUE:-\033[38;2;0;102;255m}"
|
|
40
|
-
GREEN="${GREEN:-\033[38;2;74;222;128m}"
|
|
41
|
-
YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
|
|
42
|
-
RED="${RED:-\033[38;2;248;113;113m}"
|
|
43
|
-
DIM="${DIM:-\033[2m}"
|
|
44
|
-
BOLD="${BOLD:-\033[1m}"
|
|
45
|
-
RESET="${RESET:-\033[0m}"
|
|
46
|
-
|
|
47
37
|
# ─── Structured Event Log ──────────────────────────────────────────────────
|
|
48
38
|
EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
|
|
49
39
|
|
package/scripts/sw-deps.sh
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
set -euo pipefail
|
|
7
7
|
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
8
8
|
|
|
9
|
-
VERSION="
|
|
9
|
+
VERSION="3.1.0"
|
|
10
10
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
11
|
|
|
12
12
|
# ─── Cross-platform compatibility ──────────────────────────────────────────
|
|
@@ -25,24 +25,6 @@ if [[ "$(type -t now_iso 2>/dev/null)" != "function" ]]; then
|
|
|
25
25
|
now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
|
|
26
26
|
now_epoch() { date +%s; }
|
|
27
27
|
fi
|
|
28
|
-
if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
|
|
29
|
-
emit_event() {
|
|
30
|
-
local event_type="$1"; shift; mkdir -p "${HOME}/.shipwright"
|
|
31
|
-
local payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
|
|
32
|
-
while [[ $# -gt 0 ]]; do local key="${1%%=*}" val="${1#*=}"; payload="${payload},\"${key}\":\"${val}\""; shift; done
|
|
33
|
-
echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
|
|
34
|
-
}
|
|
35
|
-
fi
|
|
36
|
-
CYAN="${CYAN:-\033[38;2;0;212;255m}"
|
|
37
|
-
PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
|
|
38
|
-
BLUE="${BLUE:-\033[38;2;0;102;255m}"
|
|
39
|
-
GREEN="${GREEN:-\033[38;2;74;222;128m}"
|
|
40
|
-
YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
|
|
41
|
-
RED="${RED:-\033[38;2;248;113;113m}"
|
|
42
|
-
DIM="${DIM:-\033[2m}"
|
|
43
|
-
BOLD="${BOLD:-\033[1m}"
|
|
44
|
-
RESET="${RESET:-\033[0m}"
|
|
45
|
-
|
|
46
28
|
# ─── Defaults ───────────────────────────────────────────────────────────────
|
|
47
29
|
DEPS_DIR="${HOME}/.shipwright/deps"
|
|
48
30
|
TEST_CMD=""
|
|
@@ -174,7 +156,7 @@ cmd_classify() {
|
|
|
174
156
|
info "Classifying PR #${pr_num}..."
|
|
175
157
|
|
|
176
158
|
local pr_data
|
|
177
|
-
pr_data=$(gh pr view "$pr_num" --json number,title,author,changedFiles,isDraft --template '{{json .}}' 2>/dev/null)
|
|
159
|
+
pr_data=$(gh pr view "$pr_num" --json number,title,author,changedFiles,isDraft --template '{{json .}}' 2>/dev/null || echo "")
|
|
178
160
|
|
|
179
161
|
if [[ -z "$pr_data" ]]; then
|
|
180
162
|
error "PR #${pr_num} not found"
|
|
@@ -415,7 +397,7 @@ cmd_batch() {
|
|
|
415
397
|
local approved=0
|
|
416
398
|
local flagged=0
|
|
417
399
|
|
|
418
|
-
|
|
400
|
+
while read -r pr_num; do
|
|
419
401
|
info "Processing PR #${pr_num}..."
|
|
420
402
|
|
|
421
403
|
local classify_json
|
|
@@ -438,7 +420,7 @@ cmd_batch() {
|
|
|
438
420
|
;;
|
|
439
421
|
esac
|
|
440
422
|
processed=$((processed + 1))
|
|
441
|
-
done
|
|
423
|
+
done < <(echo "$prs" | jq -r '.[] | .number')
|
|
442
424
|
|
|
443
425
|
echo ""
|
|
444
426
|
echo -e "${CYAN}${BOLD}═══ Batch Summary ═══${RESET}"
|
|
@@ -470,7 +452,7 @@ cmd_report() {
|
|
|
470
452
|
if [[ -n "$prs" && "$prs" != "[]" ]]; then
|
|
471
453
|
total=$(echo "$prs" | jq 'length')
|
|
472
454
|
|
|
473
|
-
|
|
455
|
+
while read -r title; do
|
|
474
456
|
if [[ "$title" =~ from\ ([^ ]+)\ to\ ([^ ]+) ]]; then
|
|
475
457
|
local from_ver="${BASH_REMATCH[1]}"
|
|
476
458
|
local to_ver="${BASH_REMATCH[2]}"
|
|
@@ -482,7 +464,7 @@ cmd_report() {
|
|
|
482
464
|
major) major_count=$((major_count + 1)) ;;
|
|
483
465
|
esac
|
|
484
466
|
fi
|
|
485
|
-
done
|
|
467
|
+
done < <(echo "$prs" | jq -r '.[] | .title')
|
|
486
468
|
fi
|
|
487
469
|
|
|
488
470
|
# Find oldest PR
|
|
@@ -491,7 +473,15 @@ cmd_report() {
|
|
|
491
473
|
local oldest_date
|
|
492
474
|
oldest_date=$(echo "$prs" | jq -r '.[0].createdAt')
|
|
493
475
|
if [[ -n "$oldest_date" && "$oldest_date" != "null" ]]; then
|
|
494
|
-
|
|
476
|
+
local oldest_epoch
|
|
477
|
+
oldest_epoch=$(date -d "$oldest_date" '+%s' 2>/dev/null || date -jf "%Y-%m-%dT%H:%M:%SZ" "$oldest_date" '+%s' 2>/dev/null || echo "")
|
|
478
|
+
if [[ -n "$oldest_epoch" ]]; then
|
|
479
|
+
local now_e; now_e=$(date +%s)
|
|
480
|
+
local age_days=$(( (now_e - oldest_epoch) / 86400 ))
|
|
481
|
+
oldest_age="${age_days} days ago"
|
|
482
|
+
else
|
|
483
|
+
oldest_age="?"
|
|
484
|
+
fi
|
|
495
485
|
fi
|
|
496
486
|
fi
|
|
497
487
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
set -euo pipefail
|
|
7
7
|
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
8
8
|
|
|
9
|
-
VERSION="
|
|
9
|
+
VERSION="3.1.0"
|
|
10
10
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
11
|
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
12
12
|
|
|
@@ -34,16 +34,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
|
|
|
34
34
|
echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
|
|
35
35
|
}
|
|
36
36
|
fi
|
|
37
|
-
CYAN="${CYAN:-\033[38;2;0;212;255m}"
|
|
38
|
-
PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
|
|
39
|
-
BLUE="${BLUE:-\033[38;2;0;102;255m}"
|
|
40
|
-
GREEN="${GREEN:-\033[38;2;74;222;128m}"
|
|
41
|
-
YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
|
|
42
|
-
RED="${RED:-\033[38;2;248;113;113m}"
|
|
43
|
-
DIM="${DIM:-\033[2m}"
|
|
44
|
-
BOLD="${BOLD:-\033[1m}"
|
|
45
|
-
RESET="${RESET:-\033[0m}"
|
|
46
|
-
|
|
47
37
|
# ─── Source Intelligence Core ─────────────────────────────────────────────
|
|
48
38
|
if [[ -f "$SCRIPT_DIR/sw-intelligence.sh" ]]; then
|
|
49
39
|
source "$SCRIPT_DIR/sw-intelligence.sh"
|
package/scripts/sw-discovery.sh
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
set -euo pipefail
|
|
8
8
|
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
9
9
|
|
|
10
|
-
VERSION="
|
|
10
|
+
VERSION="3.1.0"
|
|
11
11
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
12
12
|
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
13
13
|
|
|
14
14
|
# ─── Dependency check ─────────────────────────────────────────────────────────
|
|
15
|
-
if ! command -v jq
|
|
15
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
16
16
|
echo "ERROR: sw-discovery.sh requires 'jq'. Install with: brew install jq (macOS) or apt install jq (Linux)" >&2
|
|
17
17
|
exit 1
|
|
18
18
|
fi
|
|
@@ -41,21 +41,18 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
|
|
|
41
41
|
echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
|
|
42
42
|
}
|
|
43
43
|
fi
|
|
44
|
-
CYAN="${CYAN:-\033[38;2;0;212;255m}"
|
|
45
|
-
PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
|
|
46
|
-
BLUE="${BLUE:-\033[38;2;0;102;255m}"
|
|
47
|
-
GREEN="${GREEN:-\033[38;2;74;222;128m}"
|
|
48
|
-
YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
|
|
49
|
-
RED="${RED:-\033[38;2;248;113;113m}"
|
|
50
|
-
DIM="${DIM:-\033[2m}"
|
|
51
|
-
BOLD="${BOLD:-\033[1m}"
|
|
52
|
-
RESET="${RESET:-\033[0m}"
|
|
53
|
-
|
|
54
44
|
# ─── Discovery Storage ──────────────────────────────────────────────────────
|
|
55
45
|
DISCOVERIES_FILE="${HOME}/.shipwright/discoveries.jsonl"
|
|
56
46
|
DISCOVERIES_DIR="${HOME}/.shipwright/discoveries"
|
|
57
47
|
DISCOVERY_TTL_SECS=$((24 * 60 * 60)) # 24 hours default
|
|
58
48
|
|
|
49
|
+
# ─── Remote Discovery Server (optional) ─────────────────────────────────────
|
|
50
|
+
# Set via env var or daemon-config.json: "discovery_server_url"
|
|
51
|
+
DISCOVERY_SERVER_URL="${DISCOVERY_SERVER_URL:-}"
|
|
52
|
+
if [[ -z "$DISCOVERY_SERVER_URL" && -f ".claude/daemon-config.json" ]]; then
|
|
53
|
+
DISCOVERY_SERVER_URL=$(jq -r '.discovery_server_url // ""' ".claude/daemon-config.json" 2>/dev/null || true)
|
|
54
|
+
fi
|
|
55
|
+
|
|
59
56
|
ensure_discoveries_dir() {
|
|
60
57
|
mkdir -p "$DISCOVERIES_DIR"
|
|
61
58
|
}
|
|
@@ -91,17 +88,37 @@ broadcast_discovery() {
|
|
|
91
88
|
'{ts: $ts, ts_epoch: $ts_epoch, pipeline_id: $pipeline_id, category: $category, file_patterns: $file_patterns, discovery: $discovery, resolution: $resolution}')
|
|
92
89
|
|
|
93
90
|
echo "$entry" >> "$DISCOVERIES_FILE"
|
|
94
|
-
type rotate_jsonl
|
|
91
|
+
type rotate_jsonl >/dev/null 2>&1 && rotate_jsonl "$DISCOVERIES_FILE" 5000
|
|
92
|
+
|
|
93
|
+
# Fire-and-forget POST to remote discovery server if configured
|
|
94
|
+
if [[ -n "${DISCOVERY_SERVER_URL:-}" ]]; then
|
|
95
|
+
curl -sS -X POST "${DISCOVERY_SERVER_URL}/api/discoveries" \
|
|
96
|
+
-H "Content-Type: application/json" \
|
|
97
|
+
-d "$entry" \
|
|
98
|
+
--max-time 5 >/dev/null 2>&1 &
|
|
99
|
+
fi
|
|
100
|
+
|
|
95
101
|
success "Broadcast discovery: ${category} (${file_patterns})"
|
|
96
102
|
}
|
|
97
103
|
|
|
98
104
|
# query: find relevant discoveries for given file patterns
|
|
105
|
+
# Uses path overlap + semantic similarity (Jaccard on keywords, domain expansion)
|
|
99
106
|
query_discoveries() {
|
|
100
107
|
local file_patterns="$1"
|
|
101
108
|
local limit="${2:-10}"
|
|
102
109
|
|
|
103
110
|
ensure_discoveries_dir
|
|
104
111
|
|
|
112
|
+
# Merge remote discoveries if server configured (best-effort)
|
|
113
|
+
if [[ -n "${DISCOVERY_SERVER_URL:-}" ]]; then
|
|
114
|
+
local remote_results
|
|
115
|
+
remote_results=$(curl -sS "${DISCOVERY_SERVER_URL}/api/discoveries?patterns=${file_patterns}" \
|
|
116
|
+
--max-time 5 2>/dev/null || true)
|
|
117
|
+
if [[ -n "$remote_results" ]] && echo "$remote_results" | jq -e '.' >/dev/null 2>&1; then
|
|
118
|
+
echo "$remote_results" | jq -cr '.[]' >> "$DISCOVERIES_FILE" 2>/dev/null || true
|
|
119
|
+
fi
|
|
120
|
+
fi
|
|
121
|
+
|
|
105
122
|
[[ ! -f "$DISCOVERIES_FILE" ]] && {
|
|
106
123
|
info "No discoveries yet"
|
|
107
124
|
return 0
|
|
@@ -109,30 +126,60 @@ query_discoveries() {
|
|
|
109
126
|
|
|
110
127
|
local count=0
|
|
111
128
|
local found=false
|
|
129
|
+
local query_context
|
|
130
|
+
query_context=$(_expand_domain_keywords "$file_patterns")
|
|
112
131
|
|
|
132
|
+
# Collect candidates (path or semantic match)
|
|
133
|
+
local candidates
|
|
134
|
+
candidates=()
|
|
113
135
|
while IFS= read -r line; do
|
|
114
136
|
[[ -z "$line" ]] && continue
|
|
115
137
|
|
|
116
|
-
local disc_patterns
|
|
138
|
+
local disc_patterns discovery_desc
|
|
117
139
|
disc_patterns=$(echo "$line" | jq -r '.file_patterns // ""' 2>/dev/null || echo "")
|
|
140
|
+
discovery_desc=$(echo "$line" | jq -r '.discovery // ""' 2>/dev/null || echo "")
|
|
118
141
|
|
|
119
|
-
|
|
142
|
+
local matched=false
|
|
120
143
|
if patterns_overlap "$file_patterns" "$disc_patterns"; then
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
144
|
+
matched=true
|
|
145
|
+
else
|
|
146
|
+
# Semantic match: discovery about "authentication" matches "session verification"
|
|
147
|
+
local desc_similarity
|
|
148
|
+
desc_similarity=$(_discovery_semantic_match "$query_context" "$discovery_desc")
|
|
149
|
+
if [[ "${desc_similarity:-0}" -gt 30 ]]; then
|
|
150
|
+
matched=true
|
|
124
151
|
fi
|
|
152
|
+
fi
|
|
125
153
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
154
|
+
if [[ "$matched" == "true" ]]; then
|
|
155
|
+
candidates+=("$line")
|
|
156
|
+
fi
|
|
157
|
+
done < "$DISCOVERIES_FILE"
|
|
129
158
|
|
|
130
|
-
|
|
159
|
+
# Optionally use Claude to rank when many candidates
|
|
160
|
+
if [[ "${INTELLIGENCE_ENABLED:-auto}" != "false" ]] && command -v claude &>/dev/null 2>&1 && [[ ${#candidates[@]} -gt 5 ]]; then
|
|
161
|
+
# TODO: batch Claude call to rank by relevance (future enhancement)
|
|
162
|
+
:
|
|
163
|
+
fi
|
|
131
164
|
|
|
132
|
-
|
|
133
|
-
|
|
165
|
+
# Output up to limit
|
|
166
|
+
local line
|
|
167
|
+
for line in "${candidates[@]+"${candidates[@]}"}"; do
|
|
168
|
+
if [[ "$found" == "false" ]]; then
|
|
169
|
+
success "Found relevant discoveries:"
|
|
170
|
+
found=true
|
|
134
171
|
fi
|
|
135
|
-
|
|
172
|
+
|
|
173
|
+
local category discovery disc_patterns
|
|
174
|
+
category=$(echo "$line" | jq -r '.category' 2>/dev/null || echo "?")
|
|
175
|
+
discovery=$(echo "$line" | jq -r '.discovery' 2>/dev/null || echo "?")
|
|
176
|
+
disc_patterns=$(echo "$line" | jq -r '.file_patterns // ""' 2>/dev/null || echo "")
|
|
177
|
+
|
|
178
|
+
echo -e " ${DIM}→${RESET} [${category}] ${discovery} [${disc_patterns}]"
|
|
179
|
+
|
|
180
|
+
count=$((count + 1))
|
|
181
|
+
[[ "$count" -ge "$limit" ]] && break
|
|
182
|
+
done
|
|
136
183
|
|
|
137
184
|
if [[ "$found" == "false" ]]; then
|
|
138
185
|
info "No relevant discoveries found for patterns: ${file_patterns}"
|
|
@@ -153,8 +200,10 @@ inject_discoveries() {
|
|
|
153
200
|
|
|
154
201
|
local seen_file
|
|
155
202
|
seen_file=$(get_seen_file "$pipeline_id")
|
|
203
|
+
local query_context
|
|
204
|
+
query_context=$(_expand_domain_keywords "$file_patterns")
|
|
156
205
|
|
|
157
|
-
# Find relevant discoveries not yet seen
|
|
206
|
+
# Find relevant discoveries not yet seen (path or semantic match)
|
|
158
207
|
local new_count=0
|
|
159
208
|
local injected_entries=()
|
|
160
209
|
|
|
@@ -171,13 +220,25 @@ inject_discoveries() {
|
|
|
171
220
|
fi
|
|
172
221
|
fi
|
|
173
222
|
|
|
174
|
-
# Check if relevant
|
|
175
|
-
local disc_patterns
|
|
223
|
+
# Check if relevant: path overlap OR semantic similarity > 30
|
|
224
|
+
local disc_patterns discovery_desc
|
|
176
225
|
disc_patterns=$(echo "$line" | jq -r '.file_patterns // ""' 2>/dev/null || echo "")
|
|
226
|
+
discovery_desc=$(echo "$line" | jq -r '.discovery // ""' 2>/dev/null || echo "")
|
|
177
227
|
|
|
228
|
+
local matched=false
|
|
178
229
|
if [[ -n "$disc_patterns" ]] && patterns_overlap "$file_patterns" "$disc_patterns"; then
|
|
230
|
+
matched=true
|
|
231
|
+
else
|
|
232
|
+
local desc_similarity
|
|
233
|
+
desc_similarity=$(_discovery_semantic_match "$query_context" "$discovery_desc")
|
|
234
|
+
if [[ "${desc_similarity:-0}" -gt 30 ]]; then
|
|
235
|
+
matched=true
|
|
236
|
+
fi
|
|
237
|
+
fi
|
|
238
|
+
|
|
239
|
+
if [[ "$matched" == "true" ]]; then
|
|
179
240
|
injected_entries+=("$line")
|
|
180
|
-
((new_count
|
|
241
|
+
new_count=$((new_count + 1))
|
|
181
242
|
fi
|
|
182
243
|
done < "$DISCOVERIES_FILE"
|
|
183
244
|
|
|
@@ -227,6 +288,53 @@ inject_discoveries() {
|
|
|
227
288
|
done
|
|
228
289
|
}
|
|
229
290
|
|
|
291
|
+
# ─── Semantic matching helpers ───────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
# Domain keyword expansion for related concepts
|
|
294
|
+
_expand_domain_keywords() {
|
|
295
|
+
local text="$1"
|
|
296
|
+
local expanded="$text"
|
|
297
|
+
|
|
298
|
+
# Domain synonym groups (iterate over fixed keys to avoid set -u issues)
|
|
299
|
+
local dom
|
|
300
|
+
for dom in auth api db ui test deploy error perf; do
|
|
301
|
+
case "$dom" in
|
|
302
|
+
auth) [[ "$text" =~ [aA]uth ]] && expanded="$expanded authentication authorization login session token credential permission access" ;;
|
|
303
|
+
api) [[ "$text" =~ [aA]pi ]] && expanded="$expanded endpoint route handler request response rest graphql" ;;
|
|
304
|
+
db) [[ "$text" =~ [dD]b ]] && expanded="$expanded database query migration schema model table sql" ;;
|
|
305
|
+
ui) [[ "$text" =~ [uU]i ]] && expanded="$expanded component view render template layout style css frontend" ;;
|
|
306
|
+
test) [[ "$text" =~ [tT]est ]] && expanded="$expanded testing assertion coverage mock stub fixture spec" ;;
|
|
307
|
+
deploy) [[ "$text" =~ [dD]eploy ]] && expanded="$expanded deployment release publish ship ci cd pipeline" ;;
|
|
308
|
+
error) [[ "$text" =~ [eE]rror ]] && expanded="$expanded exception failure crash bug issue defect" ;;
|
|
309
|
+
perf) [[ "$text" =~ [pP]erf ]] && expanded="$expanded performance optimization speed latency throughput cache" ;;
|
|
310
|
+
esac
|
|
311
|
+
done
|
|
312
|
+
|
|
313
|
+
echo "$expanded"
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
# Semantic similarity between discovery descriptions (Jaccard on keywords)
|
|
317
|
+
_discovery_semantic_match() {
|
|
318
|
+
local query_desc="$1"
|
|
319
|
+
local discovery_desc="$2"
|
|
320
|
+
|
|
321
|
+
# Extract keywords from both descriptions
|
|
322
|
+
local query_words discovery_words
|
|
323
|
+
query_words=$(echo "$query_desc" | tr '[:upper:]' '[:lower:]' | tr -cs '[:alnum:]' '\n' | sort -u | grep -vE '^(the|a|an|is|are|was|were|in|on|at|to|for|of|and|or|but|not|with|this|that|from|by)$')
|
|
324
|
+
discovery_words=$(echo "$discovery_desc" | tr '[:upper:]' '[:lower:]' | tr -cs '[:alnum:]' '\n' | sort -u | grep -vE '^(the|a|an|is|are|was|were|in|on|at|to|for|of|and|or|but|not|with|this|that|from|by)$')
|
|
325
|
+
|
|
326
|
+
# Compute Jaccard similarity
|
|
327
|
+
local intersection union
|
|
328
|
+
intersection=$(comm -12 <(echo "$query_words") <(echo "$discovery_words") 2>/dev/null | wc -l | tr -d ' ')
|
|
329
|
+
union=$(sort -u <(echo "$query_words") <(echo "$discovery_words") 2>/dev/null | wc -l | tr -d ' ')
|
|
330
|
+
|
|
331
|
+
if [[ "$union" -gt 0 ]]; then
|
|
332
|
+
echo "$((intersection * 100 / union))"
|
|
333
|
+
else
|
|
334
|
+
echo "0"
|
|
335
|
+
fi
|
|
336
|
+
}
|
|
337
|
+
|
|
230
338
|
# patterns_overlap: check if two comma-separated patterns overlap
|
|
231
339
|
patterns_overlap() {
|
|
232
340
|
local patterns1="$1"
|
|
@@ -292,7 +400,7 @@ clean_discoveries() {
|
|
|
292
400
|
if [[ "$ts_epoch" -ge "$cutoff" ]]; then
|
|
293
401
|
echo "$line" >> "$tmp_file"
|
|
294
402
|
else
|
|
295
|
-
((removed_count
|
|
403
|
+
removed_count=$((removed_count + 1))
|
|
296
404
|
fi
|
|
297
405
|
done < "$DISCOVERIES_FILE"
|
|
298
406
|
|
package/scripts/sw-doc-fleet.sh
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
set -euo pipefail
|
|
7
7
|
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
8
8
|
|
|
9
|
-
VERSION="
|
|
9
|
+
VERSION="3.1.0"
|
|
10
10
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
11
|
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
12
12
|
|
|
@@ -32,16 +32,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
|
|
|
32
32
|
echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
|
|
33
33
|
}
|
|
34
34
|
fi
|
|
35
|
-
CYAN="${CYAN:-\033[38;2;0;212;255m}"
|
|
36
|
-
PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
|
|
37
|
-
BLUE="${BLUE:-\033[38;2;0;102;255m}"
|
|
38
|
-
GREEN="${GREEN:-\033[38;2;74;222;128m}"
|
|
39
|
-
YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
|
|
40
|
-
RED="${RED:-\033[38;2;248;113;113m}"
|
|
41
|
-
DIM="${DIM:-\033[2m}"
|
|
42
|
-
BOLD="${BOLD:-\033[1m}"
|
|
43
|
-
RESET="${RESET:-\033[0m}"
|
|
44
|
-
|
|
45
35
|
# ─── Constants ──────────────────────────────────────────────────────────────
|
|
46
36
|
FLEET_HOME="${HOME}/.shipwright/doc-fleet"
|
|
47
37
|
FLEET_STATE="${FLEET_HOME}/state.json"
|
|
@@ -115,9 +105,9 @@ cmd_audit() {
|
|
|
115
105
|
total_checks=$((total_checks + 1))
|
|
116
106
|
if [[ -f "${REPO_DIR}/.claude/CLAUDE.md" ]]; then
|
|
117
107
|
local claude_age_days=0
|
|
118
|
-
if command -v stat
|
|
108
|
+
if command -v stat >/dev/null 2>&1; then
|
|
119
109
|
local claude_mtime
|
|
120
|
-
claude_mtime=$(
|
|
110
|
+
claude_mtime=$(file_mtime "${REPO_DIR}/.claude/CLAUDE.md")
|
|
121
111
|
local now_epoch_val
|
|
122
112
|
now_epoch_val=$(date +%s)
|
|
123
113
|
claude_age_days=$(( (now_epoch_val - claude_mtime) / 86400 ))
|
|
@@ -376,7 +366,7 @@ cmd_launch() {
|
|
|
376
366
|
fi
|
|
377
367
|
|
|
378
368
|
# Spawn via tmux if available
|
|
379
|
-
if command -v tmux
|
|
369
|
+
if command -v tmux >/dev/null 2>&1; then
|
|
380
370
|
local session_name="docfleet-${role}"
|
|
381
371
|
|
|
382
372
|
# Kill existing session for this role if present
|
|
@@ -482,7 +472,7 @@ cmd_status() {
|
|
|
482
472
|
local active=0
|
|
483
473
|
for role in $FLEET_ROLES; do
|
|
484
474
|
local session_name="docfleet-${role}"
|
|
485
|
-
if command -v tmux
|
|
475
|
+
if command -v tmux >/dev/null 2>&1 && tmux has-session -t "$session_name" 2>/dev/null; then
|
|
486
476
|
echo -e " ${GREEN}●${RESET} ${CYAN}${role}${RESET} → tmux session: ${DIM}${session_name}${RESET}"
|
|
487
477
|
active=$((active + 1))
|
|
488
478
|
else
|
|
@@ -529,7 +519,7 @@ cmd_retire() {
|
|
|
529
519
|
local retired=0
|
|
530
520
|
for role in $roles_to_retire; do
|
|
531
521
|
local session_name="docfleet-${role}"
|
|
532
|
-
if command -v tmux
|
|
522
|
+
if command -v tmux >/dev/null 2>&1 && tmux has-session -t "$session_name" 2>/dev/null; then
|
|
533
523
|
tmux kill-session -t "$session_name" 2>/dev/null && \
|
|
534
524
|
success "Retired: ${CYAN}${role}${RESET}" || \
|
|
535
525
|
warn "Failed to retire: ${role}"
|
|
@@ -566,7 +556,7 @@ cmd_manifest() {
|
|
|
566
556
|
# Extract first heading
|
|
567
557
|
title=$(grep -m1 '^#' "$md_file" 2>/dev/null | sed 's/^#* //' || echo "$rel_path")
|
|
568
558
|
local mtime
|
|
569
|
-
mtime=$(
|
|
559
|
+
mtime=$(file_mtime "$md_file")
|
|
570
560
|
|
|
571
561
|
# Determine audience
|
|
572
562
|
local audience="contributor"
|
package/scripts/sw-docs-agent.sh
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
set -euo pipefail
|
|
7
7
|
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
8
8
|
|
|
9
|
-
VERSION="
|
|
9
|
+
VERSION="3.1.0"
|
|
10
10
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
11
|
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
12
12
|
|
|
@@ -34,16 +34,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
|
|
|
34
34
|
echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
|
|
35
35
|
}
|
|
36
36
|
fi
|
|
37
|
-
CYAN="${CYAN:-\033[38;2;0;212;255m}"
|
|
38
|
-
PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
|
|
39
|
-
BLUE="${BLUE:-\033[38;2;0;102;255m}"
|
|
40
|
-
GREEN="${GREEN:-\033[38;2;74;222;128m}"
|
|
41
|
-
YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
|
|
42
|
-
RED="${RED:-\033[38;2;248;113;113m}"
|
|
43
|
-
DIM="${DIM:-\033[2m}"
|
|
44
|
-
BOLD="${BOLD:-\033[1m}"
|
|
45
|
-
RESET="${RESET:-\033[0m}"
|
|
46
|
-
|
|
47
37
|
# ─── Documentation Agent State ────────────────────────────────────────────
|
|
48
38
|
AGENT_HOME="${HOME}/.shipwright/docs-agent"
|
|
49
39
|
FRESHNESS_DB="${AGENT_HOME}/freshness.json"
|
|
@@ -321,7 +311,7 @@ show_coverage() {
|
|
|
321
311
|
script_name=$(basename "$script" .sh | sed 's/^sw-//')
|
|
322
312
|
|
|
323
313
|
if grep -q "$script_name" "$REPO_DIR/.claude/CLAUDE.md" 2>/dev/null; then
|
|
324
|
-
((documented_count
|
|
314
|
+
documented_count=$((documented_count + 1))
|
|
325
315
|
else
|
|
326
316
|
undocumented_scripts="${undocumented_scripts}${script_name}\\n"
|
|
327
317
|
fi
|
|
@@ -371,7 +361,7 @@ scan_gaps() {
|
|
|
371
361
|
|
|
372
362
|
if [[ "$freshness" -lt 70 ]]; then
|
|
373
363
|
warn "Stale section in README: $section (freshness: ${freshness}%)"
|
|
374
|
-
((gaps_found
|
|
364
|
+
gaps_found=$((gaps_found + 1))
|
|
375
365
|
fi
|
|
376
366
|
done
|
|
377
367
|
fi
|
|
@@ -387,7 +377,7 @@ scan_gaps() {
|
|
|
387
377
|
|
|
388
378
|
if [[ "$freshness" -lt 70 ]]; then
|
|
389
379
|
warn "Stale section in CLAUDE.md: $section (freshness: ${freshness}%)"
|
|
390
|
-
((gaps_found
|
|
380
|
+
gaps_found=$((gaps_found + 1))
|
|
391
381
|
fi
|
|
392
382
|
done
|
|
393
383
|
fi
|
|
@@ -412,11 +402,11 @@ sync_docs() {
|
|
|
412
402
|
|
|
413
403
|
# Regenerate API reference
|
|
414
404
|
generate_api_reference
|
|
415
|
-
((synced_count
|
|
405
|
+
synced_count=$((synced_count + 1))
|
|
416
406
|
|
|
417
407
|
# Regenerate wiki
|
|
418
408
|
generate_wiki_pages
|
|
419
|
-
((synced_count
|
|
409
|
+
synced_count=$((synced_count + 1))
|
|
420
410
|
|
|
421
411
|
success "Documentation sync complete ($synced_count updates)"
|
|
422
412
|
emit_event "docs_sync_complete" "updates=$synced_count"
|
package/scripts/sw-docs.sh
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
set -euo pipefail
|
|
7
7
|
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
8
8
|
|
|
9
|
-
VERSION="
|
|
9
|
+
VERSION="3.1.0"
|
|
10
10
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
11
|
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
12
12
|
|
|
@@ -19,6 +19,8 @@ REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
19
19
|
[[ -f "$SCRIPT_DIR/lib/helpers.sh" ]] && source "$SCRIPT_DIR/lib/helpers.sh"
|
|
20
20
|
# Fallbacks when helpers not loaded (e.g. test env with overridden SCRIPT_DIR)
|
|
21
21
|
[[ "$(type -t info 2>/dev/null)" == "function" ]] || info() { echo -e "\033[38;2;0;212;255m\033[1m▸\033[0m $*"; }
|
|
22
|
+
# Color fallbacks when helpers not loaded
|
|
23
|
+
: "${CYAN:=}" "${BOLD:=}" "${RESET:=}" "${DIM:=}" "${GREEN:=}" "${RED:=}" "${YELLOW:=}" "${PURPLE:=}" "${WHITE:=}" "${BLUE:=}"
|
|
22
24
|
[[ "$(type -t success 2>/dev/null)" == "function" ]] || success() { echo -e "\033[38;2;74;222;128m\033[1m✓\033[0m $*"; }
|
|
23
25
|
[[ "$(type -t warn 2>/dev/null)" == "function" ]] || warn() { echo -e "\033[38;2;250;204;21m\033[1m⚠\033[0m $*"; }
|
|
24
26
|
[[ "$(type -t error 2>/dev/null)" == "function" ]] || error() { echo -e "\033[38;2;248;113;113m\033[1m✗\033[0m $*" >&2; }
|
|
@@ -34,16 +36,6 @@ if [[ "$(type -t emit_event 2>/dev/null)" != "function" ]]; then
|
|
|
34
36
|
echo "${payload}}" >> "${HOME}/.shipwright/events.jsonl"
|
|
35
37
|
}
|
|
36
38
|
fi
|
|
37
|
-
CYAN="${CYAN:-\033[38;2;0;212;255m}"
|
|
38
|
-
PURPLE="${PURPLE:-\033[38;2;124;58;237m}"
|
|
39
|
-
BLUE="${BLUE:-\033[38;2;0;102;255m}"
|
|
40
|
-
GREEN="${GREEN:-\033[38;2;74;222;128m}"
|
|
41
|
-
YELLOW="${YELLOW:-\033[38;2;250;204;21m}"
|
|
42
|
-
RED="${RED:-\033[38;2;248;113;113m}"
|
|
43
|
-
DIM="${DIM:-\033[2m}"
|
|
44
|
-
BOLD="${BOLD:-\033[1m}"
|
|
45
|
-
RESET="${RESET:-\033[0m}"
|
|
46
|
-
|
|
47
39
|
# ─── AUTO Section Processing ────────────────────────────────────────────────
|
|
48
40
|
|
|
49
41
|
# Find all files with AUTO markers
|
|
@@ -501,7 +493,7 @@ docs_wiki() {
|
|
|
501
493
|
fi
|
|
502
494
|
|
|
503
495
|
# Push to GitHub wiki
|
|
504
|
-
if [[ "${NO_GITHUB:-}" == "true" ]] || ! command -v gh
|
|
496
|
+
if [[ "${NO_GITHUB:-}" == "true" ]] || ! command -v gh >/dev/null 2>&1; then
|
|
505
497
|
warn "GitHub not available — wiki pages saved to: $wiki_dir"
|
|
506
498
|
return 0
|
|
507
499
|
fi
|