@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/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
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
+
}) : x)(function(x) {
|
|
5
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
2
8
|
|
|
3
9
|
// src/index.ts
|
|
4
10
|
import { Command } from "commander";
|
|
@@ -20,7 +26,7 @@ function setApiKey(key) {
|
|
|
20
26
|
config.set("apiKey", key);
|
|
21
27
|
}
|
|
22
28
|
function getBaseUrl() {
|
|
23
|
-
return config.get("baseUrl") || process.env.MENO_BASE_URL || "
|
|
29
|
+
return config.get("baseUrl") || process.env.MENO_BASE_URL || "https://menohq.app";
|
|
24
30
|
}
|
|
25
31
|
function setBaseUrl(url) {
|
|
26
32
|
config.set("baseUrl", url);
|
|
@@ -28,12 +34,6 @@ function setBaseUrl(url) {
|
|
|
28
34
|
function getSelectedProject() {
|
|
29
35
|
return config.get("selectedProject");
|
|
30
36
|
}
|
|
31
|
-
function setSelectedProject(project) {
|
|
32
|
-
config.set("selectedProject", project);
|
|
33
|
-
}
|
|
34
|
-
function clearSelectedProject() {
|
|
35
|
-
config.delete("selectedProject");
|
|
36
|
-
}
|
|
37
37
|
function getActiveTimer() {
|
|
38
38
|
return config.get("activeTimer");
|
|
39
39
|
}
|
|
@@ -43,6 +43,15 @@ function setActiveTimer(timer) {
|
|
|
43
43
|
function clearActiveTimer() {
|
|
44
44
|
config.delete("activeTimer");
|
|
45
45
|
}
|
|
46
|
+
function getSelectedTask() {
|
|
47
|
+
return config.get("selectedTask");
|
|
48
|
+
}
|
|
49
|
+
function setSelectedTask(task) {
|
|
50
|
+
config.set("selectedTask", task);
|
|
51
|
+
}
|
|
52
|
+
function clearSelectedTask() {
|
|
53
|
+
config.delete("selectedTask");
|
|
54
|
+
}
|
|
46
55
|
|
|
47
56
|
// src/utils/api.ts
|
|
48
57
|
import fetch from "node-fetch";
|
|
@@ -109,6 +118,19 @@ async function apiRequest(endpoint, options = {}) {
|
|
|
109
118
|
);
|
|
110
119
|
}
|
|
111
120
|
}
|
|
121
|
+
async function getTasks() {
|
|
122
|
+
const response = await apiRequest("/api/kanban/tasks");
|
|
123
|
+
return response.tasks || [];
|
|
124
|
+
}
|
|
125
|
+
async function cliAction(action, payload) {
|
|
126
|
+
return await apiRequest("/api/cli", {
|
|
127
|
+
method: "POST",
|
|
128
|
+
body: JSON.stringify({
|
|
129
|
+
action,
|
|
130
|
+
...payload
|
|
131
|
+
})
|
|
132
|
+
});
|
|
133
|
+
}
|
|
112
134
|
|
|
113
135
|
// src/commands/login.ts
|
|
114
136
|
async function login() {
|
|
@@ -168,64 +190,90 @@ Unexpected error: ${error.message}
|
|
|
168
190
|
import enquirer2 from "enquirer";
|
|
169
191
|
import ora2 from "ora";
|
|
170
192
|
import chalk2 from "chalk";
|
|
193
|
+
function getStatusColor(status) {
|
|
194
|
+
switch (status) {
|
|
195
|
+
case "Backlog":
|
|
196
|
+
return chalk2.gray;
|
|
197
|
+
case "Todo":
|
|
198
|
+
return chalk2.yellow;
|
|
199
|
+
case "InProgress":
|
|
200
|
+
return chalk2.cyan;
|
|
201
|
+
case "Review":
|
|
202
|
+
return chalk2.magenta;
|
|
203
|
+
case "Done":
|
|
204
|
+
return chalk2.green;
|
|
205
|
+
default:
|
|
206
|
+
return chalk2.white;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function formatTaskDisplay(task, isSelected) {
|
|
210
|
+
const prefix = isSelected ? "\u25CF " : " ";
|
|
211
|
+
const statusColor = getStatusColor(task.status);
|
|
212
|
+
const estimateText = task.estimatedHours ? ` \u2022 ${task.estimatedHours}h` : "";
|
|
213
|
+
return `${prefix}${chalk2.bold(task.title)} ${chalk2.dim("\u2192")} ${task.project.name} ${statusColor(`[${task.status}]`)}${estimateText}`;
|
|
214
|
+
}
|
|
171
215
|
async function selectProject() {
|
|
172
|
-
console.log(chalk2.bold("\n\u{
|
|
173
|
-
const spinner = ora2("Loading
|
|
216
|
+
console.log(chalk2.bold("\n\u{1F4CB} Select Task\n"));
|
|
217
|
+
const spinner = ora2("Loading tasks...").start();
|
|
174
218
|
try {
|
|
175
|
-
const
|
|
219
|
+
const tasks = await getTasks();
|
|
176
220
|
spinner.stop();
|
|
177
|
-
if (
|
|
178
|
-
console.log(chalk2.yellow("No
|
|
179
|
-
console.log(chalk2.dim("Create
|
|
221
|
+
if (tasks.length === 0) {
|
|
222
|
+
console.log(chalk2.yellow("No tasks found."));
|
|
223
|
+
console.log(chalk2.dim("Create tasks in the Meno dashboard first.\n"));
|
|
180
224
|
return;
|
|
181
225
|
}
|
|
182
|
-
const
|
|
226
|
+
const currentTask = getSelectedTask();
|
|
183
227
|
const choices = [
|
|
184
228
|
{
|
|
185
229
|
name: "clear",
|
|
186
230
|
message: chalk2.dim("[Clear Selection]")
|
|
187
231
|
},
|
|
188
|
-
...
|
|
189
|
-
const isSelected =
|
|
190
|
-
const prefix = isSelected ? "\u25CF " : " ";
|
|
191
|
-
const displayName = `${project.name} (${project.clientName}) \u2022 $${project.hourlyRate}/hr`;
|
|
232
|
+
...tasks.map((task) => {
|
|
233
|
+
const isSelected = currentTask?.id === task.id;
|
|
192
234
|
return {
|
|
193
|
-
name:
|
|
194
|
-
message:
|
|
195
|
-
value:
|
|
235
|
+
name: task.id,
|
|
236
|
+
message: formatTaskDisplay(task, isSelected),
|
|
237
|
+
value: task
|
|
196
238
|
};
|
|
197
239
|
})
|
|
198
240
|
];
|
|
199
241
|
const result = await enquirer2.prompt({
|
|
200
242
|
type: "select",
|
|
201
|
-
name: "
|
|
202
|
-
message: "Select a
|
|
243
|
+
name: "task",
|
|
244
|
+
message: "Select a task:",
|
|
203
245
|
choices
|
|
204
246
|
});
|
|
205
|
-
const answer = result.
|
|
247
|
+
const answer = result.task;
|
|
206
248
|
if (answer === "clear") {
|
|
207
|
-
|
|
249
|
+
clearSelectedTask();
|
|
208
250
|
console.log(chalk2.green("\n\u2713 Selection cleared\n"));
|
|
209
251
|
} else {
|
|
210
|
-
const
|
|
211
|
-
if (!
|
|
212
|
-
console.log(chalk2.red("\n\u2717
|
|
252
|
+
const task = tasks.find((t) => t.id === answer);
|
|
253
|
+
if (!task) {
|
|
254
|
+
console.log(chalk2.red("\n\u2717 Task not found\n"));
|
|
213
255
|
return;
|
|
214
256
|
}
|
|
215
|
-
|
|
216
|
-
id:
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
257
|
+
setSelectedTask({
|
|
258
|
+
id: task.id,
|
|
259
|
+
title: task.title,
|
|
260
|
+
projectId: task.projectId,
|
|
261
|
+
projectName: task.project.name,
|
|
262
|
+
status: task.status,
|
|
263
|
+
estimatedHours: task.estimatedHours,
|
|
264
|
+
hourlyRate: task.project.hourlyRate
|
|
220
265
|
});
|
|
266
|
+
const statusColor = getStatusColor(task.status);
|
|
221
267
|
console.log(
|
|
222
268
|
chalk2.green(`
|
|
223
|
-
\u2713 Selected: ${chalk2.bold(
|
|
224
|
-
`)
|
|
269
|
+
\u2713 Selected: ${chalk2.bold(task.title)}
|
|
270
|
+
`) + chalk2.dim(` Project: ${task.project.name} \u2022 $${task.project.hourlyRate}/hr
|
|
271
|
+
`) + chalk2.dim(` Status: `) + statusColor(task.status) + (task.estimatedHours ? chalk2.dim(` \u2022 Estimated: ${task.estimatedHours}h
|
|
272
|
+
`) : "\n")
|
|
225
273
|
);
|
|
226
274
|
}
|
|
227
275
|
} catch (error) {
|
|
228
|
-
spinner.fail(chalk2.red("Failed to load
|
|
276
|
+
spinner.fail(chalk2.red("Failed to load tasks"));
|
|
229
277
|
if (error instanceof ApiError) {
|
|
230
278
|
console.log(chalk2.red(`
|
|
231
279
|
Error: ${error.message}
|
|
@@ -276,35 +324,36 @@ function parseDuration(input) {
|
|
|
276
324
|
}
|
|
277
325
|
|
|
278
326
|
// src/commands/start.ts
|
|
279
|
-
async function startTimer(
|
|
327
|
+
async function startTimer(taskId) {
|
|
280
328
|
const existingTimer = getActiveTimer();
|
|
281
329
|
if (existingTimer) {
|
|
282
330
|
const elapsed = formatDuration(existingTimer.startTime);
|
|
331
|
+
const taskInfo = existingTimer.taskTitle || existingTimer.projectName;
|
|
283
332
|
console.log(
|
|
284
333
|
chalk3.yellow(
|
|
285
334
|
`
|
|
286
|
-
\u26A0 Timer already running for ${chalk3.bold(
|
|
335
|
+
\u26A0 Timer already running for ${chalk3.bold(taskInfo)} (${elapsed})`
|
|
287
336
|
)
|
|
288
337
|
);
|
|
289
338
|
console.log(chalk3.dim(`Stop it first with ${chalk3.bold("meno stop")} or discard with ${chalk3.bold("meno stop --discard")}
|
|
290
339
|
`));
|
|
291
340
|
return;
|
|
292
341
|
}
|
|
293
|
-
let
|
|
294
|
-
if (
|
|
295
|
-
const
|
|
342
|
+
let task;
|
|
343
|
+
if (taskId) {
|
|
344
|
+
const spinner2 = ora3("Loading task...").start();
|
|
296
345
|
try {
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
if (!
|
|
346
|
+
const tasks = await getTasks();
|
|
347
|
+
task = tasks.find((t) => t.id === taskId);
|
|
348
|
+
spinner2.stop();
|
|
349
|
+
if (!task) {
|
|
301
350
|
console.log(chalk3.red(`
|
|
302
|
-
\u2717
|
|
351
|
+
\u2717 Task not found: ${taskId}
|
|
303
352
|
`));
|
|
304
353
|
return;
|
|
305
354
|
}
|
|
306
355
|
} catch (error) {
|
|
307
|
-
|
|
356
|
+
spinner2.fail(chalk3.red("Failed to load task"));
|
|
308
357
|
if (error instanceof ApiError) {
|
|
309
358
|
console.log(chalk3.red(`
|
|
310
359
|
Error: ${error.message}
|
|
@@ -313,45 +362,122 @@ Error: ${error.message}
|
|
|
313
362
|
return;
|
|
314
363
|
}
|
|
315
364
|
} else {
|
|
316
|
-
const selected =
|
|
365
|
+
const selected = getSelectedTask();
|
|
317
366
|
if (!selected) {
|
|
318
367
|
console.log(chalk3.yellow(`
|
|
319
|
-
\u26A0 No
|
|
320
|
-
console.log(chalk3.dim(`Run ${chalk3.bold("meno select")} first or provide a
|
|
368
|
+
\u26A0 No task selected.`));
|
|
369
|
+
console.log(chalk3.dim(`Run ${chalk3.bold("meno select")} first or provide a task ID: ${chalk3.bold("meno start <task-id>")}
|
|
321
370
|
`));
|
|
322
371
|
return;
|
|
323
372
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
373
|
+
const spinner2 = ora3("Starting timer...").start();
|
|
374
|
+
try {
|
|
375
|
+
const tasks = await getTasks();
|
|
376
|
+
task = tasks.find((t) => t.id === selected.id);
|
|
377
|
+
spinner2.stop();
|
|
378
|
+
if (!task) {
|
|
379
|
+
console.log(chalk3.red(`
|
|
380
|
+
\u2717 Selected task not found. It may have been deleted.
|
|
381
|
+
`));
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
} catch (error) {
|
|
385
|
+
spinner2.fail(chalk3.red("Failed to load task"));
|
|
386
|
+
if (error instanceof ApiError) {
|
|
387
|
+
console.log(chalk3.red(`
|
|
388
|
+
Error: ${error.message}
|
|
389
|
+
`));
|
|
390
|
+
}
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const spinner = ora3("Starting timer...").start();
|
|
395
|
+
try {
|
|
396
|
+
const response = await cliAction("start", { taskId: task.id });
|
|
397
|
+
spinner.succeed(chalk3.green("Timer started"));
|
|
398
|
+
const startTime = response.timer?.startTime || (/* @__PURE__ */ new Date()).toISOString();
|
|
399
|
+
setActiveTimer({
|
|
400
|
+
projectId: task.projectId,
|
|
401
|
+
projectName: task.project.name,
|
|
402
|
+
startTime,
|
|
403
|
+
taskId: task.id,
|
|
404
|
+
taskTitle: task.title
|
|
405
|
+
});
|
|
406
|
+
const timestamp = new Date(startTime).toLocaleTimeString();
|
|
407
|
+
console.log(
|
|
408
|
+
chalk3.green(
|
|
409
|
+
`
|
|
410
|
+
\u23F1\uFE0F ${chalk3.bold(task.title)}
|
|
411
|
+
` + chalk3.dim(` ${task.project.name} \u2022 $${task.project.hourlyRate}/hr \u2022 [${timestamp}]`)
|
|
412
|
+
)
|
|
413
|
+
);
|
|
414
|
+
if (task.estimatedHours) {
|
|
415
|
+
console.log(chalk3.dim(` Estimated: ${task.estimatedHours}h
|
|
416
|
+
`));
|
|
417
|
+
} else {
|
|
418
|
+
console.log();
|
|
419
|
+
}
|
|
420
|
+
} catch (error) {
|
|
421
|
+
spinner.fail(chalk3.red("Failed to start timer"));
|
|
422
|
+
if (error instanceof ApiError) {
|
|
423
|
+
console.log(chalk3.red(`
|
|
424
|
+
Error: ${error.message}
|
|
425
|
+
`));
|
|
426
|
+
} else {
|
|
427
|
+
console.log(chalk3.red(`
|
|
428
|
+
Unexpected error: ${error.message}
|
|
429
|
+
`));
|
|
430
|
+
}
|
|
334
431
|
}
|
|
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
432
|
}
|
|
350
433
|
|
|
351
434
|
// src/commands/stop.ts
|
|
352
435
|
import enquirer3 from "enquirer";
|
|
353
436
|
import chalk4 from "chalk";
|
|
354
437
|
import ora4 from "ora";
|
|
438
|
+
|
|
439
|
+
// src/utils/git.ts
|
|
440
|
+
import { execSync } from "child_process";
|
|
441
|
+
function getGitInfo() {
|
|
442
|
+
try {
|
|
443
|
+
const commitHash = execSync("git rev-parse HEAD", {
|
|
444
|
+
encoding: "utf8",
|
|
445
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
446
|
+
// Suppress stderr
|
|
447
|
+
}).trim();
|
|
448
|
+
let repoUrl = execSync("git config --get remote.origin.url", {
|
|
449
|
+
encoding: "utf8",
|
|
450
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
451
|
+
}).trim();
|
|
452
|
+
if (repoUrl.startsWith("git@github.com:")) {
|
|
453
|
+
repoUrl = repoUrl.replace("git@github.com:", "https://github.com/").replace(/\.git$/, "");
|
|
454
|
+
} else if (repoUrl.endsWith(".git")) {
|
|
455
|
+
repoUrl = repoUrl.replace(/\.git$/, "");
|
|
456
|
+
}
|
|
457
|
+
if (!commitHash || !repoUrl) {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
commitHash,
|
|
462
|
+
repoUrl
|
|
463
|
+
};
|
|
464
|
+
} catch (error) {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function isGitRepository() {
|
|
469
|
+
try {
|
|
470
|
+
execSync("git rev-parse --is-inside-work-tree", {
|
|
471
|
+
encoding: "utf8",
|
|
472
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
473
|
+
});
|
|
474
|
+
return true;
|
|
475
|
+
} catch (error) {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// src/commands/stop.ts
|
|
355
481
|
async function stopTimer(options) {
|
|
356
482
|
const timer = getActiveTimer();
|
|
357
483
|
if (!timer) {
|
|
@@ -368,41 +494,20 @@ async function stopTimer(options) {
|
|
|
368
494
|
`));
|
|
369
495
|
return;
|
|
370
496
|
}
|
|
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;
|
|
497
|
+
const taskInfo = timer.taskTitle || timer.projectName;
|
|
498
|
+
const elapsed = formatDuration(timer.startTime);
|
|
396
499
|
console.log(chalk4.bold(`
|
|
397
|
-
\u23F1\uFE0F
|
|
500
|
+
\u23F1\uFE0F Stopping: ${taskInfo}`));
|
|
501
|
+
console.log(chalk4.dim(`Duration: ${elapsed}
|
|
398
502
|
`));
|
|
399
503
|
let description;
|
|
400
504
|
let shouldLog = true;
|
|
505
|
+
let commitHash;
|
|
506
|
+
let repoUrl;
|
|
401
507
|
try {
|
|
402
508
|
if (options.description) {
|
|
403
509
|
description = options.description;
|
|
404
|
-
console.log(chalk4.dim(`Description: ${description}
|
|
405
|
-
`));
|
|
510
|
+
console.log(chalk4.dim(`Description: ${description}`));
|
|
406
511
|
} else {
|
|
407
512
|
const descResult = await enquirer3.prompt({
|
|
408
513
|
type: "input",
|
|
@@ -418,6 +523,34 @@ async function stopTimer(options) {
|
|
|
418
523
|
});
|
|
419
524
|
description = descResult.description;
|
|
420
525
|
}
|
|
526
|
+
if (options.commit && options.repo) {
|
|
527
|
+
commitHash = options.commit;
|
|
528
|
+
repoUrl = options.repo;
|
|
529
|
+
console.log(chalk4.dim(`
|
|
530
|
+
\u{1F517} Evidence: ${commitHash.substring(0, 7)} @ ${repoUrl}`));
|
|
531
|
+
} else if (isGitRepository()) {
|
|
532
|
+
const gitInfo = getGitInfo();
|
|
533
|
+
if (gitInfo) {
|
|
534
|
+
const skipConfirm2 = options.noConfirm || options.yes;
|
|
535
|
+
if (skipConfirm2) {
|
|
536
|
+
commitHash = gitInfo.commitHash;
|
|
537
|
+
repoUrl = gitInfo.repoUrl;
|
|
538
|
+
console.log(chalk4.dim(`
|
|
539
|
+
\u{1F517} Evidence: ${commitHash.substring(0, 7)} @ ${repoUrl}`));
|
|
540
|
+
} else {
|
|
541
|
+
const gitResult = await enquirer3.prompt({
|
|
542
|
+
type: "confirm",
|
|
543
|
+
name: "includeGit",
|
|
544
|
+
message: `Include latest commit (${gitInfo.commitHash.substring(0, 7)}) as evidence?`,
|
|
545
|
+
initial: true
|
|
546
|
+
});
|
|
547
|
+
if (gitResult.includeGit) {
|
|
548
|
+
commitHash = gitInfo.commitHash;
|
|
549
|
+
repoUrl = gitInfo.repoUrl;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
421
554
|
const skipConfirm = options.noConfirm || options.yes;
|
|
422
555
|
if (!skipConfirm) {
|
|
423
556
|
const confirmResult = await enquirer3.prompt({
|
|
@@ -429,39 +562,44 @@ async function stopTimer(options) {
|
|
|
429
562
|
shouldLog = confirmResult.shouldLog;
|
|
430
563
|
if (!shouldLog) {
|
|
431
564
|
console.log(chalk4.yellow("\n\u2717 Entry not logged\n"));
|
|
565
|
+
clearActiveTimer();
|
|
432
566
|
return;
|
|
433
567
|
}
|
|
434
568
|
}
|
|
435
569
|
const spinner = ora4("Logging entry...").start();
|
|
436
570
|
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
|
-
})
|
|
571
|
+
const response = await cliAction("stop", {
|
|
572
|
+
description,
|
|
573
|
+
commitHash,
|
|
574
|
+
repoUrl
|
|
448
575
|
});
|
|
449
576
|
clearActiveTimer();
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
`));
|
|
577
|
+
if (response.entry) {
|
|
578
|
+
const hours = response.entry.duration.toFixed(2);
|
|
579
|
+
const amount = response.entry.amount.toFixed(2);
|
|
580
|
+
spinner.succeed(chalk4.green(`\u2713 Logged ${hours} hours ($${amount})`));
|
|
581
|
+
if (response.evidence) {
|
|
582
|
+
console.log(chalk4.dim(`\u{1F4CE} Evidence: ${response.evidence.commitHash.substring(0, 7)}`));
|
|
583
|
+
}
|
|
584
|
+
console.log();
|
|
585
|
+
} else {
|
|
586
|
+
spinner.succeed(chalk4.green("\u2713 Timer stopped"));
|
|
587
|
+
console.log();
|
|
588
|
+
}
|
|
454
589
|
} catch (error) {
|
|
455
|
-
spinner.fail(chalk4.red("Failed to
|
|
590
|
+
spinner.fail(chalk4.red("Failed to stop timer"));
|
|
456
591
|
if (error instanceof ApiError) {
|
|
457
592
|
console.log(chalk4.red(`
|
|
458
593
|
Error: ${error.message}
|
|
594
|
+
`));
|
|
595
|
+
console.log(chalk4.yellow(`Timer cleared locally. You may need to check the dashboard.
|
|
459
596
|
`));
|
|
460
597
|
} else {
|
|
461
598
|
console.log(chalk4.red(`
|
|
462
599
|
Unexpected error: ${error.message}
|
|
463
600
|
`));
|
|
464
601
|
}
|
|
602
|
+
clearActiveTimer();
|
|
465
603
|
}
|
|
466
604
|
} catch (error) {
|
|
467
605
|
console.log(chalk4.yellow("\n\u2717 Cancelled\n"));
|
|
@@ -564,27 +702,54 @@ Unexpected error: ${error.message}
|
|
|
564
702
|
// src/commands/status.ts
|
|
565
703
|
import chalk6 from "chalk";
|
|
566
704
|
import ora6 from "ora";
|
|
705
|
+
function getStatusColor2(status) {
|
|
706
|
+
switch (status) {
|
|
707
|
+
case "Backlog":
|
|
708
|
+
return chalk6.gray;
|
|
709
|
+
case "Todo":
|
|
710
|
+
return chalk6.yellow;
|
|
711
|
+
case "InProgress":
|
|
712
|
+
return chalk6.cyan;
|
|
713
|
+
case "Review":
|
|
714
|
+
return chalk6.magenta;
|
|
715
|
+
case "Done":
|
|
716
|
+
return chalk6.green;
|
|
717
|
+
default:
|
|
718
|
+
return chalk6.white;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
567
721
|
async function showStatus() {
|
|
568
722
|
console.log(chalk6.bold("\n\u{1F4CA} Meno Status\n"));
|
|
569
|
-
const
|
|
723
|
+
const selectedTask = getSelectedTask();
|
|
570
724
|
const timer = getActiveTimer();
|
|
571
|
-
if (
|
|
725
|
+
if (selectedTask) {
|
|
726
|
+
const statusColor = getStatusColor2(selectedTask.status);
|
|
572
727
|
console.log(
|
|
573
728
|
chalk6.cyan(
|
|
574
|
-
`\u{1F4CC} Selected: ${chalk6.bold(
|
|
729
|
+
`\u{1F4CC} Selected: ${chalk6.bold(selectedTask.title)}`
|
|
575
730
|
)
|
|
576
731
|
);
|
|
732
|
+
console.log(
|
|
733
|
+
chalk6.dim(
|
|
734
|
+
` ${selectedTask.projectName} \u2022 $${selectedTask.hourlyRate}/hr \u2022 `
|
|
735
|
+
) + statusColor(selectedTask.status)
|
|
736
|
+
);
|
|
737
|
+
if (selectedTask.estimatedHours) {
|
|
738
|
+
console.log(chalk6.dim(` Estimated: ${selectedTask.estimatedHours}h`));
|
|
739
|
+
}
|
|
577
740
|
}
|
|
578
741
|
if (timer) {
|
|
579
742
|
const elapsed = formatDuration(timer.startTime);
|
|
580
|
-
const
|
|
743
|
+
const taskInfo = timer.taskTitle || timer.projectName;
|
|
744
|
+
const rate = selectedTask?.hourlyRate || 0;
|
|
745
|
+
const value = calculateTimerValue(timer.startTime, rate);
|
|
581
746
|
console.log(
|
|
582
747
|
chalk6.green(
|
|
583
|
-
`\u23F1\uFE0F Running: ${chalk6.bold(
|
|
748
|
+
`\u23F1\uFE0F Running: ${chalk6.bold(taskInfo)} \u2022 ${elapsed} \u2022 $${value.toFixed(2)}`
|
|
584
749
|
)
|
|
585
750
|
);
|
|
586
751
|
}
|
|
587
|
-
if (
|
|
752
|
+
if (selectedTask || timer) {
|
|
588
753
|
console.log();
|
|
589
754
|
}
|
|
590
755
|
const spinner = ora6("Loading stats...").start();
|
|
@@ -614,13 +779,69 @@ Unexpected error: ${error.message}
|
|
|
614
779
|
}
|
|
615
780
|
}
|
|
616
781
|
|
|
782
|
+
// src/logo-ascii.ts
|
|
783
|
+
var SVG_EMBED = `<?xml version="1.0" encoding="UTF-8"?>
|
|
784
|
+
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 55.44 45.4">
|
|
785
|
+
<g id="MenoLogo">
|
|
786
|
+
<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"/>
|
|
787
|
+
<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"/>
|
|
788
|
+
<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"/>
|
|
789
|
+
</g>
|
|
790
|
+
</svg>`;
|
|
791
|
+
function fallbackAscii() {
|
|
792
|
+
return [
|
|
793
|
+
" ____ __ ",
|
|
794
|
+
" /\\ / __ \\/ /___ _ _ ",
|
|
795
|
+
" / \\ _ __ / / / / / __ \\ | | |",
|
|
796
|
+
" / /\\ \\ | '_ \\ / /_/ / / /_/ / |_| |",
|
|
797
|
+
" / ____ \\| | | |\\____/_/\\____/\\__,_|",
|
|
798
|
+
" /_/ \\_\\_| |_| ",
|
|
799
|
+
"",
|
|
800
|
+
" M E N O (logo) "
|
|
801
|
+
].join("\n");
|
|
802
|
+
}
|
|
803
|
+
async function printLogoAscii(width = 64) {
|
|
804
|
+
try {
|
|
805
|
+
const sharpImport = await import("sharp");
|
|
806
|
+
const sharp = sharpImport.default ?? sharpImport;
|
|
807
|
+
const svgBuffer = Buffer.from(SVG_EMBED, "utf8");
|
|
808
|
+
const img = sharp(svgBuffer).resize({ width }).flatten({ background: { r: 255, g: 255, b: 255 } }).grayscale();
|
|
809
|
+
const { data, info } = await img.raw().toBuffer({ resolveWithObject: true });
|
|
810
|
+
const chars = " .,:;i1tfLCG08@";
|
|
811
|
+
const rows = [];
|
|
812
|
+
const aspect = 0.5;
|
|
813
|
+
const rowStep = Math.max(1, Math.round(1 / aspect));
|
|
814
|
+
for (let y = 0; y < info.height; y += rowStep) {
|
|
815
|
+
let line = "";
|
|
816
|
+
for (let x = 0; x < info.width; x++) {
|
|
817
|
+
const idx = y * info.width + x;
|
|
818
|
+
const val = data[idx];
|
|
819
|
+
const charIdx = Math.floor((1 - val / 255) * (chars.length - 1));
|
|
820
|
+
line += chars[charIdx];
|
|
821
|
+
}
|
|
822
|
+
rows.push(line);
|
|
823
|
+
}
|
|
824
|
+
console.log(rows.join("\n"));
|
|
825
|
+
return;
|
|
826
|
+
} catch (err) {
|
|
827
|
+
console.log(fallbackAscii());
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
if (__require.main === module) {
|
|
832
|
+
printLogoAscii().catch(() => {
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
|
|
617
836
|
// src/index.ts
|
|
618
837
|
var program = new Command();
|
|
619
838
|
program.name("meno").description("CLI for Meno time tracking").version("0.1.0");
|
|
839
|
+
printLogoAscii().catch(() => {
|
|
840
|
+
});
|
|
620
841
|
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("--
|
|
842
|
+
program.command("select").description("Select a task to work on").action(selectProject);
|
|
843
|
+
program.command("start [task-id]").description("Start a timer on selected task or specific task ID").action(startTimer);
|
|
844
|
+
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
845
|
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
|
|
846
|
+
program.command("status").description("Show selected task, timer status, and unbilled stats").action(showStatus);
|
|
626
847
|
program.parse();
|