@vibecoded/work 0.0.9 → 0.0.11
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 +40 -4
- package/dist/cli.js +138 -106
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
# @vibecoded/work
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
-
|
|
3
|
+
[](https://www.npmjs.com/package/@vibecoded/work)
|
|
4
|
+
[](https://www.npmjs.com/package/@vibecoded/work)
|
|
5
|
+
|
|
6
|
+
Minimal local time-tracking CLI (Bun + TypeScript) that stores everything on disk.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- One command to start/stop tracking.
|
|
11
|
+
- Task picking with fuzzy search.
|
|
12
|
+
- Monthly CSV storage for easy exports.
|
|
13
|
+
- No servers, no accounts, no lock-in.
|
|
7
14
|
|
|
8
15
|
## Install
|
|
9
16
|
|
|
@@ -12,6 +19,15 @@ bun add -g @vibecoded/work
|
|
|
12
19
|
npm install -g @vibecoded/work
|
|
13
20
|
````
|
|
14
21
|
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
work init
|
|
26
|
+
work on "Client X" "Kickoff call"
|
|
27
|
+
work status
|
|
28
|
+
work off
|
|
29
|
+
```
|
|
30
|
+
|
|
15
31
|
## Data location
|
|
16
32
|
|
|
17
33
|
Default: `~/.work`
|
|
@@ -87,6 +103,26 @@ work info 2026-01
|
|
|
87
103
|
work info Project 2026-01 --export
|
|
88
104
|
```
|
|
89
105
|
|
|
106
|
+
## Data layout
|
|
107
|
+
|
|
108
|
+
Each project is stored in `data/<projectId>/` with:
|
|
109
|
+
|
|
110
|
+
- `tasks.json` (task index)
|
|
111
|
+
- one CSV per month: `YYYY-MM.csv`
|
|
112
|
+
|
|
113
|
+
## Build
|
|
114
|
+
|
|
115
|
+
Build the CLI binary:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
bun run build
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Versioning & releases
|
|
122
|
+
|
|
123
|
+
The published version is tracked in `package.json` and surfaced via the npm badge above.
|
|
124
|
+
Release builds are produced with `bun run build` before publishing.
|
|
125
|
+
|
|
90
126
|
## Notes
|
|
91
127
|
|
|
92
128
|
- Uses **local timezone** for day/month grouping.
|
package/dist/cli.js
CHANGED
|
@@ -2168,6 +2168,46 @@ function fmtElapsed(ms) {
|
|
|
2168
2168
|
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
|
|
2169
2169
|
}
|
|
2170
2170
|
|
|
2171
|
+
// src/lib/store.ts
|
|
2172
|
+
var PROJECTS_FILE = "projects.json";
|
|
2173
|
+
var RUNNING_FILE = "running.json";
|
|
2174
|
+
var TASKS_FILE = "tasks.json";
|
|
2175
|
+
function getProjectsPath(workHome) {
|
|
2176
|
+
return pathJoin(workHome, PROJECTS_FILE);
|
|
2177
|
+
}
|
|
2178
|
+
function getRunningPath(workHome) {
|
|
2179
|
+
return pathJoin(workHome, RUNNING_FILE);
|
|
2180
|
+
}
|
|
2181
|
+
function getProjectDir(workHome, projectId) {
|
|
2182
|
+
return pathJoin(workHome, "data", projectId);
|
|
2183
|
+
}
|
|
2184
|
+
function getTaskIndexPath(workHome, projectId) {
|
|
2185
|
+
return pathJoin(getProjectDir(workHome, projectId), TASKS_FILE);
|
|
2186
|
+
}
|
|
2187
|
+
function getMonthCsvPath(workHome, projectId, yearMonth) {
|
|
2188
|
+
return pathJoin(getProjectDir(workHome, projectId), `${yearMonth}.csv`);
|
|
2189
|
+
}
|
|
2190
|
+
async function loadProjects(workHome) {
|
|
2191
|
+
return loadJson(getProjectsPath(workHome), { projects: [] });
|
|
2192
|
+
}
|
|
2193
|
+
async function saveProjects(workHome, projects) {
|
|
2194
|
+
await saveJson(getProjectsPath(workHome), projects);
|
|
2195
|
+
}
|
|
2196
|
+
async function loadRunning(workHome) {
|
|
2197
|
+
return loadJson(getRunningPath(workHome), { session: null });
|
|
2198
|
+
}
|
|
2199
|
+
async function saveRunning(workHome, running) {
|
|
2200
|
+
await saveJson(getRunningPath(workHome), running);
|
|
2201
|
+
}
|
|
2202
|
+
async function loadTaskIndex(workHome, projectId) {
|
|
2203
|
+
await ensureDir(getProjectDir(workHome, projectId));
|
|
2204
|
+
return loadJson(getTaskIndexPath(workHome, projectId), { tasks: [] });
|
|
2205
|
+
}
|
|
2206
|
+
async function saveTaskIndex(workHome, projectId, idx) {
|
|
2207
|
+
await ensureDir(getProjectDir(workHome, projectId));
|
|
2208
|
+
await saveJson(getTaskIndexPath(workHome, projectId), idx);
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2171
2211
|
// src/cli.ts
|
|
2172
2212
|
function usage() {
|
|
2173
2213
|
console.log(`
|
|
@@ -2225,18 +2265,15 @@ async function main() {
|
|
|
2225
2265
|
}
|
|
2226
2266
|
async function cmdInit(workHome) {
|
|
2227
2267
|
await ensureWorkHome(workHome);
|
|
2228
|
-
const
|
|
2229
|
-
const
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
await saveJson(projectsPath, projects);
|
|
2233
|
-
await saveJson(runningPath, running);
|
|
2268
|
+
const projects = await loadProjects(workHome);
|
|
2269
|
+
const running = await loadRunning(workHome);
|
|
2270
|
+
await saveProjects(workHome, projects);
|
|
2271
|
+
await saveRunning(workHome, running);
|
|
2234
2272
|
console.log(`Initialized: ${workHome}`);
|
|
2235
2273
|
}
|
|
2236
2274
|
async function cmdProjects(workHome) {
|
|
2237
2275
|
await ensureWorkHome(workHome);
|
|
2238
|
-
const
|
|
2239
|
-
const projects = await loadJson(projectsPath, { projects: [] });
|
|
2276
|
+
const projects = await loadProjects(workHome);
|
|
2240
2277
|
if (!projects.projects.length) {
|
|
2241
2278
|
console.log("No projects yet. Use: work on (create one)");
|
|
2242
2279
|
return;
|
|
@@ -2250,8 +2287,7 @@ async function cmdProjects(workHome) {
|
|
|
2250
2287
|
}
|
|
2251
2288
|
async function cmdTasks(workHome, rest) {
|
|
2252
2289
|
await ensureWorkHome(workHome);
|
|
2253
|
-
const
|
|
2254
|
-
const projects = await loadJson(projectsPath, { projects: [] });
|
|
2290
|
+
const projects = await loadProjects(workHome);
|
|
2255
2291
|
const arg = rest[0]?.trim();
|
|
2256
2292
|
const project = arg ? findProjectByNameOrId(projects, arg) : await pickProjectOnly(projects);
|
|
2257
2293
|
if (!project) {
|
|
@@ -2263,17 +2299,7 @@ async function cmdTasks(workHome, rest) {
|
|
|
2263
2299
|
console.log(`No tasks for project "${project.name}" yet.`);
|
|
2264
2300
|
return;
|
|
2265
2301
|
}
|
|
2266
|
-
const tasks = [...idx.tasks].sort(
|
|
2267
|
-
const ca = a.count ?? 0;
|
|
2268
|
-
const cb = b.count ?? 0;
|
|
2269
|
-
if (cb !== ca)
|
|
2270
|
-
return cb - ca;
|
|
2271
|
-
const la = a.lastUsedAt ? Date.parse(a.lastUsedAt) : 0;
|
|
2272
|
-
const lb = b.lastUsedAt ? Date.parse(b.lastUsedAt) : 0;
|
|
2273
|
-
if (lb !== la)
|
|
2274
|
-
return lb - la;
|
|
2275
|
-
return a.name.localeCompare(b.name);
|
|
2276
|
-
});
|
|
2302
|
+
const tasks = [...idx.tasks].sort(sortTasksByUsage);
|
|
2277
2303
|
console.log(`Tasks for ${project.name}`);
|
|
2278
2304
|
console.log("-".repeat(`Tasks for ${project.name}`.length));
|
|
2279
2305
|
const rows = tasks.map((t) => [t.name, String(t.count ?? 0), t.lastUsedAt ?? ""]);
|
|
@@ -2281,30 +2307,28 @@ async function cmdTasks(workHome, rest) {
|
|
|
2281
2307
|
}
|
|
2282
2308
|
async function cmdOn(workHome, rest, flags) {
|
|
2283
2309
|
await ensureWorkHome(workHome);
|
|
2284
|
-
const
|
|
2285
|
-
const
|
|
2286
|
-
const projects = await loadJson(projectsPath, { projects: [] });
|
|
2287
|
-
const running = await loadJson(runningPath, { session: null });
|
|
2310
|
+
const projects = await loadProjects(workHome);
|
|
2311
|
+
const running = await loadRunning(workHome);
|
|
2288
2312
|
const argProject = rest[0]?.trim();
|
|
2289
2313
|
const argTask = rest.slice(1).join(" ").trim();
|
|
2290
|
-
const project = argProject ? await resolveProjectByNameOrCreate(
|
|
2314
|
+
const project = argProject ? await resolveProjectByNameOrCreate(workHome, projects, argProject) : await pickProjectOrCreate(workHome, projects);
|
|
2291
2315
|
const task = argTask ? await resolveTaskByNameOrCreate(workHome, project.id, argTask) : await pickTaskOrCreate(workHome, project.id, project.name);
|
|
2292
2316
|
if (running.session) {
|
|
2293
2317
|
const cur = running.session;
|
|
2294
2318
|
if (cur.projectId === project.id && cur.task === task) {
|
|
2295
|
-
const elapsed = fmtElapsed(
|
|
2296
|
-
console.log(`Already working on ${project.name
|
|
2319
|
+
const elapsed = fmtElapsed(sessionElapsedMs(cur));
|
|
2320
|
+
console.log(`Already working on ${formatWorkLabel(project.name, task)} (${elapsed})`);
|
|
2297
2321
|
return;
|
|
2298
2322
|
}
|
|
2299
2323
|
if (!flags.yes) {
|
|
2300
|
-
const elapsed = fmtElapsed(
|
|
2324
|
+
const elapsed = fmtElapsed(sessionElapsedMs(cur));
|
|
2301
2325
|
console.log(`You are already working on:
|
|
2302
|
-
` + ` ${cur.projectName
|
|
2326
|
+
` + ` ${formatWorkLabel(cur.projectName, cur.task)}
|
|
2303
2327
|
` + ` started: ${cur.startAt}
|
|
2304
2328
|
` + ` elapsed: ${elapsed}
|
|
2305
2329
|
|
|
2306
2330
|
` + `Switch to:
|
|
2307
|
-
` + ` ${project.name
|
|
2331
|
+
` + ` ${formatWorkLabel(project.name, task)}
|
|
2308
2332
|
`);
|
|
2309
2333
|
const ok = await dist_default2({
|
|
2310
2334
|
message: "Stop current and start the new one now?",
|
|
@@ -2328,60 +2352,45 @@ async function cmdOn(workHome, rest, flags) {
|
|
|
2328
2352
|
pausedMs: 0,
|
|
2329
2353
|
note: ""
|
|
2330
2354
|
};
|
|
2331
|
-
await
|
|
2332
|
-
console.log(`Working on ${project.name
|
|
2355
|
+
await saveRunning(workHome, { session });
|
|
2356
|
+
console.log(`Working on ${formatWorkLabel(project.name, task)}`);
|
|
2333
2357
|
}
|
|
2334
2358
|
async function cmdOff(workHome) {
|
|
2335
2359
|
await ensureWorkHome(workHome);
|
|
2336
|
-
const
|
|
2337
|
-
const running = await loadJson(runningPath, { session: null });
|
|
2360
|
+
const running = await loadRunning(workHome);
|
|
2338
2361
|
if (!running.session) {
|
|
2339
2362
|
console.error("No active session.");
|
|
2340
2363
|
process.exit(1);
|
|
2341
2364
|
}
|
|
2342
2365
|
await stopAndPersist(workHome, running.session, nowIso());
|
|
2343
|
-
await
|
|
2366
|
+
await saveRunning(workHome, { session: null });
|
|
2344
2367
|
console.log("Stopped.");
|
|
2345
2368
|
}
|
|
2346
2369
|
async function cmdStatus(workHome) {
|
|
2347
2370
|
await ensureWorkHome(workHome);
|
|
2348
|
-
const
|
|
2349
|
-
const running = await loadJson(runningPath, { session: null });
|
|
2371
|
+
const running = await loadRunning(workHome);
|
|
2350
2372
|
if (!running.session) {
|
|
2351
2373
|
console.log("No active session.");
|
|
2352
2374
|
return;
|
|
2353
2375
|
}
|
|
2354
2376
|
const s = running.session;
|
|
2355
|
-
const
|
|
2356
|
-
|
|
2357
|
-
console.log(`Running: ${s.projectName}${s.task ? " • " + s.task : ""}
|
|
2377
|
+
const elapsed = fmtElapsed(sessionElapsedMs(s));
|
|
2378
|
+
console.log(`Running: ${formatWorkLabel(s.projectName, s.task)}
|
|
2358
2379
|
` + `Started: ${s.startAt}
|
|
2359
2380
|
` + `Elapsed: ${elapsed}${s.pausedAt ? " (paused)" : ""}`);
|
|
2360
2381
|
}
|
|
2361
2382
|
async function cmdInfo(workHome, rest, flags) {
|
|
2362
2383
|
await ensureWorkHome(workHome);
|
|
2363
|
-
const
|
|
2364
|
-
const
|
|
2365
|
-
const a1 = rest[0]?.trim();
|
|
2366
|
-
const a2 = rest[1]?.trim();
|
|
2367
|
-
let projectArg = null;
|
|
2368
|
-
let monthArg = null;
|
|
2369
|
-
if (a1 && /^\d{4}-\d{2}$/.test(a1)) {
|
|
2370
|
-
monthArg = a1;
|
|
2371
|
-
} else if (a1) {
|
|
2372
|
-
projectArg = a1;
|
|
2373
|
-
}
|
|
2374
|
-
if (a2 && /^\d{4}-\d{2}$/.test(a2)) {
|
|
2375
|
-
monthArg = a2;
|
|
2376
|
-
}
|
|
2384
|
+
const projects = await loadProjects(workHome);
|
|
2385
|
+
const { projectArg, monthArg } = parseInfoArgs(rest);
|
|
2377
2386
|
const month = monthArg ?? localYearMonth(new Date);
|
|
2378
2387
|
const project = projectArg != null ? findProjectByNameOrId(projects, projectArg) : await pickProjectOnly(projects);
|
|
2379
2388
|
if (!project) {
|
|
2380
2389
|
console.error(`Project not found.`);
|
|
2381
2390
|
process.exit(1);
|
|
2382
2391
|
}
|
|
2383
|
-
const projectDir =
|
|
2384
|
-
const csvPath =
|
|
2392
|
+
const projectDir = getProjectDir(workHome, project.id);
|
|
2393
|
+
const csvPath = getMonthCsvPath(workHome, project.id, month);
|
|
2385
2394
|
const rows = await readCsvRows(csvPath);
|
|
2386
2395
|
const active = rows.filter((r) => !r.deletedAt);
|
|
2387
2396
|
const byDayMs = new Map;
|
|
@@ -2396,7 +2405,7 @@ async function cmdInfo(workHome, rest, flags) {
|
|
|
2396
2405
|
totalMs += ms;
|
|
2397
2406
|
const dayKey = localDateKey(new Date(start));
|
|
2398
2407
|
byDayMs.set(dayKey, (byDayMs.get(dayKey) ?? 0) + ms);
|
|
2399
|
-
const taskKey = (r.task
|
|
2408
|
+
const taskKey = normalizeTaskLabel(r.task);
|
|
2400
2409
|
byTaskMs.set(taskKey, (byTaskMs.get(taskKey) ?? 0) + ms);
|
|
2401
2410
|
}
|
|
2402
2411
|
const dayRows = sortMapByKey(byDayMs).map(([k, ms]) => ({
|
|
@@ -2446,7 +2455,7 @@ async function pickProjectOnly(projects) {
|
|
|
2446
2455
|
});
|
|
2447
2456
|
return projects.projects.find((p) => p.id === picked) ?? null;
|
|
2448
2457
|
}
|
|
2449
|
-
async function resolveProjectByNameOrCreate(
|
|
2458
|
+
async function resolveProjectByNameOrCreate(workHome, projects, name) {
|
|
2450
2459
|
const n = name.trim();
|
|
2451
2460
|
const found = projects.projects.find((p) => p.name.toLowerCase() === n.toLowerCase());
|
|
2452
2461
|
if (found)
|
|
@@ -2457,10 +2466,10 @@ async function resolveProjectByNameOrCreate(projects, projectsPath, name) {
|
|
|
2457
2466
|
createdAt: nowIso()
|
|
2458
2467
|
};
|
|
2459
2468
|
projects.projects.push(created);
|
|
2460
|
-
await
|
|
2469
|
+
await saveProjects(workHome, projects);
|
|
2461
2470
|
return created;
|
|
2462
2471
|
}
|
|
2463
|
-
async function pickProjectOrCreate(
|
|
2472
|
+
async function pickProjectOrCreate(workHome, projects) {
|
|
2464
2473
|
const picked = await dist_default4({
|
|
2465
2474
|
message: "Select a project",
|
|
2466
2475
|
source: async (term) => {
|
|
@@ -2478,41 +2487,22 @@ async function pickProjectOrCreate(projects, projectsPath) {
|
|
|
2478
2487
|
message: "New project name",
|
|
2479
2488
|
validate: (v) => v.trim().length ? true : "Required"
|
|
2480
2489
|
})).trim();
|
|
2481
|
-
|
|
2482
|
-
id: globalThis.crypto?.randomUUID ? globalThis.crypto.randomUUID() : randomUuidFallback(),
|
|
2483
|
-
name,
|
|
2484
|
-
createdAt: nowIso()
|
|
2485
|
-
};
|
|
2486
|
-
projects.projects.push(created);
|
|
2487
|
-
await saveJson(projectsPath, projects);
|
|
2488
|
-
return created;
|
|
2490
|
+
return resolveProjectByNameOrCreate(workHome, projects, name);
|
|
2489
2491
|
}
|
|
2490
2492
|
const found = projects.projects.find((p) => p.id === picked);
|
|
2491
2493
|
if (!found)
|
|
2492
2494
|
throw new Error("Project selection failed");
|
|
2493
2495
|
return found;
|
|
2494
2496
|
}
|
|
2495
|
-
async function loadTaskIndex(workHome, projectId) {
|
|
2496
|
-
const dir = pathJoin(workHome, "data", projectId);
|
|
2497
|
-
await ensureDir(dir);
|
|
2498
|
-
const path = pathJoin(dir, "tasks.json");
|
|
2499
|
-
return await loadJson(path, { tasks: [] });
|
|
2500
|
-
}
|
|
2501
|
-
async function saveTaskIndex(workHome, projectId, idx) {
|
|
2502
|
-
const dir = pathJoin(workHome, "data", projectId);
|
|
2503
|
-
await ensureDir(dir);
|
|
2504
|
-
const path = pathJoin(dir, "tasks.json");
|
|
2505
|
-
await saveJson(path, idx);
|
|
2506
|
-
}
|
|
2507
2497
|
async function resolveTaskByNameOrCreate(workHome, projectId, task) {
|
|
2508
2498
|
const idx = await loadTaskIndex(workHome, projectId);
|
|
2509
|
-
const
|
|
2510
|
-
const found = idx.tasks.find((x) => x.name.toLowerCase() ===
|
|
2499
|
+
const normalized = task.trim();
|
|
2500
|
+
const found = idx.tasks.find((x) => x.name.toLowerCase() === normalized.toLowerCase());
|
|
2511
2501
|
if (found)
|
|
2512
2502
|
return found.name;
|
|
2513
|
-
idx.tasks.push({ name:
|
|
2503
|
+
idx.tasks.push({ name: normalized, count: 0, lastUsedAt: null, createdAt: nowIso() });
|
|
2514
2504
|
await saveTaskIndex(workHome, projectId, idx);
|
|
2515
|
-
return
|
|
2505
|
+
return normalized;
|
|
2516
2506
|
}
|
|
2517
2507
|
async function pickTaskOrCreate(workHome, projectId, projectName) {
|
|
2518
2508
|
const idx = await loadTaskIndex(workHome, projectId);
|
|
@@ -2520,13 +2510,7 @@ async function pickTaskOrCreate(workHome, projectId, projectName) {
|
|
|
2520
2510
|
message: `Select a task for "${projectName}"`,
|
|
2521
2511
|
source: async (term) => {
|
|
2522
2512
|
const q = (term ?? "").toLowerCase();
|
|
2523
|
-
const tasks = [...idx.tasks].sort((
|
|
2524
|
-
const la = a.lastUsedAt ? Date.parse(a.lastUsedAt) : 0;
|
|
2525
|
-
const lb = b.lastUsedAt ? Date.parse(b.lastUsedAt) : 0;
|
|
2526
|
-
if (lb !== la)
|
|
2527
|
-
return lb - la;
|
|
2528
|
-
return a.name.localeCompare(b.name);
|
|
2529
|
-
}).filter((t) => t.name.toLowerCase().includes(q)).map((t) => ({ name: t.name, value: t.name }));
|
|
2513
|
+
const tasks = [...idx.tasks].sort(sortTasksByRecentUse).filter((t) => t.name.toLowerCase().includes(q)).map((t) => ({ name: t.name, value: t.name }));
|
|
2530
2514
|
return [...tasks, { name: "➕ Create new task…", value: "__create__" }];
|
|
2531
2515
|
}
|
|
2532
2516
|
});
|
|
@@ -2541,25 +2525,14 @@ async function pickTaskOrCreate(workHome, projectId, projectName) {
|
|
|
2541
2525
|
return picked;
|
|
2542
2526
|
}
|
|
2543
2527
|
async function stopAndPersist(workHome, s, endAtIso) {
|
|
2544
|
-
const projectDir =
|
|
2528
|
+
const projectDir = getProjectDir(workHome, s.projectId);
|
|
2545
2529
|
await ensureDir(projectDir);
|
|
2546
2530
|
const idx = await loadTaskIndex(workHome, s.projectId);
|
|
2547
|
-
|
|
2548
|
-
if (entry) {
|
|
2549
|
-
entry.count = (entry.count ?? 0) + 1;
|
|
2550
|
-
entry.lastUsedAt = nowIso();
|
|
2551
|
-
} else if (s.task.trim()) {
|
|
2552
|
-
idx.tasks.push({
|
|
2553
|
-
name: s.task,
|
|
2554
|
-
count: 1,
|
|
2555
|
-
lastUsedAt: nowIso(),
|
|
2556
|
-
createdAt: nowIso()
|
|
2557
|
-
});
|
|
2558
|
-
}
|
|
2531
|
+
updateTaskStats(idx, s.task);
|
|
2559
2532
|
await saveTaskIndex(workHome, s.projectId, idx);
|
|
2560
2533
|
const endAt = new Date(Date.parse(endAtIso));
|
|
2561
2534
|
const ym = localYearMonth(endAt);
|
|
2562
|
-
const csvPath =
|
|
2535
|
+
const csvPath = getMonthCsvPath(workHome, s.projectId, ym);
|
|
2563
2536
|
await ensureCsvHeader(csvPath);
|
|
2564
2537
|
const row = {
|
|
2565
2538
|
id: s.id,
|
|
@@ -2628,6 +2601,65 @@ function csvEsc(v) {
|
|
|
2628
2601
|
return `"${v.replace(/"/g, '""')}"`;
|
|
2629
2602
|
return v;
|
|
2630
2603
|
}
|
|
2604
|
+
function formatWorkLabel(projectName, task) {
|
|
2605
|
+
return `${projectName}${task ? " • " + task : ""}`;
|
|
2606
|
+
}
|
|
2607
|
+
function normalizeTaskLabel(task) {
|
|
2608
|
+
return (task ?? "").trim() || "(no task)";
|
|
2609
|
+
}
|
|
2610
|
+
function sessionElapsedMs(session) {
|
|
2611
|
+
return Date.now() - Date.parse(session.startAt) - (session.pausedMs ?? 0);
|
|
2612
|
+
}
|
|
2613
|
+
function parseInfoArgs(args) {
|
|
2614
|
+
const a1 = args[0]?.trim();
|
|
2615
|
+
const a2 = args[1]?.trim();
|
|
2616
|
+
let projectArg = null;
|
|
2617
|
+
let monthArg = null;
|
|
2618
|
+
if (a1 && /^\d{4}-\d{2}$/.test(a1)) {
|
|
2619
|
+
monthArg = a1;
|
|
2620
|
+
} else if (a1) {
|
|
2621
|
+
projectArg = a1;
|
|
2622
|
+
}
|
|
2623
|
+
if (a2 && /^\d{4}-\d{2}$/.test(a2)) {
|
|
2624
|
+
monthArg = a2;
|
|
2625
|
+
}
|
|
2626
|
+
return { projectArg, monthArg };
|
|
2627
|
+
}
|
|
2628
|
+
function sortTasksByUsage(a, b) {
|
|
2629
|
+
const ca = a.count ?? 0;
|
|
2630
|
+
const cb = b.count ?? 0;
|
|
2631
|
+
if (cb !== ca)
|
|
2632
|
+
return cb - ca;
|
|
2633
|
+
const la = a.lastUsedAt ? Date.parse(a.lastUsedAt) : 0;
|
|
2634
|
+
const lb = b.lastUsedAt ? Date.parse(b.lastUsedAt) : 0;
|
|
2635
|
+
if (lb !== la)
|
|
2636
|
+
return lb - la;
|
|
2637
|
+
return a.name.localeCompare(b.name);
|
|
2638
|
+
}
|
|
2639
|
+
function sortTasksByRecentUse(a, b) {
|
|
2640
|
+
const la = a.lastUsedAt ? Date.parse(a.lastUsedAt) : 0;
|
|
2641
|
+
const lb = b.lastUsedAt ? Date.parse(b.lastUsedAt) : 0;
|
|
2642
|
+
if (lb !== la)
|
|
2643
|
+
return lb - la;
|
|
2644
|
+
return a.name.localeCompare(b.name);
|
|
2645
|
+
}
|
|
2646
|
+
function updateTaskStats(idx, taskName) {
|
|
2647
|
+
const trimmed = taskName.trim();
|
|
2648
|
+
if (!trimmed)
|
|
2649
|
+
return;
|
|
2650
|
+
const entry = idx.tasks.find((t) => t.name.toLowerCase() === trimmed.toLowerCase());
|
|
2651
|
+
if (entry) {
|
|
2652
|
+
entry.count = (entry.count ?? 0) + 1;
|
|
2653
|
+
entry.lastUsedAt = nowIso();
|
|
2654
|
+
return;
|
|
2655
|
+
}
|
|
2656
|
+
idx.tasks.push({
|
|
2657
|
+
name: trimmed,
|
|
2658
|
+
count: 1,
|
|
2659
|
+
lastUsedAt: nowIso(),
|
|
2660
|
+
createdAt: nowIso()
|
|
2661
|
+
});
|
|
2662
|
+
}
|
|
2631
2663
|
process.on("uncaughtException", (error) => {
|
|
2632
2664
|
if (error && typeof error === "object" && error.name === "ExitPromptError") {
|
|
2633
2665
|
console.log("\uD83D\uDC4B until next time!");
|