logwork-cli 0.0.0-stage → 1.1.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.
Files changed (96) hide show
  1. package/LICENSE +11 -0
  2. package/README.md +330 -2
  3. package/RELEASE.md +89 -0
  4. package/bin/logwork.mjs +104 -0
  5. package/commands/auth.mjs +227 -0
  6. package/commands/commits.mjs +82 -0
  7. package/commands/delete.mjs +69 -0
  8. package/commands/doctor.mjs +24 -0
  9. package/commands/edit.mjs +88 -0
  10. package/commands/history.mjs +62 -0
  11. package/commands/query.mjs +52 -0
  12. package/commands/quick.mjs +163 -0
  13. package/commands/reconcile.mjs +55 -0
  14. package/commands/release.mjs +77 -0
  15. package/commands/reminder.mjs +172 -0
  16. package/commands/setup.mjs +508 -0
  17. package/commands/skill.mjs +91 -0
  18. package/commands/tui.mjs +50 -0
  19. package/commands/update.mjs +161 -0
  20. package/docs/advanced.md +334 -0
  21. package/docs/mcp-setup.md +368 -0
  22. package/docs/operations-guide.md +587 -0
  23. package/docs/recipes.md +259 -0
  24. package/docs/security.md +151 -0
  25. package/examples/mcp/antigravity.mcp_config.json +8 -0
  26. package/examples/mcp/codex.config.toml +5 -0
  27. package/examples/mcp/copilot.mcp.json +9 -0
  28. package/lib/agent-skill.mjs +16 -0
  29. package/lib/api-auth.mjs +1259 -0
  30. package/lib/api.mjs +1422 -0
  31. package/lib/apply-ledger.mjs +227 -0
  32. package/lib/atomic-file.mjs +24 -0
  33. package/lib/auth-diagnostics.mjs +113 -0
  34. package/lib/auth-errors.mjs +22 -0
  35. package/lib/auth-redaction.mjs +78 -0
  36. package/lib/auth-terminal.mjs +97 -0
  37. package/lib/auth.mjs +323 -0
  38. package/lib/batch-parser.mjs +145 -0
  39. package/lib/batch-workflow.mjs +842 -0
  40. package/lib/commit-workflow.mjs +402 -0
  41. package/lib/config.mjs +191 -0
  42. package/lib/credential-store.mjs +130 -0
  43. package/lib/diagnostics.mjs +307 -0
  44. package/lib/entrypoint.mjs +14 -0
  45. package/lib/file-lock.mjs +44 -0
  46. package/lib/http.mjs +125 -0
  47. package/lib/jira-api.mjs +258 -0
  48. package/lib/jira-auth.mjs +203 -0
  49. package/lib/jira-workflow.mjs +523 -0
  50. package/lib/keychain.mjs +105 -0
  51. package/lib/legacy-migration.mjs +66 -0
  52. package/lib/list-navigation.mjs +19 -0
  53. package/lib/logwork-config.mjs +199 -0
  54. package/lib/logwork-f03-enrich.mjs +41 -0
  55. package/lib/macos.mjs +33 -0
  56. package/lib/manual-apply-state.mjs +35 -0
  57. package/lib/manual-commands.mjs +294 -0
  58. package/lib/manual-drafts.mjs +154 -0
  59. package/lib/manual-ink-app.mjs +1654 -0
  60. package/lib/manual-ink-auth.mjs +349 -0
  61. package/lib/manual-ink-ui.mjs +675 -0
  62. package/lib/manual-input.mjs +21 -0
  63. package/lib/manual-logwork-wizard.mjs +310 -0
  64. package/lib/manual-mcp.mjs +187 -0
  65. package/lib/manual-repl.mjs +1037 -0
  66. package/lib/manual-targets.mjs +78 -0
  67. package/lib/mcp-server.mjs +833 -0
  68. package/lib/os-notification.mjs +105 -0
  69. package/lib/package-info.mjs +17 -0
  70. package/lib/paths.mjs +68 -0
  71. package/lib/project-identity.mjs +66 -0
  72. package/lib/project-mapping-workflow.mjs +309 -0
  73. package/lib/project-resolver.mjs +373 -0
  74. package/lib/query-workflow.mjs +553 -0
  75. package/lib/reconciliation-workflow.mjs +446 -0
  76. package/lib/release-doctor.mjs +448 -0
  77. package/lib/reminder-config.mjs +71 -0
  78. package/lib/reminder-scheduler.mjs +301 -0
  79. package/lib/reminder-service.mjs +118 -0
  80. package/lib/reminder-state.mjs +38 -0
  81. package/lib/reminder-workflow.mjs +299 -0
  82. package/lib/ro-delete-args.mjs +38 -0
  83. package/lib/ro-delete-workflow.mjs +115 -0
  84. package/lib/ro-edit-args.mjs +142 -0
  85. package/lib/ro-edit-workflow.mjs +632 -0
  86. package/lib/ro-entry-reader.mjs +56 -0
  87. package/lib/ro-resubmit-workflow.mjs +134 -0
  88. package/lib/terminal-auth-prompts.mjs +120 -0
  89. package/lib/ui.mjs +133 -0
  90. package/lib/update-service.mjs +350 -0
  91. package/lib/util.mjs +66 -0
  92. package/lib/windows-credential-manager.mjs +260 -0
  93. package/lib/worklog-task-resolver.mjs +113 -0
  94. package/package.json +73 -4
  95. package/setup.sh +22 -0
  96. package/skills/logwork/SKILL.md +43 -0
@@ -0,0 +1,227 @@
1
+ import {
2
+ getStoredAuthStatus,
3
+ loginResourceOptimiser,
4
+ logoutResourceOptimiser
5
+ } from '../lib/auth.mjs';
6
+ import { authenticateWithApiSession } from '../lib/api-auth.mjs';
7
+ import { createAuthDiagnosticsRecorder } from '../lib/auth-diagnostics.mjs';
8
+ import { credentialStoreLabel } from '../lib/credential-store.mjs';
9
+ import {
10
+ getStoredJiraStatus,
11
+ loginJira,
12
+ logoutJira,
13
+ safeJiraAuthError
14
+ } from '../lib/jira-auth.mjs';
15
+
16
+ // `logwork login|logout|status [ro|jira]`. Resource Optimiser is the default
17
+ // target. Secrets are only ever typed in this terminal and stored in the OS
18
+ // credential store; they are never accepted as arguments.
19
+
20
+ export async function runLogin(argv = []) {
21
+ const { target, args } = splitTarget(argv);
22
+ if (wantsHelp(args)) {
23
+ console.log(formatAuthHelp());
24
+ return 0;
25
+ }
26
+ if (target === 'jira') {
27
+ const result = await runJiraLoginCommand(parseJiraLoginArgs(args));
28
+ return result.ok ? 0 : 1;
29
+ }
30
+ parseLoginArgs(args);
31
+ const result = await runLoginCommand();
32
+ return result.ok ? 0 : 1;
33
+ }
34
+
35
+ export async function runLogout(argv = []) {
36
+ const { target, args } = splitTarget(argv);
37
+ if (wantsHelp(args)) {
38
+ console.log(formatAuthHelp());
39
+ return 0;
40
+ }
41
+ assertNoArgs(args, 'logout');
42
+ const result = target === 'jira' ? await logoutJira() : await logoutResourceOptimiser();
43
+ console.log(result.summary);
44
+ return 0;
45
+ }
46
+
47
+ export async function runStatus(argv = []) {
48
+ if (wantsHelp(argv)) {
49
+ console.log(formatAuthHelp());
50
+ return 0;
51
+ }
52
+ const { target, args } = splitTarget(argv);
53
+ assertNoArgs(args, 'status');
54
+ const targets = argv.length ? [target] : ['ro', 'jira'];
55
+ let authenticated = true;
56
+ for (const item of targets) {
57
+ const result = item === 'jira' ? await getStoredJiraStatus() : await getStoredAuthStatus();
58
+ console.log(`${item === 'jira' ? 'Jira' : 'Resource Optimiser'}: ${result.summary}`);
59
+ if (item === 'ro' && !result.authenticated) {
60
+ authenticated = false;
61
+ }
62
+ }
63
+ return authenticated ? 0 : 1;
64
+ }
65
+
66
+ export function formatAuthHelp() {
67
+ const storeLabel = credentialStoreLabel();
68
+ return `Usage:
69
+ logwork login [ro|jira] [--base-url <url>]
70
+ logwork logout [ro|jira]
71
+ logwork status [ro|jira]
72
+
73
+ Targets:
74
+ ro Resource Optimiser (default). Email, password and 2FA are asked in this terminal.
75
+ jira Jira Personal Access Token, pasted in this terminal.
76
+
77
+ Sessions are stored in ${storeLabel}, never in files or command arguments.
78
+
79
+ Options:
80
+ --base-url <url> Jira base URL for \`login jira\`. Defaults to https://jira-vnv.vinova.sg`;
81
+ }
82
+
83
+ function splitTarget(argv = []) {
84
+ const [first, ...rest] = argv;
85
+ if (first === 'ro' || first === 'jira') {
86
+ return { target: first, args: rest };
87
+ }
88
+ return { target: 'ro', args: argv };
89
+ }
90
+
91
+ function wantsHelp(args) {
92
+ return args.includes('--help') || args.includes('-h');
93
+ }
94
+
95
+ function assertNoArgs(args, command) {
96
+ if (args.length) {
97
+ throw new Error(`Unknown ${command} option: ${args[0]}`);
98
+ }
99
+ }
100
+
101
+ export async function runLoginCommand({
102
+ login = loginResourceOptimiser,
103
+ createRecorder = createAuthDiagnosticsRecorder,
104
+ stdout = console.log,
105
+ stderr = console.error
106
+ } = {}) {
107
+ stdout('Starting Resource Optimiser API auth. Secrets are requested in this terminal and are not stored.');
108
+ const diagnosticsRecorder = createRecorder();
109
+
110
+ try {
111
+ const result = await login({
112
+ authenticator: (options) => authenticateWithApiSession({
113
+ ...options,
114
+ diagnosticsRecorder
115
+ })
116
+ });
117
+ stdout(result.summary);
118
+ return {
119
+ ok: true,
120
+ result
121
+ };
122
+ } catch (error) {
123
+ stderr(error.message);
124
+ try {
125
+ const written = await diagnosticsRecorder.writeFailure(error);
126
+ stderr(`Auth failed. Sanitized diagnostics saved to ${written.path}. Attempt: ${written.attemptId}`);
127
+ } catch (writeError) {
128
+ stderr(`Auth failed. Unable to write sanitized diagnostics: ${writeError.message}`);
129
+ }
130
+ return {
131
+ ok: false,
132
+ error
133
+ };
134
+ }
135
+ }
136
+
137
+ export function parseLoginArgs(args = []) {
138
+ for (let index = 0; index < args.length; index += 1) {
139
+ const arg = args[index];
140
+
141
+ if (arg === '--api-mode') {
142
+ continue;
143
+ }
144
+
145
+ if (arg === '--browser' || arg.startsWith('--browser=')) {
146
+ throw new Error('Browser auth has been removed. Run `logwork login` for API-only auth.');
147
+ }
148
+
149
+ if (arg === '--browser-mode') {
150
+ throw new Error('Browser auth has been removed. Run `logwork login` for API-only auth.');
151
+ }
152
+
153
+ if (arg === 'record') {
154
+ throw new Error('Auth recording has been removed because auth is API-only.');
155
+ }
156
+
157
+ if (arg === '--help' || arg === '-h') {
158
+ continue;
159
+ }
160
+
161
+ throw new Error(`Unknown auth login option: ${arg}`);
162
+ }
163
+
164
+ return { mode: 'api' };
165
+ }
166
+
167
+ export async function runJiraLoginCommand({
168
+ baseUrl,
169
+ login = loginJira,
170
+ stdout = console.log,
171
+ stderr = console.error
172
+ } = {}) {
173
+ stdout('Starting Jira PAT auth. Paste the token in this terminal; it is stored only in the OS credential store.');
174
+
175
+ try {
176
+ const result = await login({
177
+ ...(baseUrl ? { baseUrl } : {})
178
+ });
179
+ stdout(result.summary);
180
+ return {
181
+ ok: true,
182
+ result
183
+ };
184
+ } catch (error) {
185
+ const safeError = safeJiraAuthError(error);
186
+ stderr(safeError.message);
187
+ return {
188
+ ok: false,
189
+ error: safeError
190
+ };
191
+ }
192
+ }
193
+
194
+ export function parseJiraLoginArgs(args = []) {
195
+ const options = {};
196
+
197
+ for (let index = 0; index < args.length; index += 1) {
198
+ const arg = args[index];
199
+
200
+ if (arg === '--help' || arg === '-h') {
201
+ continue;
202
+ }
203
+
204
+ if (arg === '--base-url') {
205
+ const value = args[index + 1];
206
+ if (!value || value.startsWith('--')) {
207
+ throw new Error('--base-url requires a value.');
208
+ }
209
+ options.baseUrl = value;
210
+ index += 1;
211
+ continue;
212
+ }
213
+
214
+ if (arg.startsWith('--base-url=')) {
215
+ const value = arg.slice('--base-url='.length);
216
+ if (!value) {
217
+ throw new Error('--base-url requires a value.');
218
+ }
219
+ options.baseUrl = value;
220
+ continue;
221
+ }
222
+
223
+ throw new Error(`Unknown jira login option: ${arg}`);
224
+ }
225
+
226
+ return options;
227
+ }
@@ -0,0 +1,82 @@
1
+ import { draftLogworkFromCommits } from '../lib/commit-workflow.mjs';
2
+ import { normalizeReconciliationPeriod } from '../lib/reconciliation-workflow.mjs';
3
+
4
+ // `logwork commits [period] [--repo path]... [--author x] [--no-bookings] [--json]`:
5
+ // turn your git commits into a logwork draft. Read-only.
6
+ export function parseCommitsArgs(args = []) {
7
+ const options = { period: 'today', repos: [], author: undefined, useBookings: true, json: false, help: false };
8
+ const positional = [];
9
+ for (let index = 0; index < args.length; index += 1) {
10
+ const arg = args[index];
11
+ if (arg === '--help' || arg === '-h') {
12
+ options.help = true;
13
+ } else if (arg === '--json') {
14
+ options.json = true;
15
+ } else if (arg === '--no-bookings') {
16
+ options.useBookings = false;
17
+ } else if (arg === '--repo' || arg === '--author') {
18
+ const value = args[index + 1];
19
+ if (!value || value.startsWith('--')) {
20
+ throw new Error(`${arg} requires a value.`);
21
+ }
22
+ if (arg === '--repo') {
23
+ options.repos.push(value);
24
+ } else {
25
+ options.author = value;
26
+ }
27
+ index += 1;
28
+ } else if (arg.startsWith('--repo=')) {
29
+ options.repos.push(arg.slice('--repo='.length));
30
+ } else if (arg.startsWith('--author=')) {
31
+ options.author = arg.slice('--author='.length);
32
+ } else if (arg.startsWith('-')) {
33
+ throw new Error(`Unknown commits option: ${arg}`);
34
+ } else {
35
+ positional.push(arg);
36
+ }
37
+ }
38
+ if (positional.length > 1) {
39
+ throw new Error('commits accepts exactly one preset period.');
40
+ }
41
+ if (positional[0]) {
42
+ options.period = normalizeReconciliationPeriod(positional[0]);
43
+ }
44
+ return options;
45
+ }
46
+
47
+ export async function run(args = [], { draft = draftLogworkFromCommits, print = console.log } = {}) {
48
+ const options = parseCommitsArgs(args);
49
+ if (options.help) {
50
+ print(formatCommitsHelp());
51
+ return 0;
52
+ }
53
+ const result = await draft({
54
+ period: options.period,
55
+ repos: options.repos,
56
+ author: options.author,
57
+ useBookings: options.useBookings
58
+ });
59
+ print(options.json ? JSON.stringify(result, null, 2) : result.summary);
60
+ return 0;
61
+ }
62
+
63
+ export function formatCommitsHelp() {
64
+ return `Usage:
65
+ logwork commits [today|yesterday|this-week|last-week|this-month|last-month]
66
+ [--repo <path>]... [--author <email>] [--no-bookings] [--json]
67
+
68
+ Reads your commits (all branches, your git user.email) from each repo (default:
69
+ the current directory) and drafts logwork lines:
70
+ +<hours> <worklog task> | <description>
71
+
72
+ - Commits are grouped per day by repo, ticket key and kind of work.
73
+ - The kind of work picks the worklog task: feat -> Feature Development,
74
+ fix -> Bug Fix, refactor/perf/review -> Code Review & Refactoring,
75
+ test -> Testing Activities, ci/build/release -> Deployment & Release,
76
+ docs/design -> Requirements & Design, chore -> Other Activities.
77
+ - Hours split the day's RO remaining time (booked - logged) by how much work
78
+ each group holds. With --no-bookings or without login, a weekday is 8h.
79
+
80
+ Nothing is written. Review the draft, then paste it into /logwork or give it
81
+ to your AI agent (log_work).`;
82
+ }
@@ -0,0 +1,69 @@
1
+ import { confirm, isCancel } from '@clack/prompts';
2
+ import { parseRoDeleteArgs } from '../lib/ro-delete-args.mjs';
3
+ import {
4
+ applyRoLogworkDelete,
5
+ previewRoLogworkDelete
6
+ } from '../lib/ro-delete-workflow.mjs';
7
+
8
+ export async function runRoDeleteCommand(args = [], {
9
+ preview = previewRoLogworkDelete,
10
+ apply = applyRoLogworkDelete,
11
+ promptConfirm = defaultConfirm,
12
+ isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY),
13
+ print = console.log
14
+ } = {}) {
15
+ const options = parseRoDeleteArgs(args);
16
+ if (options.help) {
17
+ print(formatRoDeleteHelp());
18
+ return null;
19
+ }
20
+
21
+ if (options.json && !options.yes) {
22
+ throw new Error('--json requires --yes so stdout remains valid JSON.');
23
+ }
24
+
25
+ const deletePreview = await preview({ logworkId: options.logworkId });
26
+ if (!options.json) {
27
+ print(deletePreview.summary);
28
+ }
29
+
30
+ let approved = options.yes;
31
+ if (!approved) {
32
+ if (!isTTY) {
33
+ throw new Error('Interactive confirmation is unavailable. Re-run with --yes.');
34
+ }
35
+ approved = await promptConfirm(`Delete RO logwork entry ${deletePreview.logworkId}?`);
36
+ }
37
+ if (!approved) {
38
+ print('RO logwork delete cancelled.');
39
+ return null;
40
+ }
41
+
42
+ const result = await apply({
43
+ preview: deletePreview,
44
+ confirm: true
45
+ });
46
+ print(options.json ? JSON.stringify(result, null, 2) : result.summary);
47
+ return result;
48
+ }
49
+
50
+ export function formatRoDeleteHelp() {
51
+ return `Usage:
52
+ logwork delete <logwork-id> [--yes] [--json]
53
+
54
+ Soft-deletes one Resource Optimiser logwork entry (submitted or approved only).
55
+ The command previews the entry, checks for concurrent changes, then calls DELETE /logwork/entries/:id.
56
+
57
+ Options:
58
+ -y, --yes Delete without interactive confirmation
59
+ --json Print structured JSON; requires --yes
60
+ -h, --help Show this help`;
61
+ }
62
+
63
+ async function defaultConfirm(message) {
64
+ const result = await confirm({
65
+ message,
66
+ initialValue: false
67
+ });
68
+ return !isCancel(result) && result === true;
69
+ }
@@ -0,0 +1,24 @@
1
+ import { generateDiagnosticsReport } from '../lib/diagnostics.mjs';
2
+
3
+ export async function run(args = []) {
4
+ if (args.includes('--help') || args.includes('-h')) {
5
+ printHelp();
6
+ return;
7
+ }
8
+ if (args.length) {
9
+ throw new Error(`Unknown doctor option: ${args[0]}`);
10
+ }
11
+
12
+ const result = await generateDiagnosticsReport();
13
+ console.log(result.summary);
14
+ console.log('Send this sanitized file to the developer. Do not send raw curl logs, cookies, passwords, OTPs, or tokens.');
15
+ }
16
+
17
+ function printHelp() {
18
+ console.log(`Usage:
19
+ logwork doctor
20
+
21
+ Checks your setup and writes a sanitized support report under ~/.logwork-cli/diagnostics.
22
+ The report redacts tokens, cookies, passwords, OTPs, auth codes, and raw HTML.
23
+ `);
24
+ }
@@ -0,0 +1,88 @@
1
+ import { confirm, isCancel } from '@clack/prompts';
2
+ import { parseRoEditArgs } from '../lib/ro-edit-args.mjs';
3
+ import {
4
+ applyRoLogworkEdit,
5
+ previewRoLogworkEdit
6
+ } from '../lib/ro-edit-workflow.mjs';
7
+
8
+ export async function runRoEditCommand(args = [], {
9
+ preview = previewRoLogworkEdit,
10
+ apply = applyRoLogworkEdit,
11
+ promptConfirm = defaultConfirm,
12
+ isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY),
13
+ print = console.log
14
+ } = {}) {
15
+ const options = parseRoEditArgs(args);
16
+ if (options.help) {
17
+ print(formatRoEditHelp());
18
+ return null;
19
+ }
20
+
21
+ if (options.json && !options.yes) {
22
+ throw new Error('--json requires --yes so stdout remains valid JSON.');
23
+ }
24
+
25
+ const editPreview = await preview({
26
+ logworkId: options.logworkId,
27
+ hours: options.hours,
28
+ taskName: options.taskName,
29
+ description: options.description,
30
+ typeOfWork: options.typeOfWork
31
+ });
32
+ if (!options.json) {
33
+ print(editPreview.summary);
34
+ }
35
+
36
+ let approved = options.yes;
37
+ if (!approved) {
38
+ if (!isTTY) {
39
+ throw new Error('Interactive confirmation is unavailable. Re-run with --yes.');
40
+ }
41
+ approved = await promptConfirm(`Apply RO logwork edit ${editPreview.previewId}?`);
42
+ }
43
+ if (!approved) {
44
+ print('RO logwork edit cancelled.');
45
+ return null;
46
+ }
47
+
48
+ const result = await apply({
49
+ preview: editPreview,
50
+ confirm: true
51
+ });
52
+ print(options.json ? JSON.stringify(result, null, 2) : result.summary);
53
+ return result;
54
+ }
55
+
56
+ export function formatRoEditHelp() {
57
+ return `Usage:
58
+ logwork edit <id> [--hours <h>] [--task <task>] [--desc <text>] [--type <type>] [--yes] [--json]
59
+
60
+ Changes one Resource Optimiser entry. Project and date are preserved. Get the id
61
+ from \`logwork query\`.
62
+
63
+ RO has no staff edit API, so apply replaces the entry: it creates the corrected
64
+ entry, then deletes the original (the new entry gets a new id and is approved
65
+ immediately). If the delete fails, the new entry is rolled back. Only submitted
66
+ or approved entries can be edited; rejected entries are fixed with resubmit.
67
+
68
+ Options:
69
+ --hours <h> New hours (alias: --logtimes)
70
+ --task <task> New worklog task from the project catalog, or #<id> (alias: --task-name)
71
+ --desc <text> New description: what you did (alias: --description)
72
+ --type <type> New type of work: create, correct, improve, other
73
+ (follows the new task's catalog type when the task changes)
74
+ -y, --yes Apply without the confirmation prompt
75
+ --json Print the result as JSON (requires --yes)
76
+ -h, --help Show this help
77
+
78
+ Example:
79
+ logwork edit 304032 --task "Bug Fix" --desc "Fix date picker timezone (SAP-42)"`;
80
+ }
81
+
82
+ async function defaultConfirm(message) {
83
+ const result = await confirm({
84
+ message,
85
+ initialValue: false
86
+ });
87
+ return !isCancel(result) && result === true;
88
+ }
@@ -0,0 +1,62 @@
1
+ import { queryApplyLedger } from '../lib/apply-ledger.mjs';
2
+
3
+ export function parseHistoryArgs(args = []) {
4
+ const options = {
5
+ target: undefined,
6
+ limit: 20,
7
+ json: false
8
+ };
9
+
10
+ for (let index = 0; index < args.length; index += 1) {
11
+ const arg = args[index];
12
+ if (arg === '--target') {
13
+ options.target = args[index + 1];
14
+ index += 1;
15
+ continue;
16
+ }
17
+ if (arg === '--limit') {
18
+ options.limit = Number(args[index + 1]);
19
+ index += 1;
20
+ continue;
21
+ }
22
+ if (arg === '--json') {
23
+ options.json = true;
24
+ continue;
25
+ }
26
+ if (arg === '--help' || arg === '-h') {
27
+ return { help: true };
28
+ }
29
+ throw new Error(`Unknown history option: ${arg}`);
30
+ }
31
+
32
+ return options;
33
+ }
34
+
35
+ export async function runHistoryCommand(args = [], {
36
+ query = queryApplyLedger,
37
+ print = console.log
38
+ } = {}) {
39
+ const options = parseHistoryArgs(args);
40
+ if (options.help) {
41
+ printHistoryHelp(print);
42
+ return null;
43
+ }
44
+
45
+ const result = await query({
46
+ target: options.target,
47
+ limit: options.limit
48
+ });
49
+ print(options.json ? JSON.stringify(result, null, 2) : result.summary);
50
+ return result;
51
+ }
52
+
53
+ function printHistoryHelp(print = console.log) {
54
+ print(`Usage:
55
+ logwork history [--target ro|jira|both] [--limit <count>] [--json]
56
+
57
+ Shows the local apply result ledger. Target both means entries applied from a combined Both flow.`);
58
+ }
59
+
60
+ async function main() {
61
+ await runHistoryCommand(process.argv.slice(2));
62
+ }
@@ -0,0 +1,52 @@
1
+ import { queryLogwork } from '../lib/query-workflow.mjs';
2
+ import { normalizeReconciliationPeriod } from '../lib/reconciliation-workflow.mjs';
3
+
4
+ // `logwork query [period] [--json]`: booked vs logged Resource Optimiser hours,
5
+ // with each entry's id, worklog task, description and status.
6
+ export function parseQueryArgs(args = []) {
7
+ const options = { period: 'today', json: false, help: false };
8
+ const positional = [];
9
+ for (const arg of args) {
10
+ if (arg === '--json') {
11
+ options.json = true;
12
+ continue;
13
+ }
14
+ if (arg === '--help' || arg === '-h') {
15
+ options.help = true;
16
+ continue;
17
+ }
18
+ if (arg.startsWith('-')) {
19
+ throw new Error(`Unknown query option: ${arg}`);
20
+ }
21
+ positional.push(arg);
22
+ }
23
+ if (positional.length > 1) {
24
+ throw new Error('Query accepts exactly one preset period.');
25
+ }
26
+ if (positional[0]) {
27
+ options.period = normalizeReconciliationPeriod(positional[0]);
28
+ }
29
+ return options;
30
+ }
31
+
32
+ export async function run(args = [], {
33
+ query = queryLogwork,
34
+ print = console.log
35
+ } = {}) {
36
+ const options = parseQueryArgs(args);
37
+ if (options.help) {
38
+ print(formatQueryHelp());
39
+ return 0;
40
+ }
41
+ const result = await query({ period: options.period, includeEntries: true });
42
+ print(options.json ? JSON.stringify(result, null, 2) : result.summary);
43
+ return 0;
44
+ }
45
+
46
+ export function formatQueryHelp() {
47
+ return `Usage:
48
+ logwork query [today|yesterday|this-week|last-week|this-month|last-month] [--json]
49
+
50
+ Shows booked vs logged Resource Optimiser hours per day and project, with each
51
+ entry's id (use it with \`logwork edit\` / \`logwork delete\`). Defaults to today.`;
52
+ }