securewithtrace 0.17.0 → 0.18.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 +1 -0
  2. package/dist/index.js +216 -67
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -73,6 +73,7 @@ tracecli auth logout
73
73
  ```bash
74
74
  tracecli org list
75
75
  tracecli org current
76
+ tracecli org switch
76
77
  tracecli org switch <organization-id-or-slug>
77
78
  ```
78
79
 
package/dist/index.js CHANGED
@@ -3352,6 +3352,9 @@ var {
3352
3352
  Help
3353
3353
  } = import_index.default;
3354
3354
 
3355
+ // src/auth/commands.ts
3356
+ import { createInterface } from "readline/promises";
3357
+
3355
3358
  // ../../node_modules/.bun/@ts-rest+core@3.53.0-rc.1+f9199aabcde8d6a0/node_modules/@ts-rest/core/index.esm.mjs
3356
3359
  var util;
3357
3360
  (function(util2) {
@@ -25253,6 +25256,65 @@ Use an organization ID or exact slug.`
25253
25256
  error: `Organization '${input}' not found. Run \`tracecli org list\` to see available organizations.`
25254
25257
  };
25255
25258
  }
25259
+ function resolveOrganizationSelectionInput(organizations, input) {
25260
+ const trimmedInput = input.trim();
25261
+ if (trimmedInput.length === 0) {
25262
+ return { error: "Organization selection cannot be empty." };
25263
+ }
25264
+ if (/^\d+$/.test(trimmedInput)) {
25265
+ const byNumericId = organizations.find(
25266
+ (organization) => normalizeForComparison(organization.organizationId) === normalizeForComparison(trimmedInput)
25267
+ );
25268
+ if (byNumericId) {
25269
+ return { organization: byNumericId };
25270
+ }
25271
+ const selectedIndex = Number.parseInt(trimmedInput, 10);
25272
+ if (selectedIndex < 1 || selectedIndex > organizations.length) {
25273
+ return {
25274
+ error: `Selection '${trimmedInput}' is out of range. Enter a number between 1 and ${organizations.length}.`
25275
+ };
25276
+ }
25277
+ const selectedOrganization = organizations[selectedIndex - 1];
25278
+ if (!selectedOrganization) {
25279
+ return {
25280
+ error: "Failed to resolve organization selection. Please try again with an organization ID or slug."
25281
+ };
25282
+ }
25283
+ return { organization: selectedOrganization };
25284
+ }
25285
+ return findOrganizationByInput(organizations, trimmedInput);
25286
+ }
25287
+ async function promptForOrganizationSelection(organizations, activeOrganizationId) {
25288
+ console.log("Select an organization:");
25289
+ for (const [index, organization] of organizations.entries()) {
25290
+ const marker = organization.organizationId === activeOrganizationId ? "*" : " ";
25291
+ console.log(
25292
+ `${marker} ${index + 1}) ${sanitizeForOutput(organization.name)} (${sanitizeForOutput(organization.slug)}) - ${organization.organizationId}`
25293
+ );
25294
+ }
25295
+ console.log("");
25296
+ const readline = createInterface({
25297
+ input: process.stdin,
25298
+ output: process.stdout
25299
+ });
25300
+ try {
25301
+ while (true) {
25302
+ const input = await readline.question(
25303
+ "Enter number, organization ID, slug, or name (press Enter to cancel): "
25304
+ );
25305
+ if (input.trim().length === 0) {
25306
+ return null;
25307
+ }
25308
+ const selection = resolveOrganizationSelectionInput(organizations, input);
25309
+ if ("organization" in selection) {
25310
+ return selection.organization;
25311
+ }
25312
+ console.error(selection.error);
25313
+ }
25314
+ } finally {
25315
+ readline.close();
25316
+ }
25317
+ }
25256
25318
  async function refreshAfterOrganizationSwitch(workosOrganizationId, refreshForOrganization = (organizationId) => refreshStoredAccessTokenForOrganization(organizationId)) {
25257
25319
  if (!workosOrganizationId || workosOrganizationId.trim().length === 0) {
25258
25320
  return false;
@@ -25260,7 +25322,13 @@ async function refreshAfterOrganizationSwitch(workosOrganizationId, refreshForOr
25260
25322
  return refreshForOrganization(workosOrganizationId);
25261
25323
  }
25262
25324
  function createLoginCommand() {
25263
- return new Command("login").description("Authenticate with Trace").action(async () => {
25325
+ return new Command("login").description("Authenticate with Trace").addHelpText(
25326
+ "after",
25327
+ `
25328
+ Examples:
25329
+ tracecli auth login
25330
+ tracecli auth logout && tracecli auth login # re-authenticate`
25331
+ ).action(async () => {
25264
25332
  const existingToken = readToken();
25265
25333
  if (existingToken) {
25266
25334
  console.log("You are already logged in. Run `trace auth logout` first to re-authenticate.");
@@ -25294,7 +25362,12 @@ Authenticated as ${displayName} (${tokenResponse.user.email})`);
25294
25362
  });
25295
25363
  }
25296
25364
  function createLogoutCommand() {
25297
- return new Command("logout").description("Log out of Trace").action(() => {
25365
+ return new Command("logout").description("Log out of Trace").addHelpText(
25366
+ "after",
25367
+ `
25368
+ Examples:
25369
+ tracecli auth logout`
25370
+ ).action(() => {
25298
25371
  const removed = removeToken();
25299
25372
  if (removed) {
25300
25373
  console.log("Logged out successfully. Local session data removed from ~/.trace/");
@@ -25303,66 +25376,96 @@ function createLogoutCommand() {
25303
25376
  }
25304
25377
  });
25305
25378
  }
25379
+ function buildAuthStatusInfo(token, refreshToken, selectedOrganizationId) {
25380
+ if (!token) {
25381
+ return { loggedIn: false, expired: false, hasRefreshToken: refreshToken !== null };
25382
+ }
25383
+ const base = {
25384
+ loggedIn: true,
25385
+ expired: false,
25386
+ hasRefreshToken: refreshToken !== null,
25387
+ ...selectedOrganizationId ? { activeOrg: selectedOrganizationId } : {}
25388
+ };
25389
+ try {
25390
+ const parts = token.split(".");
25391
+ const payload = parts[1];
25392
+ if (!payload) {
25393
+ return base;
25394
+ }
25395
+ const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf-8"));
25396
+ const expiry = decoded.exp ? new Date(decoded.exp * 1e3) : null;
25397
+ const isExpired = expiry ? expiry < /* @__PURE__ */ new Date() : false;
25398
+ return {
25399
+ ...base,
25400
+ ...decoded.sub ? { userId: decoded.sub } : {},
25401
+ ...decoded.org_id ? { orgId: decoded.org_id } : {},
25402
+ ...expiry ? { expires: expiry.toISOString() } : {},
25403
+ expired: isExpired
25404
+ };
25405
+ } catch {
25406
+ return base;
25407
+ }
25408
+ }
25306
25409
  function createStatusCommand() {
25307
- return new Command("status").description("Show current authentication status").action(() => {
25410
+ return new Command("status").description("Show current authentication status").option("-o, --output <format>", "Output format: table (default), json, tsv").addHelpText(
25411
+ "after",
25412
+ `
25413
+ Examples:
25414
+ tracecli auth status
25415
+ tracecli auth status --output json
25416
+ tracecli auth status --output tsv`
25417
+ ).action((options) => {
25308
25418
  const token = readToken();
25309
25419
  const refreshToken = readRefreshToken();
25310
25420
  const selectedOrganizationId = readSelectedOrganizationId();
25311
- if (token) {
25312
- try {
25313
- const parts = token.split(".");
25314
- const payload = parts[1];
25315
- if (payload) {
25316
- const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf-8"));
25317
- const expiry = decoded.exp ? new Date(decoded.exp * 1e3) : null;
25318
- const isExpired = expiry ? expiry < /* @__PURE__ */ new Date() : false;
25319
- console.log("Logged in to Trace");
25320
- if (decoded.sub) {
25321
- console.log(` User ID: ${decoded.sub}`);
25322
- }
25323
- if (decoded.org_id) {
25324
- console.log(` Org ID: ${decoded.org_id}`);
25325
- }
25326
- if (selectedOrganizationId) {
25327
- console.log(` Active Org (CLI): ${selectedOrganizationId}`);
25328
- }
25329
- if (expiry) {
25330
- console.log(` Expires: ${expiry.toISOString()}${isExpired ? " (EXPIRED)" : ""}`);
25331
- }
25332
- console.log(
25333
- ` Refresh: ${refreshToken ? "Available (auto-refresh enabled)" : "Not available"}`
25334
- );
25335
- if (isExpired) {
25336
- if (refreshToken) {
25337
- console.log(
25338
- "\nYour access token is expired, but a refresh token is available and will be used automatically."
25339
- );
25340
- } else {
25341
- console.log(
25342
- "\nYour token has expired. Run `trace auth login` after logging out to re-authenticate."
25343
- );
25344
- }
25345
- }
25346
- } else {
25347
- console.log("Logged in to Trace (access token stored at ~/.trace/token)");
25348
- if (selectedOrganizationId) {
25349
- console.log(` Active Org (CLI): ${selectedOrganizationId}`);
25350
- }
25351
- console.log(
25352
- ` Refresh: ${refreshToken ? "Available (auto-refresh enabled)" : "Not available"}`
25353
- );
25354
- }
25355
- } catch {
25356
- console.log("Logged in to Trace (access token stored at ~/.trace/token)");
25357
- if (selectedOrganizationId) {
25358
- console.log(` Active Org (CLI): ${selectedOrganizationId}`);
25359
- }
25421
+ const info = buildAuthStatusInfo(token, refreshToken, selectedOrganizationId);
25422
+ if (options.output === "json") {
25423
+ console.log(JSON.stringify(info, null, 2));
25424
+ return;
25425
+ }
25426
+ if (options.output === "tsv") {
25427
+ console.log(
25428
+ [
25429
+ String(info.loggedIn),
25430
+ String(info.expired),
25431
+ String(info.hasRefreshToken),
25432
+ info.userId ?? "",
25433
+ info.orgId ?? "",
25434
+ info.activeOrg ?? ""
25435
+ ].join(" ")
25436
+ );
25437
+ return;
25438
+ }
25439
+ if (!info.loggedIn) {
25440
+ console.log("Not logged in. Run `tracecli auth login` to authenticate.");
25441
+ return;
25442
+ }
25443
+ console.log("Logged in to Trace");
25444
+ if (info.userId) {
25445
+ console.log(` User ID: ${info.userId}`);
25446
+ }
25447
+ if (info.orgId) {
25448
+ console.log(` Org ID: ${info.orgId}`);
25449
+ }
25450
+ if (info.activeOrg) {
25451
+ console.log(` Active Org (CLI): ${info.activeOrg}`);
25452
+ }
25453
+ if (info.expires) {
25454
+ console.log(` Expires: ${info.expires}${info.expired ? " (EXPIRED)" : ""}`);
25455
+ }
25456
+ console.log(
25457
+ ` Refresh: ${info.hasRefreshToken ? "Available (auto-refresh enabled)" : "Not available"}`
25458
+ );
25459
+ if (info.expired) {
25460
+ if (info.hasRefreshToken) {
25461
+ console.log(
25462
+ "\nYour access token is expired, but a refresh token is available and will be used automatically."
25463
+ );
25464
+ } else {
25360
25465
  console.log(
25361
- ` Refresh: ${refreshToken ? "Available (auto-refresh enabled)" : "Not available"}`
25466
+ "\nYour token has expired. Run `tracecli auth login` after logging out to re-authenticate."
25362
25467
  );
25363
25468
  }
25364
- } else {
25365
- console.log("Not logged in. Run `trace auth login` to authenticate.");
25366
25469
  }
25367
25470
  });
25368
25471
  }
@@ -25459,27 +25562,57 @@ Examples:
25459
25562
  });
25460
25563
  }
25461
25564
  function createOrgSwitchCommand() {
25462
- return new Command("switch").description("Switch the active organization for subsequent CLI commands").argument("<organization>", "Organization ID, slug, or name").action(async (organization) => {
25463
- const organizationContext = await fetchOrganizationContext();
25464
- const match = findOrganizationByInput(organizationContext.organizations, organization);
25465
- if ("error" in match) {
25466
- console.error(match.error);
25565
+ return new Command("switch").description("Switch the active organization for subsequent CLI commands").argument("[organization]", "Organization ID, slug, name, or menu selection index").addHelpText(
25566
+ "after",
25567
+ `
25568
+ Examples:
25569
+ tracecli org switch
25570
+ tracecli org switch engineering
25571
+ tracecli org switch org_01KJTJFFG497Z1C54Z07AP6NA3`
25572
+ ).action(async (organization) => {
25573
+ const normalizedOrganizationInput = organization?.trim() ?? "";
25574
+ if (normalizedOrganizationInput.length === 0 && (!process.stdin.isTTY || !process.stdout.isTTY)) {
25575
+ console.error(
25576
+ "Missing required argument 'organization'. Run `tracecli org switch <organization>` in non-interactive environments."
25577
+ );
25467
25578
  process.exit(1);
25468
25579
  }
25469
- if (!isSafeOrganizationId(match.organization.organizationId)) {
25580
+ const organizationContext = await fetchOrganizationContext();
25581
+ let selectedOrganization = null;
25582
+ if (normalizedOrganizationInput.length > 0) {
25583
+ const selection = resolveOrganizationSelectionInput(
25584
+ organizationContext.organizations,
25585
+ normalizedOrganizationInput
25586
+ );
25587
+ if ("error" in selection) {
25588
+ console.error(selection.error);
25589
+ process.exit(1);
25590
+ }
25591
+ selectedOrganization = selection.organization;
25592
+ } else {
25593
+ selectedOrganization = await promptForOrganizationSelection(
25594
+ organizationContext.organizations,
25595
+ organizationContext.activeOrganization.organizationId
25596
+ );
25597
+ if (!selectedOrganization) {
25598
+ console.error("Organization switch cancelled.");
25599
+ process.exit(1);
25600
+ }
25601
+ }
25602
+ if (!isSafeOrganizationId(selectedOrganization.organizationId)) {
25470
25603
  console.error(
25471
- `Cannot switch to organization '${match.organization.organizationId}': invalid organization ID format.`
25604
+ `Cannot switch to organization '${selectedOrganization.organizationId}': invalid organization ID format.`
25472
25605
  );
25473
25606
  process.exit(1);
25474
25607
  }
25475
- writeSelectedOrganizationId(match.organization.organizationId);
25608
+ writeSelectedOrganizationId(selectedOrganization.organizationId);
25476
25609
  const refreshed = await refreshAfterOrganizationSwitch(
25477
- match.organization.workosOrganizationId
25610
+ selectedOrganization.workosOrganizationId
25478
25611
  );
25479
25612
  console.log(
25480
- `Active organization set to ${sanitizeForOutput(match.organization.name)} (${sanitizeForOutput(match.organization.slug)})`
25613
+ `Active organization set to ${sanitizeForOutput(selectedOrganization.name)} (${sanitizeForOutput(selectedOrganization.slug)})`
25481
25614
  );
25482
- console.log(` Org ID: ${match.organization.organizationId}`);
25615
+ console.log(` Org ID: ${selectedOrganization.organizationId}`);
25483
25616
  if (!refreshed) {
25484
25617
  console.warn(
25485
25618
  "Token refresh for the selected organization was not completed. If repository commands fail, run `tracecli auth login` again."
@@ -26398,6 +26531,7 @@ function parseListLimit(value) {
26398
26531
  return n;
26399
26532
  }
26400
26533
  function printRepositoriesTsv(repositories) {
26534
+ console.log("repositoryUrl platform status defaultBranch language");
26401
26535
  for (const repo of repositories) {
26402
26536
  console.log(
26403
26537
  [
@@ -26463,6 +26597,10 @@ async function runRepositoryListCommand(options) {
26463
26597
  params: { organizationId }
26464
26598
  })
26465
26599
  );
26600
+ if (result.status === 401) {
26601
+ console.error("Authentication failed. Run `tracecli auth login` to re-authenticate.");
26602
+ process.exit(1);
26603
+ }
26466
26604
  if (result.status !== 200) {
26467
26605
  console.error(`Failed to list repositories (status ${result.status}).`);
26468
26606
  process.exit(1);
@@ -26515,6 +26653,10 @@ async function runDependencyFilter(organizationId, options) {
26515
26653
  }
26516
26654
  })
26517
26655
  );
26656
+ if (result.status === 401) {
26657
+ console.error("Authentication failed. Run `tracecli auth login` to re-authenticate.");
26658
+ process.exit(1);
26659
+ }
26518
26660
  if (result.status !== 200) {
26519
26661
  console.error(`Failed to search dependencies (status ${result.status}).`);
26520
26662
  process.exit(1);
@@ -26643,7 +26785,14 @@ async function fetchCloneToken(repoUrl) {
26643
26785
  }
26644
26786
  }
26645
26787
  function createCloneCommand() {
26646
- return new Command("clone").description("Clone a repository").argument("<repository>", "Repository URL or owner/repo shorthand").argument("[directory]", "Directory to clone into").option("--branch <branch>", "Branch to clone").option("--depth <depth>", "Shallow clone depth", parseCloneDepth).action(
26788
+ return new Command("clone").description("Clone a repository").argument("<repository>", "Repository URL or owner/repo shorthand").argument("[directory]", "Directory to clone into").option("--branch <branch>", "Branch to clone").option("--depth <depth>", "Shallow clone depth", parseCloneDepth).addHelpText(
26789
+ "after",
26790
+ `
26791
+ Examples:
26792
+ tracecli repo clone securewithtrace/trace
26793
+ tracecli repo clone securewithtrace/trace trace-src
26794
+ tracecli repo clone https://github.com/owner/repo --branch main --depth 1`
26795
+ ).action(
26647
26796
  async (repository, directory, options) => {
26648
26797
  const cloneUrl = resolveRepoUrl(repository);
26649
26798
  const targetDir = directory ?? extractRepoName(repository);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securewithtrace",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Official Trace CLI for vulnerability intelligence in your terminal",
5
5
  "type": "module",
6
6
  "bin": {