@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.
Files changed (42) hide show
  1. package/dist/index.js +8401 -0
  2. package/package.json +6 -3
  3. package/.claude/rules/use-bun-instead-of-node-vite-npm-pnpm.md +0 -109
  4. package/.claude/settings.local.json +0 -12
  5. package/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +0 -111
  6. package/.devcontainer/Dockerfile +0 -102
  7. package/.devcontainer/devcontainer.json +0 -58
  8. package/.devcontainer/init-firewall.sh +0 -137
  9. package/.github/workflows/publish.yml +0 -76
  10. package/CLAUDE.md +0 -44
  11. package/index.ts +0 -2
  12. package/loop.sh +0 -206
  13. package/specs/chop.md +0 -313
  14. package/src/commands/add.ts +0 -74
  15. package/src/commands/archive.ts +0 -72
  16. package/src/commands/completion.ts +0 -232
  17. package/src/commands/done.ts +0 -38
  18. package/src/commands/edit.ts +0 -228
  19. package/src/commands/init.ts +0 -72
  20. package/src/commands/list.ts +0 -48
  21. package/src/commands/move.ts +0 -92
  22. package/src/commands/pop.ts +0 -45
  23. package/src/commands/purge.ts +0 -41
  24. package/src/commands/show.ts +0 -32
  25. package/src/commands/status.ts +0 -43
  26. package/src/config/paths.ts +0 -61
  27. package/src/errors.ts +0 -56
  28. package/src/index.ts +0 -41
  29. package/src/models/id-generator.ts +0 -39
  30. package/src/models/task.ts +0 -98
  31. package/src/storage/file-lock.ts +0 -124
  32. package/src/storage/storage-resolver.ts +0 -63
  33. package/src/storage/task-store.ts +0 -173
  34. package/src/types.ts +0 -42
  35. package/src/utils/display.ts +0 -139
  36. package/src/utils/git.ts +0 -80
  37. package/src/utils/prompts.ts +0 -88
  38. package/tests/errors.test.ts +0 -86
  39. package/tests/models/id-generator.test.ts +0 -46
  40. package/tests/models/task.test.ts +0 -186
  41. package/tests/storage/file-lock.test.ts +0 -152
  42. 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
@@ -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
- }
@@ -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
- }