harvest-worklog 0.7.0 → 0.9.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.
- checksums.yaml +4 -4
- data/README.md +13 -0
- data/lib/harvest_worklog/aggregate_cli.rb +4 -2
- data/lib/harvest_worklog/timesheet_cli.rb +81 -0
- data/lib/harvest_worklog/version.rb +1 -1
- data/lib/harvest_worklog.rb +6 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4ead4ee3da24a3522c5b5e09e694de573b4e10c2736b7e328377197efd349dcb
|
|
4
|
+
data.tar.gz: 41cce9ae00a04e151fd68914ddbb3200ae8451c837f93e95a8bd5b601ea1fc27
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2e2592d645f2153fab30539d0ad0fd103b8008c0025950c9222298fc1e06abf4cbd759f3b1a6e4a8eeba5ecf985670d13b994d430b870144afdf825a0ea44018
|
|
7
|
+
data.tar.gz: 11f9e301054fc944a9abf5a4509777cb9e6e538d3cd5be2fd50fc06b2a88feaf577b7ae22265ffe7d0a45972f5a7d25f58b99fb9ea36fd54b9eaee2edce0cd64
|
data/README.md
CHANGED
|
@@ -23,6 +23,7 @@ Personal Access Tokens are the intended Harvest authentication method for person
|
|
|
23
23
|
harvest-worklog time-off FROM TO --project NAME --task NAME [options]
|
|
24
24
|
harvest-worklog work-entry DATE --project NAME --task NAME --hours HOURS --notes NOTES [options]
|
|
25
25
|
harvest-worklog aggregate FROM TO [--project NAME] [--task NAME]
|
|
26
|
+
harvest-worklog timesheet DATE --project NAME [--task NAME]
|
|
26
27
|
```
|
|
27
28
|
|
|
28
29
|
### Time off
|
|
@@ -57,6 +58,18 @@ harvest-worklog aggregate 2026-07-17 2026-07-19 \
|
|
|
57
58
|
|
|
58
59
|
The `harvest_time_aggregates` OMP tool exposes the same optional filters and is approval-gated as a read.
|
|
59
60
|
|
|
61
|
+
### Daily timesheets
|
|
62
|
+
|
|
63
|
+
The `timesheet` command reads the authenticated user's compact daily Harvest view: task totals, entry durations when a task has multiple entries, and multiline notes. `DATE` accepts `today`, `yesterday`, or an ISO date in the CLI's local timezone.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
harvest-worklog timesheet today --project WRAP
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
OMP exposes the same CLI-only path through `/harvest-worklog today WRAP` and the read-only `harvest_time_sheet` tool. The wrapper never calls Harvest directly. A later, separate feature may upload reviewed content through the Harvest API; keep that write path separate from this read-only formatter.
|
|
70
|
+
|
|
71
|
+
Type `/harvest-worklog ` in OMP to discover the compact and explicit timesheet forms, relative date tokens, and context-valid `--project`/`--task` options. Completion keeps filter values as editable text, and the slash command delegates the selected timesheet argument vector to the CLI.
|
|
72
|
+
|
|
60
73
|
## OMP settings
|
|
61
74
|
|
|
62
75
|
- `defaultHours`: hours per business day when a time-off tool call omits `hours`; defaults to `7`.
|
|
@@ -40,14 +40,16 @@ module HarvestWorklog
|
|
|
40
40
|
raise Error, "FROM and TO are required" unless dates.length == 2
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
def self.fetch_entries(client, from:, to:)
|
|
43
|
+
def self.fetch_entries(client, from:, to:, user_id: nil)
|
|
44
44
|
entries = []
|
|
45
45
|
page = 1
|
|
46
46
|
loop do
|
|
47
|
+
params = { from: from.iso8601, to: to.iso8601, page:, per_page: PER_PAGE }
|
|
48
|
+
params[:user_id] = user_id if user_id
|
|
47
49
|
response = client.request(
|
|
48
50
|
:get,
|
|
49
51
|
"/v2/time_entries",
|
|
50
|
-
params:
|
|
52
|
+
params:
|
|
51
53
|
)
|
|
52
54
|
entries.concat(response.fetch("time_entries"))
|
|
53
55
|
page = response["next_page"]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HarvestWorklog
|
|
4
|
+
class TimesheetCLI
|
|
5
|
+
def self.run(arguments, output: $stdout, error: $stderr, client: nil, today: Date.today)
|
|
6
|
+
options = {}
|
|
7
|
+
parser = option_parser(options)
|
|
8
|
+
dates = parser.parse(arguments)
|
|
9
|
+
validate!(dates, options)
|
|
10
|
+
|
|
11
|
+
spent_date = resolve_date(dates.first, today:)
|
|
12
|
+
client ||= Marlens::HarvestApiV2::Client.from_environment
|
|
13
|
+
user_id = current_user_id(client)
|
|
14
|
+
entries = AggregateCLI.fetch_entries(client, from: spent_date, to: spent_date, user_id:).select { |entry| AggregateCLI.matches?(entry, options) }
|
|
15
|
+
print_timesheet(output, entries, spent_date:, requested_project: options[:project])
|
|
16
|
+
0
|
|
17
|
+
rescue Error, Marlens::HarvestApiV2::Error, OptionParser::ParseError, Date::Error => e
|
|
18
|
+
error.puts "Error: #{e.message}"
|
|
19
|
+
error.puts parser if parser
|
|
20
|
+
1
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.option_parser(options)
|
|
24
|
+
OptionParser.new do |opts|
|
|
25
|
+
opts.banner = "Usage: harvest-worklog timesheet DATE --project NAME [--task NAME]"
|
|
26
|
+
opts.on("--project NAME", "Filter by Harvest project name") { |value| options[:project] = value }
|
|
27
|
+
opts.on("--task NAME", "Filter by Harvest task name") { |value| options[:task] = value }
|
|
28
|
+
opts.on("-h", "--help", "Show this help") do
|
|
29
|
+
puts opts
|
|
30
|
+
exit 0
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.resolve_date(value, today: Date.today)
|
|
36
|
+
case value.downcase
|
|
37
|
+
when "today" then today
|
|
38
|
+
when "yesterday" then today - 1
|
|
39
|
+
else Date.iso8601(value)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.current_user_id(client)
|
|
44
|
+
client.request(:get, "/v2/users/me", params: {}).fetch("id")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.validate!(dates, options)
|
|
48
|
+
raise Error, "DATE is required" unless dates.length == 1
|
|
49
|
+
raise Error, "--project is required" unless options[:project] && !options[:project].empty?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.print_timesheet(output, entries, spent_date:, requested_project:)
|
|
53
|
+
project = entries.first&.dig("project", "name") || requested_project
|
|
54
|
+
output.puts "#{project} · #{spent_date.strftime("%a, %b %-d")} · #{HarvestWorklog.display_hours(AggregateCLI.total_hours(entries))}h"
|
|
55
|
+
if entries.empty?
|
|
56
|
+
output.puts
|
|
57
|
+
output.puts "No time entries."
|
|
58
|
+
return
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
entries.group_by { |entry| entry.dig("task", "name") }.sort_by { |task, _| task.downcase }.each do |task, task_entries|
|
|
62
|
+
output.puts
|
|
63
|
+
output.puts "#{task} · #{HarvestWorklog.display_hours(AggregateCLI.total_hours(task_entries))}h"
|
|
64
|
+
if task_entries.one?
|
|
65
|
+
print_notes(output, task_entries.first["notes"].to_s, indent: 2)
|
|
66
|
+
else
|
|
67
|
+
task_entries.each do |entry|
|
|
68
|
+
output.puts " #{HarvestWorklog.display_hours(Float(entry.fetch("hours")))}h"
|
|
69
|
+
print_notes(output, entry["notes"].to_s, indent: 4)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.print_notes(output, notes, indent:)
|
|
76
|
+
lines = notes.lines(chomp: true).reject(&:empty?)
|
|
77
|
+
lines = ["(no notes)"] if lines.empty?
|
|
78
|
+
lines.each { |line| output.puts "#{" " * indent}#{line}" }
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
data/lib/harvest_worklog.rb
CHANGED
|
@@ -36,11 +36,13 @@ module HarvestWorklog
|
|
|
36
36
|
WorkEntryCLI.run(command_arguments, output:, error:, client:)
|
|
37
37
|
when "aggregate"
|
|
38
38
|
AggregateCLI.run(command_arguments, output:, error:, client:)
|
|
39
|
+
when "timesheet"
|
|
40
|
+
TimesheetCLI.run(command_arguments, output:, error:, client:)
|
|
39
41
|
when "-h", "--help"
|
|
40
42
|
output.puts usage
|
|
41
43
|
0
|
|
42
44
|
else
|
|
43
|
-
error.puts "Error: choose time-off, work-entry, or
|
|
45
|
+
error.puts "Error: choose time-off, work-entry, aggregate, or timesheet"
|
|
44
46
|
error.puts usage
|
|
45
47
|
1
|
|
46
48
|
end
|
|
@@ -52,11 +54,13 @@ module HarvestWorklog
|
|
|
52
54
|
harvest-worklog time-off FROM TO --project NAME --task NAME [options]
|
|
53
55
|
harvest-worklog work-entry DATE --project NAME --task NAME --hours HOURS --notes NOTES [options]
|
|
54
56
|
harvest-worklog aggregate FROM TO [--project NAME] [--task NAME]
|
|
57
|
+
harvest-worklog timesheet DATE --project NAME [--task NAME]
|
|
55
58
|
|
|
56
59
|
Commands:
|
|
57
60
|
time-off Create one entry per local business day in a date range.
|
|
58
61
|
work-entry Create one reviewed ordinary-work entry.
|
|
59
62
|
aggregate Read time-entry totals without writing Harvest records.
|
|
63
|
+
timesheet Read a compact daily project timesheet without writing Harvest records.
|
|
60
64
|
USAGE
|
|
61
65
|
end
|
|
62
66
|
end
|
|
@@ -170,3 +174,4 @@ end
|
|
|
170
174
|
|
|
171
175
|
require "harvest_worklog/work_entry_cli"
|
|
172
176
|
require "harvest_worklog/aggregate_cli"
|
|
177
|
+
require "harvest_worklog/timesheet_cli"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: harvest-worklog
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Marlen Brunner
|
|
@@ -63,6 +63,7 @@ files:
|
|
|
63
63
|
- harvest-worklog.rb
|
|
64
64
|
- lib/harvest_worklog.rb
|
|
65
65
|
- lib/harvest_worklog/aggregate_cli.rb
|
|
66
|
+
- lib/harvest_worklog/timesheet_cli.rb
|
|
66
67
|
- lib/harvest_worklog/version.rb
|
|
67
68
|
- lib/harvest_worklog/work_entry_cli.rb
|
|
68
69
|
homepage: https://github.com/klondikemarlen/harvest-worklog
|