@ttctl/cli
Advanced tools
| import { timesheet } from "@ttctl/core"; | ||
| import type { OutputFormat } from "../../lib/output.js"; | ||
| /** | ||
| * Action handler for `ttctl timesheet show-many <id...>`. | ||
| * Batch-fetches several timesheets in one wire round-trip via | ||
| * `timesheet.showMany` (`TimesheetsByIDs`), emitting the found | ||
| * timesheets in input order. Ids that resolve to no timesheet are | ||
| * reported (pretty: a trailing "Not found" line; json / yaml consumers | ||
| * diff the returned `id`s against their input). | ||
| * | ||
| * Returns LIST-ROW fields (the same shape as `timesheet list`), NOT the | ||
| * per-day detail of `timesheet show <id>` — the batch wire op selects | ||
| * list fields only. | ||
| */ | ||
| export declare function runTimesheetShowMany(ids: string[], output: OutputFormat): Promise<void>; | ||
| /** | ||
| * Render the batch result as the `timesheet list` table plus a trailing | ||
| * "Not found" line for any requested ids the API did not return. Pure — | ||
| * directly unit-testable. | ||
| */ | ||
| export declare function formatTimesheetShowMany(items: timesheet.TimesheetListItem[], missing: string[]): string; |
| // SPDX-License-Identifier: AGPL-3.0-only | ||
| // Copyright (C) 2026 Oleksii PELYKH | ||
| import { timesheet } from "@ttctl/core"; | ||
| import { emitResult } from "../../lib/output.js"; | ||
| import { formatTimesheetsTable } from "./list.js"; | ||
| import { handleTimesheetError, loadAuthTokenOrExit } from "./shared.js"; | ||
| /** | ||
| * Action handler for `ttctl timesheet show-many <id...>`. | ||
| * Batch-fetches several timesheets in one wire round-trip via | ||
| * `timesheet.showMany` (`TimesheetsByIDs`), emitting the found | ||
| * timesheets in input order. Ids that resolve to no timesheet are | ||
| * reported (pretty: a trailing "Not found" line; json / yaml consumers | ||
| * diff the returned `id`s against their input). | ||
| * | ||
| * Returns LIST-ROW fields (the same shape as `timesheet list`), NOT the | ||
| * per-day detail of `timesheet show <id>` — the batch wire op selects | ||
| * list fields only. | ||
| */ | ||
| export async function runTimesheetShowMany(ids, output) { | ||
| const token = await loadAuthTokenOrExit("timesheet show-many", output); | ||
| let items; | ||
| try { | ||
| items = await timesheet.showMany(token, ids); | ||
| } | ||
| catch (err) { | ||
| handleTimesheetError("timesheet show-many", err, output); | ||
| } | ||
| const found = new Set(items.map((t) => t.id)); | ||
| const missing = ids.filter((id) => !found.has(id)); | ||
| emitResult(items, output, { | ||
| pretty: (data) => formatTimesheetShowMany(data, missing), | ||
| table: (data) => formatTimesheetShowMany(data, missing), | ||
| }); | ||
| } | ||
| /** | ||
| * Render the batch result as the `timesheet list` table plus a trailing | ||
| * "Not found" line for any requested ids the API did not return. Pure — | ||
| * directly unit-testable. | ||
| */ | ||
| export function formatTimesheetShowMany(items, missing) { | ||
| const table = formatTimesheetsTable(items); | ||
| if (missing.length === 0) | ||
| return table; | ||
| return `${table}\nNot found (${missing.length.toString()}): ${missing.join(", ")}`; | ||
| } |
@@ -10,2 +10,3 @@ // SPDX-License-Identifier: AGPL-3.0-only | ||
| import { runTimesheetShow } from "./show.js"; | ||
| import { runTimesheetShowMany } from "./show-many.js"; | ||
| import { runTimesheetSubmit } from "./submit.js"; | ||
@@ -64,6 +65,8 @@ import { runTimesheetUpdate } from "./update.js"; | ||
| // `LimitPagination`, NO `offset`). See ADR-007 row 3 for the grammar. | ||
| const pending = cmd.command("pending").description("Viewer-wide pending timesheets (limit-only pagination)"); | ||
| const pending = cmd | ||
| .command("pending") | ||
| .description("Viewer-wide timesheets pending submission — not client approval (limit-only pagination)"); | ||
| pending | ||
| .command("list") | ||
| .description("List viewer-wide pending timesheet billing cycles (limit-only pagination)") | ||
| .description("List viewer-wide timesheet cycles pending submission — not client approval (limit-only pagination)") | ||
| .addOption(new Option("--limit <number>", "max pending cycles to return (default: 50, the historical wire default)").argParser((raw) => parsePaginationFlag("--limit", raw))) | ||
@@ -89,2 +92,12 @@ .addOption(new Option("-o, --output <format>", "output format") | ||
| }); | ||
| cmd | ||
| .command("show-many") | ||
| .description("Show several timesheets by id in one batch fetch (≤20 ids; input order; list-row fields only)") | ||
| .argument("<id...>", "timesheet ids (BillingCycle.id from `timesheet list`)", parseIdsArg) | ||
| .addOption(new Option("-o, --output <format>", "output format") | ||
| .choices(OUTPUT_FORMATS) | ||
| .default("pretty")) | ||
| .action(async (ids, options) => { | ||
| await runTimesheetShowMany(ids, options.output); | ||
| }); | ||
| const submitCmd = cmd | ||
@@ -158,2 +171,9 @@ .command("submit") | ||
| } | ||
| // Variadic `<id...>` accumulator for `show-many`: Commander invokes a custom | ||
| // parser once per value with the accumulated array as `previous`, so it must | ||
| // append (a single-value parser would keep only the last id). Trims + rejects | ||
| // empty ids per element via `parseIdArg`. Mirrors the jobs/payments groups. | ||
| function parseIdsArg(value, previous = []) { | ||
| return [...previous, parseIdArg(value)]; | ||
| } | ||
| /** | ||
@@ -160,0 +180,0 @@ * Optional-positional variant for `submit`. `undefined` flows through |
@@ -27,6 +27,7 @@ import { timesheet } from "@ttctl/core"; | ||
| * | ||
| * id | engagement | job | week | hours | submitted | overdue | ||
| * id | engagement | job | week | hours | submitted | approved | overdue | ||
| * | ||
| * `week` shows the cycle's date range (`YYYY-MM-DD → YYYY-MM-DD`). | ||
| * `submitted` is `✓` / `·` for visual scanning; `overdue` is | ||
| * `submitted` is `✓` / `·` for visual scanning; `approved` (#849) is | ||
| * `✓` approved / `·` pending / `—` approval not required; `overdue` is | ||
| * `!` only when overdue (otherwise blank) so non-overdue rows are | ||
@@ -33,0 +34,0 @@ * visually quiet. |
@@ -30,6 +30,7 @@ // SPDX-License-Identifier: AGPL-3.0-only | ||
| * | ||
| * id | engagement | job | week | hours | submitted | overdue | ||
| * id | engagement | job | week | hours | submitted | approved | overdue | ||
| * | ||
| * `week` shows the cycle's date range (`YYYY-MM-DD → YYYY-MM-DD`). | ||
| * `submitted` is `✓` / `·` for visual scanning; `overdue` is | ||
| * `submitted` is `✓` / `·` for visual scanning; `approved` (#849) is | ||
| * `✓` approved / `·` pending / `—` approval not required; `overdue` is | ||
| * `!` only when overdue (otherwise blank) so non-overdue rows are | ||
@@ -40,3 +41,3 @@ * visually quiet. | ||
| if (items.length === 0) { | ||
| const empty = new Table({ head: ["id", "engagement", "job", "week", "hours", "submitted", "overdue"] }); | ||
| const empty = new Table({ head: ["id", "engagement", "job", "week", "hours", "submitted", "approved", "overdue"] }); | ||
| return empty.toString(); | ||
@@ -49,10 +50,19 @@ } | ||
| const submittedWidth = 10; | ||
| const approvedWidth = 10; | ||
| const overdueWidth = 8; | ||
| // 7 columns × 2 padding-char + 8 borders ≈ 22 | ||
| const remaining = Math.max(20, terminalWidth - idWidth - engagementWidth - weekWidth - hoursWidth - submittedWidth - overdueWidth - 22); | ||
| // 8 columns × 2 padding-char + 9 borders ≈ 25 | ||
| const remaining = Math.max(20, terminalWidth - | ||
| idWidth - | ||
| engagementWidth - | ||
| weekWidth - | ||
| hoursWidth - | ||
| submittedWidth - | ||
| approvedWidth - | ||
| overdueWidth - | ||
| 25); | ||
| const jobWidth = Math.max(20, remaining); | ||
| const table = new Table({ | ||
| head: ["id", "engagement", "job", "week", "hours", "submitted", "overdue"], | ||
| colWidths: [idWidth, engagementWidth, jobWidth, weekWidth, hoursWidth, submittedWidth, overdueWidth], | ||
| colAligns: ["left", "left", "left", "left", "right", "center", "center"], | ||
| head: ["id", "engagement", "job", "week", "hours", "submitted", "approved", "overdue"], | ||
| colWidths: [idWidth, engagementWidth, jobWidth, weekWidth, hoursWidth, submittedWidth, approvedWidth, overdueWidth], | ||
| colAligns: ["left", "left", "left", "left", "right", "center", "center", "center"], | ||
| wordWrap: true, | ||
@@ -70,2 +80,3 @@ }); | ||
| it.timesheetSubmitted ? "✓" : "·", | ||
| it.timesheetApproved ? "✓" : it.timesheetRequiresApproval ? "·" : "—", | ||
| it.timesheetOverdue ? "!" : "", | ||
@@ -72,0 +83,0 @@ ]); |
@@ -40,2 +40,6 @@ // SPDX-License-Identifier: AGPL-3.0-only | ||
| lines.push(` Overdue: ${String(item.timesheetOverdue)}`); | ||
| lines.push(` Requires approval: ${String(item.timesheetRequiresApproval)}`); | ||
| lines.push(` Approved: ${String(item.timesheetApproved)}`); | ||
| if (item.status !== null) | ||
| lines.push(` Status: ${item.status}`); | ||
| if (item.timesheetSubmissionOpenDatetime !== null) { | ||
@@ -42,0 +46,0 @@ lines.push(` Submission opens: ${item.timesheetSubmissionOpenDatetime}`); |
+5
-5
| { | ||
| "name": "@ttctl/cli", | ||
| "version": "0.1.1", | ||
| "version": "0.2.0", | ||
| "description": "TTCtl CLI commands and program definition", | ||
@@ -33,4 +33,4 @@ "type": "module", | ||
| "devDependencies": { | ||
| "@types/node": "^25", | ||
| "eslint": "^10.4.1", | ||
| "@types/node": "^26", | ||
| "eslint": "^10.7.0", | ||
| "typescript": "~6.0.3", | ||
@@ -40,3 +40,3 @@ "vitest": "^4.1.9" | ||
| "dependencies": { | ||
| "@clack/prompts": "^1.5.1", | ||
| "@clack/prompts": "^1.7.0", | ||
| "cli-table3": "^0.6.5", | ||
@@ -46,3 +46,3 @@ "commander": "^15.0.0", | ||
| "zod": "^4.4.3", | ||
| "@ttctl/core": "^0.1.1" | ||
| "@ttctl/core": "^0.2.0" | ||
| }, | ||
@@ -49,0 +49,0 @@ "scripts": { |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
1061854
0.44%244
0.83%22878
0.45%+ Added
- Removed
Updated
Updated