securewithtrace 0.22.0 → 0.24.0

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.
Files changed (3) hide show
  1. package/README.md +27 -5
  2. package/dist/index.js +511 -197
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -66,7 +66,8 @@ tracecli vulnerabilities list --severity HIGH
66
66
  ```bash
67
67
  tracecli auth login
68
68
  tracecli auth status
69
- tracecli auth logout
69
+ tracecli auth logout # prompts for confirmation interactively
70
+ tracecli auth logout --no-confirm # skip prompt (for CI/scripts)
70
71
  ```
71
72
 
72
73
  ### Organization context
@@ -86,14 +87,23 @@ When your account belongs to multiple organizations, use `tracecli org switch` t
86
87
  # List org repositories known to Trace
87
88
  tracecli repo list
88
89
 
90
+ # Show the next page of repositories
91
+ tracecli repo list --page 2 --limit 30
92
+
93
+ # Show all matching repositories
94
+ tracecli repo list --all
95
+
89
96
  # List only connected GitHub repositories that match "trace"
90
97
  tracecli repo list --platform GITHUB --status CONNECTED --query trace
91
98
 
92
99
  # Advanced filtering syntax (repeatable)
93
100
  tracecli repo list --filter "platform=GITHUB" --filter "repo:trace" --filter "status!=DISCONNECTED"
94
101
 
95
- # Alias command for filtering
102
+ # Alias command for filtering (all repo list flags are supported)
96
103
  tracecli repo filter --filter "language:typescript" --filter "branch=main"
104
+ tracecli repo filter --language typescript --limit 10
105
+ tracecli repo filter --output json | jq '.[].repositoryUrl'
106
+ tracecli repo filter --output tsv | cut -f1
97
107
 
98
108
  # Find repositories that use a specific dependency
99
109
  tracecli repo list --dependency lodash
@@ -105,7 +115,7 @@ tracecli repo list --dependency lodash@4.17.21
105
115
  tracecli repo list --dependency @types/node@20
106
116
 
107
117
  # Dependency results as JSON
108
- tracecli repo list --dependency react --json | jq '.[].fullName'
118
+ tracecli repo list --dependency react --output json | jq '.[].fullName'
109
119
 
110
120
  # Clone by owner/repo shorthand
111
121
  tracecli repo clone securewithtrace/trace
@@ -117,6 +127,16 @@ tracecli repo clone securewithtrace/trace --branch main --depth 1
117
127
  ### Vulnerabilities
118
128
 
119
129
  ```bash
130
+ # Fetch a single vulnerability by display ID, numeric ID, or UUID
131
+ tracecli vulnerabilities get TRACE-123
132
+
133
+ # Open the vulnerability detail page in a browser
134
+ tracecli vulnerabilities get TRACE-123 --web
135
+
136
+ # JSON or TSV output for scripting
137
+ tracecli vulnerabilities get TRACE-123 --output json
138
+ tracecli vulnerabilities get TRACE-123 --output tsv | cut -f1,10
139
+
120
140
  # List vulnerabilities across your organization (default: 30 results)
121
141
  tracecli vulnerabilities list
122
142
 
@@ -173,6 +193,8 @@ Repository filter options:
173
193
  - Supported fields: `platform`, `status`, `url`, `repo`, `branch`, `language`
174
194
  - Supported operators: `=` (exact), `!=` (not exact), `:` (contains), `!:` (not contains)
175
195
  - `--dependency`: filter by dependency name with optional `@version` (e.g. `lodash`, `lodash@4.17`, `@types/node@20`)
196
+ - `--page`: choose which page of results to display when using `--limit` (default: 1)
197
+ - `--all`: display all matching repositories (disables pagination)
176
198
 
177
199
  ### Pentests
178
200
 
@@ -198,7 +220,7 @@ tracecli pentest findings list --pentest PT-3 --output json
198
220
  # Findings as TSV
199
221
  tracecli pentest findings list --pentest PT-3 --output tsv | cut -f1,4
200
222
 
201
- # Open pentests dashboard in browser
223
+ # Open the pentest detail page in a browser
202
224
  tracecli pentest findings list --pentest PT-3 --web
203
225
  ```
204
226
 
@@ -216,7 +238,7 @@ Flags for `pentest findings list`:
216
238
  |------|-----------|-------------|
217
239
  | `--pentest <id>` | | Pentest display ID (e.g. `PT-3`) or UUID (required) |
218
240
  | `--output <format>` | `-o` | Output format: `table` (default), `json`, `tsv` |
219
- | `--web` | `-w` | Open pentests dashboard in browser |
241
+ | `--web` | `-w` | Open the pentest detail page in a browser |
220
242
 
221
243
  ### Upgrades
222
244
 
package/dist/index.js CHANGED
@@ -25063,7 +25063,7 @@ var WebhookEventTypeSchema = external_exports.enum([
25063
25063
  ]);
25064
25064
  var WebhookEndpointStatusSchema = external_exports.enum(["enabled", "disabled"]);
25065
25065
  var WebhookDeliveryStatusSchema = external_exports.enum(["succeeded", "failed"]);
25066
- var WebhookDeliverySourceSchema = external_exports.enum(["test", "mock"]);
25066
+ var WebhookDeliverySourceSchema = external_exports.enum(["test", "mock", "event"]);
25067
25067
  var WebhookEndpointSchema = external_exports.object({
25068
25068
  webhookId: external_exports.uuid(),
25069
25069
  organizationId: external_exports.uuid(),
@@ -26536,6 +26536,75 @@ function tsvSafe(value) {
26536
26536
  return (value ?? "").replace(/[\t\r\n]/g, " ");
26537
26537
  }
26538
26538
 
26539
+ // src/pager.ts
26540
+ function isInteractiveEnvironment() {
26541
+ if (process.env["NO_PAGER"] === "1" || process.env["TRACECLI_NO_PAGER"] === "1") {
26542
+ return false;
26543
+ }
26544
+ return Boolean(process.stdout.isTTY && process.stdin.isTTY);
26545
+ }
26546
+ function resolvePagerCommand() {
26547
+ const raw = process.env["PAGER"]?.trim();
26548
+ if (raw && raw.length > 0) {
26549
+ const parts = raw.split(/\s+/);
26550
+ const [command, ...args] = parts;
26551
+ if (command) return [command, ...args];
26552
+ }
26553
+ return ["less"];
26554
+ }
26555
+ function flattenItem(item, index, renderItem) {
26556
+ const rendered = renderItem(item, index);
26557
+ return typeof rendered === "string" ? [rendered] : rendered;
26558
+ }
26559
+ function printWithoutPager(options) {
26560
+ const { items, header, renderItem } = options;
26561
+ if (header !== void 0) {
26562
+ console.log(header);
26563
+ }
26564
+ items.forEach((item, index) => {
26565
+ for (const line of flattenItem(item, index, renderItem)) {
26566
+ console.log(line);
26567
+ }
26568
+ });
26569
+ }
26570
+ async function runInteractivePager(options) {
26571
+ const { items, header, renderItem, disabled } = options;
26572
+ if (disabled || items.length === 0 || !isInteractiveEnvironment()) {
26573
+ printWithoutPager({ items, header, renderItem });
26574
+ return;
26575
+ }
26576
+ const command = resolvePagerCommand();
26577
+ try {
26578
+ const proc = Bun.spawn(command, {
26579
+ stdin: "pipe",
26580
+ stdout: "inherit",
26581
+ stderr: "inherit",
26582
+ env: { LESS: "FRX", ...process.env }
26583
+ });
26584
+ const writer = proc.stdin;
26585
+ try {
26586
+ if (header !== void 0) {
26587
+ writer.write(`${header}
26588
+ `);
26589
+ }
26590
+ items.forEach((item, index) => {
26591
+ for (const line of flattenItem(item, index, renderItem)) {
26592
+ writer.write(`${line}
26593
+ `);
26594
+ }
26595
+ });
26596
+ } catch {
26597
+ }
26598
+ try {
26599
+ writer.end();
26600
+ } catch {
26601
+ }
26602
+ await proc.exited;
26603
+ } catch {
26604
+ printWithoutPager({ items, header, renderItem });
26605
+ }
26606
+ }
26607
+
26539
26608
  // src/auth/commands.ts
26540
26609
  function getClientId() {
26541
26610
  const clientId = "client_01KEZ01W5Q3107GTBY51WQ67HT";
@@ -26547,6 +26616,44 @@ function getClientId() {
26547
26616
  );
26548
26617
  }
26549
26618
  var SAFE_ORGANIZATION_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
26619
+ var ORG_FILTER_FIELDS = ["name", "slug"];
26620
+ function parseOrgFilter(expression) {
26621
+ const match = expression.match(/^([^=!:]+)(!=|!:|=|:)(.*)$/);
26622
+ if (!match || !match[1] || !match[2] || match[3] === void 0) {
26623
+ throw new Error(
26624
+ `Invalid filter expression: '${expression}'. Format: <field><operator><value> (e.g. name:acme)`
26625
+ );
26626
+ }
26627
+ const field = match[1].trim();
26628
+ const operator = match[2];
26629
+ const value = match[3].trim();
26630
+ if (!ORG_FILTER_FIELDS.includes(field)) {
26631
+ throw new Error(
26632
+ `Unknown filter field '${field}'. Supported fields: ${ORG_FILTER_FIELDS.join(", ")}`
26633
+ );
26634
+ }
26635
+ if (value.length === 0) {
26636
+ throw new Error(
26637
+ `Invalid filter expression: '${expression}'. Value cannot be empty after the operator.`
26638
+ );
26639
+ }
26640
+ return { field, operator, value };
26641
+ }
26642
+ function matchesOrgFilter(item, filter) {
26643
+ const raw = filter.field === "name" ? item.name : item.slug;
26644
+ const cellLower = raw.toLowerCase();
26645
+ const valueLower = filter.value.toLowerCase();
26646
+ switch (filter.operator) {
26647
+ case "=":
26648
+ return cellLower === valueLower;
26649
+ case "!=":
26650
+ return cellLower !== valueLower;
26651
+ case ":":
26652
+ return cellLower.includes(valueLower);
26653
+ case "!:":
26654
+ return !cellLower.includes(valueLower);
26655
+ }
26656
+ }
26550
26657
  function sanitizeForOutput(value) {
26551
26658
  return value.replace(/[\r\n\t]/g, " ").trim();
26552
26659
  }
@@ -26796,12 +26903,25 @@ ${colorSuccess(`Authenticated as ${displayName} (${tokenResponse.user.email})`)}
26796
26903
  });
26797
26904
  }
26798
26905
  function createLogoutCommand() {
26799
- return new Command("logout").description("Log out of Trace").addHelpText(
26906
+ return new Command("logout").description("Log out of Trace").option("--no-confirm", "Skip confirmation prompt (for CI/scripts)").addHelpText(
26800
26907
  "after",
26801
26908
  `
26802
26909
  Examples:
26803
- tracecli auth logout`
26804
- ).action(() => {
26910
+ tracecli auth logout
26911
+ tracecli auth logout --no-confirm`
26912
+ ).action(async (options) => {
26913
+ if (options.confirm && process.stdin.isTTY && process.stdout.isTTY) {
26914
+ const readline = createInterface2({ input: process.stdin, output: process.stdout });
26915
+ try {
26916
+ const answer = await readline.question("Are you sure you want to log out? [Y/n] ");
26917
+ if (answer.trim().toLowerCase() === "n" || answer.trim().toLowerCase() === "no") {
26918
+ console.log("Logout cancelled.");
26919
+ return;
26920
+ }
26921
+ } finally {
26922
+ readline.close();
26923
+ }
26924
+ }
26805
26925
  const removed = removeToken();
26806
26926
  if (removed) {
26807
26927
  console.log(
@@ -26857,6 +26977,9 @@ Examples:
26857
26977
  const info = buildAuthStatusInfo(token, refreshToken, selectedOrganizationId);
26858
26978
  if (options.output === "json") {
26859
26979
  console.log(JSON.stringify(info, null, 2));
26980
+ if (!info.loggedIn) {
26981
+ process.exit(1);
26982
+ }
26860
26983
  return;
26861
26984
  }
26862
26985
  if (options.output === "tsv") {
@@ -26871,11 +26994,14 @@ Examples:
26871
26994
  info.activeOrg ?? ""
26872
26995
  ].join(" ")
26873
26996
  );
26997
+ if (!info.loggedIn) {
26998
+ process.exit(1);
26999
+ }
26874
27000
  return;
26875
27001
  }
26876
27002
  if (!info.loggedIn) {
26877
- console.log(colorError("Not logged in. Run `tracecli auth login` to authenticate."));
26878
- return;
27003
+ console.error(colorError("Not logged in. Run `tracecli auth login` to authenticate."));
27004
+ process.exit(1);
26879
27005
  }
26880
27006
  console.log("Logged in to Trace");
26881
27007
  if (info.userId) {
@@ -26915,7 +27041,12 @@ function createOrgListCommand() {
26915
27041
  throw new Error("--limit must be a positive integer");
26916
27042
  }
26917
27043
  return n;
26918
- }).option("-w, --web", "Open the organization settings in a browser").addHelpText(
27044
+ }).option(
27045
+ "-f, --filter <expression>",
27046
+ "Filter expression (repeatable). Fields: name, slug. Operators: = exact, != not exact, : contains, !: not contains",
27047
+ (val, prev) => [...prev, val],
27048
+ []
27049
+ ).option("-w, --web", "Open the organization settings in a browser").option("--no-pager", "Disable the interactive arrow-key pager for table output").addHelpText(
26919
27050
  "after",
26920
27051
  `
26921
27052
  Examples:
@@ -26923,70 +27054,95 @@ Examples:
26923
27054
  tracecli org list --output json
26924
27055
  tracecli org list --output tsv | cut -f1
26925
27056
  tracecli org list --limit 5
27057
+ tracecli org list --filter "name:acme"
27058
+ tracecli org list --filter "slug!=internal"
27059
+ tracecli org list --no-pager
26926
27060
  tracecli org list --web`
26927
- ).action(async (options) => {
26928
- if (options.web) {
26929
- const slug = await resolveOrganizationSlug();
26930
- const url2 = `${getAppBaseUrl()}/dashboard/${slug}/settings`;
26931
- const opened = await openBrowser(url2);
26932
- if (!opened) {
26933
- console.log(`Open in browser: ${url2}`);
27061
+ ).action(
27062
+ async (options) => {
27063
+ if (options.web) {
27064
+ const slug = await resolveOrganizationSlug();
27065
+ const url2 = `${getAppBaseUrl()}/dashboard/${slug}/settings`;
27066
+ const opened = await openBrowser(url2);
27067
+ if (!opened) {
27068
+ console.log(`Open in browser: ${url2}`);
27069
+ }
27070
+ return;
26934
27071
  }
26935
- return;
26936
- }
26937
- const organizationContext = await fetchOrganizationContext();
26938
- const allItems = organizationContext.organizations.map(
26939
- (organization) => ({
26940
- ...organization,
26941
- active: organization.organizationId === organizationContext.activeOrganization.organizationId,
26942
- selected: organization.organizationId === organizationContext.selectedOrganizationId
26943
- })
26944
- );
26945
- const items = options.limit !== void 0 ? allItems.slice(0, options.limit) : allItems;
26946
- if (options.output === "json") {
26947
- console.log(JSON.stringify(items, null, 2));
26948
- return;
26949
- }
26950
- if (options.output === "tsv") {
26951
- console.log("name slug organizationId active");
26952
- for (const item of items) {
27072
+ const parsedFilters = [];
27073
+ for (const expr of options.filter) {
27074
+ try {
27075
+ parsedFilters.push(parseOrgFilter(expr));
27076
+ } catch (err) {
27077
+ console.error(err instanceof Error ? err.message : String(err));
27078
+ process.exit(1);
27079
+ }
27080
+ }
27081
+ const organizationContext = await fetchOrganizationContext();
27082
+ const allItems = organizationContext.organizations.map(
27083
+ (organization) => ({
27084
+ ...organization,
27085
+ active: organization.organizationId === organizationContext.activeOrganization.organizationId,
27086
+ selected: organization.organizationId === organizationContext.selectedOrganizationId
27087
+ })
27088
+ );
27089
+ const filteredItems = allItems.filter(
27090
+ (item) => parsedFilters.every((f) => matchesOrgFilter(item, f))
27091
+ );
27092
+ const items = options.limit !== void 0 ? filteredItems.slice(0, options.limit) : filteredItems;
27093
+ if (options.output === "json") {
27094
+ console.log(JSON.stringify(items, null, 2));
27095
+ return;
27096
+ }
27097
+ if (options.output === "tsv") {
27098
+ console.log("name slug organizationId active");
27099
+ for (const item of items) {
27100
+ console.log(
27101
+ [
27102
+ tsvSafe(item.name),
27103
+ tsvSafe(item.slug),
27104
+ tsvSafe(item.organizationId),
27105
+ item.active ? "true" : "false"
27106
+ ].join(" ")
27107
+ );
27108
+ }
27109
+ return;
27110
+ }
27111
+ const headers = ["", "Name", "Slug", "Org ID"];
27112
+ const rows = items.map((item) => {
27113
+ const labels = [item.active ? "active" : null, item.selected ? "selected" : null].filter(
27114
+ Boolean
27115
+ );
27116
+ const marker = item.active ? "*" : " ";
27117
+ const nameSuffix = labels.length > 0 ? ` [${labels.join(", ")}]` : "";
27118
+ return [
27119
+ marker,
27120
+ sanitizeForOutput(item.name) + nameSuffix,
27121
+ sanitizeForOutput(item.slug),
27122
+ item.organizationId
27123
+ ];
27124
+ });
27125
+ const colWidths = headers.map(
27126
+ (h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
27127
+ );
27128
+ const pad = (s, w3) => s.padEnd(w3);
27129
+ const headerLine = headers.map((h, i) => pad(h, colWidths[i] ?? h.length)).join(" ");
27130
+ const rowLines = rows.map(
27131
+ (row) => row.map((cell, i) => pad(cell, colWidths[i] ?? 0)).join(" ")
27132
+ );
27133
+ await runInteractivePager({
27134
+ items,
27135
+ header: headerLine,
27136
+ renderItem: (_item, i) => [rowLines[i] ?? ""],
27137
+ disabled: options.pager === false
27138
+ });
27139
+ if (organizationContext.staleSelectionDetected) {
26953
27140
  console.log(
26954
- [
26955
- tsvSafe(item.name),
26956
- tsvSafe(item.slug),
26957
- tsvSafe(item.organizationId),
26958
- item.active ? "true" : "false"
26959
- ].join(" ")
27141
+ "\nYour previous organization selection is no longer available and was reset."
26960
27142
  );
26961
27143
  }
26962
- return;
26963
- }
26964
- const headers = ["", "Name", "Slug", "Org ID"];
26965
- const rows = items.map((item) => {
26966
- const labels = [item.active ? "active" : null, item.selected ? "selected" : null].filter(
26967
- Boolean
26968
- );
26969
- const marker = item.active ? "*" : " ";
26970
- const nameSuffix = labels.length > 0 ? ` [${labels.join(", ")}]` : "";
26971
- return [
26972
- marker,
26973
- sanitizeForOutput(item.name) + nameSuffix,
26974
- sanitizeForOutput(item.slug),
26975
- item.organizationId
26976
- ];
26977
- });
26978
- const colWidths = headers.map(
26979
- (h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
26980
- );
26981
- const pad = (s, w3) => s.padEnd(w3);
26982
- console.log(headers.map((h, i) => pad(h, colWidths[i] ?? h.length)).join(" "));
26983
- for (const row of rows) {
26984
- console.log(row.map((cell, i) => pad(cell, colWidths[i] ?? 0)).join(" "));
26985
27144
  }
26986
- if (organizationContext.staleSelectionDetected) {
26987
- console.log("\nYour previous organization selection is no longer available and was reset.");
26988
- }
26989
- });
27145
+ );
26990
27146
  }
26991
27147
  function createOrgCurrentCommand() {
26992
27148
  return new Command("current").description("Show the active organization used by CLI commands").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option("-w, --web", "Open the organization settings in a browser").addHelpText(
@@ -27043,15 +27199,42 @@ Examples:
27043
27199
  });
27044
27200
  }
27045
27201
  function createOrgSwitchCommand() {
27046
- return new Command("switch").description("Switch the active organization for subsequent CLI commands").argument("[organization]", "Organization ID, slug, name, or menu selection index").addHelpText(
27202
+ return new Command("switch").description("Switch the active organization for subsequent CLI commands").argument("[organization]", "Organization ID, slug, name, or menu selection index").option("-w, --web", "Open the organization settings page in a browser instead of switching").addHelpText(
27047
27203
  "after",
27048
27204
  `
27049
27205
  Examples:
27050
27206
  tracecli org switch
27051
27207
  tracecli org switch engineering
27052
- tracecli org switch org_01KJTJFFG497Z1C54Z07AP6NA3`
27053
- ).action(async (organization) => {
27208
+ tracecli org switch org_01KJTJFFG497Z1C54Z07AP6NA3
27209
+ tracecli org switch engineering --web`
27210
+ ).action(async (organization, options) => {
27054
27211
  const normalizedOrganizationInput = organization?.trim() ?? "";
27212
+ if (options.web) {
27213
+ if (normalizedOrganizationInput.length > 0) {
27214
+ const organizationContext2 = await fetchOrganizationContext();
27215
+ const selection = resolveOrganizationSelectionInput(
27216
+ organizationContext2.organizations,
27217
+ normalizedOrganizationInput
27218
+ );
27219
+ if ("error" in selection) {
27220
+ console.error(selection.error);
27221
+ process.exit(1);
27222
+ }
27223
+ const url2 = `${getAppBaseUrl()}/dashboard/${selection.organization.slug}/settings`;
27224
+ const opened = await openBrowser(url2);
27225
+ if (!opened) {
27226
+ console.log(`Open in browser: ${url2}`);
27227
+ }
27228
+ } else {
27229
+ const slug = await resolveOrganizationSlug();
27230
+ const url2 = `${getAppBaseUrl()}/dashboard/${slug}/settings`;
27231
+ const opened = await openBrowser(url2);
27232
+ if (!opened) {
27233
+ console.log(`Open in browser: ${url2}`);
27234
+ }
27235
+ }
27236
+ return;
27237
+ }
27055
27238
  if (normalizedOrganizationInput.length === 0 && (!process.stdin.isTTY || !process.stdout.isTTY)) {
27056
27239
  console.error(
27057
27240
  "Missing required argument 'organization'. Run `tracecli org switch <organization>` in non-interactive environments."
@@ -27610,7 +27793,7 @@ function createListCommand() {
27610
27793
  }
27611
27794
  return Math.min(n, MAX_LIMIT);
27612
27795
  }
27613
- ).option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option("-w, --web", "Open the pentests dashboard in a browser").addHelpText(
27796
+ ).option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option("-w, --web", "Open the pentests dashboard in a browser").option("--no-pager", "Disable the interactive arrow-key pager for table output").addHelpText(
27614
27797
  "after",
27615
27798
  `
27616
27799
  Examples:
@@ -27619,84 +27802,95 @@ Examples:
27619
27802
  tracecli pentest list --limit 10
27620
27803
  tracecli pentest list --output json | jq '.[].title'
27621
27804
  tracecli pentest list --output tsv | cut -f1,2
27805
+ tracecli pentest list --no-pager
27622
27806
  tracecli pentest list --web`
27623
- ).action(async (options) => {
27624
- if (options.web) {
27625
- const slug = await resolveOrganizationSlug();
27626
- const url2 = `${getAppBaseUrl()}/dashboard/${slug}/pentests`;
27627
- const opened = await openBrowser(url2);
27628
- if (!opened) {
27629
- console.log(`Open in browser: ${url2}`);
27807
+ ).action(
27808
+ async (options) => {
27809
+ if (options.web) {
27810
+ const slug = await resolveOrganizationSlug();
27811
+ const url2 = `${getAppBaseUrl()}/dashboard/${slug}/pentests`;
27812
+ const opened = await openBrowser(url2);
27813
+ if (!opened) {
27814
+ console.log(`Open in browser: ${url2}`);
27815
+ }
27816
+ return;
27630
27817
  }
27631
- return;
27632
- }
27633
- const organizationId = await resolveOrganizationId();
27634
- const result = await authenticatedRequest(
27635
- (client) => client.pentest.listPentests({
27636
- params: { organizationId }
27637
- })
27638
- );
27639
- if (result.status === 401) {
27640
- console.error("Authentication failed. Run `tracecli auth login` to re-authenticate.");
27641
- process.exit(1);
27642
- }
27643
- if (result.status !== 200) {
27644
- console.error(`Failed to list pentests (status ${result.status}).`);
27645
- process.exit(1);
27646
- }
27647
- let filteredPentests = result.body;
27648
- if (options.status) {
27649
- filteredPentests = filteredPentests.filter((p2) => p2.status === options.status);
27650
- }
27651
- const limit = options.limit ?? DEFAULT_LIMIT;
27652
- const pentests = filteredPentests.slice(0, limit);
27653
- const isJson = options.output === "json";
27654
- const isTsv = options.output === "tsv";
27655
- if (pentests.length === 0) {
27656
- if (isJson) {
27657
- console.log("[]");
27658
- } else if (isTsv) {
27659
- console.log("displayId title status startedAt endedAt findingsTotal");
27660
- } else {
27661
- console.log("No pentests found.");
27818
+ const organizationId = await resolveOrganizationId();
27819
+ const result = await authenticatedRequest(
27820
+ (client) => client.pentest.listPentests({
27821
+ params: { organizationId }
27822
+ })
27823
+ );
27824
+ if (result.status === 401) {
27825
+ console.error("Authentication failed. Run `tracecli auth login` to re-authenticate.");
27826
+ process.exit(1);
27662
27827
  }
27663
- return;
27664
- }
27665
- if (isJson) {
27666
- console.log(JSON.stringify(pentests, null, 2));
27667
- return;
27668
- }
27669
- if (isTsv) {
27670
- console.log("displayId title status startedAt endedAt findingsTotal");
27671
- for (const pt of pentests) {
27672
- const cols = [
27673
- `PT-${pt.displayId}`,
27674
- tsvSafe2(pt.title),
27675
- pt.status,
27676
- pt.startedAt ? String(pt.startedAt) : "",
27677
- pt.endedAt ? String(pt.endedAt) : "",
27678
- String(pt.findingCounts.total)
27679
- ];
27680
- console.log(cols.join(" "));
27828
+ if (result.status !== 200) {
27829
+ console.error(`Failed to list pentests (status ${result.status}).`);
27830
+ process.exit(1);
27681
27831
  }
27682
- return;
27683
- }
27684
- console.log(`Found ${pentests.length} pentest${pentests.length === 1 ? "" : "s"}:
27685
- `);
27686
- for (const pt of pentests) {
27687
- const status = formatStatus(pt.status);
27688
- const started = formatDate(pt.startedAt);
27689
- const ended = formatDate(pt.endedAt);
27690
- const dateRange = pt.startedAt ? `${started} \u2013 ${ended}` : "Not started";
27691
- const findings = pt.findingCounts.total;
27692
- console.log(` [PT-${pt.displayId}] ${pt.title}`);
27693
- console.log(` Status: ${status} | Findings: ${findings} | ${dateRange}`);
27694
- if (pt.description) {
27695
- console.log(` ${pt.description}`);
27832
+ let filteredPentests = result.body;
27833
+ if (options.status) {
27834
+ filteredPentests = filteredPentests.filter((p2) => p2.status === options.status);
27696
27835
  }
27697
- console.log("");
27836
+ const limit = options.limit ?? DEFAULT_LIMIT;
27837
+ const pentests = filteredPentests.slice(0, limit);
27838
+ const isJson = options.output === "json";
27839
+ const isTsv = options.output === "tsv";
27840
+ if (pentests.length === 0) {
27841
+ if (isJson) {
27842
+ console.log("[]");
27843
+ } else if (isTsv) {
27844
+ console.log("displayId title status startedAt endedAt findingsTotal");
27845
+ } else {
27846
+ console.log("No pentests found.");
27847
+ }
27848
+ return;
27849
+ }
27850
+ if (isJson) {
27851
+ console.log(JSON.stringify(pentests, null, 2));
27852
+ return;
27853
+ }
27854
+ if (isTsv) {
27855
+ console.log("displayId title status startedAt endedAt findingsTotal");
27856
+ for (const pt of pentests) {
27857
+ const cols = [
27858
+ `PT-${pt.displayId}`,
27859
+ tsvSafe2(pt.title),
27860
+ pt.status,
27861
+ pt.startedAt ? String(pt.startedAt) : "",
27862
+ pt.endedAt ? String(pt.endedAt) : "",
27863
+ String(pt.findingCounts.total)
27864
+ ];
27865
+ console.log(cols.join(" "));
27866
+ }
27867
+ return;
27868
+ }
27869
+ const header = `Found ${pentests.length} pentest${pentests.length === 1 ? "" : "s"}:`;
27870
+ await runInteractivePager({
27871
+ items: pentests,
27872
+ header,
27873
+ renderItem: (pt) => renderPentestLines(pt),
27874
+ disabled: options.pager === false
27875
+ });
27698
27876
  }
27699
- });
27877
+ );
27878
+ }
27879
+ function renderPentestLines(pt) {
27880
+ const status = formatStatus(pt.status);
27881
+ const started = formatDate(pt.startedAt);
27882
+ const ended = formatDate(pt.endedAt);
27883
+ const dateRange = pt.startedAt ? `${started} \u2013 ${ended}` : "Not started";
27884
+ const findings = pt.findingCounts.total;
27885
+ const lines = [
27886
+ ` [PT-${pt.displayId}] ${pt.title}`,
27887
+ ` Status: ${status} | Findings: ${findings} | ${dateRange}`
27888
+ ];
27889
+ if (pt.description) {
27890
+ lines.push(` ${pt.description}`);
27891
+ }
27892
+ lines.push("");
27893
+ return lines;
27700
27894
  }
27701
27895
  function createFindingsListCommand() {
27702
27896
  return new Command("list").description("List findings for a pentest").requiredOption("--pentest <id>", "Pentest display ID (e.g. PT-3) or UUID").option(
@@ -27717,7 +27911,7 @@ function createFindingsListCommand() {
27717
27911
  }
27718
27912
  return Math.min(n, MAX_LIMIT);
27719
27913
  }
27720
- ).option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option("-w, --web", "Open the pentests dashboard in a browser").addHelpText(
27914
+ ).option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option("-w, --web", "Open the pentest detail page in a browser").option("--no-pager", "Disable the interactive arrow-key pager for table output").addHelpText(
27721
27915
  "after",
27722
27916
  `
27723
27917
  Examples:
@@ -27726,12 +27920,15 @@ Examples:
27726
27920
  tracecli pentest findings list --pentest PT-3 --status OPEN --limit 20
27727
27921
  tracecli pentest findings list --pentest PT-3 --output json
27728
27922
  tracecli pentest findings list --pentest PT-3 --output tsv | cut -f1,4
27923
+ tracecli pentest findings list --pentest PT-3 --no-pager
27729
27924
  tracecli pentest findings list --pentest PT-3 --web`
27730
27925
  ).action(
27731
27926
  async (options) => {
27732
27927
  if (options.web) {
27733
27928
  const slug = await resolveOrganizationSlug();
27734
- const url2 = `${getAppBaseUrl()}/dashboard/${slug}/pentests`;
27929
+ const numericMatch = options.pentest.match(/^(?:PT-)?(\d+)$/i);
27930
+ const pentestUrlId = numericMatch?.[1] ?? options.pentest;
27931
+ const url2 = `${getAppBaseUrl()}/dashboard/${slug}/pentests/${pentestUrlId}`;
27735
27932
  const opened = await openBrowser(url2);
27736
27933
  if (!opened) {
27737
27934
  console.log(`Open in browser: ${url2}`);
@@ -27802,27 +27999,33 @@ Examples:
27802
27999
  }
27803
28000
  return;
27804
28001
  }
27805
- console.log(`Found ${findings.length} finding${findings.length === 1 ? "" : "s"}:
27806
- `);
27807
- for (const finding of findings) {
27808
- const sev = formatFindingSeverity(finding.severity);
27809
- const severityLabel = colorSeverity(finding.severity, sev);
27810
- const status = formatStatus(finding.status);
27811
- const cwes = finding.cweIds.length > 0 ? ` | ${finding.cweIds.join(", ")}` : "";
27812
- const score = finding.cvssScore != null ? ` | CVSS ${finding.cvssScore.toFixed(1)}` : "";
27813
- console.log(` [${severityLabel}] ${finding.title}`);
27814
- console.log(` ${finding.category} \xB7 ${status}${cwes}${score}`);
27815
- if (finding.repositories.length > 0) {
27816
- for (const repo of finding.repositories) {
27817
- const location = repo.filePath ? `${repo.repositoryName} \xB7 ${repo.filePath}${repo.startLine != null ? `:${repo.startLine}` : ""}` : repo.repositoryName;
27818
- console.log(` ${location}`);
27819
- }
27820
- }
27821
- console.log("");
27822
- }
28002
+ const header = `Found ${findings.length} finding${findings.length === 1 ? "" : "s"}:`;
28003
+ await runInteractivePager({
28004
+ items: findings,
28005
+ header,
28006
+ renderItem: (finding) => renderPentestFindingLines(finding),
28007
+ disabled: options.pager === false
28008
+ });
27823
28009
  }
27824
28010
  );
27825
28011
  }
28012
+ function renderPentestFindingLines(finding) {
28013
+ const sev = formatFindingSeverity(finding.severity);
28014
+ const severityLabel = colorSeverity(finding.severity, sev);
28015
+ const status = formatStatus(finding.status);
28016
+ const cwes = finding.cweIds.length > 0 ? ` | ${finding.cweIds.join(", ")}` : "";
28017
+ const score = finding.cvssScore != null ? ` | CVSS ${finding.cvssScore.toFixed(1)}` : "";
28018
+ const lines = [
28019
+ ` [${severityLabel}] ${finding.title}`,
28020
+ ` ${finding.category} \xB7 ${status}${cwes}${score}`
28021
+ ];
28022
+ for (const repo of finding.repositories) {
28023
+ const location = repo.filePath ? `${repo.repositoryName} \xB7 ${repo.filePath}${repo.startLine != null ? `:${repo.startLine}` : ""}` : repo.repositoryName;
28024
+ lines.push(` ${location}`);
28025
+ }
28026
+ lines.push("");
28027
+ return lines;
28028
+ }
27826
28029
  function createFindingsCommand() {
27827
28030
  const findings = new Command("findings").description("Manage pentest findings");
27828
28031
  findings.addCommand(createFindingsListCommand());
@@ -27885,6 +28088,7 @@ function extractRepoName(input) {
27885
28088
  var BRANCH_NAME_PATTERN = /^[a-zA-Z0-9._/+-]+$/;
27886
28089
  var TRACE_GIT_ASKPASS_TOKEN_ENV = "TRACE_GIT_ASKPASS_TOKEN";
27887
28090
  var DEFAULT_LIMIT2 = 30;
28091
+ var DEFAULT_PAGE = 1;
27888
28092
  var MAX_LIMIT2 = 100;
27889
28093
  var REPOSITORY_PLATFORMS = ["GITHUB", "GITLAB", "BITBUCKET"];
27890
28094
  var REPOSITORY_STATUSES = ["CONNECTED", "DISCONNECTED"];
@@ -28102,6 +28306,44 @@ function parseListLimit(value) {
28102
28306
  }
28103
28307
  return n;
28104
28308
  }
28309
+ function parseListPage(value) {
28310
+ const n = Number(value);
28311
+ if (!Number.isInteger(n) || n < 1) {
28312
+ throw new Error("--page must be a positive integer");
28313
+ }
28314
+ return n;
28315
+ }
28316
+ function paginateResults(items, options) {
28317
+ if (options.all) {
28318
+ return {
28319
+ items: [...items],
28320
+ pagination: null
28321
+ };
28322
+ }
28323
+ const limit = options.limit ?? DEFAULT_LIMIT2;
28324
+ const page = options.page ?? DEFAULT_PAGE;
28325
+ const totalItems = items.length;
28326
+ const totalPages = Math.max(1, Math.ceil(totalItems / limit));
28327
+ if (page > totalPages) {
28328
+ throw new Error(
28329
+ `--page ${page} is out of range for ${totalItems} matching results with --limit ${limit}. Use --page between 1 and ${totalPages}, or pass --all.`
28330
+ );
28331
+ }
28332
+ const startIndex = (page - 1) * limit;
28333
+ const endIndex = Math.min(startIndex + limit, totalItems);
28334
+ return {
28335
+ items: items.slice(startIndex, endIndex),
28336
+ pagination: {
28337
+ totalItems,
28338
+ totalPages,
28339
+ page,
28340
+ hasNextPage: page < totalPages,
28341
+ hasPreviousPage: page > 1,
28342
+ startIndex,
28343
+ endIndex
28344
+ }
28345
+ };
28346
+ }
28105
28347
  function printRepositoriesTsv(repositories) {
28106
28348
  console.log("repositoryUrl platform status defaultBranch language");
28107
28349
  for (const repo of repositories) {
@@ -28116,7 +28358,7 @@ function printRepositoriesTsv(repositories) {
28116
28358
  );
28117
28359
  }
28118
28360
  }
28119
- function printRepositoriesTable(repositories) {
28361
+ function buildRepositoriesTable(repositories) {
28120
28362
  const headers = ["Repository URL", "Platform", "Status"];
28121
28363
  const rows = repositories.map(
28122
28364
  (repo) => [
@@ -28129,15 +28371,27 @@ function printRepositoriesTable(repositories) {
28129
28371
  (h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
28130
28372
  );
28131
28373
  const pad = (s, w3) => s.padEnd(w3);
28132
- console.log(headers.map((h, i) => pad(h, colWidths[i] ?? h.length)).join(" "));
28133
- for (const row of rows) {
28134
- console.log(row.map((cell, i) => pad(cell, colWidths[i] ?? 0)).join(" "));
28135
- }
28374
+ const headerLine = headers.map((h, i) => pad(h, colWidths[i] ?? h.length)).join(" ");
28375
+ const rowLines = rows.map((row) => row.map((cell, i) => pad(cell, colWidths[i] ?? 0)).join(" "));
28376
+ return { headerLine, rowLines };
28136
28377
  }
28137
28378
  function collectFilterExpression(value, previous) {
28138
28379
  previous.push(value);
28139
28380
  return previous;
28140
28381
  }
28382
+ function printPaginationHint(pagination, noun, nextPageFilterHint) {
28383
+ if (!pagination || pagination.totalPages <= 1) {
28384
+ return;
28385
+ }
28386
+ const shownStart = pagination.startIndex + 1;
28387
+ const shownEnd = pagination.endIndex;
28388
+ console.log(
28389
+ `Showing ${shownStart}-${shownEnd} of ${pagination.totalItems} ${noun} (page ${pagination.page}/${pagination.totalPages}).`
28390
+ );
28391
+ if (pagination.hasNextPage) {
28392
+ console.log(`Use --page ${pagination.page + 1}${nextPageFilterHint} (or --all) to see more.`);
28393
+ }
28394
+ }
28141
28395
  function extractErrorMessage(body) {
28142
28396
  if (typeof body === "object" && body !== null && "message" in body && typeof body.message === "string") {
28143
28397
  const message = body.message.trim();
@@ -28263,9 +28517,14 @@ function addRepositoryFilterOptions(command) {
28263
28517
  parseListLimit,
28264
28518
  DEFAULT_LIMIT2
28265
28519
  ).option(
28520
+ "--page <n>",
28521
+ `Page number to display when results exceed --limit (default ${DEFAULT_PAGE})`,
28522
+ parseListPage,
28523
+ DEFAULT_PAGE
28524
+ ).option("--all", "Display all matching repositories (disables pagination)").option(
28266
28525
  "--dependency <pkg>",
28267
28526
  "Filter repositories that use a dependency (e.g. lodash, lodash@4.17.21, @types/node@20)"
28268
- ).option("-w, --web", "Open the repositories dashboard in a browser");
28527
+ ).option("-w, --web", "Open the repositories dashboard in a browser").option("--no-pager", "Disable the interactive arrow-key pager for table output");
28269
28528
  }
28270
28529
  async function runRepositoryListCommand(options) {
28271
28530
  if (options.web) {
@@ -28314,8 +28573,17 @@ async function runRepositoryListCommand(options) {
28314
28573
  console.log("No repositories found.");
28315
28574
  return;
28316
28575
  }
28317
- const limit = options.limit ?? DEFAULT_LIMIT2;
28318
- const displayedRepositories = filteredRepositories.slice(0, limit);
28576
+ let displayedRepositories;
28577
+ let pagination = null;
28578
+ try {
28579
+ const paginatedResult = paginateResults(filteredRepositories, options);
28580
+ displayedRepositories = paginatedResult.items;
28581
+ pagination = paginatedResult.pagination;
28582
+ } catch (error48) {
28583
+ const message = error48 instanceof Error ? error48.message : "Unknown pagination error.";
28584
+ console.error(`Invalid pagination options: ${message}`);
28585
+ process.exit(1);
28586
+ }
28319
28587
  if (options.output === "json") {
28320
28588
  console.log(JSON.stringify(displayedRepositories, null, 2));
28321
28589
  return;
@@ -28324,7 +28592,18 @@ async function runRepositoryListCommand(options) {
28324
28592
  printRepositoriesTsv(displayedRepositories);
28325
28593
  return;
28326
28594
  }
28327
- printRepositoriesTable(displayedRepositories);
28595
+ const { headerLine, rowLines } = buildRepositoriesTable(displayedRepositories);
28596
+ await runInteractivePager({
28597
+ items: displayedRepositories,
28598
+ header: headerLine,
28599
+ renderItem: (_repo, i) => [rowLines[i] ?? ""],
28600
+ disabled: options.pager === false
28601
+ });
28602
+ printPaginationHint(
28603
+ pagination,
28604
+ "repositories",
28605
+ options.filter.length > 0 ? " with the same --filter flags" : ""
28606
+ );
28328
28607
  }
28329
28608
  async function runDependencyFilter(organizationId, options) {
28330
28609
  const dependency = options.dependency;
@@ -28378,8 +28657,17 @@ async function runDependencyFilter(organizationId, options) {
28378
28657
  console.log(`No repositories found using dependency '${label}'.`);
28379
28658
  return;
28380
28659
  }
28381
- const limit = options.limit ?? DEFAULT_LIMIT2;
28382
- const displayedRepos = repos.slice(0, limit);
28660
+ let displayedRepos;
28661
+ let pagination = null;
28662
+ try {
28663
+ const paginatedResult = paginateResults(repos, options);
28664
+ displayedRepos = paginatedResult.items;
28665
+ pagination = paginatedResult.pagination;
28666
+ } catch (error48) {
28667
+ const message = error48 instanceof Error ? error48.message : "Unknown pagination error.";
28668
+ console.error(`Invalid pagination options: ${message}`);
28669
+ process.exit(1);
28670
+ }
28383
28671
  if (options.output === "json") {
28384
28672
  console.log(JSON.stringify(displayedRepos, null, 2));
28385
28673
  return;
@@ -28391,10 +28679,12 @@ async function runDependencyFilter(organizationId, options) {
28391
28679
  }
28392
28680
  return;
28393
28681
  }
28394
- for (const repo of displayedRepos) {
28395
- const versions = repo.versions.join(", ");
28396
- console.log(`${repo.fullName} (${versions})`);
28397
- }
28682
+ await runInteractivePager({
28683
+ items: displayedRepos,
28684
+ renderItem: (repo) => [`${repo.fullName} (${repo.versions.join(", ")})`],
28685
+ disabled: options.pager === false
28686
+ });
28687
+ printPaginationHint(pagination, "repositories", "");
28398
28688
  }
28399
28689
  function createListCommand2() {
28400
28690
  return addRepositoryFilterOptions(
@@ -28405,6 +28695,8 @@ function createListCommand2() {
28405
28695
  Examples:
28406
28696
  tracecli repo list
28407
28697
  tracecli repo list --platform GITHUB --status CONNECTED
28698
+ tracecli repo list --page 2 --limit 30
28699
+ tracecli repo list --all
28408
28700
  tracecli repo list --output json | jq '.[].repositoryUrl'
28409
28701
  tracecli repo list --output tsv | cut -f1
28410
28702
  tracecli repo list --filter "language:typescript" --limit 10
@@ -28422,7 +28714,11 @@ function createFilterCommand() {
28422
28714
  `
28423
28715
  Examples:
28424
28716
  tracecli repo filter --filter "platform=GITHUB" --filter "status=CONNECTED"
28425
- tracecli repo filter --language typescript --output json
28717
+ tracecli repo filter --page 2 --limit 30
28718
+ tracecli repo filter --all
28719
+ tracecli repo filter --language typescript --limit 10
28720
+ tracecli repo filter --output json | jq '.[].repositoryUrl'
28721
+ tracecli repo filter --output tsv | cut -f1
28426
28722
  tracecli repo filter --web`
28427
28723
  ).action(async (options) => {
28428
28724
  await runRepositoryListCommand(options);
@@ -29093,22 +29389,24 @@ function formatSeverity(severity) {
29093
29389
  return severity;
29094
29390
  }
29095
29391
  }
29096
- function printVulnerabilities(vulnerabilities) {
29097
- const sorted = [...vulnerabilities].sort(
29392
+ function renderVulnerabilityLines(vuln) {
29393
+ const sev = formatSeverity(vuln.severity);
29394
+ const displaySeverity = colorSeverity(vuln.severity, sev);
29395
+ const status = formatStatus(vuln.status);
29396
+ const repo = vuln.repositoryName;
29397
+ const file2 = vuln.codeSnippet.filePath;
29398
+ const line = vuln.codeSnippet.startLine;
29399
+ return [
29400
+ ` [${displaySeverity}] ${vuln.title}`,
29401
+ ` ${repo} \xB7 ${file2}:${line} \xB7 ${status}`,
29402
+ ` ${vuln.traceId}`,
29403
+ ""
29404
+ ];
29405
+ }
29406
+ function sortVulnerabilitiesBySeverity(vulnerabilities) {
29407
+ return [...vulnerabilities].sort(
29098
29408
  (a, b) => (SEVERITY_ORDER[a.severity] ?? 99) - (SEVERITY_ORDER[b.severity] ?? 99)
29099
29409
  );
29100
- for (const vuln of sorted) {
29101
- const sev = formatSeverity(vuln.severity);
29102
- const displaySeverity = colorSeverity(vuln.severity, sev);
29103
- const status = formatStatus(vuln.status);
29104
- const repo = vuln.repositoryName;
29105
- const file2 = vuln.codeSnippet.filePath;
29106
- const line = vuln.codeSnippet.startLine;
29107
- console.log(` [${displaySeverity}] ${vuln.title}`);
29108
- console.log(` ${repo} \xB7 ${file2}:${line} \xB7 ${status}`);
29109
- console.log(` ${vuln.traceId}`);
29110
- console.log("");
29111
- }
29112
29410
  }
29113
29411
  function sanitizeTsvField(value) {
29114
29412
  return value.replace(/[\t\n\r]/g, " ");
@@ -29177,7 +29475,7 @@ function printVulnerabilityDetailTsv(vulnerability) {
29177
29475
  console.log(cols.join(" "));
29178
29476
  }
29179
29477
  function createGetCommand() {
29180
- return new Command("get").description("Get a vulnerability by display ID or UUID").argument("<id>", "Vulnerability display ID (e.g. TRACE-123 or 123) or UUID").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").addHelpText(
29478
+ return new Command("get").description("Get a vulnerability by display ID or UUID").argument("<id>", "Vulnerability display ID (e.g. TRACE-123 or 123) or UUID").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option("-w, --web", "Open the vulnerability detail page in a browser").addHelpText(
29181
29479
  "after",
29182
29480
  `
29183
29481
  Examples:
@@ -29185,8 +29483,18 @@ Examples:
29185
29483
  tracecli vulnerabilities get 123
29186
29484
  tracecli vulnerabilities get 00000000-0000-4000-8000-000000000123
29187
29485
  tracecli vulnerabilities get TRACE-123 --output json
29188
- tracecli vulnerabilities get TRACE-123 --output tsv`
29486
+ tracecli vulnerabilities get TRACE-123 --output tsv
29487
+ tracecli vulnerabilities get TRACE-123 --web`
29189
29488
  ).action(async (identifier, options) => {
29489
+ if (options.web) {
29490
+ const slug = await resolveOrganizationSlug();
29491
+ const url2 = `${getAppBaseUrl()}/dashboard/${slug}/vulnerabilities/${identifier}`;
29492
+ const opened = await openBrowser(url2);
29493
+ if (!opened) {
29494
+ console.log(`Open in browser: ${url2}`);
29495
+ }
29496
+ return;
29497
+ }
29190
29498
  const organizationId = await resolveOrganizationId();
29191
29499
  const result = await authenticatedRequest(
29192
29500
  (client) => client.vulnerability.getVulnerability({
@@ -29249,7 +29557,7 @@ function createListCommand3() {
29249
29557
  "Additional filter expression: field=value or field!=value. Repeatable. Supported fields: vulnerabilityType, cweId, scanMode (and any other API-supported field).",
29250
29558
  (value, previous) => [...previous, value],
29251
29559
  []
29252
- ).option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option("-w, --web", "Open the vulnerabilities dashboard in a browser").addHelpText(
29560
+ ).option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option("-w, --web", "Open the vulnerabilities dashboard in a browser").option("--no-pager", "Disable the interactive arrow-key pager for table output").addHelpText(
29253
29561
  "after",
29254
29562
  `
29255
29563
  Examples:
@@ -29260,6 +29568,7 @@ Examples:
29260
29568
  tracecli vulnerabilities list --filter vulnerabilityType=SAST --filter scanMode=sast
29261
29569
  tracecli vulnerabilities list --output json | jq '.[].title'
29262
29570
  tracecli vulnerabilities list --output tsv | cut -f1,7
29571
+ tracecli vulnerabilities list --no-pager
29263
29572
  tracecli vulnerabilities list --web`
29264
29573
  ).action(
29265
29574
  async (options) => {
@@ -29342,10 +29651,15 @@ Examples:
29342
29651
  } else if (isTsv) {
29343
29652
  printVulnerabilitiesTsv(items);
29344
29653
  } else {
29654
+ const sorted = sortVulnerabilitiesBySeverity(items);
29345
29655
  const total = collected.length;
29346
- console.log(`Found ${total} vulnerabilit${total === 1 ? "y" : "ies"}:
29347
- `);
29348
- printVulnerabilities(items);
29656
+ const header = `Found ${total} vulnerabilit${total === 1 ? "y" : "ies"}:`;
29657
+ await runInteractivePager({
29658
+ items: sorted,
29659
+ header,
29660
+ renderItem: (vuln) => renderVulnerabilityLines(vuln),
29661
+ disabled: options.pager === false
29662
+ });
29349
29663
  }
29350
29664
  }
29351
29665
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securewithtrace",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "description": "Official Trace CLI for vulnerability intelligence in your terminal",
5
5
  "type": "module",
6
6
  "bin": {