@usemeno/meno-cli 0.1.2 → 0.1.3
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/README.md +7 -6
- package/dist/index.js +100 -107
- package/package.json +1 -1
- package/src/commands/login.ts +1 -14
- package/src/commands/select.ts +103 -48
- package/src/config.ts +1 -6
- package/src/index.ts +1 -5
package/README.md
CHANGED
|
@@ -32,13 +32,13 @@ Paste your API key when prompted.
|
|
|
32
32
|
|
|
33
33
|
### `meno select`
|
|
34
34
|
|
|
35
|
-
Select a project to work on. The selected
|
|
35
|
+
Select a project first, then select a task to work on. The selected task will be used for `meno start`.
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
38
|
meno select
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
Use arrow keys to select a project, or choose "[Clear Selection]" to unset.
|
|
41
|
+
Use arrow keys to select a project and task, or choose "[Clear Selection]" to unset both.
|
|
42
42
|
|
|
43
43
|
### `meno start [project-id]`
|
|
44
44
|
|
|
@@ -92,9 +92,10 @@ meno status
|
|
|
92
92
|
## Workflow Example
|
|
93
93
|
|
|
94
94
|
```bash
|
|
95
|
-
# Morning: Select your project
|
|
95
|
+
# Morning: Select your project and task
|
|
96
96
|
meno select
|
|
97
|
-
> Website Redesign (Acme Corp)
|
|
97
|
+
> Website Redesign (Acme Corp)
|
|
98
|
+
> Build hero section → Website Redesign [InProgress]
|
|
98
99
|
|
|
99
100
|
# Start working
|
|
100
101
|
meno start
|
|
@@ -119,8 +120,8 @@ Config is stored at `~/.config/meno-cli/config.json` (cross-platform).
|
|
|
119
120
|
|
|
120
121
|
Contains:
|
|
121
122
|
- `apiKey` - Your Meno API key
|
|
122
|
-
- `baseUrl` - API endpoint (default: http://localhost:3000)
|
|
123
123
|
- `selectedProject` - Currently selected project
|
|
124
|
+
- `selectedTask` - Currently selected task
|
|
124
125
|
- `activeTimer` - Running timer state (persists across restarts)
|
|
125
126
|
|
|
126
127
|
## Troubleshooting
|
|
@@ -132,7 +133,7 @@ Run `meno login` to authenticate.
|
|
|
132
133
|
Run `meno select` to choose a project, or pass `--project <id>` to commands.
|
|
133
134
|
|
|
134
135
|
**Connection errors:**
|
|
135
|
-
Check your
|
|
136
|
+
Check your internet connection. CLI uses `https://menohq.app`.
|
|
136
137
|
|
|
137
138
|
## Development
|
|
138
139
|
|
package/dist/index.js
CHANGED
|
@@ -20,14 +20,17 @@ function setApiKey(key) {
|
|
|
20
20
|
config.set("apiKey", key);
|
|
21
21
|
}
|
|
22
22
|
function getBaseUrl() {
|
|
23
|
-
return
|
|
24
|
-
}
|
|
25
|
-
function setBaseUrl(url) {
|
|
26
|
-
config.set("baseUrl", url);
|
|
23
|
+
return "https://menohq.app";
|
|
27
24
|
}
|
|
28
25
|
function getSelectedProject() {
|
|
29
26
|
return config.get("selectedProject");
|
|
30
27
|
}
|
|
28
|
+
function setSelectedProject(project) {
|
|
29
|
+
config.set("selectedProject", project);
|
|
30
|
+
}
|
|
31
|
+
function clearSelectedProject() {
|
|
32
|
+
config.delete("selectedProject");
|
|
33
|
+
}
|
|
31
34
|
function getActiveTimer() {
|
|
32
35
|
return config.get("activeTimer");
|
|
33
36
|
}
|
|
@@ -142,19 +145,9 @@ async function login() {
|
|
|
142
145
|
}
|
|
143
146
|
});
|
|
144
147
|
const apiKey = apiKeyPrompt.apiKey;
|
|
145
|
-
const baseUrlPrompt = await enquirer.prompt({
|
|
146
|
-
type: "input",
|
|
147
|
-
name: "baseUrl",
|
|
148
|
-
message: "Base URL (press Enter for default):",
|
|
149
|
-
initial: "http://localhost:3000"
|
|
150
|
-
});
|
|
151
|
-
const baseUrl = baseUrlPrompt.baseUrl;
|
|
152
148
|
const spinner = ora("Validating API key...").start();
|
|
153
149
|
try {
|
|
154
150
|
setApiKey(apiKey);
|
|
155
|
-
if (baseUrl) {
|
|
156
|
-
setBaseUrl(baseUrl);
|
|
157
|
-
}
|
|
158
151
|
await apiRequest("/api/external/projects");
|
|
159
152
|
spinner.succeed(chalk.green("\u2713 Logged in successfully!"));
|
|
160
153
|
console.log(chalk.dim(`
|
|
@@ -207,67 +200,119 @@ function formatTaskDisplay(task, isSelected) {
|
|
|
207
200
|
return `${prefix}${chalk2.bold(task.title)} ${chalk2.dim("\u2192")} ${task.project.name} ${statusColor(`[${task.status}]`)}${estimateText}`;
|
|
208
201
|
}
|
|
209
202
|
async function selectProject() {
|
|
210
|
-
console.log(chalk2.bold("\n\u{1F4CB} Select Task\n"));
|
|
211
|
-
const spinner = ora2("Loading tasks...").start();
|
|
203
|
+
console.log(chalk2.bold("\n\u{1F4CB} Select Project & Task\n"));
|
|
204
|
+
const spinner = ora2("Loading projects and tasks...").start();
|
|
212
205
|
try {
|
|
213
|
-
const tasks = await
|
|
206
|
+
const [tasks, projectsResponse] = await Promise.all([
|
|
207
|
+
getTasks(),
|
|
208
|
+
apiRequest("/api/external/projects")
|
|
209
|
+
]);
|
|
210
|
+
const projects = projectsResponse.projects || [];
|
|
214
211
|
spinner.stop();
|
|
215
|
-
if (
|
|
216
|
-
console.log(chalk2.yellow("No
|
|
217
|
-
console.log(chalk2.dim("Create
|
|
212
|
+
if (projects.length === 0) {
|
|
213
|
+
console.log(chalk2.yellow("No active projects found."));
|
|
214
|
+
console.log(chalk2.dim("Create projects in the Meno dashboard first.\n"));
|
|
218
215
|
return;
|
|
219
216
|
}
|
|
220
|
-
const
|
|
221
|
-
const
|
|
217
|
+
const currentSelection = getSelectedTask();
|
|
218
|
+
const projectChoices = [
|
|
222
219
|
{
|
|
223
220
|
name: "clear",
|
|
224
221
|
message: chalk2.dim("[Clear Selection]")
|
|
225
222
|
},
|
|
226
|
-
...
|
|
227
|
-
const
|
|
223
|
+
...projects.map((project) => {
|
|
224
|
+
const projectTaskCount = tasks.filter((task2) => task2.projectId === project.id).length;
|
|
228
225
|
return {
|
|
229
|
-
name:
|
|
230
|
-
message:
|
|
231
|
-
value:
|
|
226
|
+
name: project.id,
|
|
227
|
+
message: `${chalk2.bold(project.name)} ${chalk2.dim("\u2022")} ${project.clientName} ${chalk2.dim(`(${projectTaskCount} task${projectTaskCount === 1 ? "" : "s"})`)}`,
|
|
228
|
+
value: project
|
|
232
229
|
};
|
|
233
230
|
})
|
|
234
231
|
];
|
|
235
|
-
const
|
|
232
|
+
const projectResult = await enquirer2.prompt({
|
|
236
233
|
type: "select",
|
|
237
|
-
name: "
|
|
238
|
-
message: "Select a
|
|
239
|
-
choices
|
|
234
|
+
name: "project",
|
|
235
|
+
message: "Select a project:",
|
|
236
|
+
choices: projectChoices
|
|
240
237
|
});
|
|
241
|
-
const
|
|
242
|
-
if (
|
|
238
|
+
const projectAnswer = projectResult.project;
|
|
239
|
+
if (projectAnswer === "clear") {
|
|
243
240
|
clearSelectedTask();
|
|
241
|
+
clearSelectedProject();
|
|
244
242
|
console.log(chalk2.green("\n\u2713 Selection cleared\n"));
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
console.log(
|
|
262
|
-
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const selectedProject = projects.find((project) => project.id === projectAnswer);
|
|
246
|
+
if (!selectedProject) {
|
|
247
|
+
console.log(chalk2.red("\n\u2717 Project not found\n"));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
setSelectedProject({
|
|
251
|
+
id: selectedProject.id,
|
|
252
|
+
name: selectedProject.name,
|
|
253
|
+
clientName: selectedProject.clientName,
|
|
254
|
+
hourlyRate: selectedProject.hourlyRate
|
|
255
|
+
});
|
|
256
|
+
const projectTasks = tasks.filter((task2) => task2.projectId === selectedProject.id);
|
|
257
|
+
if (projectTasks.length === 0) {
|
|
258
|
+
clearSelectedTask();
|
|
259
|
+
console.log(chalk2.green(`
|
|
260
|
+
\u2713 Selected project: ${chalk2.bold(selectedProject.name)}`));
|
|
261
|
+
console.log(chalk2.dim("No tasks found for this project yet.\n"));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const taskChoices = [
|
|
265
|
+
{
|
|
266
|
+
name: "clear_task",
|
|
267
|
+
message: chalk2.dim("[Clear Task Selection]")
|
|
268
|
+
},
|
|
269
|
+
...projectTasks.map((task2) => {
|
|
270
|
+
const isSelected = currentSelection?.id === task2.id;
|
|
271
|
+
return {
|
|
272
|
+
name: task2.id,
|
|
273
|
+
message: formatTaskDisplay(task2, isSelected),
|
|
274
|
+
value: task2
|
|
275
|
+
};
|
|
276
|
+
})
|
|
277
|
+
];
|
|
278
|
+
const taskResult = await enquirer2.prompt({
|
|
279
|
+
type: "select",
|
|
280
|
+
name: "task",
|
|
281
|
+
message: `Select a task in ${selectedProject.name}:`,
|
|
282
|
+
choices: taskChoices
|
|
283
|
+
});
|
|
284
|
+
const taskAnswer = taskResult.task;
|
|
285
|
+
if (taskAnswer === "clear_task") {
|
|
286
|
+
clearSelectedTask();
|
|
287
|
+
console.log(chalk2.green(`
|
|
288
|
+
\u2713 Project selected: ${chalk2.bold(selectedProject.name)}`));
|
|
289
|
+
console.log(chalk2.dim("Task selection cleared.\n"));
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const task = projectTasks.find((t) => t.id === taskAnswer);
|
|
293
|
+
if (!task) {
|
|
294
|
+
console.log(chalk2.red("\n\u2717 Task not found\n"));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
setSelectedTask({
|
|
298
|
+
id: task.id,
|
|
299
|
+
title: task.title,
|
|
300
|
+
projectId: task.projectId,
|
|
301
|
+
projectName: task.project.name,
|
|
302
|
+
status: task.status,
|
|
303
|
+
estimatedHours: task.estimatedHours,
|
|
304
|
+
hourlyRate: task.project.hourlyRate
|
|
305
|
+
});
|
|
306
|
+
const statusColor = getStatusColor(task.status);
|
|
307
|
+
console.log(
|
|
308
|
+
chalk2.green(`
|
|
263
309
|
\u2713 Selected: ${chalk2.bold(task.title)}
|
|
264
310
|
`) + chalk2.dim(` Project: ${task.project.name} \u2022 $${task.project.hourlyRate}/hr
|
|
265
311
|
`) + chalk2.dim(` Status: `) + statusColor(task.status) + (task.estimatedHours ? chalk2.dim(` \u2022 Estimated: ${task.estimatedHours}h
|
|
266
312
|
`) : "\n")
|
|
267
|
-
|
|
268
|
-
}
|
|
313
|
+
);
|
|
269
314
|
} catch (error) {
|
|
270
|
-
spinner.fail(chalk2.red("Failed to load tasks"));
|
|
315
|
+
spinner.fail(chalk2.red("Failed to load projects/tasks"));
|
|
271
316
|
if (error instanceof ApiError) {
|
|
272
317
|
console.log(chalk2.red(`
|
|
273
318
|
Error: ${error.message}
|
|
@@ -773,63 +818,11 @@ Unexpected error: ${error.message}
|
|
|
773
818
|
}
|
|
774
819
|
}
|
|
775
820
|
|
|
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
|
-
|
|
826
821
|
// src/index.ts
|
|
827
822
|
var program = new Command();
|
|
828
823
|
program.name("meno").description("CLI for Meno time tracking").version("0.1.0");
|
|
829
|
-
printLogoAscii().catch(() => {
|
|
830
|
-
});
|
|
831
824
|
program.command("login").description("Authenticate with your Meno API key").action(login);
|
|
832
|
-
program.command("select").description("Select a task to work on").action(selectProject);
|
|
825
|
+
program.command("select").description("Select a project, then select a task to work on").action(selectProject);
|
|
833
826
|
program.command("start [task-id]").description("Start a timer on selected task or specific task ID").action(startTimer);
|
|
834
827
|
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);
|
|
835
828
|
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);
|
package/package.json
CHANGED
package/src/commands/login.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import enquirer from "enquirer";
|
|
2
2
|
import ora from "ora";
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
-
import { setApiKey
|
|
4
|
+
import { setApiKey } from "../config.js";
|
|
5
5
|
import { apiRequest, ApiError} from "../utils/api.js";
|
|
6
6
|
|
|
7
7
|
export async function login() {
|
|
@@ -23,25 +23,12 @@ export async function login() {
|
|
|
23
23
|
|
|
24
24
|
const apiKey = (apiKeyPrompt as any).apiKey;
|
|
25
25
|
|
|
26
|
-
// Optional: Custom base URL
|
|
27
|
-
const baseUrlPrompt = await enquirer.prompt({
|
|
28
|
-
type: "input",
|
|
29
|
-
name: "baseUrl",
|
|
30
|
-
message: "Base URL (press Enter for default):",
|
|
31
|
-
initial: "http://localhost:3000",
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const baseUrl = (baseUrlPrompt as any).baseUrl;
|
|
35
|
-
|
|
36
26
|
// Validate API key by making a test request
|
|
37
27
|
const spinner = ora("Validating API key...").start();
|
|
38
28
|
|
|
39
29
|
try {
|
|
40
30
|
// Temporarily set the key to test it
|
|
41
31
|
setApiKey(apiKey as string);
|
|
42
|
-
if (baseUrl) {
|
|
43
|
-
setBaseUrl(baseUrl as string);
|
|
44
|
-
}
|
|
45
32
|
|
|
46
33
|
// Test the key
|
|
47
34
|
await apiRequest("/api/external/projects");
|
package/src/commands/select.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import enquirer from "enquirer";
|
|
2
2
|
import ora from "ora";
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
-
import { setSelectedTask, clearSelectedTask, getSelectedTask } from "../config.js";
|
|
5
|
-
import { getTasks, ApiError, Task } from "../utils/api.js";
|
|
4
|
+
import { setSelectedTask, clearSelectedTask, getSelectedTask, setSelectedProject, clearSelectedProject } from "../config.js";
|
|
5
|
+
import { getTasks, ApiError, Task, apiRequest, Project } from "../utils/api.js";
|
|
6
6
|
|
|
7
7
|
function getStatusColor(status: string): (text: string) => string {
|
|
8
8
|
switch (status) {
|
|
@@ -24,32 +24,88 @@ function formatTaskDisplay(task: Task, isSelected: boolean): string {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export async function selectProject() {
|
|
27
|
-
console.log(chalk.bold("\n📋 Select Task\n"));
|
|
27
|
+
console.log(chalk.bold("\n📋 Select Project & Task\n"));
|
|
28
28
|
|
|
29
|
-
const spinner = ora("Loading tasks...").start();
|
|
29
|
+
const spinner = ora("Loading projects and tasks...").start();
|
|
30
30
|
|
|
31
31
|
try {
|
|
32
|
-
const tasks = await
|
|
32
|
+
const [tasks, projectsResponse] = await Promise.all([
|
|
33
|
+
getTasks(),
|
|
34
|
+
apiRequest<{ projects: Project[] }>("/api/external/projects"),
|
|
35
|
+
]);
|
|
36
|
+
const projects = projectsResponse.projects || [];
|
|
33
37
|
|
|
34
38
|
spinner.stop();
|
|
35
39
|
|
|
36
|
-
if (
|
|
37
|
-
console.log(chalk.yellow("No
|
|
38
|
-
console.log(chalk.dim("Create
|
|
40
|
+
if (projects.length === 0) {
|
|
41
|
+
console.log(chalk.yellow("No active projects found."));
|
|
42
|
+
console.log(chalk.dim("Create projects in the Meno dashboard first.\n"));
|
|
39
43
|
return;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
const
|
|
46
|
+
const currentSelection = getSelectedTask();
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
const choices = [
|
|
48
|
+
const projectChoices = [
|
|
46
49
|
{
|
|
47
50
|
name: "clear",
|
|
48
51
|
message: chalk.dim("[Clear Selection]"),
|
|
49
52
|
},
|
|
50
|
-
...
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
+
...projects.map((project) => {
|
|
54
|
+
const projectTaskCount = tasks.filter((task) => task.projectId === project.id).length;
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
name: project.id,
|
|
58
|
+
message: `${chalk.bold(project.name)} ${chalk.dim("•")} ${project.clientName} ${chalk.dim(`(${projectTaskCount} task${projectTaskCount === 1 ? "" : "s"})`)}`,
|
|
59
|
+
value: project,
|
|
60
|
+
};
|
|
61
|
+
}),
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const projectResult = await enquirer.prompt({
|
|
65
|
+
type: "select",
|
|
66
|
+
name: "project",
|
|
67
|
+
message: "Select a project:",
|
|
68
|
+
choices: projectChoices,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const projectAnswer = (projectResult as any).project;
|
|
72
|
+
|
|
73
|
+
if (projectAnswer === "clear") {
|
|
74
|
+
clearSelectedTask();
|
|
75
|
+
clearSelectedProject();
|
|
76
|
+
console.log(chalk.green("\n✓ Selection cleared\n"));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const selectedProject = projects.find((project) => project.id === projectAnswer);
|
|
81
|
+
if (!selectedProject) {
|
|
82
|
+
console.log(chalk.red("\n✗ Project not found\n"));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
setSelectedProject({
|
|
87
|
+
id: selectedProject.id,
|
|
88
|
+
name: selectedProject.name,
|
|
89
|
+
clientName: selectedProject.clientName,
|
|
90
|
+
hourlyRate: selectedProject.hourlyRate,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const projectTasks = tasks.filter((task) => task.projectId === selectedProject.id);
|
|
94
|
+
if (projectTasks.length === 0) {
|
|
95
|
+
clearSelectedTask();
|
|
96
|
+
console.log(chalk.green(`\n✓ Selected project: ${chalk.bold(selectedProject.name)}`));
|
|
97
|
+
console.log(chalk.dim("No tasks found for this project yet.\n"));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const taskChoices = [
|
|
102
|
+
{
|
|
103
|
+
name: "clear_task",
|
|
104
|
+
message: chalk.dim("[Clear Task Selection]"),
|
|
105
|
+
},
|
|
106
|
+
...projectTasks.map((task) => {
|
|
107
|
+
const isSelected = currentSelection?.id === task.id;
|
|
108
|
+
|
|
53
109
|
return {
|
|
54
110
|
name: task.id,
|
|
55
111
|
message: formatTaskDisplay(task, isSelected),
|
|
@@ -58,47 +114,46 @@ export async function selectProject() {
|
|
|
58
114
|
}),
|
|
59
115
|
];
|
|
60
116
|
|
|
61
|
-
const
|
|
117
|
+
const taskResult = await enquirer.prompt({
|
|
62
118
|
type: "select",
|
|
63
119
|
name: "task",
|
|
64
|
-
message:
|
|
65
|
-
choices,
|
|
120
|
+
message: `Select a task in ${selectedProject.name}:`,
|
|
121
|
+
choices: taskChoices,
|
|
66
122
|
});
|
|
67
123
|
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
if (answer === "clear") {
|
|
124
|
+
const taskAnswer = (taskResult as any).task;
|
|
125
|
+
if (taskAnswer === "clear_task") {
|
|
71
126
|
clearSelectedTask();
|
|
72
|
-
console.log(chalk.green(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const task = tasks.find((t) => t.id === answer);
|
|
76
|
-
|
|
77
|
-
if (!task) {
|
|
78
|
-
console.log(chalk.red("\n✗ Task not found\n"));
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
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,
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
const statusColor = getStatusColor(task.status);
|
|
93
|
-
console.log(
|
|
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")
|
|
98
|
-
);
|
|
127
|
+
console.log(chalk.green(`\n✓ Project selected: ${chalk.bold(selectedProject.name)}`));
|
|
128
|
+
console.log(chalk.dim("Task selection cleared.\n"));
|
|
129
|
+
return;
|
|
99
130
|
}
|
|
131
|
+
|
|
132
|
+
const task = projectTasks.find((t) => t.id === taskAnswer);
|
|
133
|
+
if (!task) {
|
|
134
|
+
console.log(chalk.red("\n✗ Task not found\n"));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
setSelectedTask({
|
|
139
|
+
id: task.id,
|
|
140
|
+
title: task.title,
|
|
141
|
+
projectId: task.projectId,
|
|
142
|
+
projectName: task.project.name,
|
|
143
|
+
status: task.status,
|
|
144
|
+
estimatedHours: task.estimatedHours,
|
|
145
|
+
hourlyRate: task.project.hourlyRate,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const statusColor = getStatusColor(task.status);
|
|
149
|
+
console.log(
|
|
150
|
+
chalk.green(`\n✓ Selected: ${chalk.bold(task.title)}\n`) +
|
|
151
|
+
chalk.dim(` Project: ${task.project.name} • $${task.project.hourlyRate}/hr\n`) +
|
|
152
|
+
chalk.dim(` Status: `) + statusColor(task.status) +
|
|
153
|
+
(task.estimatedHours ? chalk.dim(` • Estimated: ${task.estimatedHours}h\n`) : "\n")
|
|
154
|
+
);
|
|
100
155
|
} catch (error) {
|
|
101
|
-
spinner.fail(chalk.red("Failed to load tasks"));
|
|
156
|
+
spinner.fail(chalk.red("Failed to load projects/tasks"));
|
|
102
157
|
|
|
103
158
|
if (error instanceof ApiError) {
|
|
104
159
|
console.log(chalk.red(`\nError: ${error.message}\n`));
|
package/src/config.ts
CHANGED
|
@@ -2,7 +2,6 @@ import Conf from "conf";
|
|
|
2
2
|
|
|
3
3
|
interface Config {
|
|
4
4
|
apiKey?: string;
|
|
5
|
-
baseUrl?: string;
|
|
6
5
|
selectedProject?: {
|
|
7
6
|
id: string;
|
|
8
7
|
name: string;
|
|
@@ -41,11 +40,7 @@ export function setApiKey(key: string): void {
|
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
export function getBaseUrl(): string {
|
|
44
|
-
return
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function setBaseUrl(url: string): void {
|
|
48
|
-
config.set("baseUrl", url);
|
|
43
|
+
return "https://menohq.app";
|
|
49
44
|
}
|
|
50
45
|
|
|
51
46
|
export function getSelectedProject() {
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,6 @@ 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";
|
|
11
10
|
|
|
12
11
|
const program = new Command();
|
|
13
12
|
|
|
@@ -16,9 +15,6 @@ program
|
|
|
16
15
|
.description("CLI for Meno time tracking")
|
|
17
16
|
.version("0.1.0");
|
|
18
17
|
|
|
19
|
-
// Print ASCII logo at CLI startup (sharp used if installed, otherwise a fallback)
|
|
20
|
-
printLogoAscii().catch(() => {});
|
|
21
|
-
|
|
22
18
|
program
|
|
23
19
|
.command("login")
|
|
24
20
|
.description("Authenticate with your Meno API key")
|
|
@@ -26,7 +22,7 @@ program
|
|
|
26
22
|
|
|
27
23
|
program
|
|
28
24
|
.command("select")
|
|
29
|
-
.description("Select a task to work on")
|
|
25
|
+
.description("Select a project, then select a task to work on")
|
|
30
26
|
.action(selectProject);
|
|
31
27
|
|
|
32
28
|
program
|