tasku 0.4.1 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 206dc71e9498feb028e4c8b05fc3de8a23d6073b1da9db0988ef0acea3fc5f5d
4
- data.tar.gz: 58dacb2562825ff61794649652c617b3ea7a4a619804a32c56067ff5ef2fd1ef
3
+ metadata.gz: d18e6bd1dfa0e69dc685364eb89aaf5a77ea20536179bb635fe5cabf419b7296
4
+ data.tar.gz: 2d37502dcef646ec9f7d5b8805ee266759e3414e6768abfc3e0393b0193c5a1e
5
5
  SHA512:
6
- metadata.gz: 719c9a295c5868d5dbeb152e35cacde49d5be9f2ad229e2818a20d3eeaecf6ae86c461ea27e2d921c5b15ecbb48f0180f3e4c4ddc56d020dd36baaf5d901fb3e
7
- data.tar.gz: 3c09b8f6970813d4a73d0772c2a92530d9e5958ad22f9fdd52c77a4f6fdc0a1b132af902ee59ba75060b25227695b48f7b22cf30541990cc763a4de41d5216c0
6
+ metadata.gz: 4d90b603cc137e7a1a296d808bfae2287d7bb3a111942a5ead559bb67684bf1a24c30a0b5a03afdced036b60eaebc2a50c4293aead2af7eb6e6caac21a39b49a
7
+ data.tar.gz: fb33681495b68a02ed4a5b82dc1b9cb04da5409d48bb90a5e6cc70128e8b8e8417b839b4b74d4e86f3ececa9bbf8d89b96f7705b14f20cd2326ce1615cd70777
data/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  All notable changes to Tasku are documented here.
4
4
 
5
+ ## [0.4.2] - 2026-10-29
6
+
7
+ ### Added
8
+ - **Auto branch checkout** — when a task is set to `in_progress` via `tasku edit ID --status in_progress`, Tasku automatically creates and checks out a git branch named `CODE-ID-name-slug` in the linked repo. If the branch already exists it switches to it instead. Configure with `tasku git link CODE PATH`, remove with `tasku git unlink CODE`, and inspect with `tasku git list`.
9
+ - **Mado: confirm before delete** — pressing `d` in the interactive task list now shows a confirmation prompt; press `y` to confirm, any other key cancels
10
+
11
+ ### Security
12
+ - All `system()` and `exec()` calls in `MadoList` now use the array form (via
13
+ `Shellwords.shellsplit`) instead of a shell string, preventing shell
14
+ interpretation of metacharacters in command arguments
15
+
5
16
  ## [0.4.1] - 2026-09-29
6
17
 
7
18
  ### Fixed
data/lib/tasku/cli.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "fileutils"
4
4
  require "json"
5
5
  require "net/http"
6
+ require "shellwords"
6
7
  require "uri"
7
8
 
8
9
  module Tasku
@@ -69,6 +70,48 @@ module Tasku
69
70
  end
70
71
  end
71
72
 
73
+ class GitApp < Thor
74
+ desc "link CODE PATH", "Link a project code to a git repo path (enables auto branch checkout)"
75
+ def link(code, path)
76
+ pastel = Pastel.new
77
+ expanded = File.expand_path(path)
78
+ unless Dir.exist?(expanded)
79
+ abort pastel.red(" Path does not exist: #{expanded}")
80
+ end
81
+ unless Dir.exist?(File.join(expanded, ".git"))
82
+ abort pastel.red(" Not a git repository: #{expanded}")
83
+ end
84
+ Tasku::Config.set_git_path(code, expanded)
85
+ puts pastel.green(" ✓ #{code.upcase} linked to #{expanded}")
86
+ puts pastel.dim(" Setting a task to in_progress will now auto-checkout a branch.")
87
+ end
88
+
89
+ desc "unlink CODE", "Remove a project code's git repo link"
90
+ def unlink(code)
91
+ pastel = Pastel.new
92
+ unless Tasku::Config.git_path_for(code)
93
+ abort pastel.yellow(" #{code.upcase} is not linked to any repo.")
94
+ end
95
+ Tasku::Config.unset_git_path(code)
96
+ puts pastel.green(" ✓ #{code.upcase} unlinked.")
97
+ end
98
+
99
+ desc "list", "List all project code → repo path links"
100
+ def list
101
+ pastel = Pastel.new
102
+ projects = Tasku::Config.all_git_projects
103
+ if projects.empty?
104
+ puts pastel.dim(" No git links configured. Use `tasku git link CODE PATH` to add one.")
105
+ return
106
+ end
107
+ puts ""
108
+ projects.each do |code, path|
109
+ puts " #{pastel.bold(code.ljust(8))} #{path}"
110
+ end
111
+ puts ""
112
+ end
113
+ end
114
+
72
115
  class CloudApp < Thor
73
116
  desc "login TOKEN", "Authenticate with Tasku Cloud using an API token"
74
117
  option :url, type: :string, desc: "Tasku Cloud URL (default: #{Tasku::Config::CLOUD_URL_DEFAULT})"
@@ -196,9 +239,10 @@ module Tasku
196
239
  abort pastel.red(" Sync failed (HTTP #{res.code}).")
197
240
  end
198
241
 
199
- body = JSON.parse(res.body)
200
- server_tasks = body["tasks"] || []
201
- synced_at = body["synced_at"]
242
+ body = JSON.parse(res.body)
243
+ server_tasks = body["tasks"] || []
244
+ server_projects = body["projects"] || []
245
+ synced_at = body["synced_at"]
202
246
 
203
247
  created = updated = conflicts = 0
204
248
 
@@ -318,6 +362,9 @@ module Tasku
318
362
  desc "config SUBCOMMAND", "Manage user preferences"
319
363
  subcommand "config", ConfigApp
320
364
 
365
+ desc "git SUBCOMMAND", "Manage git repo links for auto branch checkout"
366
+ subcommand "git", GitApp
367
+
321
368
  desc "cloud SUBCOMMAND", "Sync tasks with Tasku Cloud"
322
369
  subcommand "cloud", CloudApp
323
370
 
@@ -561,6 +608,7 @@ module Tasku
561
608
 
562
609
  task.update(attrs)
563
610
  terminal.render_updated(task)
611
+ auto_checkout_branch(task, attrs) if attrs[:status] == "in_progress"
564
612
  rescue Sequel::ValidationFailed => e
565
613
  abort pastel.red("Validation error: #{e.message}")
566
614
  end
@@ -696,7 +744,7 @@ module Tasku
696
744
  category = nil if category&.empty?
697
745
 
698
746
  priority = prompt.select("Priority?", %w[none low medium high urgent], default: 1)
699
- status = prompt.select("Status?", %w[backlog todo in_progress], default: 2)
747
+ status = prompt.select("Status?", Task::VALID_STATUSES, default: 2)
700
748
 
701
749
  start_day = prompt.ask("Start date (YYYY-MM-DD, optional):", default: "")
702
750
  start_day = nil if start_day&.empty?
@@ -884,6 +932,34 @@ module Tasku
884
932
  "\e[1;38;2;#{r};#{g};#{b}m#{text}\e[0m"
885
933
  end
886
934
 
935
+ def auto_checkout_branch(task, attrs)
936
+ return unless task.code && !task.code.empty?
937
+
938
+ repo_path = Tasku::Config.git_path_for(task.code)
939
+ return unless repo_path
940
+
941
+ slug = task.name.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-|-\z/, "")
942
+ branch = "#{task.code}-#{task.id}-#{slug}"
943
+
944
+ # Check if the branch already exists locally.
945
+ existing = `git -C #{repo_path.shellescape} branch --list #{branch.shellescape} 2>/dev/null`.strip
946
+ if existing.empty?
947
+ result = system("git", "-C", repo_path, "checkout", "-b", branch, out: :close, err: :close)
948
+ if result
949
+ puts pastel.dim(" git: created and switched to branch #{branch}")
950
+ else
951
+ puts pastel.yellow(" git: could not create branch #{branch} — is the working tree clean?")
952
+ end
953
+ else
954
+ result = system("git", "-C", repo_path, "checkout", branch, out: :close, err: :close)
955
+ if result
956
+ puts pastel.dim(" git: switched to existing branch #{branch}")
957
+ else
958
+ puts pastel.yellow(" git: could not switch to branch #{branch}")
959
+ end
960
+ end
961
+ end
962
+
887
963
  def colour_cell(col, padded_val, row)
888
964
  case col.to_s
889
965
  when "priority"
data/lib/tasku/config.rb CHANGED
@@ -104,6 +104,40 @@ module Tasku
104
104
  !cloud_token.nil?
105
105
  end
106
106
 
107
+ # ── Git project paths ──────────────────────────────────────────────────
108
+ # Maps a 3-char project code (e.g. "TAS") to an absolute repo path.
109
+ # Stored under the "git_projects" key in config.json.
110
+
111
+ def self.git_path_for(code)
112
+ git_projects[code.to_s.upcase]
113
+ end
114
+
115
+ def self.set_git_path(code, path)
116
+ current = all
117
+ current["git_projects"] ||= {}
118
+ current["git_projects"][code.to_s.upcase] = File.expand_path(path)
119
+ FileUtils.mkdir_p(File.dirname(CONFIG_PATH))
120
+ File.write(CONFIG_PATH, JSON.pretty_generate(current))
121
+ end
122
+
123
+ def self.unset_git_path(code)
124
+ current = all
125
+ return unless current["git_projects"]
126
+
127
+ current["git_projects"].delete(code.to_s.upcase)
128
+ current.delete("git_projects") if current["git_projects"].empty?
129
+ File.write(CONFIG_PATH, JSON.pretty_generate(current))
130
+ end
131
+
132
+ def self.all_git_projects
133
+ git_projects
134
+ end
135
+
136
+ def self.git_projects
137
+ all["git_projects"] || {}
138
+ end
139
+ private_class_method :git_projects
140
+
107
141
  def self.get(key)
108
142
  all[key] || VALID_KEYS.dig(key, :default)
109
143
  end
@@ -13,6 +13,7 @@ module Tasku
13
13
  # button handlers can inject it into commands like `tasku edit <id>`.
14
14
  class MadoList
15
15
  HINT = " ↑↓ navigate a add e/↵ edit d delete s stats q quit"
16
+ HINT_CONFIRM = " Delete task? Press y to confirm, any other key to cancel"
16
17
 
17
18
  def initialize(tasks, colour_map: {}, bar: {}, cols_cfg: {}, project: nil, list_cmd: nil)
18
19
  @tasks = tasks
@@ -156,7 +157,7 @@ module Tasku
156
157
  run_command("tasku edit #{@tasks[@cursor].id} -i")
157
158
  when :delete
158
159
  return if @tasks.empty?
159
- run_command("tasku delete #{@tasks[@cursor].id} -f")
160
+ confirm_delete
160
161
  when :stats
161
162
  run_and_show("tasku stats")
162
163
  when :inline_exec
@@ -175,7 +176,7 @@ module Tasku
175
176
  restore_terminal
176
177
  $stdin.cooked!
177
178
  MadoLog.log("inline_exec tasku list → exec #{cmd.inspect}")
178
- exec(cmd)
179
+ exec(*Shellwords.shellsplit(cmd))
179
180
  else
180
181
  run_command(cmd)
181
182
  end
@@ -189,9 +190,9 @@ module Tasku
189
190
  @running = false
190
191
  restore_terminal
191
192
  $stdin.cooked!
192
- system(cmd)
193
+ system(*Shellwords.shellsplit(cmd))
193
194
  MadoLog.log("run_command finished cmd=#{cmd.inspect} → exec #{@list_cmd}")
194
- exec(@list_cmd)
195
+ exec(*Shellwords.shellsplit(@list_cmd))
195
196
  end
196
197
 
197
198
  # Run a non-interactive display command, wait for any keypress via
@@ -202,12 +203,12 @@ module Tasku
202
203
  @running = false
203
204
  restore_terminal
204
205
  $stdin.cooked!
205
- system(cmd)
206
+ system(*Shellwords.shellsplit(cmd))
206
207
  $stdout.print "\r\n\e[2m Press any key to return to list…\e[0m\r\n"
207
208
  $stdout.flush
208
209
  $stdin.getch
209
210
  MadoLog.log("run_and_show keypress received → exec #{@list_cmd}")
210
- exec(@list_cmd)
211
+ exec(*Shellwords.shellsplit(@list_cmd))
211
212
  end
212
213
 
213
214
  # Show a SQL prompt, run the query, wait for keypress, then re-exec list.
@@ -227,7 +228,24 @@ module Tasku
227
228
  $stdin.getch
228
229
  end
229
230
  MadoLog.log("run_sql done → exec #{@list_cmd}")
230
- exec(@list_cmd)
231
+ exec(*Shellwords.shellsplit(@list_cmd))
232
+ end
233
+
234
+ def confirm_delete
235
+ task = @tasks[@cursor]
236
+ name_preview = task.name.length > 40 ? "#{task.name[0, 39]}…" : task.name
237
+ border = @pastel.dim(" " + "─" * (@row_width + 4))
238
+ $stdout.print "\r\n#{border}\r\n"
239
+ $stdout.print " #{@pastel.yellow("Delete: ")}#{name_preview}\r\n"
240
+ $stdout.print "#{border}\r\n"
241
+ $stdout.print "#{@pastel.dim(HINT_CONFIRM)}\r\n"
242
+ $stdout.flush
243
+ byte = $stdin.getbyte
244
+ if byte == 0x79 || byte == 0x59 # y / Y
245
+ run_command("tasku delete #{task.id} -f")
246
+ else
247
+ full_render
248
+ end
231
249
  end
232
250
 
233
251
  def restore_terminal
data/lib/tasku/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tasku
4
- VERSION = "0.4.1"
4
+ VERSION = "0.4.2"
5
5
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tasku
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tom Dringer
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2026-09-29 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: thor
@@ -82,7 +81,6 @@ dependencies:
82
81
  version: '1.6'
83
82
  description: Tasku is a terminal-based task manager with colour-coded priorities,
84
83
  status tracking, SQLite persistence, and a beautiful CLI interface.
85
- email:
86
84
  executables:
87
85
  - tasku
88
86
  extensions: []
@@ -107,7 +105,6 @@ metadata:
107
105
  homepage_uri: https://github.com/tomdringer/tasku
108
106
  source_code_uri: https://github.com/tomdringer/tasku
109
107
  changelog_uri: https://github.com/tomdringer/tasku/blob/main/CHANGELOG.md
110
- post_install_message:
111
108
  rdoc_options: []
112
109
  require_paths:
113
110
  - lib
@@ -122,8 +119,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
122
119
  - !ruby/object:Gem::Version
123
120
  version: '0'
124
121
  requirements: []
125
- rubygems_version: 3.5.3
126
- signing_key:
122
+ rubygems_version: 4.0.20
127
123
  specification_version: 4
128
124
  summary: タスクリスト — a beautiful terminal task manager
129
125
  test_files: []