workday-daemon 0.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 (56) hide show
  1. package/README.md +95 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +710 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/collectors/git-client.d.ts +16 -0
  6. package/dist/collectors/git-client.js +58 -0
  7. package/dist/collectors/git-client.js.map +1 -0
  8. package/dist/collectors/git-tracker.d.ts +43 -0
  9. package/dist/collectors/git-tracker.js +168 -0
  10. package/dist/collectors/git-tracker.js.map +1 -0
  11. package/dist/collectors/reflog-parser.d.ts +30 -0
  12. package/dist/collectors/reflog-parser.js +71 -0
  13. package/dist/collectors/reflog-parser.js.map +1 -0
  14. package/dist/collectors/snapshot-parser.d.ts +34 -0
  15. package/dist/collectors/snapshot-parser.js +79 -0
  16. package/dist/collectors/snapshot-parser.js.map +1 -0
  17. package/dist/core/activity-evaluator.d.ts +33 -0
  18. package/dist/core/activity-evaluator.js +94 -0
  19. package/dist/core/activity-evaluator.js.map +1 -0
  20. package/dist/core/config.d.ts +18 -0
  21. package/dist/core/config.js +174 -0
  22. package/dist/core/config.js.map +1 -0
  23. package/dist/core/constants.d.ts +39 -0
  24. package/dist/core/constants.js +51 -0
  25. package/dist/core/constants.js.map +1 -0
  26. package/dist/core/daily-log.d.ts +56 -0
  27. package/dist/core/daily-log.js +366 -0
  28. package/dist/core/daily-log.js.map +1 -0
  29. package/dist/core/session-tracker.d.ts +110 -0
  30. package/dist/core/session-tracker.js +460 -0
  31. package/dist/core/session-tracker.js.map +1 -0
  32. package/dist/core/status-renderer.d.ts +20 -0
  33. package/dist/core/status-renderer.js +178 -0
  34. package/dist/core/status-renderer.js.map +1 -0
  35. package/dist/core/types.d.ts +320 -0
  36. package/dist/core/types.js +52 -0
  37. package/dist/core/types.js.map +1 -0
  38. package/dist/daemon.d.ts +31 -0
  39. package/dist/daemon.js +266 -0
  40. package/dist/daemon.js.map +1 -0
  41. package/dist/http-server.d.ts +30 -0
  42. package/dist/http-server.js +342 -0
  43. package/dist/http-server.js.map +1 -0
  44. package/dist/push/jira-client.d.ts +5 -0
  45. package/dist/push/jira-client.js +84 -0
  46. package/dist/push/jira-client.js.map +1 -0
  47. package/dist/push/report-builder.d.ts +9 -0
  48. package/dist/push/report-builder.js +77 -0
  49. package/dist/push/report-builder.js.map +1 -0
  50. package/dist/push/tempo-client.d.ts +27 -0
  51. package/dist/push/tempo-client.js +92 -0
  52. package/dist/push/tempo-client.js.map +1 -0
  53. package/dist/push/tempo-pusher.d.ts +17 -0
  54. package/dist/push/tempo-pusher.js +317 -0
  55. package/dist/push/tempo-pusher.js.map +1 -0
  56. package/package.json +35 -0
package/dist/cli.js ADDED
@@ -0,0 +1,710 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import { existsSync, writeFileSync, mkdirSync } from 'node:fs';
4
+ import { dirname, join, isAbsolute } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { loadConfig, loadSecrets, getWorkdayHome, getDataDir, buildTimestamp } from './core/config.js';
7
+ import { CONFIG_FILE_NAME, SECRETS_FILE_NAME, DAEMON_SCRIPT_TS, DAEMON_SCRIPT_JS, TEMPO_REPORT_DIR, DAEMON_START_MAX_ATTEMPTS, DAEMON_START_POLL_MS, MS_PER_MINUTE, } from './core/constants.js';
8
+ import { readDailyLog, writeDailyLog, resolveSessionTarget, addManualAdjustment, setDayManualStart, computeManualMinutes, computeEffectiveDuration, computeTotalPauseDuration, computeBudgetMs, computeTotalClaimedMs, getRemainingBudgetMs, } from './core/daily-log.js';
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ // ─── HTTP client helpers ────────────────────────────────────────────────
11
+ let cachedApiBaseUrl = null;
12
+ function getApiBaseUrl() {
13
+ if (!cachedApiBaseUrl) {
14
+ const config = loadConfig();
15
+ cachedApiBaseUrl = `http://127.0.0.1:${config.apiPort}`;
16
+ }
17
+ return cachedApiBaseUrl;
18
+ }
19
+ async function apiGet(path) {
20
+ const url = `${getApiBaseUrl()}${path}`;
21
+ try {
22
+ const res = await fetch(url);
23
+ return await res.json();
24
+ }
25
+ catch (err) {
26
+ if (isConnectionRefused(err)) {
27
+ return { ok: false, error: 'Daemon is not running.' };
28
+ }
29
+ throw err;
30
+ }
31
+ }
32
+ async function apiPost(path, body) {
33
+ const url = `${getApiBaseUrl()}${path}`;
34
+ try {
35
+ const res = await fetch(url, {
36
+ method: 'POST',
37
+ headers: body ? { 'Content-Type': 'application/json' } : undefined,
38
+ body: body ? JSON.stringify(body) : undefined,
39
+ });
40
+ return await res.json();
41
+ }
42
+ catch (err) {
43
+ if (isConnectionRefused(err)) {
44
+ return { ok: false, error: 'Daemon is not running.' };
45
+ }
46
+ throw err;
47
+ }
48
+ }
49
+ function isConnectionRefused(err) {
50
+ if (err && typeof err === 'object' && 'cause' in err) {
51
+ const cause = err.cause;
52
+ if (cause && typeof cause === 'object' && 'code' in cause) {
53
+ const code = cause.code;
54
+ return code === 'ECONNREFUSED';
55
+ }
56
+ }
57
+ return false;
58
+ }
59
+ // ─── Formatting helpers ─────────────────────────────────────────────────
60
+ function formatDuration(ms) {
61
+ const totalMinutes = Math.floor(ms / MS_PER_MINUTE);
62
+ if (totalMinutes < 60)
63
+ return `${totalMinutes}m`;
64
+ const hours = totalMinutes / 60;
65
+ return `${hours.toFixed(1)}h`;
66
+ }
67
+ /** Format seconds as hours with enough precision for quarter-hour values */
68
+ function formatReportHours(seconds) {
69
+ const hours = seconds / 3600;
70
+ const rounded1 = parseFloat(hours.toFixed(1));
71
+ if (Math.abs(hours - rounded1) < 0.01)
72
+ return `${hours.toFixed(1)}h`;
73
+ return `${hours.toFixed(2)}h`;
74
+ }
75
+ function formatSessionStatus(s) {
76
+ const parts = [];
77
+ if (s.isLeader)
78
+ parts.push('LEADER');
79
+ if (s.paused && s.pauseSource)
80
+ parts.push(`PAUSED:${s.pauseSource}`);
81
+ else if (s.paused)
82
+ parts.push('PAUSED');
83
+ if (s.autoPauseDisabled)
84
+ parts.push('AUTOPAUSE OFF');
85
+ return parts.length > 0 ? ` [${parts.join(', ')}]` : '';
86
+ }
87
+ function printStatusData(data) {
88
+ console.log(`Daemon running (PID ${data.pid})`);
89
+ console.log(` Date: ${data.date}`);
90
+ console.log(` Uptime: ${formatDuration(data.uptime * 1000)}`);
91
+ if (data.openSessions.length === 0) {
92
+ console.log(' Sessions: none');
93
+ return;
94
+ }
95
+ console.log(` Sessions (${data.openSessions.length}):`);
96
+ for (let i = 0; i < data.openSessions.length; i++) {
97
+ const s = data.openSessions[i];
98
+ const task = s.task ?? '—';
99
+ const dur = formatDuration(s.effectiveDurationMs);
100
+ const manualStr = s.manualMinutes > 0 ? ` + ${s.manualMinutes}m manual` : '';
101
+ const status = formatSessionStatus(s);
102
+ const scoreStr = `score:${s.normalizedScore.toFixed(2)}`;
103
+ console.log(` #${i + 1} ${s.repo} ${task} ${s.branch} ${s.state} ${dur}${manualStr} ${scoreStr}${status}`);
104
+ }
105
+ }
106
+ function printTodayData(data) {
107
+ console.log(`Date: ${data.date} (${data.dayType}) Status: ${data.status}`);
108
+ console.log(`Total: ${formatDuration(data.totalEffectiveMs)} Signals: ${data.signalCount}`);
109
+ if (data.budgetMs > 0) {
110
+ console.log(`Budget: ${formatDuration(data.budgetMs)} | Claimed: ${formatDuration(data.claimedMs)} | Remaining: ${formatDuration(data.remainingBudgetMs)}`);
111
+ }
112
+ if (data.sessions.length === 0) {
113
+ console.log('No sessions.');
114
+ return;
115
+ }
116
+ console.log('');
117
+ for (let i = 0; i < data.sessions.length; i++) {
118
+ printSessionDetail(data.sessions[i], i + 1);
119
+ }
120
+ }
121
+ function printSessionDetail(s, index) {
122
+ const task = s.task ?? '—';
123
+ const dur = formatDuration(s.effectiveDurationMs);
124
+ const manualStr = s.manualMinutes > 0 ? ` + ${s.manualMinutes}m manual` : '';
125
+ const status = s.closedBy ? `closed(${s.closedBy})` : (s.paused ? 'paused' : s.state);
126
+ const ev = s.evidence;
127
+ const added = ev.linesAdded ?? 0;
128
+ const removed = ev.linesRemoved ?? 0;
129
+ const files = ev.filesChanged ?? 0;
130
+ const prefix = index !== undefined ? `#${index}` : s.id;
131
+ console.log(` [${prefix}] ${s.repo} ${task} ${dur}${manualStr} ${status}`);
132
+ console.log(` branch: ${s.branch} ${ev.commits} commits +${added} -${removed} ${files} files`);
133
+ if (s.pauseCount > 0) {
134
+ console.log(` pauses: ${s.pauseCount} (${formatDuration(s.totalPauseDurationMs)} total)`);
135
+ }
136
+ }
137
+ // ─── Command handlers ───────────────────────────────────────────────────
138
+ async function handleStart() {
139
+ // Check if already running
140
+ const check = await apiGet('/api/status');
141
+ if (check.ok) {
142
+ console.log('Daemon is already running.');
143
+ printStatusData(check.data);
144
+ return;
145
+ }
146
+ spawnBackground();
147
+ // Poll for HTTP readiness
148
+ const baseUrl = getApiBaseUrl();
149
+ for (let i = 0; i < DAEMON_START_MAX_ATTEMPTS; i++) {
150
+ await sleep(DAEMON_START_POLL_MS);
151
+ try {
152
+ const res = await fetch(`${baseUrl}/api/status`);
153
+ if (res.ok) {
154
+ const result = await res.json();
155
+ if (result.ok && result.data) {
156
+ printStatusData(result.data);
157
+ return;
158
+ }
159
+ }
160
+ }
161
+ catch {
162
+ // Not ready yet
163
+ }
164
+ }
165
+ console.log('Daemon spawned but not responding yet. Check logs or try: workday status');
166
+ }
167
+ async function handleStop() {
168
+ const result = await apiPost('/api/stop');
169
+ if (!result.ok) {
170
+ console.log(result.error);
171
+ return;
172
+ }
173
+ console.log(result.data.message);
174
+ }
175
+ async function handleStatus() {
176
+ const result = await apiGet('/api/status');
177
+ if (!result.ok) {
178
+ console.log(result.error);
179
+ return;
180
+ }
181
+ printStatusData(result.data);
182
+ }
183
+ async function handleToday() {
184
+ const result = await apiGet('/api/today');
185
+ if (!result.ok) {
186
+ console.log(result.error);
187
+ return;
188
+ }
189
+ printTodayData(result.data);
190
+ }
191
+ async function handlePause(args) {
192
+ const repo = args[0];
193
+ const body = repo ? { repo } : undefined;
194
+ const result = await apiPost('/api/pause', body);
195
+ if (!result.ok) {
196
+ console.log(result.error);
197
+ return;
198
+ }
199
+ const paused = result.data.paused;
200
+ if (paused.length === 0) {
201
+ console.log('No sessions to pause.');
202
+ }
203
+ else {
204
+ console.log(`Paused: ${paused.join(', ')}`);
205
+ }
206
+ }
207
+ async function handleResume() {
208
+ const result = await apiPost('/api/resume');
209
+ if (!result.ok) {
210
+ console.log(result.error);
211
+ return;
212
+ }
213
+ const resumed = result.data.resumed;
214
+ if (resumed.length === 0) {
215
+ console.log('No sessions to resume.');
216
+ }
217
+ else {
218
+ console.log(`Resumed: ${resumed.join(', ')}`);
219
+ }
220
+ }
221
+ async function handleAutoPause(args) {
222
+ const toggle = args[0]; // on | off
223
+ if (toggle !== 'on' && toggle !== 'off') {
224
+ console.log('Usage: workday autopause on|off [repo]');
225
+ return;
226
+ }
227
+ const repo = args[1];
228
+ const enabled = toggle === 'on';
229
+ const body = { enabled };
230
+ if (repo)
231
+ body.repo = repo;
232
+ const result = await apiPost('/api/autopause', body);
233
+ if (!result.ok) {
234
+ console.log(result.error);
235
+ return;
236
+ }
237
+ const target = result.data.repo ?? 'all sessions';
238
+ const state = result.data.autoPauseDisabled ? 'disabled' : 'enabled';
239
+ console.log(`Autopause ${state} for ${target}.`);
240
+ }
241
+ async function handleAdjust(args) {
242
+ // workday adjust <target> +<N> "<reason>" [--date YYYY-MM-DD]
243
+ const dateIdx = args.indexOf('--date');
244
+ let date = null;
245
+ let cmdArgs = args;
246
+ if (dateIdx !== -1) {
247
+ date = args[dateIdx + 1];
248
+ cmdArgs = [...args.slice(0, dateIdx), ...args.slice(dateIdx + 2)];
249
+ }
250
+ const target = cmdArgs[0];
251
+ const minutesStr = cmdArgs[1];
252
+ const reason = cmdArgs.slice(2).join(' ');
253
+ if (!target || !minutesStr) {
254
+ console.log('Usage: workday adjust <target> +<N> "<reason>" [--date YYYY-MM-DD]');
255
+ return;
256
+ }
257
+ const minutes = parseInt(minutesStr.replace('+', ''), 10);
258
+ if (isNaN(minutes) || minutes <= 0) {
259
+ console.log('Minutes must be a positive number (e.g. +30)');
260
+ return;
261
+ }
262
+ if (!reason) {
263
+ console.log('Reason is required');
264
+ return;
265
+ }
266
+ if (date) {
267
+ // Offline mode — past day
268
+ handleAdjustOffline(date, target, minutes, reason);
269
+ }
270
+ else {
271
+ // Online mode — via HTTP
272
+ const result = await apiPost('/api/adjust', { target, minutes, reason });
273
+ if (!result.ok) {
274
+ console.log(result.error);
275
+ return;
276
+ }
277
+ const d = result.data;
278
+ console.log(`Adjusted ${d.repo} (${d.task ?? '—'}): +${d.addedMinutes}m (total manual: ${d.totalManualMinutes}m)`);
279
+ console.log(`Remaining budget: ${formatDuration(d.remainingBudgetMs)}`);
280
+ }
281
+ }
282
+ function handleAdjustOffline(date, target, minutes, reason) {
283
+ const config = loadConfig();
284
+ const log = readDailyLog(date);
285
+ if (!log) {
286
+ console.log(`No data for ${date}`);
287
+ return;
288
+ }
289
+ const session = resolveSessionTarget(log, target);
290
+ if (!session) {
291
+ console.log(`Session not found: ${target}`);
292
+ return;
293
+ }
294
+ try {
295
+ addManualAdjustment(log, session.id, minutes, reason, config);
296
+ }
297
+ catch (err) {
298
+ console.log(err instanceof Error ? err.message : String(err));
299
+ return;
300
+ }
301
+ writeDailyLog(log);
302
+ console.log(`Adjusted ${session.repo} (${session.task ?? '—'}): +${minutes}m`);
303
+ console.log(`Total manual for session: ${computeManualMinutes(session)}m`);
304
+ console.log(`Remaining budget: ${formatDuration(getRemainingBudgetMs(log, config))}`);
305
+ }
306
+ async function handleSetStart(args) {
307
+ // workday set-start HH:MM [--date YYYY-MM-DD]
308
+ const dateIdx = args.indexOf('--date');
309
+ let date = null;
310
+ let cmdArgs = args;
311
+ if (dateIdx !== -1) {
312
+ date = args[dateIdx + 1];
313
+ cmdArgs = [...args.slice(0, dateIdx), ...args.slice(dateIdx + 2)];
314
+ }
315
+ const time = cmdArgs[0];
316
+ if (!time || !/^\d{1,2}:\d{2}$/.test(time)) {
317
+ console.log('Usage: workday set-start HH:MM [--date YYYY-MM-DD]');
318
+ return;
319
+ }
320
+ if (date) {
321
+ handleSetStartOffline(date, time);
322
+ }
323
+ else {
324
+ const result = await apiPost('/api/set-start', { time });
325
+ if (!result.ok) {
326
+ console.log(result.error);
327
+ return;
328
+ }
329
+ const d = result.data;
330
+ console.log(`Day start set to: ${d.dayStart}`);
331
+ console.log(`Budget: ${formatDuration(d.budgetMs)} | Remaining: ${formatDuration(d.remainingBudgetMs)}`);
332
+ }
333
+ }
334
+ function handleSetStartOffline(date, time) {
335
+ const config = loadConfig();
336
+ const log = readDailyLog(date);
337
+ if (!log) {
338
+ console.log(`No data for ${date}`);
339
+ return;
340
+ }
341
+ const [h, m] = time.split(':').map(Number);
342
+ const isoTimestamp = buildTimestamp(date, h, m, config.timezone);
343
+ try {
344
+ setDayManualStart(log, isoTimestamp, config);
345
+ }
346
+ catch (err) {
347
+ console.log(err instanceof Error ? err.message : String(err));
348
+ return;
349
+ }
350
+ writeDailyLog(log);
351
+ console.log(`Day start set to: ${isoTimestamp}`);
352
+ console.log(`Budget: ${formatDuration(computeBudgetMs(log, config))} | Remaining: ${formatDuration(getRemainingBudgetMs(log, config))}`);
353
+ }
354
+ async function handleDay(args) {
355
+ const date = args[0];
356
+ if (!date || !/^\d{4}-\d{2}-\d{2}$/.test(date)) {
357
+ console.log('Usage: workday day YYYY-MM-DD');
358
+ return;
359
+ }
360
+ // Try daemon first (it might be today)
361
+ const result = await apiGet(`/api/day?date=${date}`);
362
+ if (result.ok) {
363
+ printTodayData(result.data);
364
+ return;
365
+ }
366
+ // Fallback: read from disk (daemon not running or past day)
367
+ const config = loadConfig();
368
+ const log = readDailyLog(date);
369
+ if (!log) {
370
+ console.log(`No data for ${date}`);
371
+ return;
372
+ }
373
+ // Build a TodayResponse-like object from the raw log
374
+ const sessions = log.sessions.map(s => ({
375
+ id: s.id,
376
+ repo: s.repo,
377
+ task: s.task,
378
+ branch: s.branch,
379
+ state: s.state,
380
+ startedAt: s.startedAt,
381
+ activatedAt: s.activatedAt,
382
+ lastSeenAt: s.lastSeenAt,
383
+ paused: false,
384
+ pauseSource: null,
385
+ effectiveDurationMs: computeEffectiveDuration(s),
386
+ manualMinutes: computeManualMinutes(s),
387
+ score: 0,
388
+ normalizedScore: 0,
389
+ isLeader: false,
390
+ autoPauseDisabled: false,
391
+ closedBy: s.closedBy,
392
+ evidence: s.evidence,
393
+ pauseCount: s.pauses.length,
394
+ totalPauseDurationMs: computeTotalPauseDuration(s),
395
+ }));
396
+ const totalEffectiveMs = log.sessions.reduce((sum, s) => sum + computeEffectiveDuration(s), 0);
397
+ printTodayData({
398
+ date: log.date,
399
+ dayType: log.dayType,
400
+ status: log.status,
401
+ sessions,
402
+ totalEffectiveMs,
403
+ signalCount: log.signals.length,
404
+ budgetMs: computeBudgetMs(log, config),
405
+ claimedMs: computeTotalClaimedMs(log),
406
+ remainingBudgetMs: getRemainingBudgetMs(log, config),
407
+ dayStartedAt: log.dayStartedAt,
408
+ });
409
+ }
410
+ function handleInit() {
411
+ const home = getWorkdayHome();
412
+ if (!existsSync(home)) {
413
+ mkdirSync(home, { recursive: true });
414
+ console.log(`Created ${home}`);
415
+ }
416
+ const configPath = join(home, CONFIG_FILE_NAME);
417
+ if (!existsSync(configPath)) {
418
+ const template = {
419
+ repos: [],
420
+ dayBoundaryHour: 4,
421
+ taskPattern: 'PROJ-\\d+',
422
+ genericBranches: ['develop', 'main', 'master'],
423
+ session: {
424
+ diffPollSeconds: 30,
425
+ signalDeduplicationSeconds: 300,
426
+ dayBoundaryCheckSeconds: 60,
427
+ reflogCount: 20,
428
+ },
429
+ report: { roundingMinutes: 15 },
430
+ workDays: [1, 2, 3, 4, 5],
431
+ holidays: [],
432
+ };
433
+ writeFileSync(configPath, JSON.stringify(template, null, 2) + '\n', 'utf-8');
434
+ console.log(`Created ${configPath}`);
435
+ }
436
+ else {
437
+ console.log(`Config already exists: ${configPath}`);
438
+ }
439
+ const secretsPath = join(home, SECRETS_FILE_NAME);
440
+ if (!existsSync(secretsPath)) {
441
+ const template = {
442
+ Developer: 'your-git-username',
443
+ Jira_Email: 'your-email@company.com',
444
+ Jira_BaseUrl: 'https://your-company.atlassian.net',
445
+ Jira_Token: '',
446
+ Tempo_Token: '',
447
+ };
448
+ writeFileSync(secretsPath, JSON.stringify(template, null, 2) + '\n', 'utf-8');
449
+ console.log(`Created ${secretsPath}`);
450
+ }
451
+ else {
452
+ console.log(`Secrets already exists: ${secretsPath}`);
453
+ }
454
+ console.log('');
455
+ console.log('Setup instructions:');
456
+ console.log('');
457
+ console.log(` 1. ${configPath}`);
458
+ console.log(' - "repos": add absolute paths to your git repositories');
459
+ console.log(' e.g. ["C:/projects/my-app", "C:/projects/my-api"]');
460
+ console.log(' or ["/home/user/projects/my-app"]');
461
+ console.log(' - "taskPattern": change PROJ to your Jira prefix');
462
+ console.log(' e.g. "CORE-\\\\d+" for CORE-567, "WEB-\\\\d+" for WEB-123');
463
+ console.log('');
464
+ console.log(` 2. ${secretsPath}`);
465
+ console.log(' - "Developer": your git username (used to filter branches)');
466
+ console.log(' - Jira/Tempo tokens: optional, needed only for "workday tempo --push"');
467
+ console.log('');
468
+ console.log(' 3. Run: workday start');
469
+ }
470
+ async function handleDaemon() {
471
+ // Foreground mode with live status dashboard
472
+ const { Daemon } = await import('./daemon.js');
473
+ const daemon = new Daemon();
474
+ await daemon.start({ foreground: true });
475
+ }
476
+ // ─── Background spawn ───────────────────────────────────────────────────
477
+ function spawnBackground() {
478
+ const home = getWorkdayHome();
479
+ const configPath = join(home, CONFIG_FILE_NAME);
480
+ const secretsPath = join(home, SECRETS_FILE_NAME);
481
+ if (!existsSync(configPath)) {
482
+ console.error(`Cannot start daemon: ${CONFIG_FILE_NAME} not found at ${configPath}`);
483
+ process.exit(1);
484
+ }
485
+ if (!existsSync(secretsPath)) {
486
+ console.error(`Cannot start daemon: ${SECRETS_FILE_NAME} not found at ${secretsPath}`);
487
+ process.exit(1);
488
+ }
489
+ const daemonScript = resolveDaemonScript();
490
+ const child = spawn(process.execPath, [...process.execArgv, daemonScript], {
491
+ detached: true,
492
+ stdio: 'ignore',
493
+ });
494
+ child.unref();
495
+ }
496
+ function resolveDaemonScript() {
497
+ const tsPath = join(__dirname, DAEMON_SCRIPT_TS);
498
+ if (existsSync(tsPath))
499
+ return tsPath;
500
+ return join(__dirname, DAEMON_SCRIPT_JS);
501
+ }
502
+ function sleep(ms) {
503
+ return new Promise(resolve => setTimeout(resolve, ms));
504
+ }
505
+ // ─── Tempo report & push ─────────────────────────────────────────────────
506
+ /** Extract value for a named flag (e.g. --from 2026-03-01) */
507
+ function parseArgValue(args, flag) {
508
+ const idx = args.indexOf(flag);
509
+ if (idx === -1 || idx + 1 >= args.length)
510
+ return null;
511
+ return args[idx + 1];
512
+ }
513
+ /** Resolve file path: relative names go to data/tempo/, absolute paths stay as-is */
514
+ function resolveTempoFilePath(filePath) {
515
+ if (isAbsolute(filePath) || filePath.includes('/') || filePath.includes('\\')) {
516
+ return filePath;
517
+ }
518
+ const tempoDir = join(getDataDir(), TEMPO_REPORT_DIR);
519
+ if (!existsSync(tempoDir)) {
520
+ mkdirSync(tempoDir, { recursive: true });
521
+ }
522
+ return join(tempoDir, filePath);
523
+ }
524
+ async function handleTempo(args) {
525
+ const { buildReportResponse, getDefaultFromDate, getDefaultToDate } = await import('./push/report-builder.js');
526
+ const { runPush } = await import('./push/tempo-pusher.js');
527
+ const config = loadConfig();
528
+ const from = parseArgValue(args, '--from') ?? getDefaultFromDate(config);
529
+ const to = parseArgValue(args, '--to') ?? getDefaultToDate(config);
530
+ const rawFile = parseArgValue(args, '--file');
531
+ const filePath = rawFile ? resolveTempoFilePath(rawFile) : null;
532
+ const push = args.includes('--push');
533
+ if (push) {
534
+ // Push mode
535
+ const secrets = loadSecrets();
536
+ let response;
537
+ try {
538
+ response = await runPush({ from, to, commit: true, config, secrets, filePath: filePath ?? undefined });
539
+ }
540
+ catch (err) {
541
+ console.error(err instanceof Error ? err.message : String(err));
542
+ return;
543
+ }
544
+ printPushPlan(response.plan);
545
+ if (response.result) {
546
+ console.log('');
547
+ console.log(`Result: ${response.result.posted} posted, ${response.result.updated} updated, ${response.result.skipped} skipped, ${response.result.failed} failed`);
548
+ }
549
+ }
550
+ else if (filePath) {
551
+ // Save report to file
552
+ const report = buildReportResponse(from, to, config);
553
+ writeFileSync(filePath, JSON.stringify(report, null, 2), 'utf-8');
554
+ console.log(`Report saved to ${filePath}`);
555
+ printReport(report);
556
+ }
557
+ else {
558
+ // Display report
559
+ const report = buildReportResponse(from, to, config);
560
+ printReport(report);
561
+ }
562
+ }
563
+ function printReport(report) {
564
+ console.log(`Report: ${report.from} → ${report.to}`);
565
+ console.log('');
566
+ if (report.entries.length === 0) {
567
+ console.log('No data.');
568
+ return;
569
+ }
570
+ // Group by date
571
+ const byDate = new Map();
572
+ for (const entry of report.entries) {
573
+ const list = byDate.get(entry.date) ?? [];
574
+ list.push(entry);
575
+ byDate.set(entry.date, list);
576
+ }
577
+ const COL_DATE = 13;
578
+ const COL_TASK = 14;
579
+ const COL_HOURS = 8;
580
+ console.log('DATE'.padEnd(COL_DATE) + 'TASK'.padEnd(COL_TASK) + 'HOURS'.padStart(COL_HOURS));
581
+ console.log('─'.repeat(COL_DATE + COL_TASK + COL_HOURS));
582
+ const sortedDates = [...byDate.keys()].sort();
583
+ for (const date of sortedDates) {
584
+ const entries = byDate.get(date).sort((a, b) => b.totalSeconds - a.totalSeconds);
585
+ let dayTotal = 0;
586
+ for (let i = 0; i < entries.length; i++) {
587
+ const hoursStr = formatReportHours(entries[i].totalSeconds);
588
+ dayTotal += entries[i].totalSeconds;
589
+ console.log((i === 0 ? date : '').padEnd(COL_DATE)
590
+ + entries[i].task.padEnd(COL_TASK)
591
+ + hoursStr.padStart(COL_HOURS));
592
+ }
593
+ if (entries.length > 1) {
594
+ console.log(''.padEnd(COL_DATE) + '── total'.padEnd(COL_TASK) + formatReportHours(dayTotal).padStart(COL_HOURS));
595
+ }
596
+ }
597
+ console.log('─'.repeat(COL_DATE + COL_TASK + COL_HOURS));
598
+ console.log(''.padEnd(COL_DATE) + 'TOTAL'.padEnd(COL_TASK) + formatReportHours(report.totalSeconds).padStart(COL_HOURS));
599
+ // Task summary
600
+ console.log('');
601
+ console.log('Task totals:');
602
+ const tasks = Object.entries(report.taskTotals).sort((a, b) => b[1] - a[1]);
603
+ for (const [task, seconds] of tasks) {
604
+ console.log(` ${task.padEnd(14)} ${formatReportHours(seconds)}`);
605
+ }
606
+ }
607
+ function printPushPlan(plan) {
608
+ if (plan.length === 0) {
609
+ console.log('Empty plan.');
610
+ return;
611
+ }
612
+ const COL_DATE = 13;
613
+ const COL_TASK = 14;
614
+ const COL_HOURS = 8;
615
+ const COL_ACTION = 8;
616
+ console.log('');
617
+ console.log('DATE'.padEnd(COL_DATE) + 'TASK'.padEnd(COL_TASK) + 'HOURS'.padStart(COL_HOURS) + ' ' + 'ACTION'.padEnd(COL_ACTION) + ' DETAIL');
618
+ console.log('─'.repeat(COL_DATE + COL_TASK + COL_HOURS + COL_ACTION + 40));
619
+ for (const entry of plan) {
620
+ const hoursStr = formatReportHours(entry.targetSeconds);
621
+ const actionStr = entry.action.toUpperCase();
622
+ console.log(entry.date.padEnd(COL_DATE)
623
+ + entry.task.padEnd(COL_TASK)
624
+ + hoursStr.padStart(COL_HOURS)
625
+ + ' ' + actionStr.padEnd(COL_ACTION)
626
+ + ' ' + entry.detail);
627
+ }
628
+ const counts = { create: 0, update: 0, skip: 0, error: 0 };
629
+ for (const e of plan)
630
+ counts[e.action]++;
631
+ console.log('');
632
+ console.log(`Create: ${counts.create} Update: ${counts.update} Skip: ${counts.skip} Error: ${counts.error}`);
633
+ }
634
+ // ─── Main ───────────────────────────────────────────────────────────────
635
+ async function main() {
636
+ const args = process.argv.slice(2);
637
+ const command = args[0];
638
+ switch (command) {
639
+ case 'start':
640
+ await handleStart();
641
+ break;
642
+ case 'stop':
643
+ await handleStop();
644
+ break;
645
+ case 'status':
646
+ await handleStatus();
647
+ break;
648
+ case 'today':
649
+ await handleToday();
650
+ break;
651
+ case 'pause':
652
+ await handlePause(args.slice(1));
653
+ break;
654
+ case 'resume':
655
+ await handleResume();
656
+ break;
657
+ case 'autopause':
658
+ await handleAutoPause(args.slice(1));
659
+ break;
660
+ case 'adjust':
661
+ await handleAdjust(args.slice(1));
662
+ break;
663
+ case 'set-start':
664
+ await handleSetStart(args.slice(1));
665
+ break;
666
+ case 'day':
667
+ await handleDay(args.slice(1));
668
+ break;
669
+ case 'tempo':
670
+ await handleTempo(args.slice(1));
671
+ break;
672
+ case 'init':
673
+ handleInit();
674
+ break;
675
+ case 'daemon':
676
+ await handleDaemon();
677
+ break;
678
+ default:
679
+ printHelp();
680
+ }
681
+ }
682
+ function printHelp() {
683
+ console.log(`Workday — Activity Tracker & Timesheet Tool
684
+
685
+ Usage:
686
+ workday init Initialize config in ~/.workday/
687
+ workday start Start daemon and print status
688
+ workday stop Stop running daemon
689
+ workday status Show daemon status and open sessions
690
+ workday today Show today's full summary
691
+ workday day YYYY-MM-DD Show summary for a specific date
692
+ workday pause Pause all active sessions
693
+ workday pause <repo> Pause a specific repo session
694
+ workday resume Resume all paused sessions
695
+ workday autopause on|off Toggle autopause for all sessions
696
+ workday autopause on|off <repo> Toggle autopause for a specific repo
697
+ workday adjust <target> +<N> "<reason>" Add manual time (today)
698
+ workday adjust <target> +<N> "<reason>" --date DATE Add manual time (past day)
699
+ workday set-start HH:MM Set day start earlier (today)
700
+ workday set-start HH:MM --date DATE Set day start earlier (past day)
701
+ workday tempo Show report (1st of month → today)
702
+ workday tempo --from DATE --to DATE Report for a custom range
703
+ workday tempo --file report.json Save report to JSON file
704
+ workday tempo --file report.json --push Push from saved report
705
+ workday tempo --push Push computed data to Tempo
706
+
707
+ Target: session index (#1, #2) or session id (hex)`);
708
+ }
709
+ await main();
710
+ //# sourceMappingURL=cli.js.map