shipwright-cli 1.7.1 → 1.9.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/.claude/agents/code-reviewer.md +90 -0
- package/.claude/agents/devops-engineer.md +142 -0
- package/.claude/agents/pipeline-agent.md +80 -0
- package/.claude/agents/shell-script-specialist.md +150 -0
- package/.claude/agents/test-specialist.md +196 -0
- package/.claude/hooks/post-tool-use.sh +38 -0
- package/.claude/hooks/pre-tool-use.sh +25 -0
- package/.claude/hooks/session-started.sh +37 -0
- package/README.md +212 -814
- package/claude-code/CLAUDE.md.shipwright +54 -0
- package/claude-code/hooks/notify-idle.sh +2 -2
- package/claude-code/hooks/session-start.sh +24 -0
- package/claude-code/hooks/task-completed.sh +6 -2
- package/claude-code/settings.json.template +12 -0
- package/dashboard/public/app.js +4422 -0
- package/dashboard/public/index.html +816 -0
- package/dashboard/public/styles.css +4755 -0
- package/dashboard/server.ts +4315 -0
- package/docs/KNOWN-ISSUES.md +18 -10
- package/docs/TIPS.md +38 -26
- package/docs/patterns/README.md +33 -23
- package/package.json +9 -5
- package/scripts/adapters/iterm2-adapter.sh +1 -1
- package/scripts/adapters/tmux-adapter.sh +52 -23
- package/scripts/adapters/wezterm-adapter.sh +26 -14
- package/scripts/lib/compat.sh +200 -0
- package/scripts/lib/helpers.sh +72 -0
- package/scripts/postinstall.mjs +72 -13
- package/scripts/{cct → sw} +109 -21
- package/scripts/sw-adversarial.sh +274 -0
- package/scripts/sw-architecture-enforcer.sh +330 -0
- package/scripts/sw-checkpoint.sh +390 -0
- package/scripts/{cct-cleanup.sh → sw-cleanup.sh} +3 -1
- package/scripts/sw-connect.sh +619 -0
- package/scripts/{cct-cost.sh → sw-cost.sh} +368 -34
- package/scripts/{cct-daemon.sh → sw-daemon.sh} +2217 -204
- package/scripts/sw-dashboard.sh +477 -0
- package/scripts/sw-developer-simulation.sh +252 -0
- package/scripts/sw-docs.sh +635 -0
- package/scripts/sw-doctor.sh +907 -0
- package/scripts/{cct-fix.sh → sw-fix.sh} +10 -6
- package/scripts/{cct-fleet.sh → sw-fleet.sh} +498 -22
- package/scripts/sw-github-checks.sh +521 -0
- package/scripts/sw-github-deploy.sh +533 -0
- package/scripts/sw-github-graphql.sh +972 -0
- package/scripts/sw-heartbeat.sh +293 -0
- package/scripts/{cct-init.sh → sw-init.sh} +144 -11
- package/scripts/sw-intelligence.sh +1196 -0
- package/scripts/sw-jira.sh +643 -0
- package/scripts/sw-launchd.sh +364 -0
- package/scripts/sw-linear.sh +648 -0
- package/scripts/{cct-logs.sh → sw-logs.sh} +72 -2
- package/scripts/{cct-loop.sh → sw-loop.sh} +534 -44
- package/scripts/{cct-memory.sh → sw-memory.sh} +321 -38
- package/scripts/sw-patrol-meta.sh +417 -0
- package/scripts/sw-pipeline-composer.sh +455 -0
- package/scripts/{cct-pipeline.sh → sw-pipeline.sh} +2319 -178
- package/scripts/sw-predictive.sh +820 -0
- package/scripts/{cct-prep.sh → sw-prep.sh} +339 -49
- package/scripts/{cct-ps.sh → sw-ps.sh} +6 -4
- package/scripts/{cct-reaper.sh → sw-reaper.sh} +6 -4
- package/scripts/sw-remote.sh +687 -0
- package/scripts/sw-self-optimize.sh +947 -0
- package/scripts/sw-session.sh +519 -0
- package/scripts/sw-setup.sh +234 -0
- package/scripts/sw-status.sh +605 -0
- package/scripts/{cct-templates.sh → sw-templates.sh} +9 -4
- package/scripts/sw-tmux.sh +591 -0
- package/scripts/sw-tracker-jira.sh +277 -0
- package/scripts/sw-tracker-linear.sh +292 -0
- package/scripts/sw-tracker.sh +409 -0
- package/scripts/{cct-upgrade.sh → sw-upgrade.sh} +103 -46
- package/scripts/{cct-worktree.sh → sw-worktree.sh} +3 -0
- package/templates/pipelines/autonomous.json +27 -5
- package/templates/pipelines/full.json +12 -0
- package/templates/pipelines/standard.json +12 -0
- package/tmux/{claude-teams-overlay.conf → shipwright-overlay.conf} +27 -9
- package/tmux/templates/accessibility.json +34 -0
- package/tmux/templates/api-design.json +35 -0
- package/tmux/templates/architecture.json +1 -0
- package/tmux/templates/bug-fix.json +9 -0
- package/tmux/templates/code-review.json +1 -0
- package/tmux/templates/compliance.json +36 -0
- package/tmux/templates/data-pipeline.json +36 -0
- package/tmux/templates/debt-paydown.json +34 -0
- package/tmux/templates/devops.json +1 -0
- package/tmux/templates/documentation.json +1 -0
- package/tmux/templates/exploration.json +1 -0
- package/tmux/templates/feature-dev.json +1 -0
- package/tmux/templates/full-stack.json +8 -0
- package/tmux/templates/i18n.json +34 -0
- package/tmux/templates/incident-response.json +36 -0
- package/tmux/templates/migration.json +1 -0
- package/tmux/templates/observability.json +35 -0
- package/tmux/templates/onboarding.json +33 -0
- package/tmux/templates/performance.json +35 -0
- package/tmux/templates/refactor.json +1 -0
- package/tmux/templates/release.json +35 -0
- package/tmux/templates/security-audit.json +8 -0
- package/tmux/templates/spike.json +34 -0
- package/tmux/templates/testing.json +1 -0
- package/tmux/tmux.conf +98 -9
- package/scripts/cct-doctor.sh +0 -414
- package/scripts/cct-session.sh +0 -284
- package/scripts/cct-status.sh +0 -169
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
# ║ shipwright linear — Linear ↔ GitHub Bidirectional Sync ║
|
|
4
|
+
# ║ Sync issues · Update statuses · Link PRs · Pipeline integration ║
|
|
5
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
8
|
+
|
|
9
|
+
VERSION="1.9.0"
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
|
+
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
12
|
+
|
|
13
|
+
# ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
|
|
14
|
+
CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
|
|
15
|
+
PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
|
|
16
|
+
BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
|
|
17
|
+
GREEN='\033[38;2;74;222;128m' # success
|
|
18
|
+
YELLOW='\033[38;2;250;204;21m' # warning
|
|
19
|
+
RED='\033[38;2;248;113;113m' # error
|
|
20
|
+
DIM='\033[2m'
|
|
21
|
+
BOLD='\033[1m'
|
|
22
|
+
RESET='\033[0m'
|
|
23
|
+
|
|
24
|
+
# ─── Cross-platform compatibility ──────────────────────────────────────────
|
|
25
|
+
# shellcheck source=lib/compat.sh
|
|
26
|
+
[[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
|
|
27
|
+
# ─── Output Helpers ─────────────────────────────────────────────────────────
|
|
28
|
+
info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
|
|
29
|
+
success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
|
|
30
|
+
warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
|
|
31
|
+
error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
|
|
32
|
+
|
|
33
|
+
now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
|
|
34
|
+
now_epoch() { date +%s; }
|
|
35
|
+
|
|
36
|
+
# ─── Structured Event Log ──────────────────────────────────────────────────
|
|
37
|
+
EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
|
|
38
|
+
|
|
39
|
+
emit_event() {
|
|
40
|
+
local event_type="$1"
|
|
41
|
+
shift
|
|
42
|
+
local json_fields=""
|
|
43
|
+
for kv in "$@"; do
|
|
44
|
+
local key="${kv%%=*}"
|
|
45
|
+
local val="${kv#*=}"
|
|
46
|
+
if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
|
|
47
|
+
json_fields="${json_fields},\"${key}\":${val}"
|
|
48
|
+
else
|
|
49
|
+
val="${val//\"/\\\"}"
|
|
50
|
+
json_fields="${json_fields},\"${key}\":\"${val}\""
|
|
51
|
+
fi
|
|
52
|
+
done
|
|
53
|
+
mkdir -p "${HOME}/.shipwright"
|
|
54
|
+
echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# ─── Configuration ─────────────────────────────────────────────────────────
|
|
58
|
+
CONFIG_DIR="${HOME}/.shipwright"
|
|
59
|
+
LINEAR_CONFIG="${CONFIG_DIR}/linear-config.json"
|
|
60
|
+
|
|
61
|
+
# Linear Status IDs (Sethdford team)
|
|
62
|
+
STATUS_BACKLOG="147eb91d-0428-457b-bdcb-0875b847b061"
|
|
63
|
+
STATUS_TODO="f89d423b-9cad-4e60-aec9-422b64b78a4b"
|
|
64
|
+
STATUS_IN_PROGRESS="7ed39c42-434d-4239-86f3-ffa24dbf1275"
|
|
65
|
+
STATUS_IN_REVIEW="ea38a4f2-f0ee-4e0b-ae45-7e5aad45ef53"
|
|
66
|
+
STATUS_DONE="dc2430cf-0713-40c9-a8c6-889413b626e7"
|
|
67
|
+
|
|
68
|
+
LINEAR_API="https://api.linear.app/graphql"
|
|
69
|
+
|
|
70
|
+
load_config() {
|
|
71
|
+
if [[ -f "$LINEAR_CONFIG" ]]; then
|
|
72
|
+
LINEAR_API_KEY="${LINEAR_API_KEY:-$(jq -r '.api_key // empty' "$LINEAR_CONFIG" 2>/dev/null || true)}"
|
|
73
|
+
LINEAR_TEAM_ID="${LINEAR_TEAM_ID:-$(jq -r '.team_id // empty' "$LINEAR_CONFIG" 2>/dev/null || true)}"
|
|
74
|
+
LINEAR_PROJECT_ID="${LINEAR_PROJECT_ID:-$(jq -r '.project_id // empty' "$LINEAR_CONFIG" 2>/dev/null || true)}"
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
LINEAR_API_KEY="${LINEAR_API_KEY:-}"
|
|
78
|
+
LINEAR_TEAM_ID="${LINEAR_TEAM_ID:-83deb533-69d2-43ef-bc58-eadb6e72a8f2}"
|
|
79
|
+
LINEAR_PROJECT_ID="${LINEAR_PROJECT_ID:-b262d625-5bbe-47bd-9f89-df27c45eba8b}"
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
check_api_key() {
|
|
83
|
+
if [[ -z "$LINEAR_API_KEY" ]]; then
|
|
84
|
+
error "LINEAR_API_KEY not set"
|
|
85
|
+
echo ""
|
|
86
|
+
echo -e " Set via environment: ${DIM}export LINEAR_API_KEY=lin_api_...${RESET}"
|
|
87
|
+
echo -e " Or run: ${DIM}shipwright linear init${RESET}"
|
|
88
|
+
exit 1
|
|
89
|
+
fi
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# ─── Linear GraphQL Helper ────────────────────────────────────────────────
|
|
93
|
+
# Executes a GraphQL query/mutation against the Linear API.
|
|
94
|
+
# Uses jq --arg for safe JSON escaping (never string interpolation).
|
|
95
|
+
linear_graphql() {
|
|
96
|
+
local query="$1"
|
|
97
|
+
local variables="${2:-{}}"
|
|
98
|
+
|
|
99
|
+
local payload
|
|
100
|
+
payload=$(jq -n --arg q "$query" --argjson v "$variables" '{query: $q, variables: $v}')
|
|
101
|
+
|
|
102
|
+
local response
|
|
103
|
+
response=$(curl -sf -X POST "$LINEAR_API" \
|
|
104
|
+
-H "Authorization: $LINEAR_API_KEY" \
|
|
105
|
+
-H "Content-Type: application/json" \
|
|
106
|
+
-d "$payload" 2>&1) || {
|
|
107
|
+
error "Linear API request failed"
|
|
108
|
+
echo "$response" >&2
|
|
109
|
+
return 1
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# Check for GraphQL errors
|
|
113
|
+
local errors
|
|
114
|
+
errors=$(echo "$response" | jq -r '.errors[0].message // empty' 2>/dev/null || true)
|
|
115
|
+
if [[ -n "$errors" ]]; then
|
|
116
|
+
error "Linear API error: $errors"
|
|
117
|
+
return 1
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
echo "$response"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# ─── Sync: Linear Todo → GitHub Issues ─────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
cmd_sync() {
|
|
126
|
+
check_api_key
|
|
127
|
+
info "Syncing Linear Todo issues → GitHub..."
|
|
128
|
+
|
|
129
|
+
local dry_run=false
|
|
130
|
+
while [[ $# -gt 0 ]]; do
|
|
131
|
+
case "$1" in
|
|
132
|
+
--dry-run) dry_run=true; shift ;;
|
|
133
|
+
*) shift ;;
|
|
134
|
+
esac
|
|
135
|
+
done
|
|
136
|
+
|
|
137
|
+
# Fetch Linear issues in Todo status within the project
|
|
138
|
+
local query='query($teamId: String!, $projectId: String!, $stateId: String!) {
|
|
139
|
+
issues(filter: {
|
|
140
|
+
team: { id: { eq: $teamId } }
|
|
141
|
+
project: { id: { eq: $projectId } }
|
|
142
|
+
state: { id: { eq: $stateId } }
|
|
143
|
+
}) {
|
|
144
|
+
nodes {
|
|
145
|
+
id
|
|
146
|
+
identifier
|
|
147
|
+
title
|
|
148
|
+
description
|
|
149
|
+
url
|
|
150
|
+
priority
|
|
151
|
+
labels { nodes { name } }
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}'
|
|
155
|
+
|
|
156
|
+
local vars
|
|
157
|
+
vars=$(jq -n \
|
|
158
|
+
--arg teamId "$LINEAR_TEAM_ID" \
|
|
159
|
+
--arg projectId "$LINEAR_PROJECT_ID" \
|
|
160
|
+
--arg stateId "$STATUS_TODO" \
|
|
161
|
+
'{teamId: $teamId, projectId: $projectId, stateId: $stateId}')
|
|
162
|
+
|
|
163
|
+
local response
|
|
164
|
+
response=$(linear_graphql "$query" "$vars") || return 1
|
|
165
|
+
|
|
166
|
+
local count
|
|
167
|
+
count=$(echo "$response" | jq '.data.issues.nodes | length')
|
|
168
|
+
if [[ "$count" -eq 0 ]]; then
|
|
169
|
+
info "No Linear issues in Todo status"
|
|
170
|
+
return 0
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
info "Found ${count} Linear issue(s) in Todo"
|
|
174
|
+
|
|
175
|
+
local synced=0
|
|
176
|
+
local skipped=0
|
|
177
|
+
|
|
178
|
+
# Process each Linear issue
|
|
179
|
+
local i=0
|
|
180
|
+
while [[ $i -lt $count ]]; do
|
|
181
|
+
local issue
|
|
182
|
+
issue=$(echo "$response" | jq ".data.issues.nodes[$i]")
|
|
183
|
+
local linear_id linear_identifier title description url priority
|
|
184
|
+
linear_id=$(echo "$issue" | jq -r '.id')
|
|
185
|
+
linear_identifier=$(echo "$issue" | jq -r '.identifier')
|
|
186
|
+
title=$(echo "$issue" | jq -r '.title')
|
|
187
|
+
description=$(echo "$issue" | jq -r '.description // ""')
|
|
188
|
+
url=$(echo "$issue" | jq -r '.url')
|
|
189
|
+
priority=$(echo "$issue" | jq -r '.priority // 0')
|
|
190
|
+
|
|
191
|
+
# Check if GitHub issue already exists for this Linear issue
|
|
192
|
+
local existing_gh
|
|
193
|
+
existing_gh=$(gh issue list --label "ready-to-build" --search "Linear: ${linear_identifier}" --json number --jq '.[0].number // empty' 2>/dev/null || true)
|
|
194
|
+
|
|
195
|
+
if [[ -n "$existing_gh" ]]; then
|
|
196
|
+
echo -e " ${DIM}Skip${RESET} ${linear_identifier}: ${title} ${DIM}(GitHub #${existing_gh})${RESET}"
|
|
197
|
+
skipped=$((skipped + 1))
|
|
198
|
+
i=$((i + 1))
|
|
199
|
+
continue
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
# Map priority to label
|
|
203
|
+
local priority_label=""
|
|
204
|
+
case "$priority" in
|
|
205
|
+
1) priority_label="priority-urgent" ;;
|
|
206
|
+
2) priority_label="priority-high" ;;
|
|
207
|
+
3) priority_label="priority-medium" ;;
|
|
208
|
+
4) priority_label="priority-low" ;;
|
|
209
|
+
esac
|
|
210
|
+
|
|
211
|
+
# Build GitHub issue body with Linear back-link
|
|
212
|
+
local gh_body
|
|
213
|
+
gh_body=$(printf "## %s\n\n%s\n\n---\n**Linear:** [%s](%s)\n**Linear ID:** %s" \
|
|
214
|
+
"$title" "$description" "$linear_identifier" "$url" "$linear_id")
|
|
215
|
+
|
|
216
|
+
if [[ "$dry_run" == "true" ]]; then
|
|
217
|
+
echo -e " ${CYAN}Would create${RESET} GitHub issue: ${title} ${DIM}(${linear_identifier})${RESET}"
|
|
218
|
+
else
|
|
219
|
+
# Create GitHub issue
|
|
220
|
+
local labels="ready-to-build"
|
|
221
|
+
if [[ -n "$priority_label" ]]; then
|
|
222
|
+
labels="${labels},${priority_label}"
|
|
223
|
+
fi
|
|
224
|
+
|
|
225
|
+
local gh_num
|
|
226
|
+
gh_num=$(gh issue create --title "$title" --body "$gh_body" --label "$labels" --json number --jq '.number' 2>&1) || {
|
|
227
|
+
error "Failed to create GitHub issue for ${linear_identifier}: ${gh_num}"
|
|
228
|
+
i=$((i + 1))
|
|
229
|
+
continue
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
# Add comment on Linear issue linking back to GitHub
|
|
233
|
+
local comment_body
|
|
234
|
+
comment_body=$(printf "Synced to GitHub issue #%s\n\nThe daemon will pick this up for autonomous delivery." "$gh_num")
|
|
235
|
+
linear_add_comment "$linear_id" "$comment_body" || true
|
|
236
|
+
|
|
237
|
+
# Move Linear issue to In Progress
|
|
238
|
+
linear_update_status "$linear_id" "$STATUS_IN_PROGRESS" || true
|
|
239
|
+
|
|
240
|
+
success "${linear_identifier} → GitHub #${gh_num}: ${title}"
|
|
241
|
+
emit_event "linear.sync" "linear_id=$linear_identifier" "github_issue=$gh_num" "title=$title"
|
|
242
|
+
synced=$((synced + 1))
|
|
243
|
+
fi
|
|
244
|
+
|
|
245
|
+
i=$((i + 1))
|
|
246
|
+
done
|
|
247
|
+
|
|
248
|
+
echo ""
|
|
249
|
+
if [[ "$dry_run" == "true" ]]; then
|
|
250
|
+
info "Dry run: ${synced} would be created, ${skipped} already synced"
|
|
251
|
+
else
|
|
252
|
+
success "Synced ${synced} issue(s), ${skipped} already linked"
|
|
253
|
+
fi
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
# ─── Update: GitHub → Linear Status ──────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
cmd_update() {
|
|
259
|
+
check_api_key
|
|
260
|
+
|
|
261
|
+
if [[ $# -lt 2 ]]; then
|
|
262
|
+
error "Usage: shipwright linear update <github-issue-num> <status>"
|
|
263
|
+
echo ""
|
|
264
|
+
echo -e " Statuses: ${CYAN}started${RESET} | ${CYAN}review${RESET} | ${CYAN}done${RESET} | ${CYAN}failed${RESET}"
|
|
265
|
+
echo ""
|
|
266
|
+
echo -e " ${DIM}shipwright linear update 42 started${RESET} # → In Progress"
|
|
267
|
+
echo -e " ${DIM}shipwright linear update 42 review${RESET} # → In Review"
|
|
268
|
+
echo -e " ${DIM}shipwright linear update 42 done${RESET} # → Done"
|
|
269
|
+
echo -e " ${DIM}shipwright linear update 42 failed${RESET} # → adds failure comment"
|
|
270
|
+
exit 1
|
|
271
|
+
fi
|
|
272
|
+
|
|
273
|
+
local gh_issue="$1"
|
|
274
|
+
local status="$2"
|
|
275
|
+
local detail="${3:-}"
|
|
276
|
+
|
|
277
|
+
# Find the Linear issue ID from the GitHub issue body
|
|
278
|
+
local linear_id
|
|
279
|
+
linear_id=$(gh issue view "$gh_issue" --json body --jq '.body' 2>/dev/null | \
|
|
280
|
+
grep -o 'Linear ID:.*' | sed 's/.*\*\*Linear ID:\*\* //' | tr -d '[:space:]' || true)
|
|
281
|
+
|
|
282
|
+
if [[ -z "$linear_id" ]]; then
|
|
283
|
+
error "No Linear ID found in GitHub issue #${gh_issue}"
|
|
284
|
+
echo -e " ${DIM}The issue body must contain: **Linear ID:** <uuid>${RESET}"
|
|
285
|
+
return 1
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
# Map status to Linear state
|
|
289
|
+
local target_state="" status_name=""
|
|
290
|
+
case "$status" in
|
|
291
|
+
started|in-progress|in_progress)
|
|
292
|
+
target_state="$STATUS_IN_PROGRESS"
|
|
293
|
+
status_name="In Progress"
|
|
294
|
+
;;
|
|
295
|
+
review|in-review|in_review|pr)
|
|
296
|
+
target_state="$STATUS_IN_REVIEW"
|
|
297
|
+
status_name="In Review"
|
|
298
|
+
;;
|
|
299
|
+
done|completed|merged)
|
|
300
|
+
target_state="$STATUS_DONE"
|
|
301
|
+
status_name="Done"
|
|
302
|
+
;;
|
|
303
|
+
failed|error)
|
|
304
|
+
# Don't change status, just add a comment
|
|
305
|
+
local comment="Pipeline failed for GitHub issue #${gh_issue}"
|
|
306
|
+
if [[ -n "$detail" ]]; then
|
|
307
|
+
comment="${comment}\n\n${detail}"
|
|
308
|
+
fi
|
|
309
|
+
linear_add_comment "$linear_id" "$comment" || return 1
|
|
310
|
+
warn "Added failure comment to Linear issue"
|
|
311
|
+
emit_event "linear.update" "github_issue=$gh_issue" "status=failed"
|
|
312
|
+
return 0
|
|
313
|
+
;;
|
|
314
|
+
*)
|
|
315
|
+
error "Unknown status: ${status}"
|
|
316
|
+
echo -e " Valid: ${CYAN}started${RESET} | ${CYAN}review${RESET} | ${CYAN}done${RESET} | ${CYAN}failed${RESET}"
|
|
317
|
+
return 1
|
|
318
|
+
;;
|
|
319
|
+
esac
|
|
320
|
+
|
|
321
|
+
linear_update_status "$linear_id" "$target_state" || return 1
|
|
322
|
+
|
|
323
|
+
# Add status transition comment
|
|
324
|
+
local comment="Status updated to **${status_name}** (GitHub #${gh_issue})"
|
|
325
|
+
if [[ -n "$detail" ]]; then
|
|
326
|
+
comment="${comment}\n\n${detail}"
|
|
327
|
+
fi
|
|
328
|
+
linear_add_comment "$linear_id" "$comment" || true
|
|
329
|
+
|
|
330
|
+
success "Linear issue updated → ${status_name} (GitHub #${gh_issue})"
|
|
331
|
+
emit_event "linear.update" "github_issue=$gh_issue" "status=$status"
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
# ─── Status Dashboard ────────────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
cmd_status() {
|
|
337
|
+
check_api_key
|
|
338
|
+
|
|
339
|
+
echo -e "${PURPLE}${BOLD}━━━ Linear Sync Status ━━━${RESET}"
|
|
340
|
+
echo ""
|
|
341
|
+
|
|
342
|
+
# Count issues by status
|
|
343
|
+
local statuses=("$STATUS_BACKLOG:Backlog" "$STATUS_TODO:Todo" "$STATUS_IN_PROGRESS:In Progress" "$STATUS_IN_REVIEW:In Review" "$STATUS_DONE:Done")
|
|
344
|
+
|
|
345
|
+
for entry in "${statuses[@]}"; do
|
|
346
|
+
local state_id="${entry%%:*}"
|
|
347
|
+
local state_name="${entry#*:}"
|
|
348
|
+
|
|
349
|
+
local query='query($teamId: String!, $projectId: String!, $stateId: String!) {
|
|
350
|
+
issues(filter: {
|
|
351
|
+
team: { id: { eq: $teamId } }
|
|
352
|
+
project: { id: { eq: $projectId } }
|
|
353
|
+
state: { id: { eq: $stateId } }
|
|
354
|
+
}) {
|
|
355
|
+
nodes { id identifier title url }
|
|
356
|
+
}
|
|
357
|
+
}'
|
|
358
|
+
|
|
359
|
+
local vars
|
|
360
|
+
vars=$(jq -n \
|
|
361
|
+
--arg teamId "$LINEAR_TEAM_ID" \
|
|
362
|
+
--arg projectId "$LINEAR_PROJECT_ID" \
|
|
363
|
+
--arg stateId "$state_id" \
|
|
364
|
+
'{teamId: $teamId, projectId: $projectId, stateId: $stateId}')
|
|
365
|
+
|
|
366
|
+
local response
|
|
367
|
+
response=$(linear_graphql "$query" "$vars" 2>/dev/null) || {
|
|
368
|
+
echo -e " ${RED}✗${RESET} ${state_name}: ${DIM}(API error)${RESET}"
|
|
369
|
+
continue
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
local count
|
|
373
|
+
count=$(echo "$response" | jq '.data.issues.nodes | length')
|
|
374
|
+
|
|
375
|
+
local color="$DIM"
|
|
376
|
+
case "$state_name" in
|
|
377
|
+
"In Progress") color="$CYAN" ;;
|
|
378
|
+
"In Review") color="$BLUE" ;;
|
|
379
|
+
"Done") color="$GREEN" ;;
|
|
380
|
+
"Todo") color="$YELLOW" ;;
|
|
381
|
+
esac
|
|
382
|
+
|
|
383
|
+
echo -e " ${color}${BOLD}${state_name}${RESET} ${count}"
|
|
384
|
+
|
|
385
|
+
# Show individual issues for active states
|
|
386
|
+
if [[ "$count" -gt 0 ]] && [[ "$state_name" != "Done" ]] && [[ "$state_name" != "Backlog" ]]; then
|
|
387
|
+
local j=0
|
|
388
|
+
while [[ $j -lt $count ]]; do
|
|
389
|
+
local id title
|
|
390
|
+
id=$(echo "$response" | jq -r ".data.issues.nodes[$j].identifier")
|
|
391
|
+
title=$(echo "$response" | jq -r ".data.issues.nodes[$j].title")
|
|
392
|
+
echo -e " ${DIM}${id}${RESET} ${title}"
|
|
393
|
+
j=$((j + 1))
|
|
394
|
+
done
|
|
395
|
+
fi
|
|
396
|
+
done
|
|
397
|
+
|
|
398
|
+
echo ""
|
|
399
|
+
|
|
400
|
+
# Show recent sync events
|
|
401
|
+
if [[ -f "$EVENTS_FILE" ]]; then
|
|
402
|
+
local recent_syncs
|
|
403
|
+
recent_syncs=$(grep '"type":"linear\.' "$EVENTS_FILE" 2>/dev/null | tail -5 || true)
|
|
404
|
+
if [[ -n "$recent_syncs" ]]; then
|
|
405
|
+
echo -e "${BOLD}Recent Activity${RESET}"
|
|
406
|
+
echo "$recent_syncs" | while IFS= read -r line; do
|
|
407
|
+
local ts type
|
|
408
|
+
ts=$(echo "$line" | jq -r '.ts' 2>/dev/null || true)
|
|
409
|
+
type=$(echo "$line" | jq -r '.type' 2>/dev/null || true)
|
|
410
|
+
local short_ts="${ts:-unknown}"
|
|
411
|
+
echo -e " ${DIM}${short_ts}${RESET} ${type}"
|
|
412
|
+
done
|
|
413
|
+
echo ""
|
|
414
|
+
fi
|
|
415
|
+
fi
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
# ─── Init: Save Configuration ────────────────────────────────────────────
|
|
419
|
+
|
|
420
|
+
cmd_init() {
|
|
421
|
+
echo -e "${PURPLE}${BOLD}━━━ Linear Integration Setup ━━━${RESET}"
|
|
422
|
+
echo ""
|
|
423
|
+
|
|
424
|
+
mkdir -p "$CONFIG_DIR"
|
|
425
|
+
|
|
426
|
+
# API Key
|
|
427
|
+
local api_key="${LINEAR_API_KEY:-}"
|
|
428
|
+
if [[ -z "$api_key" ]]; then
|
|
429
|
+
echo -e " ${CYAN}1.${RESET} Go to ${DIM}https://linear.app/settings/api${RESET}"
|
|
430
|
+
echo -e " ${CYAN}2.${RESET} Create a personal API key"
|
|
431
|
+
echo -e " ${CYAN}3.${RESET} Paste it below"
|
|
432
|
+
echo ""
|
|
433
|
+
read -rp " Linear API Key: " api_key
|
|
434
|
+
if [[ -z "$api_key" ]]; then
|
|
435
|
+
error "API key is required"
|
|
436
|
+
exit 1
|
|
437
|
+
fi
|
|
438
|
+
fi
|
|
439
|
+
|
|
440
|
+
local team_id="${LINEAR_TEAM_ID:-83deb533-69d2-43ef-bc58-eadb6e72a8f2}"
|
|
441
|
+
local project_id="${LINEAR_PROJECT_ID:-b262d625-5bbe-47bd-9f89-df27c45eba8b}"
|
|
442
|
+
|
|
443
|
+
# Write config atomically
|
|
444
|
+
local tmp_config="${LINEAR_CONFIG}.tmp"
|
|
445
|
+
jq -n \
|
|
446
|
+
--arg api_key "$api_key" \
|
|
447
|
+
--arg team_id "$team_id" \
|
|
448
|
+
--arg project_id "$project_id" \
|
|
449
|
+
--arg created_at "$(now_iso)" \
|
|
450
|
+
'{
|
|
451
|
+
api_key: $api_key,
|
|
452
|
+
team_id: $team_id,
|
|
453
|
+
project_id: $project_id,
|
|
454
|
+
created_at: $created_at
|
|
455
|
+
}' > "$tmp_config"
|
|
456
|
+
mv "$tmp_config" "$LINEAR_CONFIG"
|
|
457
|
+
chmod 600 "$LINEAR_CONFIG"
|
|
458
|
+
|
|
459
|
+
success "Configuration saved to ${LINEAR_CONFIG}"
|
|
460
|
+
echo ""
|
|
461
|
+
|
|
462
|
+
# Validate the key works
|
|
463
|
+
info "Validating API key..."
|
|
464
|
+
LINEAR_API_KEY="$api_key"
|
|
465
|
+
local test_query='query { viewer { id name email } }'
|
|
466
|
+
local test_response
|
|
467
|
+
test_response=$(linear_graphql "$test_query") || {
|
|
468
|
+
error "API key validation failed — check your key"
|
|
469
|
+
exit 1
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
local viewer_name
|
|
473
|
+
viewer_name=$(echo "$test_response" | jq -r '.data.viewer.name // "Unknown"')
|
|
474
|
+
success "Authenticated as: ${viewer_name}"
|
|
475
|
+
|
|
476
|
+
emit_event "linear.init" "user=$viewer_name"
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
# ─── Helper: Update Linear Issue Status ──────────────────────────────────
|
|
480
|
+
|
|
481
|
+
linear_update_status() {
|
|
482
|
+
local issue_id="$1"
|
|
483
|
+
local state_id="$2"
|
|
484
|
+
|
|
485
|
+
local query='mutation($issueId: String!, $stateId: String!) {
|
|
486
|
+
issueUpdate(id: $issueId, input: { stateId: $stateId }) {
|
|
487
|
+
issue { id identifier }
|
|
488
|
+
}
|
|
489
|
+
}'
|
|
490
|
+
|
|
491
|
+
local vars
|
|
492
|
+
vars=$(jq -n --arg issueId "$issue_id" --arg stateId "$state_id" \
|
|
493
|
+
'{issueId: $issueId, stateId: $stateId}')
|
|
494
|
+
|
|
495
|
+
linear_graphql "$query" "$vars" >/dev/null
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
# ─── Helper: Add Comment to Linear Issue ─────────────────────────────────
|
|
499
|
+
|
|
500
|
+
linear_add_comment() {
|
|
501
|
+
local issue_id="$1"
|
|
502
|
+
local body="$2"
|
|
503
|
+
|
|
504
|
+
local query='mutation($issueId: String!, $body: String!) {
|
|
505
|
+
commentCreate(input: { issueId: $issueId, body: $body }) {
|
|
506
|
+
comment { id }
|
|
507
|
+
}
|
|
508
|
+
}'
|
|
509
|
+
|
|
510
|
+
local vars
|
|
511
|
+
vars=$(jq -n --arg issueId "$issue_id" --arg body "$body" \
|
|
512
|
+
'{issueId: $issueId, body: $body}')
|
|
513
|
+
|
|
514
|
+
linear_graphql "$query" "$vars" >/dev/null
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
# ─── Helper: Attach PR link to Linear Issue ──────────────────────────────
|
|
518
|
+
|
|
519
|
+
linear_attach_pr() {
|
|
520
|
+
local issue_id="$1"
|
|
521
|
+
local pr_url="$2"
|
|
522
|
+
local pr_title="${3:-Pull Request}"
|
|
523
|
+
|
|
524
|
+
local body
|
|
525
|
+
body=$(printf "PR linked: [%s](%s)" "$pr_title" "$pr_url")
|
|
526
|
+
linear_add_comment "$issue_id" "$body"
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
# ─── Daemon Integration: Notify Linear on Pipeline Events ────────────────
|
|
530
|
+
# Called by sw-daemon.sh at spawn, stage transition, completion, and failure.
|
|
531
|
+
# This function is designed to be sourced or called externally.
|
|
532
|
+
|
|
533
|
+
linear_notify() {
|
|
534
|
+
local event="$1"
|
|
535
|
+
local gh_issue="${2:-}"
|
|
536
|
+
local detail="${3:-}"
|
|
537
|
+
|
|
538
|
+
# Delegate to tracker router if available (preferred path)
|
|
539
|
+
if [[ -f "$SCRIPT_DIR/sw-tracker.sh" ]]; then
|
|
540
|
+
"$SCRIPT_DIR/sw-tracker.sh" notify "$event" "$gh_issue" "$detail" 2>/dev/null || true
|
|
541
|
+
return 0
|
|
542
|
+
fi
|
|
543
|
+
|
|
544
|
+
# Fallback: direct Linear notification (backward compatibility)
|
|
545
|
+
load_config
|
|
546
|
+
if [[ -z "$LINEAR_API_KEY" ]]; then
|
|
547
|
+
return 0 # silently skip if no Linear integration
|
|
548
|
+
fi
|
|
549
|
+
|
|
550
|
+
# Find the Linear issue ID from GitHub issue
|
|
551
|
+
local linear_id=""
|
|
552
|
+
if [[ -n "$gh_issue" ]]; then
|
|
553
|
+
linear_id=$(gh issue view "$gh_issue" --json body --jq '.body' 2>/dev/null | \
|
|
554
|
+
grep -o 'Linear ID:.*' | sed 's/.*\*\*Linear ID:\*\* //' | tr -d '[:space:]' || true)
|
|
555
|
+
fi
|
|
556
|
+
|
|
557
|
+
if [[ -z "$linear_id" ]]; then
|
|
558
|
+
return 0 # no linked Linear issue
|
|
559
|
+
fi
|
|
560
|
+
|
|
561
|
+
case "$event" in
|
|
562
|
+
spawn|started)
|
|
563
|
+
linear_update_status "$linear_id" "$STATUS_IN_PROGRESS" || true
|
|
564
|
+
linear_add_comment "$linear_id" "Pipeline started for GitHub issue #${gh_issue}" || true
|
|
565
|
+
;;
|
|
566
|
+
review|pr-created)
|
|
567
|
+
linear_update_status "$linear_id" "$STATUS_IN_REVIEW" || true
|
|
568
|
+
if [[ -n "$detail" ]]; then
|
|
569
|
+
linear_attach_pr "$linear_id" "$detail" "PR for #${gh_issue}" || true
|
|
570
|
+
fi
|
|
571
|
+
;;
|
|
572
|
+
completed|done)
|
|
573
|
+
linear_update_status "$linear_id" "$STATUS_DONE" || true
|
|
574
|
+
linear_add_comment "$linear_id" "Pipeline completed successfully for GitHub issue #${gh_issue}" || true
|
|
575
|
+
;;
|
|
576
|
+
failed)
|
|
577
|
+
local msg="Pipeline failed for GitHub issue #${gh_issue}"
|
|
578
|
+
if [[ -n "$detail" ]]; then
|
|
579
|
+
msg="${msg}\n\nDetails:\n${detail}"
|
|
580
|
+
fi
|
|
581
|
+
linear_add_comment "$linear_id" "$msg" || true
|
|
582
|
+
;;
|
|
583
|
+
esac
|
|
584
|
+
|
|
585
|
+
emit_event "linear.notify" "event=$event" "github_issue=$gh_issue"
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
# ─── Help ────────────────────────────────────────────────────────────────
|
|
589
|
+
|
|
590
|
+
show_help() {
|
|
591
|
+
echo -e "${CYAN}${BOLD}shipwright linear${RESET} — Linear ↔ GitHub Bidirectional Sync"
|
|
592
|
+
echo ""
|
|
593
|
+
echo -e "${BOLD}USAGE${RESET}"
|
|
594
|
+
echo -e " ${CYAN}shipwright linear${RESET} <command> [options]"
|
|
595
|
+
echo ""
|
|
596
|
+
echo -e "${BOLD}COMMANDS${RESET}"
|
|
597
|
+
echo -e " ${CYAN}sync${RESET} [--dry-run] Sync Linear Todo issues → GitHub"
|
|
598
|
+
echo -e " ${CYAN}update${RESET} <issue> <status> Update linked Linear ticket status"
|
|
599
|
+
echo -e " ${CYAN}status${RESET} Show sync dashboard"
|
|
600
|
+
echo -e " ${CYAN}init${RESET} Configure Linear API key"
|
|
601
|
+
echo -e " ${CYAN}help${RESET} Show this help"
|
|
602
|
+
echo ""
|
|
603
|
+
echo -e "${BOLD}STATUS VALUES${RESET}"
|
|
604
|
+
echo -e " ${CYAN}started${RESET} Pipeline spawned → Linear: In Progress"
|
|
605
|
+
echo -e " ${CYAN}review${RESET} PR created → Linear: In Review"
|
|
606
|
+
echo -e " ${CYAN}done${RESET} Pipeline complete → Linear: Done"
|
|
607
|
+
echo -e " ${CYAN}failed${RESET} Pipeline failed → Linear: adds failure comment"
|
|
608
|
+
echo ""
|
|
609
|
+
echo -e "${BOLD}EXAMPLES${RESET}"
|
|
610
|
+
echo -e " ${DIM}shipwright linear init${RESET} # Set up API key"
|
|
611
|
+
echo -e " ${DIM}shipwright linear sync${RESET} # Sync Todo → GitHub"
|
|
612
|
+
echo -e " ${DIM}shipwright linear sync --dry-run${RESET} # Preview what would sync"
|
|
613
|
+
echo -e " ${DIM}shipwright linear update 42 started${RESET} # Mark as In Progress"
|
|
614
|
+
echo -e " ${DIM}shipwright linear update 42 review${RESET} # Mark as In Review"
|
|
615
|
+
echo -e " ${DIM}shipwright linear update 42 done${RESET} # Mark as Done"
|
|
616
|
+
echo -e " ${DIM}shipwright linear status${RESET} # Show dashboard"
|
|
617
|
+
echo ""
|
|
618
|
+
echo -e "${BOLD}ENVIRONMENT${RESET}"
|
|
619
|
+
echo -e " ${DIM}LINEAR_API_KEY${RESET} API key (or use 'linear init' to save)"
|
|
620
|
+
echo -e " ${DIM}LINEAR_TEAM_ID${RESET} Override team ID"
|
|
621
|
+
echo -e " ${DIM}LINEAR_PROJECT_ID${RESET} Override project ID"
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
# ─── Command Router ─────────────────────────────────────────────────────
|
|
625
|
+
|
|
626
|
+
main() {
|
|
627
|
+
load_config
|
|
628
|
+
|
|
629
|
+
local cmd="${1:-help}"
|
|
630
|
+
shift 2>/dev/null || true
|
|
631
|
+
|
|
632
|
+
case "$cmd" in
|
|
633
|
+
sync) cmd_sync "$@" ;;
|
|
634
|
+
update) cmd_update "$@" ;;
|
|
635
|
+
status) cmd_status "$@" ;;
|
|
636
|
+
init) cmd_init "$@" ;;
|
|
637
|
+
notify) linear_notify "$@" ;;
|
|
638
|
+
help|--help|-h) show_help ;;
|
|
639
|
+
*)
|
|
640
|
+
error "Unknown command: ${cmd}"
|
|
641
|
+
echo ""
|
|
642
|
+
show_help
|
|
643
|
+
exit 1
|
|
644
|
+
;;
|
|
645
|
+
esac
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
main "$@"
|