@usemeno/meno-cli 0.1.0 → 0.1.1
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/Asset 1.svg +8 -0
- package/README.md +1 -1
- package/dist/index.js +349 -128
- package/package.json +7 -2
- package/src/commands/select.ts +57 -32
- package/src/commands/start.ts +75 -40
- package/src/commands/status.ts +30 -8
- package/src/commands/stop.ts +75 -45
- package/src/config.ts +32 -1
- package/src/index.ts +10 -5
- package/src/logo-ascii.ts +61 -0
- package/src/utils/api.ts +79 -0
- package/src/utils/git.ts +62 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usemeno/meno-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Command-line interface for Meno time tracking",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
"dev": "tsup src/index.ts --format esm --watch",
|
|
13
13
|
"prepublishOnly": "npm run build"
|
|
14
14
|
},
|
|
15
|
-
"keywords": [
|
|
15
|
+
"keywords": [
|
|
16
|
+
"meno",
|
|
17
|
+
"time-tracking",
|
|
18
|
+
"cli",
|
|
19
|
+
"productivity"
|
|
20
|
+
],
|
|
16
21
|
"author": "",
|
|
17
22
|
"license": "MIT",
|
|
18
23
|
"dependencies": {
|
package/src/commands/select.ts
CHANGED
|
@@ -1,79 +1,104 @@
|
|
|
1
1
|
import enquirer from "enquirer";
|
|
2
2
|
import ora from "ora";
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { setSelectedTask, clearSelectedTask, getSelectedTask } from "../config.js";
|
|
5
|
+
import { getTasks, ApiError, Task } from "../utils/api.js";
|
|
6
|
+
|
|
7
|
+
function getStatusColor(status: string): (text: string) => string {
|
|
8
|
+
switch (status) {
|
|
9
|
+
case "Backlog": return chalk.gray;
|
|
10
|
+
case "Todo": return chalk.yellow;
|
|
11
|
+
case "InProgress": return chalk.cyan;
|
|
12
|
+
case "Review": return chalk.magenta;
|
|
13
|
+
case "Done": return chalk.green;
|
|
14
|
+
default: return chalk.white;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function formatTaskDisplay(task: Task, isSelected: boolean): string {
|
|
19
|
+
const prefix = isSelected ? "● " : " ";
|
|
20
|
+
const statusColor = getStatusColor(task.status);
|
|
21
|
+
const estimateText = task.estimatedHours ? ` • ${task.estimatedHours}h` : "";
|
|
22
|
+
|
|
23
|
+
return `${prefix}${chalk.bold(task.title)} ${chalk.dim("→")} ${task.project.name} ${statusColor(`[${task.status}]`)}${estimateText}`;
|
|
24
|
+
}
|
|
6
25
|
|
|
7
26
|
export async function selectProject() {
|
|
8
|
-
console.log(chalk.bold("\n
|
|
27
|
+
console.log(chalk.bold("\n📋 Select Task\n"));
|
|
9
28
|
|
|
10
|
-
const spinner = ora("Loading
|
|
29
|
+
const spinner = ora("Loading tasks...").start();
|
|
11
30
|
|
|
12
31
|
try {
|
|
13
|
-
const
|
|
32
|
+
const tasks = await getTasks();
|
|
14
33
|
|
|
15
34
|
spinner.stop();
|
|
16
35
|
|
|
17
|
-
if (
|
|
18
|
-
console.log(chalk.yellow("No
|
|
19
|
-
console.log(chalk.dim("Create
|
|
36
|
+
if (tasks.length === 0) {
|
|
37
|
+
console.log(chalk.yellow("No tasks found."));
|
|
38
|
+
console.log(chalk.dim("Create tasks in the Meno dashboard first.\n"));
|
|
20
39
|
return;
|
|
21
40
|
}
|
|
22
41
|
|
|
23
|
-
const
|
|
42
|
+
const currentTask = getSelectedTask();
|
|
24
43
|
|
|
25
|
-
// Build choices
|
|
44
|
+
// Build choices - group by status or show all
|
|
26
45
|
const choices = [
|
|
27
46
|
{
|
|
28
47
|
name: "clear",
|
|
29
48
|
message: chalk.dim("[Clear Selection]"),
|
|
30
49
|
},
|
|
31
|
-
...
|
|
32
|
-
const isSelected =
|
|
33
|
-
const prefix = isSelected ? "● " : " ";
|
|
34
|
-
const displayName = `${project.name} (${project.clientName}) • $${project.hourlyRate}/hr`;
|
|
50
|
+
...tasks.map((task) => {
|
|
51
|
+
const isSelected = currentTask?.id === task.id;
|
|
35
52
|
|
|
36
53
|
return {
|
|
37
|
-
name:
|
|
38
|
-
message:
|
|
39
|
-
value:
|
|
54
|
+
name: task.id,
|
|
55
|
+
message: formatTaskDisplay(task, isSelected),
|
|
56
|
+
value: task,
|
|
40
57
|
};
|
|
41
58
|
}),
|
|
42
59
|
];
|
|
43
60
|
|
|
44
61
|
const result = await enquirer.prompt({
|
|
45
62
|
type: "select",
|
|
46
|
-
name: "
|
|
47
|
-
message: "Select a
|
|
63
|
+
name: "task",
|
|
64
|
+
message: "Select a task:",
|
|
48
65
|
choices,
|
|
49
66
|
});
|
|
50
67
|
|
|
51
|
-
const answer = (result as any).
|
|
68
|
+
const answer = (result as any).task;
|
|
52
69
|
|
|
53
70
|
if (answer === "clear") {
|
|
54
|
-
|
|
71
|
+
clearSelectedTask();
|
|
55
72
|
console.log(chalk.green("\n✓ Selection cleared\n"));
|
|
56
73
|
} else {
|
|
57
|
-
// Find the full
|
|
58
|
-
const
|
|
74
|
+
// Find the full task object by ID
|
|
75
|
+
const task = tasks.find((t) => t.id === answer);
|
|
59
76
|
|
|
60
|
-
if (!
|
|
61
|
-
console.log(chalk.red("\n✗
|
|
77
|
+
if (!task) {
|
|
78
|
+
console.log(chalk.red("\n✗ Task not found\n"));
|
|
62
79
|
return;
|
|
63
80
|
}
|
|
64
81
|
|
|
65
|
-
|
|
66
|
-
id:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
82
|
+
setSelectedTask({
|
|
83
|
+
id: task.id,
|
|
84
|
+
title: task.title,
|
|
85
|
+
projectId: task.projectId,
|
|
86
|
+
projectName: task.project.name,
|
|
87
|
+
status: task.status,
|
|
88
|
+
estimatedHours: task.estimatedHours,
|
|
89
|
+
hourlyRate: task.project.hourlyRate,
|
|
70
90
|
});
|
|
91
|
+
|
|
92
|
+
const statusColor = getStatusColor(task.status);
|
|
71
93
|
console.log(
|
|
72
|
-
chalk.green(`\n✓ Selected: ${chalk.bold(
|
|
94
|
+
chalk.green(`\n✓ Selected: ${chalk.bold(task.title)}\n`) +
|
|
95
|
+
chalk.dim(` Project: ${task.project.name} • $${task.project.hourlyRate}/hr\n`) +
|
|
96
|
+
chalk.dim(` Status: `) + statusColor(task.status) +
|
|
97
|
+
(task.estimatedHours ? chalk.dim(` • Estimated: ${task.estimatedHours}h\n`) : "\n")
|
|
73
98
|
);
|
|
74
99
|
}
|
|
75
100
|
} catch (error) {
|
|
76
|
-
spinner.fail(chalk.red("Failed to load
|
|
101
|
+
spinner.fail(chalk.red("Failed to load tasks"));
|
|
77
102
|
|
|
78
103
|
if (error instanceof ApiError) {
|
|
79
104
|
console.log(chalk.red(`\nError: ${error.message}\n`));
|
package/src/commands/start.ts
CHANGED
|
@@ -1,77 +1,112 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import ora from "ora";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { getSelectedTask, getActiveTimer, setActiveTimer } from "../config.js";
|
|
4
|
+
import { getTasks, cliAction, ApiError, Task, CliStartResponse } from "../utils/api.js";
|
|
5
5
|
import { formatDuration } from "../utils/timer.js";
|
|
6
6
|
|
|
7
|
-
export async function startTimer(
|
|
7
|
+
export async function startTimer(taskId?: string) {
|
|
8
8
|
// Check if timer is already running
|
|
9
9
|
const existingTimer = getActiveTimer();
|
|
10
10
|
if (existingTimer) {
|
|
11
11
|
const elapsed = formatDuration(existingTimer.startTime);
|
|
12
|
+
const taskInfo = existingTimer.taskTitle || existingTimer.projectName;
|
|
12
13
|
console.log(
|
|
13
14
|
chalk.yellow(
|
|
14
|
-
`\n⚠ Timer already running for ${chalk.bold(
|
|
15
|
+
`\n⚠ Timer already running for ${chalk.bold(taskInfo)} (${elapsed})`
|
|
15
16
|
)
|
|
16
17
|
);
|
|
17
18
|
console.log(chalk.dim(`Stop it first with ${chalk.bold("meno stop")} or discard with ${chalk.bold("meno stop --discard")}\n`));
|
|
18
19
|
return;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
let
|
|
22
|
+
let task: Task | undefined;
|
|
22
23
|
|
|
23
|
-
if (
|
|
24
|
-
// Start with explicit
|
|
25
|
-
const spinner = ora("Loading
|
|
24
|
+
if (taskId) {
|
|
25
|
+
// Start with explicit task ID
|
|
26
|
+
const spinner = ora("Loading task...").start();
|
|
26
27
|
try {
|
|
27
|
-
const
|
|
28
|
-
|
|
28
|
+
const tasks = await getTasks();
|
|
29
|
+
task = tasks.find((t) => t.id === taskId);
|
|
29
30
|
spinner.stop();
|
|
30
31
|
|
|
31
|
-
if (!
|
|
32
|
-
console.log(chalk.red(`\n✗
|
|
32
|
+
if (!task) {
|
|
33
|
+
console.log(chalk.red(`\n✗ Task not found: ${taskId}\n`));
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
35
36
|
} catch (error) {
|
|
36
|
-
spinner.fail(chalk.red("Failed to load
|
|
37
|
+
spinner.fail(chalk.red("Failed to load task"));
|
|
37
38
|
if (error instanceof ApiError) {
|
|
38
39
|
console.log(chalk.red(`\nError: ${error.message}\n`));
|
|
39
40
|
}
|
|
40
41
|
return;
|
|
41
42
|
}
|
|
42
43
|
} else {
|
|
43
|
-
// Use selected
|
|
44
|
-
const selected =
|
|
44
|
+
// Use selected task
|
|
45
|
+
const selected = getSelectedTask();
|
|
45
46
|
if (!selected) {
|
|
46
|
-
console.log(chalk.yellow(`\n⚠ No
|
|
47
|
-
console.log(chalk.dim(`Run ${chalk.bold("meno select")} first or provide a
|
|
47
|
+
console.log(chalk.yellow(`\n⚠ No task selected.`));
|
|
48
|
+
console.log(chalk.dim(`Run ${chalk.bold("meno select")} first or provide a task ID: ${chalk.bold("meno start <task-id>")}\n`));
|
|
48
49
|
return;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
52
|
+
// Fetch full task details to ensure we have latest status
|
|
53
|
+
const spinner = ora("Starting timer...").start();
|
|
54
|
+
try {
|
|
55
|
+
const tasks = await getTasks();
|
|
56
|
+
task = tasks.find((t) => t.id === selected.id);
|
|
57
|
+
spinner.stop();
|
|
58
|
+
|
|
59
|
+
if (!task) {
|
|
60
|
+
console.log(chalk.red(`\n✗ Selected task not found. It may have been deleted.\n`));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
spinner.fail(chalk.red("Failed to load task"));
|
|
65
|
+
if (error instanceof ApiError) {
|
|
66
|
+
console.log(chalk.red(`\nError: ${error.message}\n`));
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
61
70
|
}
|
|
62
71
|
|
|
63
|
-
//
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
// Call API to start timer (API will auto-stop any running timer)
|
|
73
|
+
const spinner = ora("Starting timer...").start();
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const response = await cliAction("start", { taskId: task.id }) as CliStartResponse;
|
|
77
|
+
|
|
78
|
+
spinner.succeed(chalk.green("Timer started"));
|
|
79
|
+
|
|
80
|
+
// Store timer info locally for offline reference
|
|
81
|
+
const startTime = response.timer?.startTime || new Date().toISOString();
|
|
82
|
+
setActiveTimer({
|
|
83
|
+
projectId: task.projectId,
|
|
84
|
+
projectName: task.project.name,
|
|
85
|
+
startTime,
|
|
86
|
+
taskId: task.id,
|
|
87
|
+
taskTitle: task.title,
|
|
88
|
+
});
|
|
70
89
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
90
|
+
const timestamp = new Date(startTime).toLocaleTimeString();
|
|
91
|
+
console.log(
|
|
92
|
+
chalk.green(
|
|
93
|
+
`\n⏱️ ${chalk.bold(task.title)}\n` +
|
|
94
|
+
chalk.dim(` ${task.project.name} • $${task.project.hourlyRate}/hr • [${timestamp}]`)
|
|
95
|
+
)
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
if (task.estimatedHours) {
|
|
99
|
+
console.log(chalk.dim(` Estimated: ${task.estimatedHours}h\n`));
|
|
100
|
+
} else {
|
|
101
|
+
console.log();
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
spinner.fail(chalk.red("Failed to start timer"));
|
|
105
|
+
|
|
106
|
+
if (error instanceof ApiError) {
|
|
107
|
+
console.log(chalk.red(`\nError: ${error.message}\n`));
|
|
108
|
+
} else {
|
|
109
|
+
console.log(chalk.red(`\nUnexpected error: ${(error as Error).message}\n`));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
77
112
|
}
|
package/src/commands/status.ts
CHANGED
|
@@ -1,36 +1,58 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import ora from "ora";
|
|
3
|
-
import {
|
|
3
|
+
import { getSelectedTask, getActiveTimer } from "../config.js";
|
|
4
4
|
import { apiRequest, ApiError, Stats } from "../utils/api.js";
|
|
5
5
|
import { formatDuration, calculateTimerValue } from "../utils/timer.js";
|
|
6
6
|
|
|
7
|
+
function getStatusColor(status: string): (text: string) => string {
|
|
8
|
+
switch (status) {
|
|
9
|
+
case "Backlog": return chalk.gray;
|
|
10
|
+
case "Todo": return chalk.yellow;
|
|
11
|
+
case "InProgress": return chalk.cyan;
|
|
12
|
+
case "Review": return chalk.magenta;
|
|
13
|
+
case "Done": return chalk.green;
|
|
14
|
+
default: return chalk.white;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
7
18
|
export async function showStatus() {
|
|
8
19
|
console.log(chalk.bold("\n📊 Meno Status\n"));
|
|
9
20
|
|
|
10
|
-
const
|
|
21
|
+
const selectedTask = getSelectedTask();
|
|
11
22
|
const timer = getActiveTimer();
|
|
12
23
|
|
|
13
|
-
// Show selected
|
|
14
|
-
if (
|
|
24
|
+
// Show selected task
|
|
25
|
+
if (selectedTask) {
|
|
26
|
+
const statusColor = getStatusColor(selectedTask.status);
|
|
15
27
|
console.log(
|
|
16
28
|
chalk.cyan(
|
|
17
|
-
`📌 Selected: ${chalk.bold(
|
|
29
|
+
`📌 Selected: ${chalk.bold(selectedTask.title)}`
|
|
18
30
|
)
|
|
19
31
|
);
|
|
32
|
+
console.log(
|
|
33
|
+
chalk.dim(
|
|
34
|
+
` ${selectedTask.projectName} • $${selectedTask.hourlyRate}/hr • `
|
|
35
|
+
) + statusColor(selectedTask.status)
|
|
36
|
+
);
|
|
37
|
+
if (selectedTask.estimatedHours) {
|
|
38
|
+
console.log(chalk.dim(` Estimated: ${selectedTask.estimatedHours}h`));
|
|
39
|
+
}
|
|
20
40
|
}
|
|
21
41
|
|
|
22
42
|
// Show active timer
|
|
23
43
|
if (timer) {
|
|
24
44
|
const elapsed = formatDuration(timer.startTime);
|
|
25
|
-
const
|
|
45
|
+
const taskInfo = timer.taskTitle || timer.projectName;
|
|
46
|
+
const rate = selectedTask?.hourlyRate || 0;
|
|
47
|
+
const value = calculateTimerValue(timer.startTime, rate);
|
|
26
48
|
console.log(
|
|
27
49
|
chalk.green(
|
|
28
|
-
`⏱️ Running: ${chalk.bold(
|
|
50
|
+
`⏱️ Running: ${chalk.bold(taskInfo)} • ${elapsed} • $${value.toFixed(2)}`
|
|
29
51
|
)
|
|
30
52
|
);
|
|
31
53
|
}
|
|
32
54
|
|
|
33
|
-
if (
|
|
55
|
+
if (selectedTask || timer) {
|
|
34
56
|
console.log(); // Add spacing
|
|
35
57
|
}
|
|
36
58
|
|
package/src/commands/stop.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import enquirer from "enquirer";
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import ora from "ora";
|
|
4
|
-
import { getActiveTimer, clearActiveTimer
|
|
5
|
-
import {
|
|
6
|
-
import { formatDuration
|
|
4
|
+
import { getActiveTimer, clearActiveTimer } from "../config.js";
|
|
5
|
+
import { cliAction, ApiError, CliStopResponse } from "../utils/api.js";
|
|
6
|
+
import { formatDuration } from "../utils/timer.js";
|
|
7
|
+
import { getGitInfo, isGitRepository } from "../utils/git.js";
|
|
7
8
|
|
|
8
9
|
interface StopOptions {
|
|
9
10
|
discard?: boolean;
|
|
10
11
|
description?: string;
|
|
11
12
|
noConfirm?: boolean;
|
|
12
13
|
yes?: boolean;
|
|
13
|
-
|
|
14
|
+
commit?: string;
|
|
15
|
+
repo?: string;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export async function stopTimer(options: StopOptions) {
|
|
@@ -29,42 +31,22 @@ export async function stopTimer(options: StopOptions) {
|
|
|
29
31
|
return;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
let formatted: string;
|
|
34
|
+
const taskInfo = timer.taskTitle || timer.projectName;
|
|
35
|
+
const elapsed = formatDuration(timer.startTime);
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
duration = parseDuration(options.adjust);
|
|
39
|
-
if (duration === 0) {
|
|
40
|
-
console.log(chalk.red(`\n✗ Invalid duration format: ${options.adjust}\n`));
|
|
41
|
-
console.log(chalk.dim(`Use format like: 1.5h, 90m, 45\n`));
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
const hours = Math.floor(duration);
|
|
45
|
-
const minutes = Math.round((duration - hours) * 60);
|
|
46
|
-
formatted = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
|
|
47
|
-
console.log(chalk.yellow(`\n⚠ Adjusted duration from timer\n`));
|
|
48
|
-
} else {
|
|
49
|
-
duration = calculateElapsedHours(timer.startTime);
|
|
50
|
-
formatted = formatDuration(timer.startTime);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Get project details for rate calculation
|
|
54
|
-
const project = getSelectedProject();
|
|
55
|
-
const hourlyRate = project?.hourlyRate || 0;
|
|
56
|
-
const amount = duration * hourlyRate;
|
|
57
|
-
|
|
58
|
-
console.log(chalk.bold(`\n⏱️ Duration: ${formatted} • $${amount.toFixed(2)}\n`));
|
|
37
|
+
console.log(chalk.bold(`\n⏱️ Stopping: ${taskInfo}`));
|
|
38
|
+
console.log(chalk.dim(`Duration: ${elapsed}\n`));
|
|
59
39
|
|
|
60
40
|
let description: string;
|
|
61
41
|
let shouldLog = true;
|
|
42
|
+
let commitHash: string | undefined;
|
|
43
|
+
let repoUrl: string | undefined;
|
|
62
44
|
|
|
63
45
|
try {
|
|
64
46
|
// Get description (from flag or prompt)
|
|
65
47
|
if (options.description) {
|
|
66
48
|
description = options.description;
|
|
67
|
-
console.log(chalk.dim(`Description: ${description}
|
|
49
|
+
console.log(chalk.dim(`Description: ${description}`));
|
|
68
50
|
} else {
|
|
69
51
|
const descResult = await enquirer.prompt({
|
|
70
52
|
type: "input",
|
|
@@ -81,6 +63,40 @@ export async function stopTimer(options: StopOptions) {
|
|
|
81
63
|
description = (descResult as any).description;
|
|
82
64
|
}
|
|
83
65
|
|
|
66
|
+
// Handle git integration
|
|
67
|
+
if (options.commit && options.repo) {
|
|
68
|
+
// Manual git info provided
|
|
69
|
+
commitHash = options.commit;
|
|
70
|
+
repoUrl = options.repo;
|
|
71
|
+
console.log(chalk.dim(`\n🔗 Evidence: ${commitHash.substring(0, 7)} @ ${repoUrl}`));
|
|
72
|
+
} else if (isGitRepository()) {
|
|
73
|
+
// Auto-detect git info and prompt
|
|
74
|
+
const gitInfo = getGitInfo();
|
|
75
|
+
|
|
76
|
+
if (gitInfo) {
|
|
77
|
+
const skipConfirm = options.noConfirm || options.yes;
|
|
78
|
+
|
|
79
|
+
if (skipConfirm) {
|
|
80
|
+
// Auto-include if -y flag
|
|
81
|
+
commitHash = gitInfo.commitHash;
|
|
82
|
+
repoUrl = gitInfo.repoUrl;
|
|
83
|
+
console.log(chalk.dim(`\n🔗 Evidence: ${commitHash.substring(0, 7)} @ ${repoUrl}`));
|
|
84
|
+
} else {
|
|
85
|
+
const gitResult = await enquirer.prompt({
|
|
86
|
+
type: "confirm",
|
|
87
|
+
name: "includeGit",
|
|
88
|
+
message: `Include latest commit (${gitInfo.commitHash.substring(0, 7)}) as evidence?`,
|
|
89
|
+
initial: true,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if ((gitResult as any).includeGit) {
|
|
93
|
+
commitHash = gitInfo.commitHash;
|
|
94
|
+
repoUrl = gitInfo.repoUrl;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
84
100
|
// Confirm logging (unless --no-confirm or -y flag)
|
|
85
101
|
const skipConfirm = options.noConfirm || options.yes;
|
|
86
102
|
if (!skipConfirm) {
|
|
@@ -94,36 +110,50 @@ export async function stopTimer(options: StopOptions) {
|
|
|
94
110
|
|
|
95
111
|
if (!shouldLog) {
|
|
96
112
|
console.log(chalk.yellow("\n✗ Entry not logged\n"));
|
|
113
|
+
clearActiveTimer();
|
|
97
114
|
return;
|
|
98
115
|
}
|
|
99
116
|
}
|
|
100
117
|
|
|
101
|
-
//
|
|
118
|
+
// Call API to stop timer
|
|
102
119
|
const spinner = ora("Logging entry...").start();
|
|
103
120
|
|
|
104
121
|
try {
|
|
105
|
-
const response = await
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
duration: Math.round(duration * 100) / 100, // Round to 2 decimals
|
|
111
|
-
date: new Date().toISOString().split("T")[0], // Today
|
|
112
|
-
billable: true,
|
|
113
|
-
}),
|
|
114
|
-
});
|
|
122
|
+
const response = await cliAction("stop", {
|
|
123
|
+
description,
|
|
124
|
+
commitHash,
|
|
125
|
+
repoUrl,
|
|
126
|
+
}) as CliStopResponse;
|
|
115
127
|
|
|
116
128
|
clearActiveTimer();
|
|
117
|
-
|
|
118
|
-
|
|
129
|
+
|
|
130
|
+
if (response.entry) {
|
|
131
|
+
const hours = response.entry.duration.toFixed(2);
|
|
132
|
+
const amount = response.entry.amount.toFixed(2);
|
|
133
|
+
|
|
134
|
+
spinner.succeed(chalk.green(`✓ Logged ${hours} hours ($${amount})`));
|
|
135
|
+
|
|
136
|
+
if (response.evidence) {
|
|
137
|
+
console.log(chalk.dim(`📎 Evidence: ${response.evidence.commitHash.substring(0, 7)}`));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.log();
|
|
141
|
+
} else {
|
|
142
|
+
spinner.succeed(chalk.green("✓ Timer stopped"));
|
|
143
|
+
console.log();
|
|
144
|
+
}
|
|
119
145
|
} catch (error) {
|
|
120
|
-
spinner.fail(chalk.red("Failed to
|
|
146
|
+
spinner.fail(chalk.red("Failed to stop timer"));
|
|
121
147
|
|
|
122
148
|
if (error instanceof ApiError) {
|
|
123
149
|
console.log(chalk.red(`\nError: ${error.message}\n`));
|
|
150
|
+
console.log(chalk.yellow(`Timer cleared locally. You may need to check the dashboard.\n`));
|
|
124
151
|
} else {
|
|
125
152
|
console.log(chalk.red(`\nUnexpected error: ${(error as Error).message}\n`));
|
|
126
153
|
}
|
|
154
|
+
|
|
155
|
+
// Clear local timer even on error to avoid stuck state
|
|
156
|
+
clearActiveTimer();
|
|
127
157
|
}
|
|
128
158
|
} catch (error) {
|
|
129
159
|
// User cancelled
|
package/src/config.ts
CHANGED
|
@@ -9,11 +9,22 @@ interface Config {
|
|
|
9
9
|
clientName: string;
|
|
10
10
|
hourlyRate: number;
|
|
11
11
|
};
|
|
12
|
+
selectedTask?: {
|
|
13
|
+
id: string;
|
|
14
|
+
title: string;
|
|
15
|
+
projectId: string;
|
|
16
|
+
projectName: string;
|
|
17
|
+
status: string;
|
|
18
|
+
estimatedHours?: number;
|
|
19
|
+
hourlyRate: number;
|
|
20
|
+
};
|
|
12
21
|
activeTimer?: {
|
|
13
22
|
projectId: string;
|
|
14
23
|
projectName: string;
|
|
15
24
|
startTime: string;
|
|
16
25
|
description?: string;
|
|
26
|
+
taskId?: string;
|
|
27
|
+
taskTitle?: string;
|
|
17
28
|
};
|
|
18
29
|
}
|
|
19
30
|
|
|
@@ -30,7 +41,7 @@ export function setApiKey(key: string): void {
|
|
|
30
41
|
}
|
|
31
42
|
|
|
32
43
|
export function getBaseUrl(): string {
|
|
33
|
-
return config.get("baseUrl") || process.env.MENO_BASE_URL || "
|
|
44
|
+
return config.get("baseUrl") || process.env.MENO_BASE_URL || "https://menohq.app";
|
|
34
45
|
}
|
|
35
46
|
|
|
36
47
|
export function setBaseUrl(url: string): void {
|
|
@@ -71,6 +82,26 @@ export function clearActiveTimer(): void {
|
|
|
71
82
|
config.delete("activeTimer");
|
|
72
83
|
}
|
|
73
84
|
|
|
85
|
+
export function getSelectedTask() {
|
|
86
|
+
return config.get("selectedTask");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function setSelectedTask(task: {
|
|
90
|
+
id: string;
|
|
91
|
+
title: string;
|
|
92
|
+
projectId: string;
|
|
93
|
+
projectName: string;
|
|
94
|
+
status: string;
|
|
95
|
+
estimatedHours?: number;
|
|
96
|
+
hourlyRate: number;
|
|
97
|
+
}): void {
|
|
98
|
+
config.set("selectedTask", task);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function clearSelectedTask(): void {
|
|
102
|
+
config.delete("selectedTask");
|
|
103
|
+
}
|
|
104
|
+
|
|
74
105
|
export function clearConfig(): void {
|
|
75
106
|
config.clear();
|
|
76
107
|
}
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { startTimer } from "./commands/start.js";
|
|
|
7
7
|
import { stopTimer } from "./commands/stop.js";
|
|
8
8
|
import { logTime } from "./commands/log.js";
|
|
9
9
|
import { showStatus } from "./commands/status.js";
|
|
10
|
+
import { printLogoAscii } from "./logo-ascii.js";
|
|
10
11
|
|
|
11
12
|
const program = new Command();
|
|
12
13
|
|
|
@@ -15,6 +16,9 @@ program
|
|
|
15
16
|
.description("CLI for Meno time tracking")
|
|
16
17
|
.version("0.1.0");
|
|
17
18
|
|
|
19
|
+
// Print ASCII logo at CLI startup (sharp used if installed, otherwise a fallback)
|
|
20
|
+
printLogoAscii().catch(() => {});
|
|
21
|
+
|
|
18
22
|
program
|
|
19
23
|
.command("login")
|
|
20
24
|
.description("Authenticate with your Meno API key")
|
|
@@ -22,12 +26,12 @@ program
|
|
|
22
26
|
|
|
23
27
|
program
|
|
24
28
|
.command("select")
|
|
25
|
-
.description("Select a
|
|
29
|
+
.description("Select a task to work on")
|
|
26
30
|
.action(selectProject);
|
|
27
31
|
|
|
28
32
|
program
|
|
29
|
-
.command("start [
|
|
30
|
-
.description("Start a timer on selected
|
|
33
|
+
.command("start [task-id]")
|
|
34
|
+
.description("Start a timer on selected task or specific task ID")
|
|
31
35
|
.action(startTimer);
|
|
32
36
|
|
|
33
37
|
program
|
|
@@ -37,7 +41,8 @@ program
|
|
|
37
41
|
.option("-d, --description <text>", "Entry description (skip prompt)")
|
|
38
42
|
.option("-y, --yes", "Auto-confirm (skip confirmation prompt)")
|
|
39
43
|
.option("--no-confirm", "Skip confirmation prompt")
|
|
40
|
-
.option("--
|
|
44
|
+
.option("--commit <hash>", "Git commit hash for evidence")
|
|
45
|
+
.option("--repo <url>", "Git repository URL for evidence")
|
|
41
46
|
.action(stopTimer);
|
|
42
47
|
|
|
43
48
|
program
|
|
@@ -49,7 +54,7 @@ program
|
|
|
49
54
|
|
|
50
55
|
program
|
|
51
56
|
.command("status")
|
|
52
|
-
.description("Show selected
|
|
57
|
+
.description("Show selected task, timer status, and unbilled stats")
|
|
53
58
|
.action(showStatus);
|
|
54
59
|
|
|
55
60
|
program.parse();
|