clocktopus 1.9.0 → 1.10.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.
- package/dist/index.js +29 -20
- package/dist/lib/hook-prompt.js +5 -11
- package/dist/lib/hook-script.js +4 -3
- package/dist/lib/husky-install.js +2 -4
- package/dist/lib/simple-prompt.js +72 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
-
import inquirer from 'inquirer';
|
|
4
3
|
import chalk from 'chalk';
|
|
5
|
-
import { Clockify } from './clockify.js';
|
|
6
4
|
import * as fs from 'fs';
|
|
7
5
|
import * as path from 'path';
|
|
8
6
|
import { fileURLToPath } from 'url';
|
|
@@ -10,14 +8,25 @@ import { createRequire } from 'module';
|
|
|
10
8
|
import { execSync } from 'child_process';
|
|
11
9
|
import { completeLatestSession, getLatestSession, setSessionJiraWorklogId } from './lib/db.js';
|
|
12
10
|
import { isClockifyEnabled } from './lib/credentials.js';
|
|
13
|
-
import { stopJiraTimer } from './lib/jira.js';
|
|
14
|
-
import { startDashboard } from './dashboard/server.js';
|
|
15
11
|
import { ensureNativeAddons } from './lib/ensure-native-addons.js';
|
|
16
12
|
import { DASHBOARD_PORT, DASHBOARD_URL } from './lib/constants.js';
|
|
17
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
18
14
|
const __dirname = path.dirname(__filename);
|
|
19
15
|
const program = new Command();
|
|
20
|
-
|
|
16
|
+
// Clockify + Jira pull in axios (via follow-redirects), which in Bun triggers
|
|
17
|
+
// a tty WriteStream crash when loaded from a git hook context. Defer.
|
|
18
|
+
let _clockify = null;
|
|
19
|
+
async function clockify() {
|
|
20
|
+
if (!_clockify) {
|
|
21
|
+
const { Clockify } = await import('./clockify.js');
|
|
22
|
+
_clockify = new Clockify();
|
|
23
|
+
}
|
|
24
|
+
return _clockify;
|
|
25
|
+
}
|
|
26
|
+
async function stopJiraTimer(...args) {
|
|
27
|
+
const { stopJiraTimer: fn } = await import('./lib/jira.js');
|
|
28
|
+
return fn(...args);
|
|
29
|
+
}
|
|
21
30
|
async function getLocalProjects() {
|
|
22
31
|
const dataDir = path.join(__dirname, '../data');
|
|
23
32
|
const localProjectsPath = path.join(dataDir, 'local-projects.json');
|
|
@@ -39,7 +48,7 @@ async function getLocalProjects() {
|
|
|
39
48
|
}
|
|
40
49
|
}
|
|
41
50
|
async function getWorkspaceAndUser() {
|
|
42
|
-
const user = await clockify.getUser();
|
|
51
|
+
const user = await (await clockify()).getUser();
|
|
43
52
|
if (!user) {
|
|
44
53
|
console.log(chalk.red('[index] Could not connect to Clockify. Please check your API key.'));
|
|
45
54
|
process.exit(1);
|
|
@@ -72,7 +81,7 @@ program
|
|
|
72
81
|
return;
|
|
73
82
|
}
|
|
74
83
|
const { workspaceId } = await getWorkspaceAndUser();
|
|
75
|
-
let projects = await clockify.getProjects(workspaceId);
|
|
84
|
+
let projects = await (await clockify()).getProjects(workspaceId);
|
|
76
85
|
let localProjects = await getLocalProjects();
|
|
77
86
|
if (localProjects.length === 0) {
|
|
78
87
|
const allProjects = projects.map((p) => ({ id: p.id, name: p.name }));
|
|
@@ -89,6 +98,7 @@ program
|
|
|
89
98
|
console.log(chalk.yellow('No projects found in your workspace.'));
|
|
90
99
|
return;
|
|
91
100
|
}
|
|
101
|
+
const inquirer = (await import('inquirer')).default;
|
|
92
102
|
const { selectedProjectId } = await inquirer.prompt([
|
|
93
103
|
{
|
|
94
104
|
type: 'list',
|
|
@@ -113,7 +123,7 @@ program
|
|
|
113
123
|
const latestSession = getLatestSession();
|
|
114
124
|
if (isClockifyEnabled()) {
|
|
115
125
|
const { workspaceId, userId } = await getWorkspaceAndUser();
|
|
116
|
-
const stoppedEntry = await clockify.stopTimer(workspaceId, userId);
|
|
126
|
+
const stoppedEntry = await (await clockify()).stopTimer(workspaceId, userId);
|
|
117
127
|
if (!stoppedEntry) {
|
|
118
128
|
console.log(chalk.yellow('No timer was running.'));
|
|
119
129
|
return;
|
|
@@ -148,7 +158,7 @@ program
|
|
|
148
158
|
.action(async () => {
|
|
149
159
|
if (isClockifyEnabled()) {
|
|
150
160
|
const { workspaceId, userId } = await getWorkspaceAndUser();
|
|
151
|
-
const activeEntry = await clockify.getActiveTimer(workspaceId, userId);
|
|
161
|
+
const activeEntry = await (await clockify()).getActiveTimer(workspaceId, userId);
|
|
152
162
|
if (activeEntry) {
|
|
153
163
|
const startTime = new Date(activeEntry.timeInterval.start);
|
|
154
164
|
const duration = (new Date().getTime() - startTime.getTime()) / 1000;
|
|
@@ -192,7 +202,7 @@ program
|
|
|
192
202
|
const clockifyOn = isClockifyEnabled();
|
|
193
203
|
const latestSession = getLatestSession();
|
|
194
204
|
if (clockifyOn) {
|
|
195
|
-
const activeEntry = await clockify.getActiveTimer(workspaceId, userId);
|
|
205
|
+
const activeEntry = await (await clockify()).getActiveTimer(workspaceId, userId);
|
|
196
206
|
if (!activeEntry)
|
|
197
207
|
return false;
|
|
198
208
|
}
|
|
@@ -203,7 +213,7 @@ program
|
|
|
203
213
|
console.log(chalk.yellow(reason));
|
|
204
214
|
const completedAt = new Date().toISOString();
|
|
205
215
|
if (clockifyOn) {
|
|
206
|
-
const stoppedEntry = await clockify.stopTimer(workspaceId, userId);
|
|
216
|
+
const stoppedEntry = await (await clockify()).stopTimer(workspaceId, userId);
|
|
207
217
|
if (!stoppedEntry)
|
|
208
218
|
return false;
|
|
209
219
|
}
|
|
@@ -243,10 +253,10 @@ program
|
|
|
243
253
|
if (isClockifyEnabled()) {
|
|
244
254
|
if (!latestSession.projectId)
|
|
245
255
|
return;
|
|
246
|
-
const activeEntry = await clockify.getActiveTimer(workspaceId, userId);
|
|
256
|
+
const activeEntry = await (await clockify()).getActiveTimer(workspaceId, userId);
|
|
247
257
|
if (activeEntry)
|
|
248
258
|
return;
|
|
249
|
-
await clockify.startTimer(workspaceId, latestSession.projectId, latestSession.description, latestSession.jiraTicket ?? undefined);
|
|
259
|
+
await (await clockify()).startTimer(workspaceId, latestSession.projectId, latestSession.description, latestSession.jiraTicket ?? undefined);
|
|
250
260
|
console.log(chalk.green('Timer restarted for the last used project.'));
|
|
251
261
|
lastResumeAt = Date.now();
|
|
252
262
|
return;
|
|
@@ -352,7 +362,8 @@ program
|
|
|
352
362
|
program
|
|
353
363
|
.command('dash')
|
|
354
364
|
.description(`Start the Clocktopus web dashboard on localhost:${DASHBOARD_PORT}.`)
|
|
355
|
-
.action(() => {
|
|
365
|
+
.action(async () => {
|
|
366
|
+
const { startDashboard } = await import('./dashboard/server.js');
|
|
356
367
|
startDashboard();
|
|
357
368
|
});
|
|
358
369
|
const isDev = __dirname.includes('/Projects/') || __dirname.includes('/src/');
|
|
@@ -492,7 +503,8 @@ program
|
|
|
492
503
|
const { installHuskyHook } = await import('./lib/husky-install.js');
|
|
493
504
|
const result = installHuskyHook(process.cwd());
|
|
494
505
|
if (result.installed) {
|
|
495
|
-
|
|
506
|
+
const verb = result.overwritten ? 'Overwrote' : 'Installed';
|
|
507
|
+
console.log(chalk.green(`${verb} husky post-checkout at ${result.path}.`));
|
|
496
508
|
console.log(chalk.gray(' Commit it so teammates using husky get it too.'));
|
|
497
509
|
return;
|
|
498
510
|
}
|
|
@@ -500,10 +512,6 @@ program
|
|
|
500
512
|
console.error(chalk.red('No .husky/ directory found. Run from the root of a husky-enabled repo.'));
|
|
501
513
|
process.exit(1);
|
|
502
514
|
}
|
|
503
|
-
if (result.reason === 'already-exists') {
|
|
504
|
-
console.log(chalk.yellow(`Existing ${result.path} left alone.`));
|
|
505
|
-
console.log(chalk.gray(' Add this line manually: exec ~/.clocktopus/hooks/post-checkout "$@"'));
|
|
506
|
-
}
|
|
507
515
|
});
|
|
508
516
|
program
|
|
509
517
|
.command('hook:prompt <branch>')
|
|
@@ -516,7 +524,8 @@ program
|
|
|
516
524
|
catch (e) {
|
|
517
525
|
const msg = e instanceof Error ? e.message : String(e);
|
|
518
526
|
console.error(chalk.red(`Hook prompt failed: ${msg}`));
|
|
519
|
-
process.exit(0); // never block git checkout
|
|
520
527
|
}
|
|
528
|
+
// Force exit — /dev/tty fs streams and the sqlite handle keep the event loop alive.
|
|
529
|
+
process.exit(0);
|
|
521
530
|
});
|
|
522
531
|
program.parse(process.argv);
|
package/dist/lib/hook-prompt.js
CHANGED
|
@@ -1,21 +1,15 @@
|
|
|
1
|
-
import inquirer from 'inquirer';
|
|
2
1
|
import chalk from 'chalk';
|
|
3
|
-
import * as fs from 'fs';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
import { fileURLToPath } from 'url';
|
|
6
2
|
import { extractTicket } from './branch-parser.js';
|
|
7
3
|
import { isRepoIgnored as realIsRepoIgnored } from './hook-ignore.js';
|
|
8
4
|
import { isClockifyEnabled as realIsClockifyEnabled } from './credentials.js';
|
|
9
5
|
import { getJiraSummary as realGetJiraSummary } from './jira-summary.js';
|
|
10
|
-
import { getOpenSession as realGetOpenSession } from './db.js';
|
|
6
|
+
import { getOpenSession as realGetOpenSession, getActiveProjects } from './db.js';
|
|
11
7
|
import { matchProjectByTicket } from './project-matcher.js';
|
|
8
|
+
import { simplePrompt } from './simple-prompt.js';
|
|
12
9
|
import { startTimer as realStartTimer } from './start-timer.js';
|
|
13
10
|
function defaultReadProjects() {
|
|
14
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
-
const __dirname = path.dirname(__filename);
|
|
16
|
-
const p = path.join(__dirname, '../data/local-projects.json');
|
|
17
11
|
try {
|
|
18
|
-
return
|
|
12
|
+
return getActiveProjects().map((p) => ({ id: p.id, name: p.name }));
|
|
19
13
|
}
|
|
20
14
|
catch {
|
|
21
15
|
return [];
|
|
@@ -28,7 +22,7 @@ export async function runHookPrompt(branch, opts) {
|
|
|
28
22
|
const getJiraSummary = d.getJiraSummary ?? realGetJiraSummary;
|
|
29
23
|
const getOpenSession = d.getOpenSession ?? realGetOpenSession;
|
|
30
24
|
const readProjects = d.readProjects ?? defaultReadProjects;
|
|
31
|
-
const prompt = d.prompt ??
|
|
25
|
+
const prompt = d.prompt ?? simplePrompt;
|
|
32
26
|
const startTimer = d.startTimer ?? realStartTimer;
|
|
33
27
|
if (isRepoIgnored(opts.cwd)) {
|
|
34
28
|
return { started: false, ticket: null, projectId: null, description: null, reason: 'ignored' };
|
|
@@ -51,7 +45,7 @@ export async function runHookPrompt(branch, opts) {
|
|
|
51
45
|
const promptMsg = ticket
|
|
52
46
|
? `Start timer for ${chalk.bold(ticket)} (branch: ${branch})?`
|
|
53
47
|
: `Start timer for branch ${chalk.bold(branch)}?`;
|
|
54
|
-
const confirmAnswer = await prompt([{ type: 'confirm', name: 'confirmStart', message: promptMsg, default:
|
|
48
|
+
const confirmAnswer = await prompt([{ type: 'confirm', name: 'confirmStart', message: promptMsg, default: false }]);
|
|
55
49
|
if (!confirmAnswer.confirmStart) {
|
|
56
50
|
return { started: false, ticket, projectId: null, description: null, reason: 'declined' };
|
|
57
51
|
}
|
package/dist/lib/hook-script.js
CHANGED
|
@@ -6,8 +6,9 @@ if [ "$3" != "1" ]; then
|
|
|
6
6
|
exit 0
|
|
7
7
|
fi
|
|
8
8
|
|
|
9
|
-
# Require
|
|
10
|
-
|
|
9
|
+
# Require a tty on stdout; otherwise the prompt has nowhere to render.
|
|
10
|
+
# Git commonly redirects hook stdin, so we only check stdout here.
|
|
11
|
+
if ! [ -t 1 ]; then
|
|
11
12
|
exit 0
|
|
12
13
|
fi
|
|
13
14
|
|
|
@@ -24,6 +25,6 @@ if ! command -v clocktopus >/dev/null 2>&1; then
|
|
|
24
25
|
exit 0
|
|
25
26
|
fi
|
|
26
27
|
|
|
27
|
-
clocktopus hook:prompt "$branch" </dev/tty
|
|
28
|
+
NO_COLOR=1 FORCE_COLOR=0 clocktopus hook:prompt "$branch" </dev/tty || true
|
|
28
29
|
exit 0
|
|
29
30
|
`;
|
|
@@ -13,9 +13,7 @@ export function installHuskyHook(cwd) {
|
|
|
13
13
|
return { installed: false, reason: 'no-husky-dir' };
|
|
14
14
|
}
|
|
15
15
|
const target = path.join(huskyDir, 'post-checkout');
|
|
16
|
-
|
|
17
|
-
return { installed: false, reason: 'already-exists', path: target };
|
|
18
|
-
}
|
|
16
|
+
const overwritten = fs.existsSync(target);
|
|
19
17
|
fs.writeFileSync(target, huskyHookBody(), { mode: 0o755 });
|
|
20
|
-
return { installed: true, path: target };
|
|
18
|
+
return { installed: true, path: target, overwritten };
|
|
21
19
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
function readLineSync(fd) {
|
|
3
|
+
const buf = Buffer.alloc(1);
|
|
4
|
+
let line = '';
|
|
5
|
+
while (true) {
|
|
6
|
+
const n = fs.readSync(fd, buf, 0, 1, null);
|
|
7
|
+
if (n === 0)
|
|
8
|
+
return line;
|
|
9
|
+
const ch = buf.toString('utf8');
|
|
10
|
+
if (ch === '\n')
|
|
11
|
+
return line;
|
|
12
|
+
if (ch === '\r')
|
|
13
|
+
continue;
|
|
14
|
+
line += ch;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export async function simplePrompt(qs) {
|
|
18
|
+
let inFd;
|
|
19
|
+
let outFd;
|
|
20
|
+
try {
|
|
21
|
+
inFd = fs.openSync('/dev/tty', 'r');
|
|
22
|
+
outFd = fs.openSync('/dev/tty', 'w');
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new Error('simplePrompt: cannot open /dev/tty');
|
|
26
|
+
}
|
|
27
|
+
const write = (s) => fs.writeSync(outFd, s);
|
|
28
|
+
const out = {};
|
|
29
|
+
try {
|
|
30
|
+
for (const raw of qs) {
|
|
31
|
+
const q = raw;
|
|
32
|
+
const label = q.message.replace(/:\s*$/, '');
|
|
33
|
+
if (q.type === 'confirm') {
|
|
34
|
+
const def = q.default !== false;
|
|
35
|
+
write(`${label} ${def ? '[Y/n]' : '[y/N]'}: `);
|
|
36
|
+
const answer = readLineSync(inFd).trim().toLowerCase();
|
|
37
|
+
out[q.name] = answer === '' ? def : answer === 'y' || answer === 'yes';
|
|
38
|
+
}
|
|
39
|
+
else if (q.type === 'input') {
|
|
40
|
+
const def = typeof q.default === 'string' ? q.default : '';
|
|
41
|
+
write(def ? `${label} [${def}]: ` : `${label}: `);
|
|
42
|
+
const answer = readLineSync(inFd).trim();
|
|
43
|
+
out[q.name] = answer || def;
|
|
44
|
+
}
|
|
45
|
+
else if (q.type === 'list') {
|
|
46
|
+
write(`${label}\n`);
|
|
47
|
+
const choices = q.choices ?? [];
|
|
48
|
+
choices.forEach((c, i) => write(` ${i + 1}) ${c.name}\n`));
|
|
49
|
+
while (true) {
|
|
50
|
+
write(`Pick 1-${choices.length}: `);
|
|
51
|
+
const answer = readLineSync(inFd).trim();
|
|
52
|
+
const n = Number.parseInt(answer, 10);
|
|
53
|
+
if (Number.isFinite(n) && n >= 1 && n <= choices.length) {
|
|
54
|
+
out[q.name] = choices[n - 1].value;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
try {
|
|
63
|
+
fs.closeSync(inFd);
|
|
64
|
+
}
|
|
65
|
+
catch { }
|
|
66
|
+
try {
|
|
67
|
+
fs.closeSync(outFd);
|
|
68
|
+
}
|
|
69
|
+
catch { }
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clocktopus",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Time-tracking automation for Clockify with idle monitoring, Jira integration, Google Calendar sync, CLI, web dashboard, and desktop app.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|