claude-plugin-viban 1.0.37 → 1.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/.claude-plugin/plugin.json +1 -1
- package/bin/viban +25 -3
- package/commands/sync.md +99 -0
- package/install.sh +13 -32
- package/package.json +1 -1
- package/scripts/providers/github.sh +268 -0
- package/scripts/sync.sh +530 -0
- package/skills/sync/SKILL.md +100 -0
package/bin/viban
CHANGED
|
@@ -35,7 +35,7 @@ check_dependencies() {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
# Only check dependencies for interactive commands (not help)
|
|
38
|
-
[[ "$1" != "help" && "$1" != "--help" && "$1" != "-h" ]] && check_dependencies
|
|
38
|
+
[[ "$1" != "help" && "$1" != "--help" && "$1" != "-h" && "$1" != "sync" ]] && check_dependencies
|
|
39
39
|
|
|
40
40
|
# ============================================================
|
|
41
41
|
# Auto-update Check (once per day, cached)
|
|
@@ -113,7 +113,7 @@ auto_update_check() {
|
|
|
113
113
|
|
|
114
114
|
# Run auto-update for all commands except help/version/update itself
|
|
115
115
|
case "$1" in
|
|
116
|
-
help|--help|-h|--version|-v|update) ;;
|
|
116
|
+
help|--help|-h|--version|-v|update|sync) ;;
|
|
117
117
|
*) auto_update_check "$@" ;;
|
|
118
118
|
esac
|
|
119
119
|
|
|
@@ -246,7 +246,7 @@ EOF
|
|
|
246
246
|
|
|
247
247
|
# Auto-initialize for commands that need data (not help/init)
|
|
248
248
|
case "$1" in
|
|
249
|
-
help|--help|-h|--version|-v|update|init) ;;
|
|
249
|
+
help|--help|-h|--version|-v|update|init|sync) ;;
|
|
250
250
|
*) init_viban_json ;;
|
|
251
251
|
esac
|
|
252
252
|
|
|
@@ -1571,6 +1571,26 @@ cmd_migrate() {
|
|
|
1571
1571
|
' "$VIBAN_JSON"
|
|
1572
1572
|
}
|
|
1573
1573
|
|
|
1574
|
+
cmd_sync() {
|
|
1575
|
+
local provider="${VIBAN_SYNC_PROVIDER:-}"
|
|
1576
|
+
# Auto-detect provider from existing sync.json or default to github
|
|
1577
|
+
if [[ -z "$provider" && -f "$VIBAN_DATA_DIR/sync.json" ]]; then
|
|
1578
|
+
provider=$(jq -r '.provider // "github"' "$VIBAN_DATA_DIR/sync.json")
|
|
1579
|
+
fi
|
|
1580
|
+
provider="${provider:-github}"
|
|
1581
|
+
|
|
1582
|
+
local provider_script="$VIBAN_SCRIPT_DIR/scripts/providers/${provider}.sh"
|
|
1583
|
+
if [[ ! -f "$provider_script" ]]; then
|
|
1584
|
+
echo "Error: Unknown sync provider '$provider'"
|
|
1585
|
+
echo "Available: $(ls "$VIBAN_SCRIPT_DIR/scripts/providers/" 2>/dev/null | sed 's/\.sh$//' | tr '\n' ' ')"
|
|
1586
|
+
exit 1
|
|
1587
|
+
fi
|
|
1588
|
+
|
|
1589
|
+
VIBAN_JSON="$VIBAN_JSON" VIBAN_DATA_DIR="$VIBAN_DATA_DIR" \
|
|
1590
|
+
VIBAN_PROVIDER="$provider" VIBAN_SCRIPT_DIR="$VIBAN_SCRIPT_DIR" \
|
|
1591
|
+
bash "$VIBAN_SCRIPT_DIR/scripts/sync.sh" "$@"
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1574
1594
|
main() {
|
|
1575
1595
|
check_deps
|
|
1576
1596
|
init_json
|
|
@@ -1585,6 +1605,7 @@ main() {
|
|
|
1585
1605
|
edit) [[ -z "$2" ]] && { echo "Usage: viban edit <id>"; exit 1; }; edit_issue "$2";;
|
|
1586
1606
|
priority) cmd_priority "$2" "$3";;
|
|
1587
1607
|
migrate) cmd_migrate;;
|
|
1608
|
+
sync) shift; cmd_sync "$@";;
|
|
1588
1609
|
--version|-v)
|
|
1589
1610
|
# Get version from package.json
|
|
1590
1611
|
if [[ -f "$VIBAN_SCRIPT_DIR/package.json" ]]; then
|
|
@@ -1616,6 +1637,7 @@ main() {
|
|
|
1616
1637
|
echo " viban edit <id> Edit task in editor"
|
|
1617
1638
|
echo " viban get <id> Get task details (JSON)"
|
|
1618
1639
|
echo " viban migrate Migrate: extract type from title"
|
|
1640
|
+
echo " viban sync Sync with external issue tracker (GitHub, etc.)"
|
|
1619
1641
|
echo " viban update Update to latest version (if available)"
|
|
1620
1642
|
echo ""
|
|
1621
1643
|
echo " Priority: P0=CRITICAL, P1=HIGH, P2=MEDIUM, P3=LOW"
|
package/commands/sync.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Sync viban board with external issue tracker (GitHub, Jira, etc.)"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# /sync - External Issue Tracker Sync
|
|
6
|
+
|
|
7
|
+
Sync the viban board with an external issue tracker. Currently supports GitHub Issues via `gh` CLI.
|
|
8
|
+
|
|
9
|
+
> **Principle**: Show what will happen before doing it. Never sync without user confirmation.
|
|
10
|
+
|
|
11
|
+
## Input
|
|
12
|
+
|
|
13
|
+
**User Input**: `$ARGUMENTS`
|
|
14
|
+
|
|
15
|
+
## Step 1: Check Sync Configuration
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Check if sync is already configured
|
|
19
|
+
if [ -f "$(git rev-parse --git-common-dir 2>/dev/null || echo .viban)/sync.json" ]; then
|
|
20
|
+
echo "Sync configured"
|
|
21
|
+
viban sync --status
|
|
22
|
+
else
|
|
23
|
+
echo "Sync not configured"
|
|
24
|
+
fi
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- If **not configured**, proceed to Step 2
|
|
28
|
+
- If **configured**, skip to Step 3
|
|
29
|
+
|
|
30
|
+
## Step 2: Initialize Sync
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
viban sync --init
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This will:
|
|
37
|
+
- Auto-detect the provider from git remote (defaults to GitHub)
|
|
38
|
+
- Check that `gh` CLI is installed and authenticated
|
|
39
|
+
- Create required labels on the remote repo
|
|
40
|
+
- Initialize `sync.json` metadata
|
|
41
|
+
|
|
42
|
+
If initialization fails, report the error and suggest fixes (install `gh`, run `gh auth login`, etc.).
|
|
43
|
+
|
|
44
|
+
## Step 3: Preview Changes (Dry Run)
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
viban sync --dry-run
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Show the user what will happen:
|
|
51
|
+
- `<-` Issues to pull from remote
|
|
52
|
+
- `->` Cards to push to remote
|
|
53
|
+
- `==` Unchanged items
|
|
54
|
+
- `!!` Conflicts (and resolution strategy)
|
|
55
|
+
|
|
56
|
+
## Step 4: Confirm and Sync
|
|
57
|
+
|
|
58
|
+
Ask the user for confirmation using AskUserQuestion:
|
|
59
|
+
|
|
60
|
+
- header: "Sync"
|
|
61
|
+
- question: "Apply these sync changes?"
|
|
62
|
+
- options:
|
|
63
|
+
- "Yes, sync now"
|
|
64
|
+
- "Sync and push new local cards too (--push-new)"
|
|
65
|
+
- "Pull only (remote -> local)"
|
|
66
|
+
- "Cancel"
|
|
67
|
+
- multiSelect: false
|
|
68
|
+
|
|
69
|
+
Based on the answer:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Yes, sync now
|
|
73
|
+
viban sync
|
|
74
|
+
|
|
75
|
+
# With push-new
|
|
76
|
+
viban sync --push-new
|
|
77
|
+
|
|
78
|
+
# Pull only
|
|
79
|
+
viban sync --pull-only
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Step 5: Report Results
|
|
83
|
+
|
|
84
|
+
Show the sync summary:
|
|
85
|
+
```
|
|
86
|
+
Sync complete:
|
|
87
|
+
Pulled: N new/updated cards from remote
|
|
88
|
+
Pushed: N cards to remote
|
|
89
|
+
Conflicts: N (resolved by: remote wins)
|
|
90
|
+
Unchanged: N
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Notes
|
|
94
|
+
|
|
95
|
+
- **First sync imports all open issues** as backlog cards with `github:N` external IDs
|
|
96
|
+
- **Conflicts**: when both sides changed, remote wins by default (with warning)
|
|
97
|
+
- **Closed issues**: remote closed issues move viban card to `review` status
|
|
98
|
+
- **Done cards**: `viban done` then sync closes the remote issue
|
|
99
|
+
- **New local cards** are NOT pushed unless `--push-new` is specified (local-first default)
|
package/install.sh
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
set -e
|
|
6
6
|
|
|
7
|
+
main() {
|
|
8
|
+
|
|
7
9
|
RED='\033[0;31m'
|
|
8
10
|
GREEN='\033[0;32m'
|
|
9
11
|
YELLOW='\033[0;33m'
|
|
@@ -172,39 +174,14 @@ echo ""
|
|
|
172
174
|
echo -e "${BOLD}Registering Claude Code plugin...${NC}"
|
|
173
175
|
echo ""
|
|
174
176
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
# Get npm global prefix to find viban
|
|
181
|
-
NPM_PREFIX=$(npm prefix -g)
|
|
182
|
-
VIBAN_PLUGIN_DIR="${NPM_PREFIX}/lib/node_modules/claude-plugin-viban"
|
|
183
|
-
|
|
184
|
-
# Check if plugins.json exists
|
|
185
|
-
if [[ -f "$CLAUDE_PLUGINS_FILE" ]]; then
|
|
186
|
-
# Check if viban is already registered
|
|
187
|
-
if jq -e '.plugins[] | select(.name == "viban")' "$CLAUDE_PLUGINS_FILE" > /dev/null 2>&1; then
|
|
188
|
-
echo -e "${GREEN}✓${NC} Plugin already registered"
|
|
189
|
-
else
|
|
190
|
-
# Add viban to existing plugins
|
|
191
|
-
jq --arg path "$VIBAN_PLUGIN_DIR" '.plugins += [{"name": "viban", "path": $path}]' "$CLAUDE_PLUGINS_FILE" > "${CLAUDE_PLUGINS_FILE}.tmp"
|
|
192
|
-
mv "${CLAUDE_PLUGINS_FILE}.tmp" "$CLAUDE_PLUGINS_FILE"
|
|
193
|
-
echo -e "${GREEN}✓${NC} Plugin registered"
|
|
194
|
-
fi
|
|
177
|
+
if command -v claude &> /dev/null; then
|
|
178
|
+
claude plugin marketplace add https://github.com/happy-nut/claude-plugin-viban 2>/dev/null || true
|
|
179
|
+
claude plugin install viban 2>/dev/null || true
|
|
180
|
+
echo -e "${GREEN}✓${NC} Plugin registered in Claude Code"
|
|
195
181
|
else
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
{
|
|
199
|
-
"plugins": [
|
|
200
|
-
{
|
|
201
|
-
"name": "viban",
|
|
202
|
-
"path": "$VIBAN_PLUGIN_DIR"
|
|
203
|
-
}
|
|
204
|
-
]
|
|
205
|
-
}
|
|
206
|
-
EOF
|
|
207
|
-
echo -e "${GREEN}✓${NC} Plugin registered"
|
|
182
|
+
echo -e "${YELLOW}!${NC} Claude Code CLI not found. To register the plugin manually, run:"
|
|
183
|
+
echo -e " ${YELLOW}claude plugin marketplace add https://github.com/happy-nut/claude-plugin-viban${NC}"
|
|
184
|
+
echo -e " ${YELLOW}claude plugin install viban${NC}"
|
|
208
185
|
fi
|
|
209
186
|
|
|
210
187
|
echo ""
|
|
@@ -220,3 +197,7 @@ echo -e " ${YELLOW}viban help${NC} Show all commands"
|
|
|
220
197
|
echo ""
|
|
221
198
|
echo -e "In Claude Code, the plugin is now available."
|
|
222
199
|
echo ""
|
|
200
|
+
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
main "$@"
|
package/package.json
CHANGED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# GitHub provider for viban sync
|
|
3
|
+
# Implements the provider interface using the `gh` CLI
|
|
4
|
+
|
|
5
|
+
# ============================================================
|
|
6
|
+
# Provider Interface
|
|
7
|
+
# ============================================================
|
|
8
|
+
|
|
9
|
+
provider_name() {
|
|
10
|
+
echo "github"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
provider_check_deps() {
|
|
14
|
+
if ! command -v gh &>/dev/null; then
|
|
15
|
+
echo "Error: gh CLI not found"
|
|
16
|
+
echo " Install: brew install gh"
|
|
17
|
+
echo " Or visit: https://cli.github.com/"
|
|
18
|
+
return 1
|
|
19
|
+
fi
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
provider_check_auth() {
|
|
23
|
+
if ! gh auth status &>/dev/null; then
|
|
24
|
+
echo "Error: Not authenticated with GitHub"
|
|
25
|
+
echo " Run: gh auth login"
|
|
26
|
+
return 1
|
|
27
|
+
fi
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
provider_detect_config() {
|
|
31
|
+
local remote_url
|
|
32
|
+
remote_url=$(git remote get-url origin 2>/dev/null) || {
|
|
33
|
+
echo "Error: No git remote 'origin' found"
|
|
34
|
+
return 1
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
local repo=""
|
|
38
|
+
# SSH format: git@github.com:owner/repo.git
|
|
39
|
+
if [[ "$remote_url" =~ git@github\.com:([^/]+/[^/.]+)(\.git)?$ ]]; then
|
|
40
|
+
repo="${BASH_REMATCH[1]}"
|
|
41
|
+
# HTTPS format: https://github.com/owner/repo.git
|
|
42
|
+
elif [[ "$remote_url" =~ github\.com/([^/]+/[^/.]+)(\.git)?$ ]]; then
|
|
43
|
+
repo="${BASH_REMATCH[1]}"
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
if [[ -z "$repo" ]]; then
|
|
47
|
+
echo "Error: Could not detect GitHub repo from remote URL: $remote_url"
|
|
48
|
+
return 1
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
echo "{\"repo\":\"$repo\"}"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# ============================================================
|
|
55
|
+
# Label Mapping
|
|
56
|
+
# ============================================================
|
|
57
|
+
|
|
58
|
+
# Status labels
|
|
59
|
+
_gh_status_to_viban() {
|
|
60
|
+
local gh_state="$1" labels="$2"
|
|
61
|
+
if [[ "$gh_state" == "closed" ]]; then
|
|
62
|
+
echo "done"
|
|
63
|
+
elif echo "$labels" | grep -q "review"; then
|
|
64
|
+
echo "review"
|
|
65
|
+
elif echo "$labels" | grep -q "in-progress"; then
|
|
66
|
+
echo "in_progress"
|
|
67
|
+
else
|
|
68
|
+
echo "backlog"
|
|
69
|
+
fi
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_viban_status_labels() {
|
|
73
|
+
local st="$1"
|
|
74
|
+
case "$st" in
|
|
75
|
+
in_progress) echo "in-progress" ;;
|
|
76
|
+
review) echo "review" ;;
|
|
77
|
+
*) echo "" ;;
|
|
78
|
+
esac
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Priority mapping
|
|
82
|
+
_gh_priority_to_viban() {
|
|
83
|
+
local labels="$1"
|
|
84
|
+
if echo "$labels" | grep -q "P0-critical"; then echo "P0"
|
|
85
|
+
elif echo "$labels" | grep -q "P1-high"; then echo "P1"
|
|
86
|
+
elif echo "$labels" | grep -q "P2-medium"; then echo "P2"
|
|
87
|
+
elif echo "$labels" | grep -q "P3-low"; then echo "P3"
|
|
88
|
+
else echo "P3"
|
|
89
|
+
fi
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_viban_priority_label() {
|
|
93
|
+
local priority="$1"
|
|
94
|
+
case "$priority" in
|
|
95
|
+
P0) echo "P0-critical" ;;
|
|
96
|
+
P1) echo "P1-high" ;;
|
|
97
|
+
P2) echo "P2-medium" ;;
|
|
98
|
+
P3) echo "P3-low" ;;
|
|
99
|
+
*) echo "" ;;
|
|
100
|
+
esac
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# Type mapping
|
|
104
|
+
_gh_type_to_viban() {
|
|
105
|
+
local labels="$1"
|
|
106
|
+
if echo "$labels" | grep -q "bug"; then echo "bug"
|
|
107
|
+
elif echo "$labels" | grep -q "enhancement"; then echo "feat"
|
|
108
|
+
elif echo "$labels" | grep -q "chore"; then echo "chore"
|
|
109
|
+
elif echo "$labels" | grep -q "refactor"; then echo "refactor"
|
|
110
|
+
else echo ""
|
|
111
|
+
fi
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
_viban_type_label() {
|
|
115
|
+
local type="$1"
|
|
116
|
+
case "$type" in
|
|
117
|
+
bug) echo "bug" ;;
|
|
118
|
+
feat) echo "enhancement" ;;
|
|
119
|
+
chore) echo "chore" ;;
|
|
120
|
+
refactor) echo "refactor" ;;
|
|
121
|
+
*) echo "" ;;
|
|
122
|
+
esac
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# ============================================================
|
|
126
|
+
# Core Provider Functions
|
|
127
|
+
# ============================================================
|
|
128
|
+
|
|
129
|
+
provider_fetch_issues() {
|
|
130
|
+
local repo="$1"
|
|
131
|
+
|
|
132
|
+
local issues
|
|
133
|
+
issues=$(gh issue list --repo "$repo" --state open --json number,title,body,labels,updatedAt --limit 200 2>/dev/null) || {
|
|
134
|
+
echo "Error: Failed to fetch issues from $repo"
|
|
135
|
+
return 1
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# Transform to normalized format
|
|
139
|
+
echo "$issues" | jq '[.[] | {
|
|
140
|
+
remote_id: (.number | tostring),
|
|
141
|
+
title: .title,
|
|
142
|
+
description: (.body // ""),
|
|
143
|
+
status: (
|
|
144
|
+
if ([.labels[].name] | any(. == "review")) then "review"
|
|
145
|
+
elif ([.labels[].name] | any(. == "in-progress")) then "in_progress"
|
|
146
|
+
else "backlog"
|
|
147
|
+
end
|
|
148
|
+
),
|
|
149
|
+
priority: (
|
|
150
|
+
if ([.labels[].name] | any(. == "P0-critical")) then "P0"
|
|
151
|
+
elif ([.labels[].name] | any(. == "P1-high")) then "P1"
|
|
152
|
+
elif ([.labels[].name] | any(. == "P2-medium")) then "P2"
|
|
153
|
+
elif ([.labels[].name] | any(. == "P3-low")) then "P3"
|
|
154
|
+
else "P3"
|
|
155
|
+
end
|
|
156
|
+
),
|
|
157
|
+
type: (
|
|
158
|
+
if ([.labels[].name] | any(. == "bug")) then "bug"
|
|
159
|
+
elif ([.labels[].name] | any(. == "enhancement")) then "feat"
|
|
160
|
+
elif ([.labels[].name] | any(. == "chore")) then "chore"
|
|
161
|
+
elif ([.labels[].name] | any(. == "refactor")) then "refactor"
|
|
162
|
+
else null
|
|
163
|
+
end
|
|
164
|
+
),
|
|
165
|
+
updated_at: .updatedAt
|
|
166
|
+
}]'
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
provider_create_issue() {
|
|
170
|
+
local repo="$1"
|
|
171
|
+
# Read normalized JSON from stdin
|
|
172
|
+
local issue_json
|
|
173
|
+
issue_json=$(cat)
|
|
174
|
+
|
|
175
|
+
local title body labels_args
|
|
176
|
+
title=$(echo "$issue_json" | jq -r '.title')
|
|
177
|
+
body=$(echo "$issue_json" | jq -r '.description // ""')
|
|
178
|
+
|
|
179
|
+
# Build labels
|
|
180
|
+
local labels=()
|
|
181
|
+
local status_label priority_label type_label
|
|
182
|
+
|
|
183
|
+
status_label=$(_viban_status_labels "$(echo "$issue_json" | jq -r '.status // "backlog"')")
|
|
184
|
+
[[ -n "$status_label" ]] && labels+=("$status_label")
|
|
185
|
+
|
|
186
|
+
priority_label=$(_viban_priority_label "$(echo "$issue_json" | jq -r '.priority // "P3"')")
|
|
187
|
+
[[ -n "$priority_label" ]] && labels+=("$priority_label")
|
|
188
|
+
|
|
189
|
+
type_label=$(_viban_type_label "$(echo "$issue_json" | jq -r '.type // ""')")
|
|
190
|
+
[[ -n "$type_label" ]] && labels+=("$type_label")
|
|
191
|
+
|
|
192
|
+
local label_args=()
|
|
193
|
+
for l in "${labels[@]}"; do
|
|
194
|
+
label_args+=(--label "$l")
|
|
195
|
+
done
|
|
196
|
+
|
|
197
|
+
local result
|
|
198
|
+
result=$(gh issue create --repo "$repo" --title "$title" --body "$body" "${label_args[@]}" 2>/dev/null) || {
|
|
199
|
+
echo "Error: Failed to create issue '$title'"
|
|
200
|
+
return 1
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# Extract issue number from URL (gh returns URL like https://github.com/owner/repo/issues/42)
|
|
204
|
+
echo "$result" | grep -o '[0-9]*$'
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
provider_update_issue() {
|
|
208
|
+
local repo="$1" remote_id="$2"
|
|
209
|
+
# Read normalized JSON from stdin
|
|
210
|
+
local issue_json
|
|
211
|
+
issue_json=$(cat)
|
|
212
|
+
|
|
213
|
+
local title body
|
|
214
|
+
title=$(echo "$issue_json" | jq -r '.title')
|
|
215
|
+
body=$(echo "$issue_json" | jq -r '.description // ""')
|
|
216
|
+
|
|
217
|
+
# Collect all desired labels
|
|
218
|
+
local labels=()
|
|
219
|
+
local status_label priority_label type_label
|
|
220
|
+
|
|
221
|
+
status_label=$(_viban_status_labels "$(echo "$issue_json" | jq -r '.status // "backlog"')")
|
|
222
|
+
[[ -n "$status_label" ]] && labels+=("$status_label")
|
|
223
|
+
|
|
224
|
+
priority_label=$(_viban_priority_label "$(echo "$issue_json" | jq -r '.priority // "P3"')")
|
|
225
|
+
[[ -n "$priority_label" ]] && labels+=("$priority_label")
|
|
226
|
+
|
|
227
|
+
type_label=$(_viban_type_label "$(echo "$issue_json" | jq -r '.type // ""')")
|
|
228
|
+
[[ -n "$type_label" ]] && labels+=("$type_label")
|
|
229
|
+
|
|
230
|
+
# Remove old status/priority/type labels, then add new ones
|
|
231
|
+
local remove_labels="in-progress,review,P0-critical,P1-high,P2-medium,P3-low,bug,enhancement,chore,refactor"
|
|
232
|
+
gh issue edit "$remote_id" --repo "$repo" --title "$title" --body "$body" \
|
|
233
|
+
--remove-label "$remove_labels" 2>/dev/null
|
|
234
|
+
|
|
235
|
+
if [[ ${#labels[@]} -gt 0 ]]; then
|
|
236
|
+
local add_labels
|
|
237
|
+
add_labels=$(IFS=,; echo "${labels[*]}")
|
|
238
|
+
gh issue edit "$remote_id" --repo "$repo" --add-label "$add_labels" 2>/dev/null
|
|
239
|
+
fi
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
provider_close_issue() {
|
|
243
|
+
local repo="$1" remote_id="$2"
|
|
244
|
+
gh issue close "$remote_id" --repo "$repo" 2>/dev/null || {
|
|
245
|
+
echo "Error: Failed to close issue #$remote_id"
|
|
246
|
+
return 1
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
provider_ensure_labels() {
|
|
251
|
+
local repo="$1"
|
|
252
|
+
|
|
253
|
+
local required_labels=(
|
|
254
|
+
"in-progress:Status: In Progress:0E8A16"
|
|
255
|
+
"review:Status: In Review:FBCA04"
|
|
256
|
+
"P0-critical:Priority: Critical:B60205"
|
|
257
|
+
"P1-high:Priority: High:D93F0B"
|
|
258
|
+
"P2-medium:Priority: Medium:FBCA04"
|
|
259
|
+
"P3-low:Priority: Low:0E8A16"
|
|
260
|
+
"chore:Type: Chore:EEEEEE"
|
|
261
|
+
"refactor:Type: Refactor:C5DEF5"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
for label_def in "${required_labels[@]}"; do
|
|
265
|
+
IFS=: read -r name desc color <<< "$label_def"
|
|
266
|
+
gh label create "$name" --repo "$repo" --description "$desc" --color "$color" 2>/dev/null || true
|
|
267
|
+
done
|
|
268
|
+
}
|
package/scripts/sync.sh
ADDED
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# viban sync - Core sync engine (provider-agnostic)
|
|
3
|
+
# Orchestrates two-way sync between viban board and external issue trackers.
|
|
4
|
+
#
|
|
5
|
+
# Environment variables (set by bin/viban cmd_sync):
|
|
6
|
+
# VIBAN_JSON - Path to viban.json
|
|
7
|
+
# VIBAN_DATA_DIR - Path to viban data directory
|
|
8
|
+
# VIBAN_PROVIDER - Provider name (e.g., "github")
|
|
9
|
+
# VIBAN_SCRIPT_DIR - Path to viban install directory
|
|
10
|
+
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
SYNC_JSON="${VIBAN_DATA_DIR}/sync.json"
|
|
14
|
+
PROVIDER_SCRIPT="${VIBAN_SCRIPT_DIR}/scripts/providers/${VIBAN_PROVIDER}.sh"
|
|
15
|
+
|
|
16
|
+
# ============================================================
|
|
17
|
+
# Provider Loading
|
|
18
|
+
# ============================================================
|
|
19
|
+
|
|
20
|
+
load_provider() {
|
|
21
|
+
if [[ ! -f "$PROVIDER_SCRIPT" ]]; then
|
|
22
|
+
echo "Error: Provider script not found: $PROVIDER_SCRIPT"
|
|
23
|
+
return 1
|
|
24
|
+
fi
|
|
25
|
+
source "$PROVIDER_SCRIPT"
|
|
26
|
+
|
|
27
|
+
# Validate provider interface
|
|
28
|
+
local required_funcs=(
|
|
29
|
+
provider_name provider_check_deps provider_check_auth
|
|
30
|
+
provider_detect_config provider_fetch_issues provider_create_issue
|
|
31
|
+
provider_update_issue provider_close_issue provider_ensure_labels
|
|
32
|
+
)
|
|
33
|
+
for func in "${required_funcs[@]}"; do
|
|
34
|
+
if ! declare -f "$func" &>/dev/null; then
|
|
35
|
+
echo "Error: Provider '${VIBAN_PROVIDER}' missing required function: $func"
|
|
36
|
+
return 1
|
|
37
|
+
fi
|
|
38
|
+
done
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# ============================================================
|
|
42
|
+
# Sync Metadata (sync.json)
|
|
43
|
+
# ============================================================
|
|
44
|
+
|
|
45
|
+
read_sync_meta() {
|
|
46
|
+
if [[ -f "$SYNC_JSON" ]]; then
|
|
47
|
+
cat "$SYNC_JSON"
|
|
48
|
+
else
|
|
49
|
+
echo '{}'
|
|
50
|
+
fi
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
write_sync_meta() {
|
|
54
|
+
local data="$1"
|
|
55
|
+
echo "$data" > "${SYNC_JSON}.tmp" && mv "${SYNC_JSON}.tmp" "$SYNC_JSON"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get_issue_meta() {
|
|
59
|
+
local viban_id="$1"
|
|
60
|
+
read_sync_meta | jq -r --arg id "$viban_id" '.issues[$id] // empty'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
set_issue_meta() {
|
|
64
|
+
local viban_id="$1" remote_id="$2" remote_updated="$3" viban_updated="$4"
|
|
65
|
+
local meta
|
|
66
|
+
meta=$(read_sync_meta)
|
|
67
|
+
meta=$(echo "$meta" | jq --arg vid "$viban_id" --arg rid "$remote_id" \
|
|
68
|
+
--arg ru "$remote_updated" --arg vu "$viban_updated" \
|
|
69
|
+
'.issues[$vid] = {remote_id: $rid, remote_updated_at: $ru, viban_updated_at: $vu}')
|
|
70
|
+
write_sync_meta "$meta"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# ============================================================
|
|
74
|
+
# Sync Init
|
|
75
|
+
# ============================================================
|
|
76
|
+
|
|
77
|
+
sync_init() {
|
|
78
|
+
local repo_override="$1"
|
|
79
|
+
|
|
80
|
+
echo "Initializing sync..."
|
|
81
|
+
|
|
82
|
+
# Check provider dependencies and auth
|
|
83
|
+
provider_check_deps || exit 1
|
|
84
|
+
provider_check_auth || exit 1
|
|
85
|
+
|
|
86
|
+
# Detect or use provided config
|
|
87
|
+
local config
|
|
88
|
+
if [[ -n "$repo_override" ]]; then
|
|
89
|
+
config="{\"repo\":\"$repo_override\"}"
|
|
90
|
+
else
|
|
91
|
+
config=$(provider_detect_config) || exit 1
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
local repo
|
|
95
|
+
repo=$(echo "$config" | jq -r '.repo')
|
|
96
|
+
echo "Detected $(provider_name) repo: $repo"
|
|
97
|
+
|
|
98
|
+
# Ensure required labels exist
|
|
99
|
+
echo "Ensuring labels..."
|
|
100
|
+
provider_ensure_labels "$repo"
|
|
101
|
+
|
|
102
|
+
# Initialize sync.json
|
|
103
|
+
local now
|
|
104
|
+
now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
105
|
+
write_sync_meta "$(jq -n \
|
|
106
|
+
--arg provider "$(provider_name)" \
|
|
107
|
+
--argjson config "$config" \
|
|
108
|
+
--arg now "$now" \
|
|
109
|
+
'{provider: $provider, provider_config: $config, last_sync_at: $now, issues: {}}')"
|
|
110
|
+
|
|
111
|
+
echo "Sync initialized for $(provider_name) ($repo)"
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# ============================================================
|
|
115
|
+
# Sync Pull (remote -> viban)
|
|
116
|
+
# ============================================================
|
|
117
|
+
|
|
118
|
+
sync_pull() {
|
|
119
|
+
local repo="$1" dry_run="${2:-false}"
|
|
120
|
+
local pulled=0 updated=0 unchanged=0
|
|
121
|
+
|
|
122
|
+
local remote_issues
|
|
123
|
+
remote_issues=$(provider_fetch_issues "$repo") || exit 1
|
|
124
|
+
|
|
125
|
+
local provider_prefix
|
|
126
|
+
provider_prefix="$(provider_name):"
|
|
127
|
+
|
|
128
|
+
local count
|
|
129
|
+
count=$(echo "$remote_issues" | jq 'length')
|
|
130
|
+
|
|
131
|
+
for i in $(seq 0 $((count - 1))); do
|
|
132
|
+
local issue
|
|
133
|
+
issue=$(echo "$remote_issues" | jq ".[$i]")
|
|
134
|
+
|
|
135
|
+
local remote_id title description status priority type remote_updated
|
|
136
|
+
remote_id=$(echo "$issue" | jq -r '.remote_id')
|
|
137
|
+
title=$(echo "$issue" | jq -r '.title')
|
|
138
|
+
description=$(echo "$issue" | jq -r '.description // ""')
|
|
139
|
+
status=$(echo "$issue" | jq -r '.status')
|
|
140
|
+
priority=$(echo "$issue" | jq -r '.priority // "P3"')
|
|
141
|
+
type=$(echo "$issue" | jq -r '.type // ""')
|
|
142
|
+
remote_updated=$(echo "$issue" | jq -r '.updated_at')
|
|
143
|
+
|
|
144
|
+
local ext_id="${provider_prefix}${remote_id}"
|
|
145
|
+
|
|
146
|
+
# Find existing viban card with this external_id
|
|
147
|
+
local viban_card
|
|
148
|
+
viban_card=$(jq -r --arg eid "$ext_id" \
|
|
149
|
+
'.issues[] | select(.external_id == $eid)' "$VIBAN_JSON" 2>/dev/null)
|
|
150
|
+
|
|
151
|
+
if [[ -z "$viban_card" || "$viban_card" == "null" ]]; then
|
|
152
|
+
# New remote issue -> import
|
|
153
|
+
if [[ "$dry_run" == "true" ]]; then
|
|
154
|
+
echo " <- ${ext_id} \"${title}\" (new card to create)"
|
|
155
|
+
else
|
|
156
|
+
# Use jq to add card directly
|
|
157
|
+
local now viban_id
|
|
158
|
+
now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
159
|
+
viban_id=$(jq -r '.next_id // (([.issues[].id] | max // 0) + 1)' "$VIBAN_JSON")
|
|
160
|
+
|
|
161
|
+
local type_val="null"
|
|
162
|
+
[[ -n "$type" && "$type" != "null" ]] && type_val="\"$type\""
|
|
163
|
+
|
|
164
|
+
jq --arg id "$viban_id" --arg title "$title" --arg desc "$description" \
|
|
165
|
+
--arg status "$status" --arg priority "$priority" \
|
|
166
|
+
--arg ext_id "$ext_id" --arg now "$now" --argjson type_val "$type_val" \
|
|
167
|
+
'.next_id = ((.next_id // 0) + 1) |
|
|
168
|
+
.issues += [{
|
|
169
|
+
id: ($id | tonumber),
|
|
170
|
+
title: $title,
|
|
171
|
+
description: $desc,
|
|
172
|
+
status: $status,
|
|
173
|
+
priority: $priority,
|
|
174
|
+
type: $type_val,
|
|
175
|
+
external_id: $ext_id,
|
|
176
|
+
attachments: [],
|
|
177
|
+
assigned_to: null,
|
|
178
|
+
created_at: $now,
|
|
179
|
+
updated_at: $now
|
|
180
|
+
}]' "$VIBAN_JSON" > "${VIBAN_JSON}.tmp" && mv "${VIBAN_JSON}.tmp" "$VIBAN_JSON"
|
|
181
|
+
|
|
182
|
+
set_issue_meta "$viban_id" "$remote_id" "$remote_updated" "$now"
|
|
183
|
+
echo " <- ${ext_id} \"${title}\" (new card created)"
|
|
184
|
+
fi
|
|
185
|
+
((pulled++))
|
|
186
|
+
else
|
|
187
|
+
# Existing card - check for changes
|
|
188
|
+
local viban_id viban_updated
|
|
189
|
+
viban_id=$(echo "$viban_card" | jq -r '.id')
|
|
190
|
+
viban_updated=$(echo "$viban_card" | jq -r '.updated_at')
|
|
191
|
+
|
|
192
|
+
# Get last known sync timestamps
|
|
193
|
+
local meta
|
|
194
|
+
meta=$(get_issue_meta "$viban_id")
|
|
195
|
+
|
|
196
|
+
if [[ -z "$meta" ]]; then
|
|
197
|
+
# First time seeing this linked card in sync - record and skip
|
|
198
|
+
set_issue_meta "$viban_id" "$remote_id" "$remote_updated" "$viban_updated"
|
|
199
|
+
echo " == ${ext_id} \"${title}\" (tracking started)"
|
|
200
|
+
((unchanged++))
|
|
201
|
+
continue
|
|
202
|
+
fi
|
|
203
|
+
|
|
204
|
+
local last_remote_updated last_viban_updated
|
|
205
|
+
last_remote_updated=$(echo "$meta" | jq -r '.remote_updated_at')
|
|
206
|
+
last_viban_updated=$(echo "$meta" | jq -r '.viban_updated_at')
|
|
207
|
+
|
|
208
|
+
local remote_changed=false viban_changed=false
|
|
209
|
+
[[ "$remote_updated" != "$last_remote_updated" ]] && remote_changed=true
|
|
210
|
+
[[ "$viban_updated" != "$last_viban_updated" ]] && viban_changed=true
|
|
211
|
+
|
|
212
|
+
if [[ "$remote_changed" == "false" && "$viban_changed" == "false" ]]; then
|
|
213
|
+
echo " == ${ext_id} \"${title}\" (no changes)"
|
|
214
|
+
((unchanged++))
|
|
215
|
+
elif [[ "$remote_changed" == "true" && "$viban_changed" == "false" ]]; then
|
|
216
|
+
# Only remote changed -> pull
|
|
217
|
+
if [[ "$dry_run" == "true" ]]; then
|
|
218
|
+
echo " <- ${ext_id} \"${title}\" (remote updated, will pull)"
|
|
219
|
+
else
|
|
220
|
+
local now
|
|
221
|
+
now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
222
|
+
|
|
223
|
+
local type_val="null"
|
|
224
|
+
[[ -n "$type" && "$type" != "null" ]] && type_val="\"$type\""
|
|
225
|
+
|
|
226
|
+
jq --argjson vid "$viban_id" --arg title "$title" --arg desc "$description" \
|
|
227
|
+
--arg status "$status" --arg priority "$priority" \
|
|
228
|
+
--arg now "$now" --argjson type_val "$type_val" \
|
|
229
|
+
'(.issues[] | select((.id | tonumber) == $vid)) |=
|
|
230
|
+
. + {title: $title, description: $desc, status: $status,
|
|
231
|
+
priority: $priority, type: $type_val, updated_at: $now}' \
|
|
232
|
+
"$VIBAN_JSON" > "${VIBAN_JSON}.tmp" && mv "${VIBAN_JSON}.tmp" "$VIBAN_JSON"
|
|
233
|
+
|
|
234
|
+
set_issue_meta "$viban_id" "$remote_id" "$remote_updated" "$now"
|
|
235
|
+
echo " <- ${ext_id} \"${title}\" (pulled remote changes)"
|
|
236
|
+
fi
|
|
237
|
+
((updated++))
|
|
238
|
+
elif [[ "$remote_changed" == "false" && "$viban_changed" == "true" ]]; then
|
|
239
|
+
# Only viban changed -> will be handled in push phase
|
|
240
|
+
echo " == ${ext_id} \"${title}\" (local changes, will push)"
|
|
241
|
+
((unchanged++))
|
|
242
|
+
else
|
|
243
|
+
# Both changed -> conflict resolution (remote wins by default)
|
|
244
|
+
if [[ "$dry_run" == "true" ]]; then
|
|
245
|
+
echo " !! ${ext_id} \"${title}\" (conflict: remote wins)"
|
|
246
|
+
else
|
|
247
|
+
local now
|
|
248
|
+
now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
249
|
+
|
|
250
|
+
local type_val="null"
|
|
251
|
+
[[ -n "$type" && "$type" != "null" ]] && type_val="\"$type\""
|
|
252
|
+
|
|
253
|
+
jq --argjson vid "$viban_id" --arg title "$title" --arg desc "$description" \
|
|
254
|
+
--arg status "$status" --arg priority "$priority" \
|
|
255
|
+
--arg now "$now" --argjson type_val "$type_val" \
|
|
256
|
+
'(.issues[] | select((.id | tonumber) == $vid)) |=
|
|
257
|
+
. + {title: $title, description: $desc, status: $status,
|
|
258
|
+
priority: $priority, type: $type_val, updated_at: $now}' \
|
|
259
|
+
"$VIBAN_JSON" > "${VIBAN_JSON}.tmp" && mv "${VIBAN_JSON}.tmp" "$VIBAN_JSON"
|
|
260
|
+
|
|
261
|
+
set_issue_meta "$viban_id" "$remote_id" "$remote_updated" "$now"
|
|
262
|
+
echo " !! ${ext_id} \"${title}\" (conflict: remote wins)"
|
|
263
|
+
fi
|
|
264
|
+
((pulled++))
|
|
265
|
+
fi
|
|
266
|
+
fi
|
|
267
|
+
done
|
|
268
|
+
|
|
269
|
+
echo "Pull: $pulled new/conflict, $updated updated, $unchanged unchanged"
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
# ============================================================
|
|
273
|
+
# Sync Push (viban -> remote)
|
|
274
|
+
# ============================================================
|
|
275
|
+
|
|
276
|
+
sync_push() {
|
|
277
|
+
local repo="$1" dry_run="${2:-false}" push_new="${3:-false}"
|
|
278
|
+
local pushed=0 closed=0 unchanged=0
|
|
279
|
+
|
|
280
|
+
local provider_prefix
|
|
281
|
+
provider_prefix="$(provider_name):"
|
|
282
|
+
|
|
283
|
+
# Get all viban cards with this provider's external_id
|
|
284
|
+
local linked_cards
|
|
285
|
+
linked_cards=$(jq -r --arg prefix "$provider_prefix" \
|
|
286
|
+
'[.issues[] | select(.external_id != null and (.external_id | startswith($prefix)))]' \
|
|
287
|
+
"$VIBAN_JSON")
|
|
288
|
+
|
|
289
|
+
local count
|
|
290
|
+
count=$(echo "$linked_cards" | jq 'length')
|
|
291
|
+
|
|
292
|
+
for i in $(seq 0 $((count - 1))); do
|
|
293
|
+
local card
|
|
294
|
+
card=$(echo "$linked_cards" | jq ".[$i]")
|
|
295
|
+
|
|
296
|
+
local viban_id ext_id remote_id title status viban_updated
|
|
297
|
+
viban_id=$(echo "$card" | jq -r '.id')
|
|
298
|
+
ext_id=$(echo "$card" | jq -r '.external_id')
|
|
299
|
+
remote_id="${ext_id#${provider_prefix}}"
|
|
300
|
+
title=$(echo "$card" | jq -r '.title')
|
|
301
|
+
status=$(echo "$card" | jq -r '.status')
|
|
302
|
+
viban_updated=$(echo "$card" | jq -r '.updated_at')
|
|
303
|
+
|
|
304
|
+
# Get last known sync timestamps
|
|
305
|
+
local meta
|
|
306
|
+
meta=$(get_issue_meta "$viban_id")
|
|
307
|
+
if [[ -z "$meta" ]]; then
|
|
308
|
+
((unchanged++))
|
|
309
|
+
continue
|
|
310
|
+
fi
|
|
311
|
+
|
|
312
|
+
local last_viban_updated
|
|
313
|
+
last_viban_updated=$(echo "$meta" | jq -r '.viban_updated_at')
|
|
314
|
+
|
|
315
|
+
if [[ "$viban_updated" == "$last_viban_updated" ]]; then
|
|
316
|
+
((unchanged++))
|
|
317
|
+
continue
|
|
318
|
+
fi
|
|
319
|
+
|
|
320
|
+
# Viban card changed since last sync -> push
|
|
321
|
+
if [[ "$dry_run" == "true" ]]; then
|
|
322
|
+
echo " -> ${ext_id} \"${title}\" (will push local changes)"
|
|
323
|
+
((pushed++))
|
|
324
|
+
else
|
|
325
|
+
echo "$card" | jq '{
|
|
326
|
+
title: .title,
|
|
327
|
+
description: (.description // ""),
|
|
328
|
+
status: .status,
|
|
329
|
+
priority: (.priority // "P3"),
|
|
330
|
+
type: (.type // "")
|
|
331
|
+
}' | provider_update_issue "$repo" "$remote_id"
|
|
332
|
+
|
|
333
|
+
local now remote_updated
|
|
334
|
+
now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
335
|
+
# Fetch updated remote timestamp
|
|
336
|
+
remote_updated="$now"
|
|
337
|
+
|
|
338
|
+
set_issue_meta "$viban_id" "$remote_id" "$remote_updated" "$viban_updated"
|
|
339
|
+
echo " -> ${ext_id} \"${title}\" (pushed)"
|
|
340
|
+
((pushed++))
|
|
341
|
+
fi
|
|
342
|
+
done
|
|
343
|
+
|
|
344
|
+
# Handle push-new: viban cards without external_id
|
|
345
|
+
if [[ "$push_new" == "true" ]]; then
|
|
346
|
+
local unlinked_cards
|
|
347
|
+
unlinked_cards=$(jq -r \
|
|
348
|
+
'[.issues[] | select(.external_id == null or .external_id == "")]' \
|
|
349
|
+
"$VIBAN_JSON")
|
|
350
|
+
|
|
351
|
+
local unlinked_count
|
|
352
|
+
unlinked_count=$(echo "$unlinked_cards" | jq 'length')
|
|
353
|
+
|
|
354
|
+
for i in $(seq 0 $((unlinked_count - 1))); do
|
|
355
|
+
local card
|
|
356
|
+
card=$(echo "$unlinked_cards" | jq ".[$i]")
|
|
357
|
+
|
|
358
|
+
local viban_id title
|
|
359
|
+
viban_id=$(echo "$card" | jq -r '.id')
|
|
360
|
+
title=$(echo "$card" | jq -r '.title')
|
|
361
|
+
|
|
362
|
+
if [[ "$dry_run" == "true" ]]; then
|
|
363
|
+
echo " -> (new) #${viban_id} \"${title}\" (will create remote issue)"
|
|
364
|
+
((pushed++))
|
|
365
|
+
else
|
|
366
|
+
local new_remote_id
|
|
367
|
+
new_remote_id=$(echo "$card" | jq '{
|
|
368
|
+
title: .title,
|
|
369
|
+
description: (.description // ""),
|
|
370
|
+
status: .status,
|
|
371
|
+
priority: (.priority // "P3"),
|
|
372
|
+
type: (.type // "")
|
|
373
|
+
}' | provider_create_issue "$repo") || {
|
|
374
|
+
echo " !! Failed to create remote issue for #${viban_id}"
|
|
375
|
+
continue
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
local ext_id="${provider_prefix}${new_remote_id}"
|
|
379
|
+
local now
|
|
380
|
+
now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
381
|
+
|
|
382
|
+
# Update viban card with external_id
|
|
383
|
+
jq --argjson vid "$viban_id" --arg eid "$ext_id" --arg now "$now" \
|
|
384
|
+
'(.issues[] | select((.id | tonumber) == $vid)) |= . + {external_id: $eid, updated_at: $now}' \
|
|
385
|
+
"$VIBAN_JSON" > "${VIBAN_JSON}.tmp" && mv "${VIBAN_JSON}.tmp" "$VIBAN_JSON"
|
|
386
|
+
|
|
387
|
+
set_issue_meta "$viban_id" "$new_remote_id" "$now" "$now"
|
|
388
|
+
echo " -> ${ext_id} #${viban_id} \"${title}\" (created remote issue)"
|
|
389
|
+
((pushed++))
|
|
390
|
+
fi
|
|
391
|
+
done
|
|
392
|
+
fi
|
|
393
|
+
|
|
394
|
+
echo "Push: $pushed pushed, $closed closed, $unchanged unchanged"
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
# ============================================================
|
|
398
|
+
# Sync Status
|
|
399
|
+
# ============================================================
|
|
400
|
+
|
|
401
|
+
sync_status() {
|
|
402
|
+
if [[ ! -f "$SYNC_JSON" ]]; then
|
|
403
|
+
echo "Sync not configured. Run: viban sync --init"
|
|
404
|
+
return 1
|
|
405
|
+
fi
|
|
406
|
+
|
|
407
|
+
local meta
|
|
408
|
+
meta=$(read_sync_meta)
|
|
409
|
+
|
|
410
|
+
local provider repo last_sync tracked_count
|
|
411
|
+
provider=$(echo "$meta" | jq -r '.provider')
|
|
412
|
+
repo=$(echo "$meta" | jq -r '.provider_config.repo // "unknown"')
|
|
413
|
+
last_sync=$(echo "$meta" | jq -r '.last_sync_at // "never"')
|
|
414
|
+
tracked_count=$(echo "$meta" | jq '.issues | length')
|
|
415
|
+
|
|
416
|
+
local provider_prefix="${provider}:"
|
|
417
|
+
local total_cards linked_cards unlinked_cards
|
|
418
|
+
total_cards=$(jq '.issues | length' "$VIBAN_JSON")
|
|
419
|
+
linked_cards=$(jq --arg prefix "$provider_prefix" \
|
|
420
|
+
'[.issues[] | select(.external_id != null and (.external_id | startswith($prefix)))] | length' \
|
|
421
|
+
"$VIBAN_JSON")
|
|
422
|
+
unlinked_cards=$((total_cards - linked_cards))
|
|
423
|
+
|
|
424
|
+
echo "Sync status:"
|
|
425
|
+
echo " Provider: $provider ($repo)"
|
|
426
|
+
echo " Last sync: $last_sync"
|
|
427
|
+
echo " Tracked: $tracked_count issues"
|
|
428
|
+
echo " Linked cards: $linked_cards"
|
|
429
|
+
echo " Unlinked cards: $unlinked_cards"
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
# ============================================================
|
|
433
|
+
# Full Sync
|
|
434
|
+
# ============================================================
|
|
435
|
+
|
|
436
|
+
sync_full() {
|
|
437
|
+
local repo="$1" dry_run="${2:-false}" push_new="${3:-false}"
|
|
438
|
+
|
|
439
|
+
echo "Syncing with $(provider_name) ($repo)..."
|
|
440
|
+
sync_pull "$repo" "$dry_run"
|
|
441
|
+
sync_push "$repo" "$dry_run" "$push_new"
|
|
442
|
+
|
|
443
|
+
if [[ "$dry_run" == "false" ]]; then
|
|
444
|
+
# Update last_sync_at
|
|
445
|
+
local now
|
|
446
|
+
now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
447
|
+
local meta
|
|
448
|
+
meta=$(read_sync_meta)
|
|
449
|
+
meta=$(echo "$meta" | jq --arg now "$now" '.last_sync_at = $now')
|
|
450
|
+
write_sync_meta "$meta"
|
|
451
|
+
fi
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
# ============================================================
|
|
455
|
+
# Main CLI Parser
|
|
456
|
+
# ============================================================
|
|
457
|
+
|
|
458
|
+
main() {
|
|
459
|
+
local action="full"
|
|
460
|
+
local dry_run=false
|
|
461
|
+
local push_new=false
|
|
462
|
+
local pull_only=false
|
|
463
|
+
local push_only=false
|
|
464
|
+
local repo_override=""
|
|
465
|
+
|
|
466
|
+
while [[ $# -gt 0 ]]; do
|
|
467
|
+
case "$1" in
|
|
468
|
+
--init) action="init"; shift ;;
|
|
469
|
+
--status) action="status"; shift ;;
|
|
470
|
+
--dry-run) dry_run=true; shift ;;
|
|
471
|
+
--push-new) push_new=true; shift ;;
|
|
472
|
+
--pull-only) pull_only=true; shift ;;
|
|
473
|
+
--push-only) push_only=true; shift ;;
|
|
474
|
+
--repo) repo_override="$2"; shift 2 ;;
|
|
475
|
+
--provider) shift 2 ;; # Already handled by bin/viban
|
|
476
|
+
*) echo "Unknown option: $1"; exit 1 ;;
|
|
477
|
+
esac
|
|
478
|
+
done
|
|
479
|
+
|
|
480
|
+
# Load provider
|
|
481
|
+
load_provider || exit 1
|
|
482
|
+
|
|
483
|
+
# Handle init
|
|
484
|
+
if [[ "$action" == "init" ]]; then
|
|
485
|
+
sync_init "$repo_override"
|
|
486
|
+
return
|
|
487
|
+
fi
|
|
488
|
+
|
|
489
|
+
# Handle status
|
|
490
|
+
if [[ "$action" == "status" ]]; then
|
|
491
|
+
sync_status
|
|
492
|
+
return
|
|
493
|
+
fi
|
|
494
|
+
|
|
495
|
+
# For sync operations, check that sync is configured
|
|
496
|
+
if [[ ! -f "$SYNC_JSON" ]]; then
|
|
497
|
+
echo "Sync not configured. Run: viban sync --init"
|
|
498
|
+
exit 1
|
|
499
|
+
fi
|
|
500
|
+
|
|
501
|
+
# Check provider deps and auth
|
|
502
|
+
provider_check_deps || exit 1
|
|
503
|
+
provider_check_auth || exit 1
|
|
504
|
+
|
|
505
|
+
# Get repo from sync config or override
|
|
506
|
+
local repo
|
|
507
|
+
if [[ -n "$repo_override" ]]; then
|
|
508
|
+
repo="$repo_override"
|
|
509
|
+
else
|
|
510
|
+
repo=$(jq -r '.provider_config.repo' "$SYNC_JSON")
|
|
511
|
+
fi
|
|
512
|
+
|
|
513
|
+
if [[ -z "$repo" || "$repo" == "null" ]]; then
|
|
514
|
+
echo "Error: No repo configured. Run: viban sync --init"
|
|
515
|
+
exit 1
|
|
516
|
+
fi
|
|
517
|
+
|
|
518
|
+
# Execute sync
|
|
519
|
+
if [[ "$pull_only" == "true" ]]; then
|
|
520
|
+
echo "Pulling from $(provider_name) ($repo)..."
|
|
521
|
+
sync_pull "$repo" "$dry_run"
|
|
522
|
+
elif [[ "$push_only" == "true" ]]; then
|
|
523
|
+
echo "Pushing to $(provider_name) ($repo)..."
|
|
524
|
+
sync_push "$repo" "$dry_run" "$push_new"
|
|
525
|
+
else
|
|
526
|
+
sync_full "$repo" "$dry_run" "$push_new"
|
|
527
|
+
fi
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
main "$@"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sync
|
|
3
|
+
description: "Sync viban board with external issue tracker (GitHub, Jira, etc.)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /sync - External Issue Tracker Sync
|
|
7
|
+
|
|
8
|
+
Sync the viban board with an external issue tracker. Currently supports GitHub Issues via `gh` CLI.
|
|
9
|
+
|
|
10
|
+
> **Principle**: Show what will happen before doing it. Never sync without user confirmation.
|
|
11
|
+
|
|
12
|
+
## Input
|
|
13
|
+
|
|
14
|
+
**User Input**: `$ARGUMENTS`
|
|
15
|
+
|
|
16
|
+
## Step 1: Check Sync Configuration
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Check if sync is already configured
|
|
20
|
+
if [ -f "$(git rev-parse --git-common-dir 2>/dev/null || echo .viban)/sync.json" ]; then
|
|
21
|
+
echo "Sync configured"
|
|
22
|
+
viban sync --status
|
|
23
|
+
else
|
|
24
|
+
echo "Sync not configured"
|
|
25
|
+
fi
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- If **not configured**, proceed to Step 2
|
|
29
|
+
- If **configured**, skip to Step 3
|
|
30
|
+
|
|
31
|
+
## Step 2: Initialize Sync
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
viban sync --init
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This will:
|
|
38
|
+
- Auto-detect the provider from git remote (defaults to GitHub)
|
|
39
|
+
- Check that `gh` CLI is installed and authenticated
|
|
40
|
+
- Create required labels on the remote repo
|
|
41
|
+
- Initialize `sync.json` metadata
|
|
42
|
+
|
|
43
|
+
If initialization fails, report the error and suggest fixes (install `gh`, run `gh auth login`, etc.).
|
|
44
|
+
|
|
45
|
+
## Step 3: Preview Changes (Dry Run)
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
viban sync --dry-run
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Show the user what will happen:
|
|
52
|
+
- `<-` Issues to pull from remote
|
|
53
|
+
- `->` Cards to push to remote
|
|
54
|
+
- `==` Unchanged items
|
|
55
|
+
- `!!` Conflicts (and resolution strategy)
|
|
56
|
+
|
|
57
|
+
## Step 4: Confirm and Sync
|
|
58
|
+
|
|
59
|
+
Ask the user for confirmation using AskUserQuestion:
|
|
60
|
+
|
|
61
|
+
- header: "Sync"
|
|
62
|
+
- question: "Apply these sync changes?"
|
|
63
|
+
- options:
|
|
64
|
+
- "Yes, sync now"
|
|
65
|
+
- "Sync and push new local cards too (--push-new)"
|
|
66
|
+
- "Pull only (remote -> local)"
|
|
67
|
+
- "Cancel"
|
|
68
|
+
- multiSelect: false
|
|
69
|
+
|
|
70
|
+
Based on the answer:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Yes, sync now
|
|
74
|
+
viban sync
|
|
75
|
+
|
|
76
|
+
# With push-new
|
|
77
|
+
viban sync --push-new
|
|
78
|
+
|
|
79
|
+
# Pull only
|
|
80
|
+
viban sync --pull-only
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Step 5: Report Results
|
|
84
|
+
|
|
85
|
+
Show the sync summary:
|
|
86
|
+
```
|
|
87
|
+
Sync complete:
|
|
88
|
+
Pulled: N new/updated cards from remote
|
|
89
|
+
Pushed: N cards to remote
|
|
90
|
+
Conflicts: N (resolved by: remote wins)
|
|
91
|
+
Unchanged: N
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Notes
|
|
95
|
+
|
|
96
|
+
- **First sync imports all open issues** as backlog cards with `github:N` external IDs
|
|
97
|
+
- **Conflicts**: when both sides changed, remote wins by default (with warning)
|
|
98
|
+
- **Closed issues**: remote closed issues move viban card to `review` status
|
|
99
|
+
- **Done cards**: `viban done` then sync closes the remote issue
|
|
100
|
+
- **New local cards** are NOT pushed unless `--push-new` is specified (local-first default)
|