@usemeno/meno-cli 0.1.0 → 0.1.2
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 +339 -128
- package/package.json +11 -5
- 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 +54 -0
- package/src/utils/api.ts +79 -0
- package/src/utils/git.ts +62 -0
package/Asset 1.svg
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 55.44 45.4">
|
|
3
|
+
<g id="MenoLogo">
|
|
4
|
+
<path d="M43.78,28.72l-3.22-14.3-13.31,13.4c-2.15-1.65-3.92-3.4-5.43-5.75L42.15.78c.61-.64,3.06-1.02,3.67-.61s1.52,1.79,1.66,2.64l4.05,25.48-7.75.43Z"/>
|
|
5
|
+
<path d="M19.61,20.03l-6.25-5.12-2.48,13.76-8.11-.12L7.13,1.25c.22-1.13,4.18-1.52,5.05-.57l12.62,13.69-5.2,5.66Z"/>
|
|
6
|
+
<path d="M55.44,38.35c0,3.89-3.15,7.04-7.04,7.04-2.53,0-4.77-1.35-6.01-3.37H.36c-.4-1.61-.44-3.89-.23-6.03l41.85-.55c1.1-2.45,3.57-4.14,6.42-4.14,3.9,0,7.04,3.15,7.04,7.04Z"/>
|
|
7
|
+
</g>
|
|
8
|
+
</svg>
|
package/README.md
CHANGED
|
@@ -132,7 +132,7 @@ Run `meno login` to authenticate.
|
|
|
132
132
|
Run `meno select` to choose a project, or pass `--project <id>` to commands.
|
|
133
133
|
|
|
134
134
|
**Connection errors:**
|
|
135
|
-
Check your `baseUrl` in config. For production, use `https://app
|
|
135
|
+
Check your `baseUrl` in config. For production, use `https://menohq.app`.
|
|
136
136
|
|
|
137
137
|
## Development
|
|
138
138
|
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ function setApiKey(key) {
|
|
|
20
20
|
config.set("apiKey", key);
|
|
21
21
|
}
|
|
22
22
|
function getBaseUrl() {
|
|
23
|
-
return config.get("baseUrl") || process.env.MENO_BASE_URL || "
|
|
23
|
+
return config.get("baseUrl") || process.env.MENO_BASE_URL || "https://menohq.app";
|
|
24
24
|
}
|
|
25
25
|
function setBaseUrl(url) {
|
|
26
26
|
config.set("baseUrl", url);
|
|
@@ -28,12 +28,6 @@ function setBaseUrl(url) {
|
|
|
28
28
|
function getSelectedProject() {
|
|
29
29
|
return config.get("selectedProject");
|
|
30
30
|
}
|
|
31
|
-
function setSelectedProject(project) {
|
|
32
|
-
config.set("selectedProject", project);
|
|
33
|
-
}
|
|
34
|
-
function clearSelectedProject() {
|
|
35
|
-
config.delete("selectedProject");
|
|
36
|
-
}
|
|
37
31
|
function getActiveTimer() {
|
|
38
32
|
return config.get("activeTimer");
|
|
39
33
|
}
|
|
@@ -43,6 +37,15 @@ function setActiveTimer(timer) {
|
|
|
43
37
|
function clearActiveTimer() {
|
|
44
38
|
config.delete("activeTimer");
|
|
45
39
|
}
|
|
40
|
+
function getSelectedTask() {
|
|
41
|
+
return config.get("selectedTask");
|
|
42
|
+
}
|
|
43
|
+
function setSelectedTask(task) {
|
|
44
|
+
config.set("selectedTask", task);
|
|
45
|
+
}
|
|
46
|
+
function clearSelectedTask() {
|
|
47
|
+
config.delete("selectedTask");
|
|
48
|
+
}
|
|
46
49
|
|
|
47
50
|
// src/utils/api.ts
|
|
48
51
|
import fetch from "node-fetch";
|
|
@@ -109,6 +112,19 @@ async function apiRequest(endpoint, options = {}) {
|
|
|
109
112
|
);
|
|
110
113
|
}
|
|
111
114
|
}
|
|
115
|
+
async function getTasks() {
|
|
116
|
+
const response = await apiRequest("/api/kanban/tasks");
|
|
117
|
+
return response.tasks || [];
|
|
118
|
+
}
|
|
119
|
+
async function cliAction(action, payload) {
|
|
120
|
+
return await apiRequest("/api/cli", {
|
|
121
|
+
method: "POST",
|
|
122
|
+
body: JSON.stringify({
|
|
123
|
+
action,
|
|
124
|
+
...payload
|
|
125
|
+
})
|
|
126
|
+
});
|
|
127
|
+
}
|
|
112
128
|
|
|
113
129
|
// src/commands/login.ts
|
|
114
130
|
async function login() {
|
|
@@ -168,64 +184,90 @@ Unexpected error: ${error.message}
|
|
|
168
184
|
import enquirer2 from "enquirer";
|
|
169
185
|
import ora2 from "ora";
|
|
170
186
|
import chalk2 from "chalk";
|
|
187
|
+
function getStatusColor(status) {
|
|
188
|
+
switch (status) {
|
|
189
|
+
case "Backlog":
|
|
190
|
+
return chalk2.gray;
|
|
191
|
+
case "Todo":
|
|
192
|
+
return chalk2.yellow;
|
|
193
|
+
case "InProgress":
|
|
194
|
+
return chalk2.cyan;
|
|
195
|
+
case "Review":
|
|
196
|
+
return chalk2.magenta;
|
|
197
|
+
case "Done":
|
|
198
|
+
return chalk2.green;
|
|
199
|
+
default:
|
|
200
|
+
return chalk2.white;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function formatTaskDisplay(task, isSelected) {
|
|
204
|
+
const prefix = isSelected ? "\u25CF " : " ";
|
|
205
|
+
const statusColor = getStatusColor(task.status);
|
|
206
|
+
const estimateText = task.estimatedHours ? ` \u2022 ${task.estimatedHours}h` : "";
|
|
207
|
+
return `${prefix}${chalk2.bold(task.title)} ${chalk2.dim("\u2192")} ${task.project.name} ${statusColor(`[${task.status}]`)}${estimateText}`;
|
|
208
|
+
}
|
|
171
209
|
async function selectProject() {
|
|
172
|
-
console.log(chalk2.bold("\n\u{
|
|
173
|
-
const spinner = ora2("Loading
|
|
210
|
+
console.log(chalk2.bold("\n\u{1F4CB} Select Task\n"));
|
|
211
|
+
const spinner = ora2("Loading tasks...").start();
|
|
174
212
|
try {
|
|
175
|
-
const
|
|
213
|
+
const tasks = await getTasks();
|
|
176
214
|
spinner.stop();
|
|
177
|
-
if (
|
|
178
|
-
console.log(chalk2.yellow("No
|
|
179
|
-
console.log(chalk2.dim("Create
|
|
215
|
+
if (tasks.length === 0) {
|
|
216
|
+
console.log(chalk2.yellow("No tasks found."));
|
|
217
|
+
console.log(chalk2.dim("Create tasks in the Meno dashboard first.\n"));
|
|
180
218
|
return;
|
|
181
219
|
}
|
|
182
|
-
const
|
|
220
|
+
const currentTask = getSelectedTask();
|
|
183
221
|
const choices = [
|
|
184
222
|
{
|
|
185
223
|
name: "clear",
|
|
186
224
|
message: chalk2.dim("[Clear Selection]")
|
|
187
225
|
},
|
|
188
|
-
...
|
|
189
|
-
const isSelected =
|
|
190
|
-
const prefix = isSelected ? "\u25CF " : " ";
|
|
191
|
-
const displayName = `${project.name} (${project.clientName}) \u2022 $${project.hourlyRate}/hr`;
|
|
226
|
+
...tasks.map((task) => {
|
|
227
|
+
const isSelected = currentTask?.id === task.id;
|
|
192
228
|
return {
|
|
193
|
-
name:
|
|
194
|
-
message:
|
|
195
|
-
value:
|
|
229
|
+
name: task.id,
|
|
230
|
+
message: formatTaskDisplay(task, isSelected),
|
|
231
|
+
value: task
|
|
196
232
|
};
|
|
197
233
|
})
|
|
198
234
|
];
|
|
199
235
|
const result = await enquirer2.prompt({
|
|
200
236
|
type: "select",
|
|
201
|
-
name: "
|
|
202
|
-
message: "Select a
|
|
237
|
+
name: "task",
|
|
238
|
+
message: "Select a task:",
|
|
203
239
|
choices
|
|
204
240
|
});
|
|
205
|
-
const answer = result.
|
|
241
|
+
const answer = result.task;
|
|
206
242
|
if (answer === "clear") {
|
|
207
|
-
|
|
243
|
+
clearSelectedTask();
|
|
208
244
|
console.log(chalk2.green("\n\u2713 Selection cleared\n"));
|
|
209
245
|
} else {
|
|
210
|
-
const
|
|
211
|
-
if (!
|
|
212
|
-
console.log(chalk2.red("\n\u2717
|
|
246
|
+
const task = tasks.find((t) => t.id === answer);
|
|
247
|
+
if (!task) {
|
|
248
|
+
console.log(chalk2.red("\n\u2717 Task not found\n"));
|
|
213
249
|
return;
|
|
214
250
|
}
|
|
215
|
-
|
|
216
|
-
id:
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
251
|
+
setSelectedTask({
|
|
252
|
+
id: task.id,
|
|
253
|
+
title: task.title,
|
|
254
|
+
projectId: task.projectId,
|
|
255
|
+
projectName: task.project.name,
|
|
256
|
+
status: task.status,
|
|
257
|
+
estimatedHours: task.estimatedHours,
|
|
258
|
+
hourlyRate: task.project.hourlyRate
|
|
220
259
|
});
|
|
260
|
+
const statusColor = getStatusColor(task.status);
|
|
221
261
|
console.log(
|
|
222
262
|
chalk2.green(`
|
|
223
|
-
\u2713 Selected: ${chalk2.bold(
|
|
224
|
-
`)
|
|
263
|
+
\u2713 Selected: ${chalk2.bold(task.title)}
|
|
264
|
+
`) + chalk2.dim(` Project: ${task.project.name} \u2022 $${task.project.hourlyRate}/hr
|
|
265
|
+
`) + chalk2.dim(` Status: `) + statusColor(task.status) + (task.estimatedHours ? chalk2.dim(` \u2022 Estimated: ${task.estimatedHours}h
|
|
266
|
+
`) : "\n")
|
|
225
267
|
);
|
|
226
268
|
}
|
|
227
269
|
} catch (error) {
|
|
228
|
-
spinner.fail(chalk2.red("Failed to load
|
|
270
|
+
spinner.fail(chalk2.red("Failed to load tasks"));
|
|
229
271
|
if (error instanceof ApiError) {
|
|
230
272
|
console.log(chalk2.red(`
|
|
231
273
|
Error: ${error.message}
|
|
@@ -276,35 +318,36 @@ function parseDuration(input) {
|
|
|
276
318
|
}
|
|
277
319
|
|
|
278
320
|
// src/commands/start.ts
|
|
279
|
-
async function startTimer(
|
|
321
|
+
async function startTimer(taskId) {
|
|
280
322
|
const existingTimer = getActiveTimer();
|
|
281
323
|
if (existingTimer) {
|
|
282
324
|
const elapsed = formatDuration(existingTimer.startTime);
|
|
325
|
+
const taskInfo = existingTimer.taskTitle || existingTimer.projectName;
|
|
283
326
|
console.log(
|
|
284
327
|
chalk3.yellow(
|
|
285
328
|
`
|
|
286
|
-
\u26A0 Timer already running for ${chalk3.bold(
|
|
329
|
+
\u26A0 Timer already running for ${chalk3.bold(taskInfo)} (${elapsed})`
|
|
287
330
|
)
|
|
288
331
|
);
|
|
289
332
|
console.log(chalk3.dim(`Stop it first with ${chalk3.bold("meno stop")} or discard with ${chalk3.bold("meno stop --discard")}
|
|
290
333
|
`));
|
|
291
334
|
return;
|
|
292
335
|
}
|
|
293
|
-
let
|
|
294
|
-
if (
|
|
295
|
-
const
|
|
336
|
+
let task;
|
|
337
|
+
if (taskId) {
|
|
338
|
+
const spinner2 = ora3("Loading task...").start();
|
|
296
339
|
try {
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
if (!
|
|
340
|
+
const tasks = await getTasks();
|
|
341
|
+
task = tasks.find((t) => t.id === taskId);
|
|
342
|
+
spinner2.stop();
|
|
343
|
+
if (!task) {
|
|
301
344
|
console.log(chalk3.red(`
|
|
302
|
-
\u2717
|
|
345
|
+
\u2717 Task not found: ${taskId}
|
|
303
346
|
`));
|
|
304
347
|
return;
|
|
305
348
|
}
|
|
306
349
|
} catch (error) {
|
|
307
|
-
|
|
350
|
+
spinner2.fail(chalk3.red("Failed to load task"));
|
|
308
351
|
if (error instanceof ApiError) {
|
|
309
352
|
console.log(chalk3.red(`
|
|
310
353
|
Error: ${error.message}
|
|
@@ -313,45 +356,122 @@ Error: ${error.message}
|
|
|
313
356
|
return;
|
|
314
357
|
}
|
|
315
358
|
} else {
|
|
316
|
-
const selected =
|
|
359
|
+
const selected = getSelectedTask();
|
|
317
360
|
if (!selected) {
|
|
318
361
|
console.log(chalk3.yellow(`
|
|
319
|
-
\u26A0 No
|
|
320
|
-
console.log(chalk3.dim(`Run ${chalk3.bold("meno select")} first or provide a
|
|
362
|
+
\u26A0 No task selected.`));
|
|
363
|
+
console.log(chalk3.dim(`Run ${chalk3.bold("meno select")} first or provide a task ID: ${chalk3.bold("meno start <task-id>")}
|
|
321
364
|
`));
|
|
322
365
|
return;
|
|
323
366
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
367
|
+
const spinner2 = ora3("Starting timer...").start();
|
|
368
|
+
try {
|
|
369
|
+
const tasks = await getTasks();
|
|
370
|
+
task = tasks.find((t) => t.id === selected.id);
|
|
371
|
+
spinner2.stop();
|
|
372
|
+
if (!task) {
|
|
373
|
+
console.log(chalk3.red(`
|
|
374
|
+
\u2717 Selected task not found. It may have been deleted.
|
|
375
|
+
`));
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
} catch (error) {
|
|
379
|
+
spinner2.fail(chalk3.red("Failed to load task"));
|
|
380
|
+
if (error instanceof ApiError) {
|
|
381
|
+
console.log(chalk3.red(`
|
|
382
|
+
Error: ${error.message}
|
|
383
|
+
`));
|
|
384
|
+
}
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const spinner = ora3("Starting timer...").start();
|
|
389
|
+
try {
|
|
390
|
+
const response = await cliAction("start", { taskId: task.id });
|
|
391
|
+
spinner.succeed(chalk3.green("Timer started"));
|
|
392
|
+
const startTime = response.timer?.startTime || (/* @__PURE__ */ new Date()).toISOString();
|
|
393
|
+
setActiveTimer({
|
|
394
|
+
projectId: task.projectId,
|
|
395
|
+
projectName: task.project.name,
|
|
396
|
+
startTime,
|
|
397
|
+
taskId: task.id,
|
|
398
|
+
taskTitle: task.title
|
|
399
|
+
});
|
|
400
|
+
const timestamp = new Date(startTime).toLocaleTimeString();
|
|
401
|
+
console.log(
|
|
402
|
+
chalk3.green(
|
|
403
|
+
`
|
|
404
|
+
\u23F1\uFE0F ${chalk3.bold(task.title)}
|
|
405
|
+
` + chalk3.dim(` ${task.project.name} \u2022 $${task.project.hourlyRate}/hr \u2022 [${timestamp}]`)
|
|
406
|
+
)
|
|
407
|
+
);
|
|
408
|
+
if (task.estimatedHours) {
|
|
409
|
+
console.log(chalk3.dim(` Estimated: ${task.estimatedHours}h
|
|
410
|
+
`));
|
|
411
|
+
} else {
|
|
412
|
+
console.log();
|
|
413
|
+
}
|
|
414
|
+
} catch (error) {
|
|
415
|
+
spinner.fail(chalk3.red("Failed to start timer"));
|
|
416
|
+
if (error instanceof ApiError) {
|
|
417
|
+
console.log(chalk3.red(`
|
|
418
|
+
Error: ${error.message}
|
|
419
|
+
`));
|
|
420
|
+
} else {
|
|
421
|
+
console.log(chalk3.red(`
|
|
422
|
+
Unexpected error: ${error.message}
|
|
423
|
+
`));
|
|
424
|
+
}
|
|
334
425
|
}
|
|
335
|
-
const startTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
336
|
-
setActiveTimer({
|
|
337
|
-
projectId: project.id,
|
|
338
|
-
projectName: project.name,
|
|
339
|
-
startTime
|
|
340
|
-
});
|
|
341
|
-
const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
342
|
-
console.log(
|
|
343
|
-
chalk3.green(
|
|
344
|
-
`
|
|
345
|
-
\u23F1\uFE0F Started: ${chalk3.bold(project.name)} \u2022 $${project.hourlyRate}/hr \u2022 [${timestamp}]
|
|
346
|
-
`
|
|
347
|
-
)
|
|
348
|
-
);
|
|
349
426
|
}
|
|
350
427
|
|
|
351
428
|
// src/commands/stop.ts
|
|
352
429
|
import enquirer3 from "enquirer";
|
|
353
430
|
import chalk4 from "chalk";
|
|
354
431
|
import ora4 from "ora";
|
|
432
|
+
|
|
433
|
+
// src/utils/git.ts
|
|
434
|
+
import { execSync } from "child_process";
|
|
435
|
+
function getGitInfo() {
|
|
436
|
+
try {
|
|
437
|
+
const commitHash = execSync("git rev-parse HEAD", {
|
|
438
|
+
encoding: "utf8",
|
|
439
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
440
|
+
// Suppress stderr
|
|
441
|
+
}).trim();
|
|
442
|
+
let repoUrl = execSync("git config --get remote.origin.url", {
|
|
443
|
+
encoding: "utf8",
|
|
444
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
445
|
+
}).trim();
|
|
446
|
+
if (repoUrl.startsWith("git@github.com:")) {
|
|
447
|
+
repoUrl = repoUrl.replace("git@github.com:", "https://github.com/").replace(/\.git$/, "");
|
|
448
|
+
} else if (repoUrl.endsWith(".git")) {
|
|
449
|
+
repoUrl = repoUrl.replace(/\.git$/, "");
|
|
450
|
+
}
|
|
451
|
+
if (!commitHash || !repoUrl) {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
return {
|
|
455
|
+
commitHash,
|
|
456
|
+
repoUrl
|
|
457
|
+
};
|
|
458
|
+
} catch (error) {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function isGitRepository() {
|
|
463
|
+
try {
|
|
464
|
+
execSync("git rev-parse --is-inside-work-tree", {
|
|
465
|
+
encoding: "utf8",
|
|
466
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
467
|
+
});
|
|
468
|
+
return true;
|
|
469
|
+
} catch (error) {
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/commands/stop.ts
|
|
355
475
|
async function stopTimer(options) {
|
|
356
476
|
const timer = getActiveTimer();
|
|
357
477
|
if (!timer) {
|
|
@@ -368,41 +488,20 @@ async function stopTimer(options) {
|
|
|
368
488
|
`));
|
|
369
489
|
return;
|
|
370
490
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
if (options.adjust) {
|
|
374
|
-
duration = parseDuration(options.adjust);
|
|
375
|
-
if (duration === 0) {
|
|
376
|
-
console.log(chalk4.red(`
|
|
377
|
-
\u2717 Invalid duration format: ${options.adjust}
|
|
378
|
-
`));
|
|
379
|
-
console.log(chalk4.dim(`Use format like: 1.5h, 90m, 45
|
|
380
|
-
`));
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
const hours = Math.floor(duration);
|
|
384
|
-
const minutes = Math.round((duration - hours) * 60);
|
|
385
|
-
formatted = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
|
|
386
|
-
console.log(chalk4.yellow(`
|
|
387
|
-
\u26A0 Adjusted duration from timer
|
|
388
|
-
`));
|
|
389
|
-
} else {
|
|
390
|
-
duration = calculateElapsedHours(timer.startTime);
|
|
391
|
-
formatted = formatDuration(timer.startTime);
|
|
392
|
-
}
|
|
393
|
-
const project = getSelectedProject();
|
|
394
|
-
const hourlyRate = project?.hourlyRate || 0;
|
|
395
|
-
const amount = duration * hourlyRate;
|
|
491
|
+
const taskInfo = timer.taskTitle || timer.projectName;
|
|
492
|
+
const elapsed = formatDuration(timer.startTime);
|
|
396
493
|
console.log(chalk4.bold(`
|
|
397
|
-
\u23F1\uFE0F
|
|
494
|
+
\u23F1\uFE0F Stopping: ${taskInfo}`));
|
|
495
|
+
console.log(chalk4.dim(`Duration: ${elapsed}
|
|
398
496
|
`));
|
|
399
497
|
let description;
|
|
400
498
|
let shouldLog = true;
|
|
499
|
+
let commitHash;
|
|
500
|
+
let repoUrl;
|
|
401
501
|
try {
|
|
402
502
|
if (options.description) {
|
|
403
503
|
description = options.description;
|
|
404
|
-
console.log(chalk4.dim(`Description: ${description}
|
|
405
|
-
`));
|
|
504
|
+
console.log(chalk4.dim(`Description: ${description}`));
|
|
406
505
|
} else {
|
|
407
506
|
const descResult = await enquirer3.prompt({
|
|
408
507
|
type: "input",
|
|
@@ -418,6 +517,34 @@ async function stopTimer(options) {
|
|
|
418
517
|
});
|
|
419
518
|
description = descResult.description;
|
|
420
519
|
}
|
|
520
|
+
if (options.commit && options.repo) {
|
|
521
|
+
commitHash = options.commit;
|
|
522
|
+
repoUrl = options.repo;
|
|
523
|
+
console.log(chalk4.dim(`
|
|
524
|
+
\u{1F517} Evidence: ${commitHash.substring(0, 7)} @ ${repoUrl}`));
|
|
525
|
+
} else if (isGitRepository()) {
|
|
526
|
+
const gitInfo = getGitInfo();
|
|
527
|
+
if (gitInfo) {
|
|
528
|
+
const skipConfirm2 = options.noConfirm || options.yes;
|
|
529
|
+
if (skipConfirm2) {
|
|
530
|
+
commitHash = gitInfo.commitHash;
|
|
531
|
+
repoUrl = gitInfo.repoUrl;
|
|
532
|
+
console.log(chalk4.dim(`
|
|
533
|
+
\u{1F517} Evidence: ${commitHash.substring(0, 7)} @ ${repoUrl}`));
|
|
534
|
+
} else {
|
|
535
|
+
const gitResult = await enquirer3.prompt({
|
|
536
|
+
type: "confirm",
|
|
537
|
+
name: "includeGit",
|
|
538
|
+
message: `Include latest commit (${gitInfo.commitHash.substring(0, 7)}) as evidence?`,
|
|
539
|
+
initial: true
|
|
540
|
+
});
|
|
541
|
+
if (gitResult.includeGit) {
|
|
542
|
+
commitHash = gitInfo.commitHash;
|
|
543
|
+
repoUrl = gitInfo.repoUrl;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
421
548
|
const skipConfirm = options.noConfirm || options.yes;
|
|
422
549
|
if (!skipConfirm) {
|
|
423
550
|
const confirmResult = await enquirer3.prompt({
|
|
@@ -429,39 +556,44 @@ async function stopTimer(options) {
|
|
|
429
556
|
shouldLog = confirmResult.shouldLog;
|
|
430
557
|
if (!shouldLog) {
|
|
431
558
|
console.log(chalk4.yellow("\n\u2717 Entry not logged\n"));
|
|
559
|
+
clearActiveTimer();
|
|
432
560
|
return;
|
|
433
561
|
}
|
|
434
562
|
}
|
|
435
563
|
const spinner = ora4("Logging entry...").start();
|
|
436
564
|
try {
|
|
437
|
-
const response = await
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
description,
|
|
442
|
-
duration: Math.round(duration * 100) / 100,
|
|
443
|
-
// Round to 2 decimals
|
|
444
|
-
date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
|
|
445
|
-
// Today
|
|
446
|
-
billable: true
|
|
447
|
-
})
|
|
565
|
+
const response = await cliAction("stop", {
|
|
566
|
+
description,
|
|
567
|
+
commitHash,
|
|
568
|
+
repoUrl
|
|
448
569
|
});
|
|
449
570
|
clearActiveTimer();
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
`));
|
|
571
|
+
if (response.entry) {
|
|
572
|
+
const hours = response.entry.duration.toFixed(2);
|
|
573
|
+
const amount = response.entry.amount.toFixed(2);
|
|
574
|
+
spinner.succeed(chalk4.green(`\u2713 Logged ${hours} hours ($${amount})`));
|
|
575
|
+
if (response.evidence) {
|
|
576
|
+
console.log(chalk4.dim(`\u{1F4CE} Evidence: ${response.evidence.commitHash.substring(0, 7)}`));
|
|
577
|
+
}
|
|
578
|
+
console.log();
|
|
579
|
+
} else {
|
|
580
|
+
spinner.succeed(chalk4.green("\u2713 Timer stopped"));
|
|
581
|
+
console.log();
|
|
582
|
+
}
|
|
454
583
|
} catch (error) {
|
|
455
|
-
spinner.fail(chalk4.red("Failed to
|
|
584
|
+
spinner.fail(chalk4.red("Failed to stop timer"));
|
|
456
585
|
if (error instanceof ApiError) {
|
|
457
586
|
console.log(chalk4.red(`
|
|
458
587
|
Error: ${error.message}
|
|
588
|
+
`));
|
|
589
|
+
console.log(chalk4.yellow(`Timer cleared locally. You may need to check the dashboard.
|
|
459
590
|
`));
|
|
460
591
|
} else {
|
|
461
592
|
console.log(chalk4.red(`
|
|
462
593
|
Unexpected error: ${error.message}
|
|
463
594
|
`));
|
|
464
595
|
}
|
|
596
|
+
clearActiveTimer();
|
|
465
597
|
}
|
|
466
598
|
} catch (error) {
|
|
467
599
|
console.log(chalk4.yellow("\n\u2717 Cancelled\n"));
|
|
@@ -564,27 +696,54 @@ Unexpected error: ${error.message}
|
|
|
564
696
|
// src/commands/status.ts
|
|
565
697
|
import chalk6 from "chalk";
|
|
566
698
|
import ora6 from "ora";
|
|
699
|
+
function getStatusColor2(status) {
|
|
700
|
+
switch (status) {
|
|
701
|
+
case "Backlog":
|
|
702
|
+
return chalk6.gray;
|
|
703
|
+
case "Todo":
|
|
704
|
+
return chalk6.yellow;
|
|
705
|
+
case "InProgress":
|
|
706
|
+
return chalk6.cyan;
|
|
707
|
+
case "Review":
|
|
708
|
+
return chalk6.magenta;
|
|
709
|
+
case "Done":
|
|
710
|
+
return chalk6.green;
|
|
711
|
+
default:
|
|
712
|
+
return chalk6.white;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
567
715
|
async function showStatus() {
|
|
568
716
|
console.log(chalk6.bold("\n\u{1F4CA} Meno Status\n"));
|
|
569
|
-
const
|
|
717
|
+
const selectedTask = getSelectedTask();
|
|
570
718
|
const timer = getActiveTimer();
|
|
571
|
-
if (
|
|
719
|
+
if (selectedTask) {
|
|
720
|
+
const statusColor = getStatusColor2(selectedTask.status);
|
|
572
721
|
console.log(
|
|
573
722
|
chalk6.cyan(
|
|
574
|
-
`\u{1F4CC} Selected: ${chalk6.bold(
|
|
723
|
+
`\u{1F4CC} Selected: ${chalk6.bold(selectedTask.title)}`
|
|
575
724
|
)
|
|
576
725
|
);
|
|
726
|
+
console.log(
|
|
727
|
+
chalk6.dim(
|
|
728
|
+
` ${selectedTask.projectName} \u2022 $${selectedTask.hourlyRate}/hr \u2022 `
|
|
729
|
+
) + statusColor(selectedTask.status)
|
|
730
|
+
);
|
|
731
|
+
if (selectedTask.estimatedHours) {
|
|
732
|
+
console.log(chalk6.dim(` Estimated: ${selectedTask.estimatedHours}h`));
|
|
733
|
+
}
|
|
577
734
|
}
|
|
578
735
|
if (timer) {
|
|
579
736
|
const elapsed = formatDuration(timer.startTime);
|
|
580
|
-
const
|
|
737
|
+
const taskInfo = timer.taskTitle || timer.projectName;
|
|
738
|
+
const rate = selectedTask?.hourlyRate || 0;
|
|
739
|
+
const value = calculateTimerValue(timer.startTime, rate);
|
|
581
740
|
console.log(
|
|
582
741
|
chalk6.green(
|
|
583
|
-
`\u23F1\uFE0F Running: ${chalk6.bold(
|
|
742
|
+
`\u23F1\uFE0F Running: ${chalk6.bold(taskInfo)} \u2022 ${elapsed} \u2022 $${value.toFixed(2)}`
|
|
584
743
|
)
|
|
585
744
|
);
|
|
586
745
|
}
|
|
587
|
-
if (
|
|
746
|
+
if (selectedTask || timer) {
|
|
588
747
|
console.log();
|
|
589
748
|
}
|
|
590
749
|
const spinner = ora6("Loading stats...").start();
|
|
@@ -614,13 +773,65 @@ Unexpected error: ${error.message}
|
|
|
614
773
|
}
|
|
615
774
|
}
|
|
616
775
|
|
|
776
|
+
// src/logo-ascii.ts
|
|
777
|
+
var SVG_EMBED = `<?xml version="1.0" encoding="UTF-8"?>
|
|
778
|
+
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 55.44 45.4">
|
|
779
|
+
<g id="MenoLogo">
|
|
780
|
+
<path d="M43.78,28.72l-3.22-14.3-13.31,13.4c-2.15-1.65-3.92-3.4-5.43-5.75L42.15.78c.61-.64,3.06-1.02,3.67-.61s1.52,1.79,1.66,2.64l4.05,25.48-7.75.43Z"/>
|
|
781
|
+
<path d="M19.61,20.03l-6.25-5.12-2.48,13.76-8.11-.12L7.13,1.25c.22-1.13,4.18-1.52,5.05-.57l12.62,13.69-5.2,5.66Z"/>
|
|
782
|
+
<path d="M55.44,38.35c0,3.89-3.15,7.04-7.04,7.04-2.53,0-4.77-1.35-6.01-3.37H.36c-.4-1.61-.44-3.89-.23-6.03l41.85-.55c1.1-2.45,3.57-4.14,6.42-4.14,3.9,0,7.04,3.15,7.04,7.04Z"/>
|
|
783
|
+
</g>
|
|
784
|
+
</svg>`;
|
|
785
|
+
function fallbackAscii() {
|
|
786
|
+
return [
|
|
787
|
+
" ____ __ ",
|
|
788
|
+
" /\\ / __ \\/ /___ _ _ ",
|
|
789
|
+
" / \\ _ __ / / / / / __ \\ | | |",
|
|
790
|
+
" / /\\ \\ | '_ \\ / /_/ / / /_/ / |_| |",
|
|
791
|
+
" / ____ \\| | | |\\____/_/\\____/\\__,_|",
|
|
792
|
+
" /_/ \\_\\_| |_| ",
|
|
793
|
+
"",
|
|
794
|
+
" M E N O (logo) "
|
|
795
|
+
].join("\n");
|
|
796
|
+
}
|
|
797
|
+
async function printLogoAscii(width = 64) {
|
|
798
|
+
try {
|
|
799
|
+
const sharpImport = await import("sharp");
|
|
800
|
+
const sharp = sharpImport.default ?? sharpImport;
|
|
801
|
+
const svgBuffer = Buffer.from(SVG_EMBED, "utf8");
|
|
802
|
+
const img = sharp(svgBuffer).resize({ width }).flatten({ background: { r: 255, g: 255, b: 255 } }).grayscale();
|
|
803
|
+
const { data, info } = await img.raw().toBuffer({ resolveWithObject: true });
|
|
804
|
+
const chars = " .,:;i1tfLCG08@";
|
|
805
|
+
const rows = [];
|
|
806
|
+
const aspect = 0.5;
|
|
807
|
+
const rowStep = Math.max(1, Math.round(1 / aspect));
|
|
808
|
+
for (let y = 0; y < info.height; y += rowStep) {
|
|
809
|
+
let line = "";
|
|
810
|
+
for (let x = 0; x < info.width; x++) {
|
|
811
|
+
const idx = y * info.width + x;
|
|
812
|
+
const val = data[idx];
|
|
813
|
+
const charIdx = Math.floor((1 - val / 255) * (chars.length - 1));
|
|
814
|
+
line += chars[charIdx];
|
|
815
|
+
}
|
|
816
|
+
rows.push(line);
|
|
817
|
+
}
|
|
818
|
+
console.log(rows.join("\n"));
|
|
819
|
+
return;
|
|
820
|
+
} catch (err) {
|
|
821
|
+
console.log(fallbackAscii());
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
617
826
|
// src/index.ts
|
|
618
827
|
var program = new Command();
|
|
619
828
|
program.name("meno").description("CLI for Meno time tracking").version("0.1.0");
|
|
829
|
+
printLogoAscii().catch(() => {
|
|
830
|
+
});
|
|
620
831
|
program.command("login").description("Authenticate with your Meno API key").action(login);
|
|
621
|
-
program.command("select").description("Select a
|
|
622
|
-
program.command("start [
|
|
623
|
-
program.command("stop").description("Stop the running timer and log the entry").option("--discard", "Discard the timer without logging").option("-d, --description <text>", "Entry description (skip prompt)").option("-y, --yes", "Auto-confirm (skip confirmation prompt)").option("--no-confirm", "Skip confirmation prompt").option("--
|
|
832
|
+
program.command("select").description("Select a task to work on").action(selectProject);
|
|
833
|
+
program.command("start [task-id]").description("Start a timer on selected task or specific task ID").action(startTimer);
|
|
834
|
+
program.command("stop").description("Stop the running timer and log the entry").option("--discard", "Discard the timer without logging").option("-d, --description <text>", "Entry description (skip prompt)").option("-y, --yes", "Auto-confirm (skip confirmation prompt)").option("--no-confirm", "Skip confirmation prompt").option("--commit <hash>", "Git commit hash for evidence").option("--repo <url>", "Git repository URL for evidence").action(stopTimer);
|
|
624
835
|
program.command("log <description>").description("Log time manually").option("-d, --duration <duration>", "Duration (e.g., 45m, 1.5h, 90)").option("-p, --project <id>", "Project ID").action(logTime);
|
|
625
|
-
program.command("status").description("Show selected
|
|
836
|
+
program.command("status").description("Show selected task, timer status, and unbilled stats").action(showStatus);
|
|
626
837
|
program.parse();
|