@da1z/chop 0.0.1

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/.claude/rules/use-bun-instead-of-node-vite-npm-pnpm.md +109 -0
  2. package/.claude/settings.local.json +12 -0
  3. package/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +111 -0
  4. package/.devcontainer/Dockerfile +102 -0
  5. package/.devcontainer/devcontainer.json +58 -0
  6. package/.devcontainer/init-firewall.sh +137 -0
  7. package/.github/workflows/publish.yml +76 -0
  8. package/CLAUDE.md +44 -0
  9. package/README.md +15 -0
  10. package/index.ts +2 -0
  11. package/loop.sh +206 -0
  12. package/package.json +27 -0
  13. package/specs/chop.md +313 -0
  14. package/src/commands/add.ts +74 -0
  15. package/src/commands/archive.ts +72 -0
  16. package/src/commands/completion.ts +232 -0
  17. package/src/commands/done.ts +38 -0
  18. package/src/commands/edit.ts +228 -0
  19. package/src/commands/init.ts +72 -0
  20. package/src/commands/list.ts +48 -0
  21. package/src/commands/move.ts +92 -0
  22. package/src/commands/pop.ts +45 -0
  23. package/src/commands/purge.ts +41 -0
  24. package/src/commands/show.ts +32 -0
  25. package/src/commands/status.ts +43 -0
  26. package/src/config/paths.ts +61 -0
  27. package/src/errors.ts +56 -0
  28. package/src/index.ts +41 -0
  29. package/src/models/id-generator.ts +39 -0
  30. package/src/models/task.ts +98 -0
  31. package/src/storage/file-lock.ts +124 -0
  32. package/src/storage/storage-resolver.ts +63 -0
  33. package/src/storage/task-store.ts +173 -0
  34. package/src/types.ts +42 -0
  35. package/src/utils/display.ts +139 -0
  36. package/src/utils/git.ts +80 -0
  37. package/src/utils/prompts.ts +88 -0
  38. package/tests/errors.test.ts +86 -0
  39. package/tests/models/id-generator.test.ts +46 -0
  40. package/tests/models/task.test.ts +186 -0
  41. package/tests/storage/file-lock.test.ts +152 -0
  42. package/tsconfig.json +9 -0
@@ -0,0 +1,232 @@
1
+ import type { Command } from "commander";
2
+ import { TaskStore } from "../storage/task-store.ts";
3
+
4
+ // Generate bash completion script
5
+ function generateBashCompletion(): string {
6
+ return `# Bash completion for chop
7
+ # Add this to your ~/.bashrc or ~/.bash_profile:
8
+ # eval "$(chop completion bash)"
9
+ # or: eval "$(ch completion bash)"
10
+
11
+ _chop_completions() {
12
+ local cur prev words cword
13
+
14
+ # Use bash-completion if available, otherwise fallback to basic parsing
15
+ if declare -F _init_completion >/dev/null 2>&1; then
16
+ _init_completion || return
17
+ else
18
+ cur="\${COMP_WORDS[COMP_CWORD]}"
19
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
20
+ words=("\${COMP_WORDS[@]}")
21
+ cword=$COMP_CWORD
22
+ fi
23
+
24
+ local commands="init add a list ls pop p done d status move mv mt mb archive ar purge edit e show s completion"
25
+
26
+ # If completing the first argument (command name)
27
+ if [[ $cword -eq 1 ]]; then
28
+ COMPREPLY=($(compgen -W "$commands" -- "$cur"))
29
+ return
30
+ fi
31
+
32
+ # If completing argument for commands that need task ID
33
+ local cmd="\${words[1]}"
34
+ case "$cmd" in
35
+ done|d|move|mv|mt|mb|archive|ar|edit|e|show|s)
36
+ # Get task IDs from chop
37
+ local task_ids
38
+ task_ids=$(chop completion --list-ids 2>/dev/null)
39
+ COMPREPLY=($(compgen -W "$task_ids" -- "$cur"))
40
+ return
41
+ ;;
42
+ esac
43
+ }
44
+
45
+ complete -F _chop_completions chop
46
+ complete -F _chop_completions ch
47
+ `;
48
+ }
49
+
50
+ // Generate zsh completion script
51
+ function generateZshCompletion(): string {
52
+ return `#compdef chop ch
53
+ # Zsh completion for chop
54
+ # Add this to your ~/.zshrc:
55
+ # eval "$(chop completion zsh)"
56
+ # or: eval "$(ch completion zsh)"
57
+
58
+ _chop() {
59
+ local -a commands
60
+ commands=(
61
+ 'init:Initialize chop in current directory'
62
+ 'add:Add a new task'
63
+ 'a:Add a new task (alias)'
64
+ 'list:List tasks'
65
+ 'ls:List tasks (alias)'
66
+ 'pop:Start working on next task'
67
+ 'p:Pop next task (alias)'
68
+ 'done:Mark a task as done'
69
+ 'd:Mark done (alias)'
70
+ 'status:Change task status'
71
+ 'move:Move a task in the queue'
72
+ 'mv:Move task (alias)'
73
+ 'mt:Move task to top'
74
+ 'mb:Move task to bottom'
75
+ 'archive:Archive a task'
76
+ 'ar:Archive task (alias)'
77
+ 'purge:Purge archived tasks'
78
+ 'edit:Edit a task'
79
+ 'e:Edit task (alias)'
80
+ 'show:Display full task info'
81
+ 's:Show task (alias)'
82
+ 'completion:Generate shell completions'
83
+ )
84
+
85
+ _arguments -C \\
86
+ '1: :->command' \\
87
+ '*: :->args'
88
+
89
+ case $state in
90
+ command)
91
+ _describe 'command' commands
92
+ ;;
93
+ args)
94
+ case $words[2] in
95
+ done|d|move|mv|mt|mb|archive|ar|edit|e|show|s)
96
+ local -a task_ids
97
+ task_ids=(\${(f)"$(chop completion --list-ids 2>/dev/null)"})
98
+ _describe 'task id' task_ids
99
+ ;;
100
+ esac
101
+ ;;
102
+ esac
103
+ }
104
+
105
+ compdef _chop chop
106
+ compdef _chop ch
107
+ `;
108
+ }
109
+
110
+ // Generate fish completion script
111
+ function generateFishCompletion(): string {
112
+ return `# Fish completion for chop
113
+ # Add this to your ~/.config/fish/completions/chop.fish:
114
+ # chop completion fish > ~/.config/fish/completions/chop.fish
115
+ # or: ch completion fish > ~/.config/fish/completions/ch.fish
116
+
117
+ # Disable file completion by default
118
+ complete -c chop -f
119
+ complete -c ch -f
120
+
121
+ # All commands including aliases
122
+ set -l all_cmds init add a list ls pop p done d status move mv mt mb archive ar purge edit e show s completion
123
+
124
+ # Commands for chop
125
+ complete -c chop -n "not __fish_seen_subcommand_from $all_cmds" -a init -d "Initialize chop in current directory"
126
+ complete -c chop -n "not __fish_seen_subcommand_from $all_cmds" -a add -d "Add a new task"
127
+ complete -c chop -n "not __fish_seen_subcommand_from $all_cmds" -a a -d "Add a new task (alias)"
128
+ complete -c chop -n "not __fish_seen_subcommand_from $all_cmds" -a list -d "List tasks"
129
+ complete -c chop -n "not __fish_seen_subcommand_from $all_cmds" -a ls -d "List tasks (alias)"
130
+ complete -c chop -n "not __fish_seen_subcommand_from $all_cmds" -a pop -d "Start working on next task"
131
+ complete -c chop -n "not __fish_seen_subcommand_from $all_cmds" -a p -d "Pop next task (alias)"
132
+ complete -c chop -n "not __fish_seen_subcommand_from $all_cmds" -a done -d "Mark a task as done"
133
+ complete -c chop -n "not __fish_seen_subcommand_from $all_cmds" -a d -d "Mark done (alias)"
134
+ complete -c chop -n "not __fish_seen_subcommand_from $all_cmds" -a status -d "Change task status"
135
+ complete -c chop -n "not __fish_seen_subcommand_from $all_cmds" -a move -d "Move a task in the queue"
136
+ complete -c chop -n "not __fish_seen_subcommand_from $all_cmds" -a mv -d "Move task (alias)"
137
+ complete -c chop -n "not __fish_seen_subcommand_from $all_cmds" -a mt -d "Move task to top"
138
+ complete -c chop -n "not __fish_seen_subcommand_from $all_cmds" -a mb -d "Move task to bottom"
139
+ complete -c chop -n "not __fish_seen_subcommand_from $all_cmds" -a archive -d "Archive a task"
140
+ complete -c chop -n "not __fish_seen_subcommand_from $all_cmds" -a ar -d "Archive task (alias)"
141
+ complete -c chop -n "not __fish_seen_subcommand_from $all_cmds" -a purge -d "Purge archived tasks"
142
+ complete -c chop -n "not __fish_seen_subcommand_from $all_cmds" -a edit -d "Edit a task"
143
+ complete -c chop -n "not __fish_seen_subcommand_from $all_cmds" -a e -d "Edit task (alias)"
144
+ complete -c chop -n "not __fish_seen_subcommand_from $all_cmds" -a show -d "Display full task info"
145
+ complete -c chop -n "not __fish_seen_subcommand_from $all_cmds" -a s -d "Show task (alias)"
146
+ complete -c chop -n "not __fish_seen_subcommand_from $all_cmds" -a completion -d "Generate shell completions"
147
+
148
+ # Task ID completion for commands that need it
149
+ complete -c chop -n "__fish_seen_subcommand_from done d move mv mt mb archive ar edit e show s" -a "(chop completion --list-ids 2>/dev/null)"
150
+
151
+ # Same for ch alias
152
+ complete -c ch -n "not __fish_seen_subcommand_from $all_cmds" -a init -d "Initialize chop in current directory"
153
+ complete -c ch -n "not __fish_seen_subcommand_from $all_cmds" -a add -d "Add a new task"
154
+ complete -c ch -n "not __fish_seen_subcommand_from $all_cmds" -a a -d "Add a new task (alias)"
155
+ complete -c ch -n "not __fish_seen_subcommand_from $all_cmds" -a list -d "List tasks"
156
+ complete -c ch -n "not __fish_seen_subcommand_from $all_cmds" -a ls -d "List tasks (alias)"
157
+ complete -c ch -n "not __fish_seen_subcommand_from $all_cmds" -a pop -d "Start working on next task"
158
+ complete -c ch -n "not __fish_seen_subcommand_from $all_cmds" -a p -d "Pop next task (alias)"
159
+ complete -c ch -n "not __fish_seen_subcommand_from $all_cmds" -a done -d "Mark a task as done"
160
+ complete -c ch -n "not __fish_seen_subcommand_from $all_cmds" -a d -d "Mark done (alias)"
161
+ complete -c ch -n "not __fish_seen_subcommand_from $all_cmds" -a status -d "Change task status"
162
+ complete -c ch -n "not __fish_seen_subcommand_from $all_cmds" -a move -d "Move a task in the queue"
163
+ complete -c ch -n "not __fish_seen_subcommand_from $all_cmds" -a mv -d "Move task (alias)"
164
+ complete -c ch -n "not __fish_seen_subcommand_from $all_cmds" -a mt -d "Move task to top"
165
+ complete -c ch -n "not __fish_seen_subcommand_from $all_cmds" -a mb -d "Move task to bottom"
166
+ complete -c ch -n "not __fish_seen_subcommand_from $all_cmds" -a archive -d "Archive a task"
167
+ complete -c ch -n "not __fish_seen_subcommand_from $all_cmds" -a ar -d "Archive task (alias)"
168
+ complete -c ch -n "not __fish_seen_subcommand_from $all_cmds" -a purge -d "Purge archived tasks"
169
+ complete -c ch -n "not __fish_seen_subcommand_from $all_cmds" -a edit -d "Edit a task"
170
+ complete -c ch -n "not __fish_seen_subcommand_from $all_cmds" -a e -d "Edit task (alias)"
171
+ complete -c ch -n "not __fish_seen_subcommand_from $all_cmds" -a show -d "Display full task info"
172
+ complete -c ch -n "not __fish_seen_subcommand_from $all_cmds" -a s -d "Show task (alias)"
173
+ complete -c ch -n "not __fish_seen_subcommand_from $all_cmds" -a completion -d "Generate shell completions"
174
+
175
+ complete -c ch -n "__fish_seen_subcommand_from done d move mv mt mb archive ar edit e show s" -a "(ch completion --list-ids 2>/dev/null)"
176
+ `;
177
+ }
178
+
179
+ // Get list of task IDs for completion
180
+ async function getTaskIds(): Promise<string[]> {
181
+ try {
182
+ const store = await TaskStore.create();
183
+ const data = await store.readTasks();
184
+ return data.tasks.map((task) => task.id);
185
+ } catch {
186
+ // Return empty array if we can't read tasks (e.g., not initialized)
187
+ return [];
188
+ }
189
+ }
190
+
191
+ export function registerCompletionCommand(program: Command): void {
192
+ program
193
+ .command("completion [shell]")
194
+ .description("Generate shell completion script")
195
+ .option("--list-ids", "List task IDs (internal use)")
196
+ .action(async (shell: string | undefined, options: { listIds?: boolean }) => {
197
+ // Internal option for listing task IDs during completion
198
+ if (options.listIds) {
199
+ const ids = await getTaskIds();
200
+ console.log(ids.join("\n"));
201
+ return;
202
+ }
203
+
204
+ // Determine shell type
205
+ const targetShell = shell || detectShell();
206
+
207
+ switch (targetShell) {
208
+ case "bash":
209
+ console.log(generateBashCompletion());
210
+ break;
211
+ case "zsh":
212
+ console.log(generateZshCompletion());
213
+ break;
214
+ case "fish":
215
+ console.log(generateFishCompletion());
216
+ break;
217
+ default:
218
+ console.error(`Unknown shell: ${targetShell}`);
219
+ console.error("Supported shells: bash, zsh, fish");
220
+ process.exit(1);
221
+ }
222
+ });
223
+ }
224
+
225
+ // Detect current shell from environment
226
+ function detectShell(): string {
227
+ const shell = process.env.SHELL || "";
228
+ if (shell.includes("zsh")) return "zsh";
229
+ if (shell.includes("fish")) return "fish";
230
+ if (shell.includes("bash")) return "bash";
231
+ return "bash"; // Default to bash
232
+ }
@@ -0,0 +1,38 @@
1
+ import type { Command } from "commander";
2
+ import { TaskStore } from "../storage/task-store.ts";
3
+ import { findTaskById } from "../models/task.ts";
4
+ import { TaskNotFoundError } from "../errors.ts";
5
+
6
+ export function registerDoneCommand(program: Command): void {
7
+ program
8
+ .command("done <id>")
9
+ .alias("d")
10
+ .description("Mark a task as done")
11
+ .action(async (id: string) => {
12
+ try {
13
+ const store = await TaskStore.create();
14
+
15
+ await store.atomicUpdate((data) => {
16
+ const task = findTaskById(id, data.tasks);
17
+
18
+ if (!task) {
19
+ throw new TaskNotFoundError(id);
20
+ }
21
+
22
+ task.status = "done";
23
+ task.updatedAt = new Date().toISOString();
24
+
25
+ return { data, result: task };
26
+ });
27
+
28
+ console.log(`Marked task ${id} as done`);
29
+ } catch (error) {
30
+ if (error instanceof Error) {
31
+ console.error(error.message);
32
+ } else {
33
+ console.error("An unexpected error occurred");
34
+ }
35
+ process.exit(1);
36
+ }
37
+ });
38
+ }
@@ -0,0 +1,228 @@
1
+ import type { Command } from "commander";
2
+ import { tmpdir } from "os";
3
+ import { join } from "path";
4
+ import { unlinkSync } from "node:fs";
5
+ import { TaskStore } from "../storage/task-store.ts";
6
+ import { findTaskById, isValidStatus } from "../models/task.ts";
7
+ import { TaskNotFoundError, ChopError, InvalidStatusError } from "../errors.ts";
8
+ import type { Task, TaskStatus, TaskEditData } from "../types.ts";
9
+
10
+ // Convert a task to YAML-like format for editing
11
+ function taskToYaml(task: Task): string {
12
+ const lines: string[] = [];
13
+
14
+ lines.push(`title: ${task.title}`);
15
+
16
+ if (task.description) {
17
+ lines.push("description: |");
18
+ for (const line of task.description.split("\n")) {
19
+ lines.push(` ${line}`);
20
+ }
21
+ } else {
22
+ lines.push("description:");
23
+ }
24
+
25
+ lines.push(`status: ${task.status}`);
26
+
27
+ if (task.dependsOn.length > 0) {
28
+ lines.push("depends_on:");
29
+ for (const dep of task.dependsOn) {
30
+ lines.push(` - ${dep}`);
31
+ }
32
+ } else {
33
+ lines.push("depends_on:");
34
+ }
35
+
36
+ return lines.join("\n") + "\n";
37
+ }
38
+
39
+ // Parse YAML-like format back to task edit data
40
+ function yamlToTaskEdit(content: string): TaskEditData {
41
+ const lines = content.split("\n");
42
+ const result: TaskEditData = {
43
+ title: "",
44
+ status: "open",
45
+ depends_on: [],
46
+ };
47
+
48
+ let inDescription = false;
49
+ let descriptionLines: string[] = [];
50
+ let inDependsOn = false;
51
+
52
+ for (const line of lines) {
53
+ // Check for field starts
54
+ if (line.startsWith("title:")) {
55
+ inDescription = false;
56
+ inDependsOn = false;
57
+ result.title = line.slice("title:".length).trim();
58
+ } else if (line.startsWith("description:")) {
59
+ inDescription = true;
60
+ inDependsOn = false;
61
+ descriptionLines = [];
62
+ const inline = line.slice("description:".length).trim();
63
+ if (inline && inline !== "|") {
64
+ descriptionLines.push(inline);
65
+ inDescription = false;
66
+ }
67
+ } else if (line.startsWith("status:")) {
68
+ inDescription = false;
69
+ inDependsOn = false;
70
+ result.status = line.slice("status:".length).trim() as TaskStatus;
71
+ } else if (line.startsWith("depends_on:")) {
72
+ inDescription = false;
73
+ inDependsOn = true;
74
+ result.depends_on = [];
75
+ } else if (inDescription && (line.startsWith(" ") || line === "")) {
76
+ // Description continuation (indented lines)
77
+ if (line.startsWith(" ")) {
78
+ descriptionLines.push(line.slice(2));
79
+ } else if (line === "" && descriptionLines.length > 0) {
80
+ descriptionLines.push("");
81
+ }
82
+ } else if (inDependsOn && line.startsWith(" - ")) {
83
+ // Depends on list item
84
+ result.depends_on.push(line.slice(4).trim());
85
+ } else if (line.trim() && !line.startsWith(" ")) {
86
+ // New field - end description
87
+ inDescription = false;
88
+ inDependsOn = false;
89
+ }
90
+ }
91
+
92
+ // Set description if we collected any lines
93
+ if (descriptionLines.length > 0) {
94
+ // Trim trailing empty lines
95
+ while (descriptionLines.length > 0 && descriptionLines[descriptionLines.length - 1] === "") {
96
+ descriptionLines.pop();
97
+ }
98
+ result.description = descriptionLines.join("\n");
99
+ }
100
+
101
+ return result;
102
+ }
103
+
104
+ interface EditOptions {
105
+ title?: string;
106
+ desc?: string;
107
+ }
108
+
109
+ export function registerEditCommand(program: Command): void {
110
+ program
111
+ .command("edit <id>")
112
+ .alias("e")
113
+ .description("Edit a task in your default editor")
114
+ .option("-t, --title <title>", "Set task title directly")
115
+ .option("-d, --desc <description>", "Set task description directly")
116
+ .action(async (id: string, options: EditOptions) => {
117
+ try {
118
+ const store = await TaskStore.create();
119
+
120
+ // Inline edit mode: if --title or --desc provided, update directly
121
+ if (options.title !== undefined || options.desc !== undefined) {
122
+ // Validate title is not empty if provided
123
+ if (options.title !== undefined && options.title.trim() === "") {
124
+ throw new ChopError("Title cannot be empty");
125
+ }
126
+
127
+ const updatedTask = await store.atomicUpdate((data) => {
128
+ const task = findTaskById(id, data.tasks);
129
+ if (!task) {
130
+ throw new TaskNotFoundError(id);
131
+ }
132
+
133
+ if (options.title !== undefined) {
134
+ task.title = options.title.trim();
135
+ }
136
+ if (options.desc !== undefined) {
137
+ task.description = options.desc.trim() || undefined;
138
+ }
139
+ task.updatedAt = new Date().toISOString();
140
+
141
+ return { data, result: task };
142
+ });
143
+
144
+ console.log(`Updated task ${updatedTask.id}`);
145
+ return;
146
+ }
147
+
148
+ // Editor mode: open task in external editor
149
+ const data = await store.readTasks();
150
+
151
+ // Find the task
152
+ const task = findTaskById(id, data.tasks);
153
+ if (!task) {
154
+ throw new TaskNotFoundError(id);
155
+ }
156
+
157
+ // Create temp file with YAML content
158
+ const tempPath = join(tmpdir(), `chop-edit-${task.id}.yaml`);
159
+ const originalContent = taskToYaml(task);
160
+ await Bun.write(tempPath, originalContent);
161
+
162
+ // Get editor from environment and parse into command + args
163
+ const editorEnv = process.env.EDITOR || process.env.VISUAL || "vi";
164
+ const editorParts = editorEnv.split(/\s+/).filter(Boolean);
165
+ const editorCmd = editorParts[0] || "vi";
166
+ const editorArgs = [...editorParts.slice(1), tempPath];
167
+
168
+ // Open editor
169
+ const proc = Bun.spawn([editorCmd, ...editorArgs], {
170
+ stdin: "inherit",
171
+ stdout: "inherit",
172
+ stderr: "inherit",
173
+ });
174
+ await proc.exited;
175
+
176
+ // Read edited content
177
+ const editedContent = await Bun.file(tempPath).text();
178
+
179
+ // Check if content was changed
180
+ if (editedContent.trim() === "" || editedContent === originalContent) {
181
+ console.log("No changes made. Edit cancelled.");
182
+ // Clean up temp file
183
+ unlinkSync(tempPath);
184
+ return;
185
+ }
186
+
187
+ // Parse edited content
188
+ const editData = yamlToTaskEdit(editedContent);
189
+
190
+ // Validate
191
+ if (!editData.title) {
192
+ throw new ChopError("Title cannot be empty");
193
+ }
194
+
195
+ if (!isValidStatus(editData.status)) {
196
+ throw new InvalidStatusError(editData.status);
197
+ }
198
+
199
+ // Apply changes
200
+ await store.atomicUpdate((data) => {
201
+ const task = findTaskById(id, data.tasks);
202
+ if (!task) {
203
+ throw new TaskNotFoundError(id);
204
+ }
205
+
206
+ task.title = editData.title;
207
+ task.description = editData.description;
208
+ task.status = editData.status;
209
+ task.dependsOn = editData.depends_on;
210
+ task.updatedAt = new Date().toISOString();
211
+
212
+ return { data, result: task };
213
+ });
214
+
215
+ console.log(`Updated task ${task.id}`);
216
+
217
+ // Clean up temp file
218
+ unlinkSync(tempPath);
219
+ } catch (error) {
220
+ if (error instanceof Error) {
221
+ console.error(error.message);
222
+ } else {
223
+ console.error("An unexpected error occurred");
224
+ }
225
+ process.exit(1);
226
+ }
227
+ });
228
+ }
@@ -0,0 +1,72 @@
1
+ import type { Command } from "commander";
2
+ import { AlreadyInitializedError } from "../errors.ts";
3
+ import { isInitialized } from "../storage/storage-resolver.ts";
4
+ import { TaskStore } from "../storage/task-store.ts";
5
+ import type { StorageLocation } from "../types.ts";
6
+ import { getGitRepoRoot } from "../utils/git.ts";
7
+ import { confirm, select } from "../utils/prompts.ts";
8
+
9
+ export function registerInitCommand(program: Command): void {
10
+ program
11
+ .command("init")
12
+ .description("Initialize chop for the current repository")
13
+ .action(async () => {
14
+ try {
15
+ // Check if already initialized
16
+ if (await isInitialized()) {
17
+ throw new AlreadyInitializedError();
18
+ }
19
+
20
+ // Prompt for storage location
21
+ const location = await select<StorageLocation>(
22
+ "Where would you like to store tasks?",
23
+ [
24
+ {
25
+ value: "local",
26
+ label: "Local (in .chop/ directory in this repo)",
27
+ },
28
+ { value: "global", label: "Global (~/.local/share/chop/)" },
29
+ ]
30
+ );
31
+
32
+ // Initialize storage
33
+ await TaskStore.initialize(location);
34
+
35
+ // If local, ask about gitignore
36
+ if (location === "local") {
37
+ const addToGitignore = await confirm(
38
+ "Add .chop/ to .gitignore? (Choose no to share tasks with your team)",
39
+ true
40
+ );
41
+
42
+ if (addToGitignore) {
43
+ const repoRoot = await getGitRepoRoot();
44
+ const gitignorePath = `${repoRoot}/.gitignore`;
45
+ const gitignoreFile = Bun.file(gitignorePath);
46
+
47
+ let content = "";
48
+ if (await gitignoreFile.exists()) {
49
+ content = await gitignoreFile.text();
50
+ }
51
+
52
+ // Check if .chop is already in gitignore
53
+ if (!content.includes(".chop")) {
54
+ const newContent =
55
+ content + (content.endsWith("\n") ? "" : "\n") + ".chop/\n";
56
+ await Bun.write(gitignorePath, newContent);
57
+ console.log("Added .chop/ to .gitignore");
58
+ }
59
+ }
60
+ }
61
+
62
+ console.log(`Initialized chop with ${location} storage.`);
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
+ }
@@ -0,0 +1,48 @@
1
+ import type { Command } from "commander";
2
+ import { TaskStore } from "../storage/task-store.ts";
3
+ import { formatTaskTable } from "../utils/display.ts";
4
+ import type { Task } from "../types.ts";
5
+
6
+ export function registerListCommand(program: Command): void {
7
+ program
8
+ .command("list")
9
+ .alias("ls")
10
+ .description("List tasks")
11
+ .option("-o, --open", "Show only open and draft tasks (default)")
12
+ .option("-p, --progress", "Show only in-progress tasks")
13
+ .option("--done", "Show only done tasks")
14
+ .option("-a, --all", "Show all non-archived tasks")
15
+ .action(async (options) => {
16
+ try {
17
+ const store = await TaskStore.create();
18
+ const data = await store.readTasks();
19
+
20
+ let filteredTasks: Task[];
21
+
22
+ if (options.all) {
23
+ // All non-archived tasks
24
+ filteredTasks = data.tasks.filter((t) => t.status !== "archived");
25
+ } else if (options.progress) {
26
+ // Only in-progress
27
+ filteredTasks = data.tasks.filter((t) => t.status === "in-progress");
28
+ } else if (options.done) {
29
+ // Only done
30
+ filteredTasks = data.tasks.filter((t) => t.status === "done");
31
+ } else {
32
+ // Default: open and draft
33
+ filteredTasks = data.tasks.filter(
34
+ (t) => t.status === "open" || t.status === "draft"
35
+ );
36
+ }
37
+
38
+ console.log(formatTaskTable(filteredTasks, data.tasks));
39
+ } catch (error) {
40
+ if (error instanceof Error) {
41
+ console.error(error.message);
42
+ } else {
43
+ console.error("An unexpected error occurred");
44
+ }
45
+ process.exit(1);
46
+ }
47
+ });
48
+ }