New:Socket for Asana Is Now Available.Learn more
Sign In

@ttctl/cli

Package Overview
Dependencies
Maintainers
1
Versions
24
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ttctl/cli - npm Package Compare versions

Comparing version
0.1.1
to
0.2.0
+21
dist/commands/timesheet/show-many.d.ts
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(", ")}`;
}
+22
-2

@@ -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

+3
-2

@@ -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}`);

{
"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": {