@usemeno/meno-cli 0.1.1 → 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 -117
- package/package.json +5 -4
- 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/src/logo-ascii.ts +0 -7
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
|
@@ -1,10 +1,4 @@
|
|
|
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
|
-
});
|
|
8
2
|
|
|
9
3
|
// src/index.ts
|
|
10
4
|
import { Command } from "commander";
|
|
@@ -26,14 +20,17 @@ function setApiKey(key) {
|
|
|
26
20
|
config.set("apiKey", key);
|
|
27
21
|
}
|
|
28
22
|
function getBaseUrl() {
|
|
29
|
-
return
|
|
30
|
-
}
|
|
31
|
-
function setBaseUrl(url) {
|
|
32
|
-
config.set("baseUrl", url);
|
|
23
|
+
return "https://menohq.app";
|
|
33
24
|
}
|
|
34
25
|
function getSelectedProject() {
|
|
35
26
|
return config.get("selectedProject");
|
|
36
27
|
}
|
|
28
|
+
function setSelectedProject(project) {
|
|
29
|
+
config.set("selectedProject", project);
|
|
30
|
+
}
|
|
31
|
+
function clearSelectedProject() {
|
|
32
|
+
config.delete("selectedProject");
|
|
33
|
+
}
|
|
37
34
|
function getActiveTimer() {
|
|
38
35
|
return config.get("activeTimer");
|
|
39
36
|
}
|
|
@@ -148,19 +145,9 @@ async function login() {
|
|
|
148
145
|
}
|
|
149
146
|
});
|
|
150
147
|
const apiKey = apiKeyPrompt.apiKey;
|
|
151
|
-
const baseUrlPrompt = await enquirer.prompt({
|
|
152
|
-
type: "input",
|
|
153
|
-
name: "baseUrl",
|
|
154
|
-
message: "Base URL (press Enter for default):",
|
|
155
|
-
initial: "http://localhost:3000"
|
|
156
|
-
});
|
|
157
|
-
const baseUrl = baseUrlPrompt.baseUrl;
|
|
158
148
|
const spinner = ora("Validating API key...").start();
|
|
159
149
|
try {
|
|
160
150
|
setApiKey(apiKey);
|
|
161
|
-
if (baseUrl) {
|
|
162
|
-
setBaseUrl(baseUrl);
|
|
163
|
-
}
|
|
164
151
|
await apiRequest("/api/external/projects");
|
|
165
152
|
spinner.succeed(chalk.green("\u2713 Logged in successfully!"));
|
|
166
153
|
console.log(chalk.dim(`
|
|
@@ -213,67 +200,119 @@ function formatTaskDisplay(task, isSelected) {
|
|
|
213
200
|
return `${prefix}${chalk2.bold(task.title)} ${chalk2.dim("\u2192")} ${task.project.name} ${statusColor(`[${task.status}]`)}${estimateText}`;
|
|
214
201
|
}
|
|
215
202
|
async function selectProject() {
|
|
216
|
-
console.log(chalk2.bold("\n\u{1F4CB} Select Task\n"));
|
|
217
|
-
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();
|
|
218
205
|
try {
|
|
219
|
-
const tasks = await
|
|
206
|
+
const [tasks, projectsResponse] = await Promise.all([
|
|
207
|
+
getTasks(),
|
|
208
|
+
apiRequest("/api/external/projects")
|
|
209
|
+
]);
|
|
210
|
+
const projects = projectsResponse.projects || [];
|
|
220
211
|
spinner.stop();
|
|
221
|
-
if (
|
|
222
|
-
console.log(chalk2.yellow("No
|
|
223
|
-
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"));
|
|
224
215
|
return;
|
|
225
216
|
}
|
|
226
|
-
const
|
|
227
|
-
const
|
|
217
|
+
const currentSelection = getSelectedTask();
|
|
218
|
+
const projectChoices = [
|
|
228
219
|
{
|
|
229
220
|
name: "clear",
|
|
230
221
|
message: chalk2.dim("[Clear Selection]")
|
|
231
222
|
},
|
|
232
|
-
...
|
|
233
|
-
const
|
|
223
|
+
...projects.map((project) => {
|
|
224
|
+
const projectTaskCount = tasks.filter((task2) => task2.projectId === project.id).length;
|
|
234
225
|
return {
|
|
235
|
-
name:
|
|
236
|
-
message:
|
|
237
|
-
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
|
|
238
229
|
};
|
|
239
230
|
})
|
|
240
231
|
];
|
|
241
|
-
const
|
|
232
|
+
const projectResult = await enquirer2.prompt({
|
|
242
233
|
type: "select",
|
|
243
|
-
name: "
|
|
244
|
-
message: "Select a
|
|
245
|
-
choices
|
|
234
|
+
name: "project",
|
|
235
|
+
message: "Select a project:",
|
|
236
|
+
choices: projectChoices
|
|
246
237
|
});
|
|
247
|
-
const
|
|
248
|
-
if (
|
|
238
|
+
const projectAnswer = projectResult.project;
|
|
239
|
+
if (projectAnswer === "clear") {
|
|
249
240
|
clearSelectedTask();
|
|
241
|
+
clearSelectedProject();
|
|
250
242
|
console.log(chalk2.green("\n\u2713 Selection cleared\n"));
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
console.log(
|
|
268
|
-
|
|
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(`
|
|
269
309
|
\u2713 Selected: ${chalk2.bold(task.title)}
|
|
270
310
|
`) + chalk2.dim(` Project: ${task.project.name} \u2022 $${task.project.hourlyRate}/hr
|
|
271
311
|
`) + chalk2.dim(` Status: `) + statusColor(task.status) + (task.estimatedHours ? chalk2.dim(` \u2022 Estimated: ${task.estimatedHours}h
|
|
272
312
|
`) : "\n")
|
|
273
|
-
|
|
274
|
-
}
|
|
313
|
+
);
|
|
275
314
|
} catch (error) {
|
|
276
|
-
spinner.fail(chalk2.red("Failed to load tasks"));
|
|
315
|
+
spinner.fail(chalk2.red("Failed to load projects/tasks"));
|
|
277
316
|
if (error instanceof ApiError) {
|
|
278
317
|
console.log(chalk2.red(`
|
|
279
318
|
Error: ${error.message}
|
|
@@ -779,67 +818,11 @@ Unexpected error: ${error.message}
|
|
|
779
818
|
}
|
|
780
819
|
}
|
|
781
820
|
|
|
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
|
-
|
|
836
821
|
// src/index.ts
|
|
837
822
|
var program = new Command();
|
|
838
823
|
program.name("meno").description("CLI for Meno time tracking").version("0.1.0");
|
|
839
|
-
printLogoAscii().catch(() => {
|
|
840
|
-
});
|
|
841
824
|
program.command("login").description("Authenticate with your Meno API key").action(login);
|
|
842
|
-
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);
|
|
843
826
|
program.command("start [task-id]").description("Start a timer on selected task or specific task ID").action(startTimer);
|
|
844
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);
|
|
845
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usemeno/meno-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Command-line interface for Meno time tracking",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -21,12 +21,13 @@
|
|
|
21
21
|
"author": "",
|
|
22
22
|
"license": "MIT",
|
|
23
23
|
"dependencies": {
|
|
24
|
+
"chalk": "^5.3.0",
|
|
24
25
|
"commander": "^12.0.0",
|
|
25
26
|
"conf": "^13.0.1",
|
|
26
|
-
"ora": "^8.0.1",
|
|
27
27
|
"enquirer": "^2.4.1",
|
|
28
|
-
"
|
|
29
|
-
"
|
|
28
|
+
"node-fetch": "^3.3.2",
|
|
29
|
+
"ora": "^8.0.1",
|
|
30
|
+
"sharp": "^0.34.5"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"@types/node": "^20.11.0",
|
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
|
package/src/logo-ascii.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
|
|
4
1
|
const SVG_EMBED = `<?xml version="1.0" encoding="UTF-8"?>
|
|
5
2
|
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 55.44 45.4">
|
|
6
3
|
<g id="MenoLogo">
|
|
@@ -55,7 +52,3 @@ export async function printLogoAscii(width = 64) {
|
|
|
55
52
|
return;
|
|
56
53
|
}
|
|
57
54
|
}
|
|
58
|
-
|
|
59
|
-
if (require.main === module) {
|
|
60
|
-
printLogoAscii().catch(() => {});
|
|
61
|
-
}
|