@da1z/chop 0.0.1 → 0.0.2
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/dist/index.js +8401 -0
- package/package.json +6 -3
- package/.claude/rules/use-bun-instead-of-node-vite-npm-pnpm.md +0 -109
- package/.claude/settings.local.json +0 -12
- package/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +0 -111
- package/.devcontainer/Dockerfile +0 -102
- package/.devcontainer/devcontainer.json +0 -58
- package/.devcontainer/init-firewall.sh +0 -137
- package/.github/workflows/publish.yml +0 -76
- package/CLAUDE.md +0 -44
- package/index.ts +0 -2
- package/loop.sh +0 -206
- package/specs/chop.md +0 -313
- package/src/commands/add.ts +0 -74
- package/src/commands/archive.ts +0 -72
- package/src/commands/completion.ts +0 -232
- package/src/commands/done.ts +0 -38
- package/src/commands/edit.ts +0 -228
- package/src/commands/init.ts +0 -72
- package/src/commands/list.ts +0 -48
- package/src/commands/move.ts +0 -92
- package/src/commands/pop.ts +0 -45
- package/src/commands/purge.ts +0 -41
- package/src/commands/show.ts +0 -32
- package/src/commands/status.ts +0 -43
- package/src/config/paths.ts +0 -61
- package/src/errors.ts +0 -56
- package/src/index.ts +0 -41
- package/src/models/id-generator.ts +0 -39
- package/src/models/task.ts +0 -98
- package/src/storage/file-lock.ts +0 -124
- package/src/storage/storage-resolver.ts +0 -63
- package/src/storage/task-store.ts +0 -173
- package/src/types.ts +0 -42
- package/src/utils/display.ts +0 -139
- package/src/utils/git.ts +0 -80
- package/src/utils/prompts.ts +0 -88
- package/tests/errors.test.ts +0 -86
- package/tests/models/id-generator.test.ts +0 -46
- package/tests/models/task.test.ts +0 -186
- package/tests/storage/file-lock.test.ts +0 -152
- package/tsconfig.json +0 -9
package/loop.sh
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
|
|
3
|
-
# loop - Execute Claude Code N times to work on tasks with realtime progress
|
|
4
|
-
# Usage: ./loop [N]
|
|
5
|
-
# N - Number of times to run Claude Code (default: 1)
|
|
6
|
-
# Requires: bash 3.0+, jq
|
|
7
|
-
|
|
8
|
-
set -e
|
|
9
|
-
set -o pipefail
|
|
10
|
-
|
|
11
|
-
# Handle interrupt signals gracefully
|
|
12
|
-
cleanup() {
|
|
13
|
-
echo -e "\n${YELLOW:-}Interrupted. Exiting...${NC:-}"
|
|
14
|
-
exit 130
|
|
15
|
-
}
|
|
16
|
-
trap cleanup INT TERM
|
|
17
|
-
|
|
18
|
-
N=${1:-1}
|
|
19
|
-
|
|
20
|
-
if ! [[ "$N" =~ ^[0-9]+$ ]] || [ "$N" -lt 1 ]; then
|
|
21
|
-
echo "Error: N must be a positive integer"
|
|
22
|
-
echo "Usage: ./loop [N]"
|
|
23
|
-
exit 1
|
|
24
|
-
fi
|
|
25
|
-
|
|
26
|
-
# Check for jq dependency
|
|
27
|
-
if ! command -v jq &>/dev/null; then
|
|
28
|
-
echo "Error: jq is required for parsing JSON output"
|
|
29
|
-
echo "Install with your package manager:"
|
|
30
|
-
echo " macOS: brew install jq"
|
|
31
|
-
echo " Ubuntu/Debian: sudo apt install jq"
|
|
32
|
-
exit 1
|
|
33
|
-
fi
|
|
34
|
-
|
|
35
|
-
PROMPT='Execute ch pop to get the task you need to work on. Implement the task and make sure all tests pass and there are no TypeScript errors. Before marking the task as done, use @agent-general-purpose to perform a code review of your changes - ensure the subagent is satisfied with the code quality, all tests pass, and there are no TypeScript errors. Only after the code review is approved, mark the task as done using ch done <id> and commit changes using pk branch create -am <message>.'
|
|
36
|
-
|
|
37
|
-
# ANSI color codes
|
|
38
|
-
BLUE='\033[0;34m'
|
|
39
|
-
GREEN='\033[0;32m'
|
|
40
|
-
YELLOW='\033[0;33m'
|
|
41
|
-
CYAN='\033[0;36m'
|
|
42
|
-
GRAY='\033[0;90m'
|
|
43
|
-
BOLD='\033[1m'
|
|
44
|
-
NC='\033[0m' # No Color
|
|
45
|
-
|
|
46
|
-
# Parse and display stream-json output with realtime progress
|
|
47
|
-
parse_stream() {
|
|
48
|
-
local turn=0
|
|
49
|
-
|
|
50
|
-
while IFS= read -r line; do
|
|
51
|
-
# Skip empty lines
|
|
52
|
-
[[ -z "$line" ]] && continue
|
|
53
|
-
|
|
54
|
-
# Parse JSON type
|
|
55
|
-
type=$(echo "$line" | jq -r '.type // empty' 2>/dev/null)
|
|
56
|
-
[[ -z "$type" ]] && continue
|
|
57
|
-
|
|
58
|
-
case "$type" in
|
|
59
|
-
system)
|
|
60
|
-
subtype=$(echo "$line" | jq -r '.subtype // empty')
|
|
61
|
-
if [[ "$subtype" == "init" ]]; then
|
|
62
|
-
model=$(echo "$line" | jq -r '.model // "unknown"')
|
|
63
|
-
session_id=$(echo "$line" | jq -r '.session_id // "unknown"')
|
|
64
|
-
echo -e "${GRAY}Session: ${session_id:0:8}... | Model: $model${NC}"
|
|
65
|
-
fi
|
|
66
|
-
;;
|
|
67
|
-
|
|
68
|
-
assistant)
|
|
69
|
-
turn=$((turn + 1))
|
|
70
|
-
# Check for tool use or text response
|
|
71
|
-
content_type=$(echo "$line" | jq -r '.message.content[0].type // empty')
|
|
72
|
-
|
|
73
|
-
if [[ "$content_type" == "tool_use" ]]; then
|
|
74
|
-
tool_name=$(echo "$line" | jq -r '.message.content[0].name // "unknown"')
|
|
75
|
-
|
|
76
|
-
# Get tool-specific details
|
|
77
|
-
case "$tool_name" in
|
|
78
|
-
Bash)
|
|
79
|
-
desc=$(echo "$line" | jq -r '.message.content[0].input.description // empty')
|
|
80
|
-
cmd=$(echo "$line" | jq -r '.message.content[0].input.command // empty')
|
|
81
|
-
|
|
82
|
-
# Always show the command
|
|
83
|
-
if [[ -n "$desc" ]]; then
|
|
84
|
-
echo -e "${CYAN}[$turn]${NC} ${YELLOW}⚡ $tool_name${NC}: $desc"
|
|
85
|
-
echo -e " ${GRAY}└─ $ ${cmd}${NC}"
|
|
86
|
-
else
|
|
87
|
-
echo -e "${CYAN}[$turn]${NC} ${YELLOW}⚡ $tool_name${NC}: ${GRAY}$ ${cmd}${NC}"
|
|
88
|
-
fi
|
|
89
|
-
;;
|
|
90
|
-
Read)
|
|
91
|
-
file=$(echo "$line" | jq -r '.message.content[0].input.file_path // empty')
|
|
92
|
-
echo -e "${CYAN}[$turn]${NC} ${YELLOW}📖 $tool_name${NC}: ${GRAY}${file##*/}${NC}"
|
|
93
|
-
;;
|
|
94
|
-
Edit | Write)
|
|
95
|
-
file=$(echo "$line" | jq -r '.message.content[0].input.file_path // empty')
|
|
96
|
-
echo -e "${CYAN}[$turn]${NC} ${YELLOW}✏️ $tool_name${NC}: ${GRAY}${file##*/}${NC}"
|
|
97
|
-
;;
|
|
98
|
-
Glob)
|
|
99
|
-
pattern=$(echo "$line" | jq -r '.message.content[0].input.pattern // empty')
|
|
100
|
-
echo -e "${CYAN}[$turn]${NC} ${YELLOW}🔍 $tool_name${NC}: ${GRAY}$pattern${NC}"
|
|
101
|
-
;;
|
|
102
|
-
Grep)
|
|
103
|
-
pattern=$(echo "$line" | jq -r '.message.content[0].input.pattern // empty')
|
|
104
|
-
echo -e "${CYAN}[$turn]${NC} ${YELLOW}🔎 $tool_name${NC}: ${GRAY}$pattern${NC}"
|
|
105
|
-
;;
|
|
106
|
-
Task)
|
|
107
|
-
desc=$(echo "$line" | jq -r '.message.content[0].input.description // empty')
|
|
108
|
-
agent_type=$(echo "$line" | jq -r '.message.content[0].input.subagent_type // empty')
|
|
109
|
-
echo -e "${CYAN}[$turn]${NC} ${YELLOW}🤖 $tool_name${NC}: ${GRAY}$agent_type - $desc${NC}"
|
|
110
|
-
;;
|
|
111
|
-
TodoWrite)
|
|
112
|
-
echo -e "${CYAN}[$turn]${NC} ${YELLOW}📝 $tool_name${NC}: ${GRAY}Updating task list${NC}"
|
|
113
|
-
;;
|
|
114
|
-
WebSearch)
|
|
115
|
-
query=$(echo "$line" | jq -r '.message.content[0].input.query // empty')
|
|
116
|
-
echo -e "${CYAN}[$turn]${NC} ${YELLOW}🌐 $tool_name${NC}: ${GRAY}$query${NC}"
|
|
117
|
-
;;
|
|
118
|
-
WebFetch)
|
|
119
|
-
url=$(echo "$line" | jq -r '.message.content[0].input.url // empty')
|
|
120
|
-
echo -e "${CYAN}[$turn]${NC} ${YELLOW}🌐 $tool_name${NC}: ${GRAY}$url${NC}"
|
|
121
|
-
;;
|
|
122
|
-
LSP)
|
|
123
|
-
operation=$(echo "$line" | jq -r '.message.content[0].input.operation // empty')
|
|
124
|
-
file=$(echo "$line" | jq -r '.message.content[0].input.filePath // empty')
|
|
125
|
-
echo -e "${CYAN}[$turn]${NC} ${YELLOW}🔗 $tool_name${NC}: ${GRAY}$operation on ${file##*/}${NC}"
|
|
126
|
-
;;
|
|
127
|
-
*)
|
|
128
|
-
echo -e "${CYAN}[$turn]${NC} ${YELLOW}🔧 $tool_name${NC}"
|
|
129
|
-
;;
|
|
130
|
-
esac
|
|
131
|
-
elif [[ "$content_type" == "text" ]]; then
|
|
132
|
-
# Show a brief snippet of text response
|
|
133
|
-
text=$(echo "$line" | jq -r '.message.content[0].text // empty')
|
|
134
|
-
if [[ -n "$text" ]]; then
|
|
135
|
-
if [[ ${#text} -gt 80 ]]; then
|
|
136
|
-
echo -e "${CYAN}[$turn]${NC} ${BLUE}💬${NC} ${GRAY}${text:0:80}...${NC}"
|
|
137
|
-
else
|
|
138
|
-
echo -e "${CYAN}[$turn]${NC} ${BLUE}💬${NC} ${GRAY}${text}${NC}"
|
|
139
|
-
fi
|
|
140
|
-
fi
|
|
141
|
-
fi
|
|
142
|
-
;;
|
|
143
|
-
|
|
144
|
-
user)
|
|
145
|
-
# Tool result - show brief status
|
|
146
|
-
is_error=$(echo "$line" | jq -r '.message.content[0].is_error // false')
|
|
147
|
-
if [[ "$is_error" == "true" ]]; then
|
|
148
|
-
echo -e " ${GRAY}└─ ❌ Error${NC}"
|
|
149
|
-
fi
|
|
150
|
-
;;
|
|
151
|
-
|
|
152
|
-
result)
|
|
153
|
-
subtype=$(echo "$line" | jq -r '.subtype // empty')
|
|
154
|
-
duration_ms=$(echo "$line" | jq -r '.duration_ms // 0')
|
|
155
|
-
num_turns=$(echo "$line" | jq -r '.num_turns // 0')
|
|
156
|
-
cost=$(echo "$line" | jq -r '.total_cost_usd // 0')
|
|
157
|
-
|
|
158
|
-
# Convert ms to readable format
|
|
159
|
-
duration_sec=$((duration_ms / 1000))
|
|
160
|
-
duration_min=$((duration_sec / 60))
|
|
161
|
-
duration_remainder=$((duration_sec % 60))
|
|
162
|
-
|
|
163
|
-
if [[ "$duration_min" -gt 0 ]]; then
|
|
164
|
-
duration_str="${duration_min}m ${duration_remainder}s"
|
|
165
|
-
else
|
|
166
|
-
duration_str="${duration_sec}s"
|
|
167
|
-
fi
|
|
168
|
-
|
|
169
|
-
# Format cost
|
|
170
|
-
cost_str=$(printf "%.4f" "$cost")
|
|
171
|
-
|
|
172
|
-
echo ""
|
|
173
|
-
if [[ "$subtype" == "success" ]]; then
|
|
174
|
-
echo -e "${GREEN}✅ Completed${NC} | ${GRAY}Turns: $num_turns | Duration: $duration_str | Cost: \$$cost_str${NC}"
|
|
175
|
-
else
|
|
176
|
-
echo -e "${YELLOW}⚠️ Finished with status: $subtype${NC} | ${GRAY}Turns: $num_turns | Duration: $duration_str | Cost: \$$cost_str${NC}"
|
|
177
|
-
fi
|
|
178
|
-
;;
|
|
179
|
-
esac
|
|
180
|
-
done
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
echo -e "${BOLD}Starting loop: running Claude Code $N time(s)${NC}"
|
|
184
|
-
|
|
185
|
-
for ((i = 1; i <= N; i++)); do
|
|
186
|
-
echo ""
|
|
187
|
-
echo -e "${BOLD}===========================================${NC}"
|
|
188
|
-
echo -e "${BOLD}Run $i of $N${NC}"
|
|
189
|
-
echo -e "${BOLD}===========================================${NC}"
|
|
190
|
-
echo ""
|
|
191
|
-
|
|
192
|
-
if [ "$N" -eq 1 ]; then
|
|
193
|
-
# Interactive mode for single run - no streaming
|
|
194
|
-
claude "$PROMPT"
|
|
195
|
-
else
|
|
196
|
-
# Non-interactive mode with streaming progress
|
|
197
|
-
# Use script to provide pseudo-TTY since docker sandbox requires TTY
|
|
198
|
-
claude -p "$PROMPT" --dangerously-skip-permissions --output-format stream-json --verbose | parse_stream
|
|
199
|
-
fi
|
|
200
|
-
|
|
201
|
-
echo ""
|
|
202
|
-
echo -e "${GREEN}Run $i completed${NC}"
|
|
203
|
-
done
|
|
204
|
-
|
|
205
|
-
echo ""
|
|
206
|
-
echo -e "${BOLD}Loop finished: completed $N run(s)${NC}"
|
package/specs/chop.md
DELETED
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
# Chop CLI Specification
|
|
2
|
-
|
|
3
|
-
A simple, file-based task management CLI tool designed for developers working across multiple repositories.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
**Name:** `chop` (short alias: `ch`)
|
|
8
|
-
**Purpose:** Queue-based task management with dependency support, optimized for multi-process concurrent access.
|
|
9
|
-
|
|
10
|
-
## Core Concepts
|
|
11
|
-
|
|
12
|
-
### Task Model
|
|
13
|
-
|
|
14
|
-
Each task contains:
|
|
15
|
-
- **ID**: Hybrid format - short hash (7 chars) + sequential number (e.g., `a1b2c3d-1`)
|
|
16
|
-
- **Title**: Short description (required)
|
|
17
|
-
- **Description**: Optional longer details
|
|
18
|
-
- **Status**: `draft` | `open` | `in-progress` | `done` | `archived`
|
|
19
|
-
- **Dependencies**: List of task IDs this task depends on
|
|
20
|
-
- **Created**: Timestamp of creation
|
|
21
|
-
- **Updated**: Timestamp of last modification
|
|
22
|
-
|
|
23
|
-
### Project Detection
|
|
24
|
-
|
|
25
|
-
Projects are identified by git repository:
|
|
26
|
-
1. First, attempt to use git remote origin URL as project identifier (survives cloning to different paths)
|
|
27
|
-
2. Fall back to git repository root path for local-only repos
|
|
28
|
-
3. Error if run outside a git repository
|
|
29
|
-
|
|
30
|
-
### Storage Strategy
|
|
31
|
-
|
|
32
|
-
Two storage modes, configured per-project during `chop init`:
|
|
33
|
-
|
|
34
|
-
1. **Local storage** (in-repo): `.chop/tasks.json` in repository root
|
|
35
|
-
- Can be committed (team shared) or gitignored (personal)
|
|
36
|
-
|
|
37
|
-
2. **Global storage**: `~/.local/share/chop/<project-id>/tasks.json`
|
|
38
|
-
- Never pollutes the repository
|
|
39
|
-
- Project ID derived from remote URL or repo path
|
|
40
|
-
|
|
41
|
-
**Resolution order:**
|
|
42
|
-
1. Check for `.chop/tasks.json` in repo root
|
|
43
|
-
2. Fall back to global storage location
|
|
44
|
-
3. Error if neither exists (requires `chop init`)
|
|
45
|
-
|
|
46
|
-
**Archive storage:** Archived tasks are moved to a separate file (`tasks.archived.json`) to avoid loading them into memory during normal operations.
|
|
47
|
-
|
|
48
|
-
### Concurrency Model
|
|
49
|
-
|
|
50
|
-
File locking with retry strategy to handle multiple processes:
|
|
51
|
-
- Use OS-level file locks when reading/writing
|
|
52
|
-
- Retry up to 5 times with exponential backoff (100ms, 200ms, 400ms, 800ms, 1600ms)
|
|
53
|
-
- Critical for `chop pop` to ensure atomic "get + mark in-progress" operation
|
|
54
|
-
- Works correctly across multiple checkouts of the same repository sharing global storage
|
|
55
|
-
|
|
56
|
-
## Commands
|
|
57
|
-
|
|
58
|
-
### Initialization
|
|
59
|
-
|
|
60
|
-
```
|
|
61
|
-
chop init
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
Interactive setup prompting for:
|
|
65
|
-
1. Storage location: local (in-repo) or global
|
|
66
|
-
2. If local: add to .gitignore? (y/n)
|
|
67
|
-
|
|
68
|
-
Creates the storage directory and empty tasks file.
|
|
69
|
-
|
|
70
|
-
### Adding Tasks
|
|
71
|
-
|
|
72
|
-
```
|
|
73
|
-
chop add <title> [options]
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
Options:
|
|
77
|
-
- `--top` / `-t`: Add to top of queue (default: bottom)
|
|
78
|
-
- `--bottom` / `-b`: Add to bottom of queue (explicit)
|
|
79
|
-
- `--desc <description>` / `-d`: Add description
|
|
80
|
-
- `--depends-on <id>`: Add dependency (can be repeated)
|
|
81
|
-
|
|
82
|
-
If `--depends-on` is used without an ID, show interactive picker of existing tasks.
|
|
83
|
-
|
|
84
|
-
Examples:
|
|
85
|
-
```
|
|
86
|
-
chop add "Implement login page"
|
|
87
|
-
chop add "Write tests" --top
|
|
88
|
-
chop add "Deploy to staging" --depends-on a1b2c3d-1 --depends-on e5f6g7h-2
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
### Listing Tasks
|
|
92
|
-
|
|
93
|
-
```
|
|
94
|
-
chop list [options]
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
Options:
|
|
98
|
-
- `--open` / `-o`: Show only open tasks (default behavior)
|
|
99
|
-
- `--progress` / `-p`: Show only in-progress tasks
|
|
100
|
-
- `--done`: Show only done tasks
|
|
101
|
-
- `--all` / `-a`: Show all non-archived tasks
|
|
102
|
-
|
|
103
|
-
Output format (compact table):
|
|
104
|
-
```
|
|
105
|
-
ID STATUS TITLE
|
|
106
|
-
a1b2c3d-1 open Implement login page
|
|
107
|
-
e5f6g7h-2 in-progress Write unit tests
|
|
108
|
-
f8g9h0i-3 open [blocked] Deploy to staging
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
Tasks with incomplete dependencies are marked `[blocked]` but still shown.
|
|
112
|
-
|
|
113
|
-
### Getting Next Task
|
|
114
|
-
|
|
115
|
-
```
|
|
116
|
-
chop pop
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
Atomically:
|
|
120
|
-
1. Find the first `open` task (by creation order) that has no incomplete dependencies
|
|
121
|
-
2. Mark it as `in-progress`
|
|
122
|
-
3. Display the task details
|
|
123
|
-
|
|
124
|
-
If no tasks available, print "No tasks available" and exit with code 0.
|
|
125
|
-
|
|
126
|
-
Tasks with incomplete dependencies are silently skipped.
|
|
127
|
-
|
|
128
|
-
### Completing Tasks
|
|
129
|
-
|
|
130
|
-
```
|
|
131
|
-
chop done <id>
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
Mark a task as `done`. Requires explicit task ID since multiple tasks can be in-progress.
|
|
135
|
-
|
|
136
|
-
### Changing Status
|
|
137
|
-
|
|
138
|
-
```
|
|
139
|
-
chop status <id> <status>
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
Change task status to: `open`, `in-progress`, or `done`.
|
|
143
|
-
|
|
144
|
-
Examples:
|
|
145
|
-
```
|
|
146
|
-
chop status a1b2c3d-1 in-progress
|
|
147
|
-
chop status e5f6g7h-2 open
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
### Archiving Tasks
|
|
151
|
-
|
|
152
|
-
```
|
|
153
|
-
chop archive <id>
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
Move a task to archived storage. Requires confirmation.
|
|
157
|
-
|
|
158
|
-
Cascade behavior:
|
|
159
|
-
- If task has dependents (other tasks depend on it), show warning listing affected tasks
|
|
160
|
-
- Prompt for confirmation to archive all affected tasks together
|
|
161
|
-
- On confirm, archive the task and all its dependents
|
|
162
|
-
|
|
163
|
-
When archiving a task that depends on other tasks, just remove the dependency links.
|
|
164
|
-
|
|
165
|
-
### Purging Archives
|
|
166
|
-
|
|
167
|
-
```
|
|
168
|
-
chop purge
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
Permanently delete all archived tasks. Requires confirmation.
|
|
172
|
-
|
|
173
|
-
### Editing Tasks
|
|
174
|
-
|
|
175
|
-
```
|
|
176
|
-
chop edit <id>
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
Opens task in `$EDITOR` (or `vi` if not set) as a temporary file with YAML/JSON format:
|
|
180
|
-
```yaml
|
|
181
|
-
title: Implement login page
|
|
182
|
-
description: |
|
|
183
|
-
Create login form with email/password
|
|
184
|
-
Add validation and error handling
|
|
185
|
-
status: open
|
|
186
|
-
depends_on:
|
|
187
|
-
- a1b2c3d-1
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
Save and close to apply changes. Cancel (exit without saving or delete content) to abort.
|
|
191
|
-
|
|
192
|
-
### Moving Tasks
|
|
193
|
-
|
|
194
|
-
```
|
|
195
|
-
chop move <id> [options]
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
Options:
|
|
199
|
-
- `--top` / `-t`: Move to top of queue
|
|
200
|
-
- `--bottom` / `-b`: Move to bottom of queue
|
|
201
|
-
|
|
202
|
-
Examples:
|
|
203
|
-
```
|
|
204
|
-
chop move a1b2c3d-1 --top
|
|
205
|
-
chop move e5f6g7h-2 --bottom
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
### Help
|
|
209
|
-
|
|
210
|
-
```
|
|
211
|
-
chop
|
|
212
|
-
chop --help
|
|
213
|
-
chop <command> --help
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
Running `chop` with no arguments shows help.
|
|
217
|
-
|
|
218
|
-
## File Formats
|
|
219
|
-
|
|
220
|
-
### tasks.json
|
|
221
|
-
|
|
222
|
-
```json
|
|
223
|
-
{
|
|
224
|
-
"version": 1,
|
|
225
|
-
"lastSequence": 3,
|
|
226
|
-
"tasks": [
|
|
227
|
-
{
|
|
228
|
-
"id": "a1b2c3d-1",
|
|
229
|
-
"title": "Implement login page",
|
|
230
|
-
"description": "Create login form with validation",
|
|
231
|
-
"status": "open",
|
|
232
|
-
"dependsOn": [],
|
|
233
|
-
"createdAt": "2024-01-15T10:30:00Z",
|
|
234
|
-
"updatedAt": "2024-01-15T10:30:00Z"
|
|
235
|
-
}
|
|
236
|
-
]
|
|
237
|
-
}
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
### tasks.archived.json
|
|
241
|
-
|
|
242
|
-
Same format as tasks.json, containing only archived tasks.
|
|
243
|
-
|
|
244
|
-
### config.json
|
|
245
|
-
|
|
246
|
-
Located at `.chop/config.json` (local) or `~/.config/chop/config.json` (global):
|
|
247
|
-
|
|
248
|
-
```json
|
|
249
|
-
{
|
|
250
|
-
"defaultStorage": "global",
|
|
251
|
-
"projects": {
|
|
252
|
-
"github.com/user/repo": {
|
|
253
|
-
"storage": "local"
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
## Error Handling
|
|
260
|
-
|
|
261
|
-
Errors are concise one-liners:
|
|
262
|
-
- `Error: Not in a git repository`
|
|
263
|
-
- `Error: Project not initialized. Run 'chop init'`
|
|
264
|
-
- `Error: Task a1b2c3d-1 not found`
|
|
265
|
-
- `Error: Cannot acquire lock. Another process is accessing tasks`
|
|
266
|
-
- `Error: Task has unarchived dependents: e5f6g7h-2, f8g9h0i-3`
|
|
267
|
-
|
|
268
|
-
## Exit Codes
|
|
269
|
-
|
|
270
|
-
- `0`: Success
|
|
271
|
-
- `1`: Error (with message to stderr)
|
|
272
|
-
|
|
273
|
-
## Implementation Notes
|
|
274
|
-
|
|
275
|
-
### Technology Stack
|
|
276
|
-
- Runtime: Bun
|
|
277
|
-
- Language: TypeScript
|
|
278
|
-
- Argument parsing: Built-in or minimal dependency
|
|
279
|
-
- File locking: OS-level locks via Bun APIs
|
|
280
|
-
|
|
281
|
-
### Concurrency Safety
|
|
282
|
-
|
|
283
|
-
For operations that modify state (`pop`, `done`, `status`, `add`, etc.):
|
|
284
|
-
1. Acquire exclusive file lock
|
|
285
|
-
2. Read current state
|
|
286
|
-
3. Validate operation is still valid
|
|
287
|
-
4. Write updated state
|
|
288
|
-
5. Release lock
|
|
289
|
-
|
|
290
|
-
This ensures correctness when:
|
|
291
|
-
- Multiple terminal sessions in same repo
|
|
292
|
-
- Multiple checkouts of same repo sharing global storage
|
|
293
|
-
- CI/CD pipelines running concurrent tasks
|
|
294
|
-
|
|
295
|
-
### ID Generation
|
|
296
|
-
|
|
297
|
-
1. Generate random bytes and create short hash (7 chars)
|
|
298
|
-
2. Increment `lastSequence` counter
|
|
299
|
-
3. Combine: `{hash}-{sequence}`
|
|
300
|
-
|
|
301
|
-
This provides:
|
|
302
|
-
- Uniqueness (hash prevents collisions)
|
|
303
|
-
- Human-friendliness (sequence for easy reference)
|
|
304
|
-
- Merge safety (hash portion prevents conflicts)
|
|
305
|
-
|
|
306
|
-
## Future Considerations (Not in v1)
|
|
307
|
-
|
|
308
|
-
- Shell completions (bash/zsh/fish)
|
|
309
|
-
- Integration hooks (on-status-change scripts)
|
|
310
|
-
- Cross-project dependencies
|
|
311
|
-
- Priority levels
|
|
312
|
-
- Tags/labels for filtering
|
|
313
|
-
- Time tracking
|
package/src/commands/add.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import type { Command } from "commander";
|
|
2
|
-
import { TaskStore } from "../storage/task-store.ts";
|
|
3
|
-
import { createTask } from "../models/task.ts";
|
|
4
|
-
import { selectTasks } from "../utils/prompts.ts";
|
|
5
|
-
import { formatTaskDetail } from "../utils/display.ts";
|
|
6
|
-
|
|
7
|
-
// Collect multiple --depends-on values
|
|
8
|
-
function collectDependencies(value: string, previous: string[]): string[] {
|
|
9
|
-
return previous.concat([value]);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function registerAddCommand(program: Command): void {
|
|
13
|
-
program
|
|
14
|
-
.command("add <title>")
|
|
15
|
-
.alias("a")
|
|
16
|
-
.description("Add a new task to the queue")
|
|
17
|
-
.option("-t, --top", "Add to top of queue")
|
|
18
|
-
.option("-b, --bottom", "Add to bottom of queue (default)")
|
|
19
|
-
.option("-d, --desc <description>", "Add description")
|
|
20
|
-
.option("--draft", "Create task as draft status")
|
|
21
|
-
.option("--depends-on [id]", "Add dependency (can be repeated)", collectDependencies, [])
|
|
22
|
-
.action(async (title: string, options) => {
|
|
23
|
-
try {
|
|
24
|
-
const store = await TaskStore.create();
|
|
25
|
-
|
|
26
|
-
// Handle interactive dependency selection if --depends-on used without value
|
|
27
|
-
let dependsOn: string[] = options.dependsOn;
|
|
28
|
-
|
|
29
|
-
// Check if --depends-on was used with no value (will be empty string)
|
|
30
|
-
if (dependsOn.includes("")) {
|
|
31
|
-
// Remove empty strings
|
|
32
|
-
dependsOn = dependsOn.filter((id: string) => id !== "");
|
|
33
|
-
|
|
34
|
-
// Show interactive picker
|
|
35
|
-
const tasksData = await store.readTasks();
|
|
36
|
-
const availableTasks = tasksData.tasks
|
|
37
|
-
.filter((t) => t.status !== "archived")
|
|
38
|
-
.map((t) => ({ id: t.id, title: t.title }));
|
|
39
|
-
|
|
40
|
-
const selectedIds = await selectTasks("Select tasks to depend on:", availableTasks);
|
|
41
|
-
dependsOn = [...dependsOn, ...selectedIds];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const newTask = await store.atomicUpdate((data) => {
|
|
45
|
-
const { task, newSequence } = createTask(data.lastSequence, {
|
|
46
|
-
title,
|
|
47
|
-
description: options.desc,
|
|
48
|
-
dependsOn,
|
|
49
|
-
status: options.draft ? "draft" : "open",
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
if (options.top) {
|
|
53
|
-
data.tasks.unshift(task);
|
|
54
|
-
} else {
|
|
55
|
-
data.tasks.push(task);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
data.lastSequence = newSequence;
|
|
59
|
-
|
|
60
|
-
return { data, result: task };
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
console.log(`Added task: ${newTask.id}`);
|
|
64
|
-
console.log(formatTaskDetail(newTask));
|
|
65
|
-
} catch (error) {
|
|
66
|
-
if (error instanceof Error) {
|
|
67
|
-
console.error(error.message);
|
|
68
|
-
} else {
|
|
69
|
-
console.error("An unexpected error occurred");
|
|
70
|
-
}
|
|
71
|
-
process.exit(1);
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
}
|
package/src/commands/archive.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import type { Command } from "commander";
|
|
2
|
-
import { TaskStore } from "../storage/task-store.ts";
|
|
3
|
-
import { findTaskById, findAllDependents } from "../models/task.ts";
|
|
4
|
-
import { TaskNotFoundError } from "../errors.ts";
|
|
5
|
-
import { confirm } from "../utils/prompts.ts";
|
|
6
|
-
|
|
7
|
-
export function registerArchiveCommand(program: Command): void {
|
|
8
|
-
program
|
|
9
|
-
.command("archive <id>")
|
|
10
|
-
.alias("ar")
|
|
11
|
-
.description("Archive a task")
|
|
12
|
-
.action(async (id: string) => {
|
|
13
|
-
try {
|
|
14
|
-
const store = await TaskStore.create();
|
|
15
|
-
const data = await store.readTasks();
|
|
16
|
-
|
|
17
|
-
// Find the task
|
|
18
|
-
const task = findTaskById(id, data.tasks);
|
|
19
|
-
if (!task) {
|
|
20
|
-
throw new TaskNotFoundError(id);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Find all dependents (tasks that depend on this one)
|
|
24
|
-
const dependents = findAllDependents(task.id, data.tasks);
|
|
25
|
-
const unarchivedDependents = dependents.filter((t) => t.status !== "archived");
|
|
26
|
-
|
|
27
|
-
if (unarchivedDependents.length > 0) {
|
|
28
|
-
// Show warning about dependents
|
|
29
|
-
console.log(`Warning: The following tasks depend on ${task.id}:`);
|
|
30
|
-
for (const dep of unarchivedDependents) {
|
|
31
|
-
console.log(` - ${dep.id}: ${dep.title}`);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const confirmed = await confirm(
|
|
35
|
-
"Archive this task and all its dependents?",
|
|
36
|
-
false
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
if (!confirmed) {
|
|
40
|
-
console.log("Archive cancelled.");
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Archive all tasks (main task + all dependents)
|
|
45
|
-
const tasksToArchive = [task, ...unarchivedDependents];
|
|
46
|
-
for (const t of tasksToArchive) {
|
|
47
|
-
await store.archiveTask(t.id);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
console.log(`Archived ${tasksToArchive.length} task(s)`);
|
|
51
|
-
} else {
|
|
52
|
-
// Confirm archiving single task
|
|
53
|
-
const confirmed = await confirm(`Archive task ${task.id}?`, true);
|
|
54
|
-
|
|
55
|
-
if (!confirmed) {
|
|
56
|
-
console.log("Archive cancelled.");
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
await store.archiveTask(task.id);
|
|
61
|
-
console.log(`Archived task ${task.id}`);
|
|
62
|
-
}
|
|
63
|
-
} catch (error) {
|
|
64
|
-
if (error instanceof Error) {
|
|
65
|
-
console.error(error.message);
|
|
66
|
-
} else {
|
|
67
|
-
console.error("An unexpected error occurred");
|
|
68
|
-
}
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
}
|