ardent-cli-beta 0.0.13 → 0.0.15

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.
@@ -2,7 +2,7 @@ import { ArdentCommand } from '../../command.js';
2
2
  import { listBranches } from '../../branches/client.js';
3
3
  import { replaceBranchCache } from '../../branches/context.js';
4
4
  import { requireConnector } from '../../connectors/context.js';
5
- import { terminalText } from '../../terminal.js';
5
+ import { renderTable } from '../../table.js';
6
6
  export default class BranchList extends ArdentCommand {
7
7
  static description = 'List branches in the selected connector';
8
8
  static helpGroup = 'Branches';
@@ -13,9 +13,15 @@ export default class BranchList extends ArdentCommand {
13
13
  if (!this.jsonEnabled()) {
14
14
  if (branches.length === 0)
15
15
  this.log('No branches found.');
16
- const width = Math.max(0, ...branches.map(({ name }) => terminalText(name).length));
17
- for (const branch of branches)
18
- this.log(`${branch.id === updated.selectedBranch?.id ? '*' : ' '} ${terminalText(branch.name).padEnd(width)} ${terminalText(branch.state)}`);
16
+ else {
17
+ const lines = renderTable(branches, [
18
+ { header: 'Selected', value: ({ id }) => id === updated.selectedBranch?.id ? '●' : '' },
19
+ { header: 'Title', maxWidth: 30, shrinkPriority: 0, value: ({ name }) => name },
20
+ { header: 'Status', shrinkPriority: 1, value: ({ state }) => state },
21
+ ], process.stdout.columns ?? 120);
22
+ for (const line of lines)
23
+ this.log(line);
24
+ }
19
25
  }
20
26
  return { branches };
21
27
  }
@@ -2,7 +2,7 @@ import { loadSession } from '../../auth/credentials.js';
2
2
  import { ArdentCommand } from '../../command.js';
3
3
  import { listConnectors } from '../../connectors/client.js';
4
4
  import { replaceConnectorCache } from '../../connectors/context.js';
5
- import { terminalText } from '../../terminal.js';
5
+ import { renderTable } from '../../table.js';
6
6
  export default class ConnectorList extends ArdentCommand {
7
7
  static description = 'List connectors in the selected project';
8
8
  static helpGroup = 'Connectors';
@@ -17,9 +17,20 @@ export default class ConnectorList extends ArdentCommand {
17
17
  if (!this.jsonEnabled()) {
18
18
  if (connectors.length === 0)
19
19
  this.log('No connectors found.');
20
- const nameWidth = Math.max(0, ...connectors.map(({ name }) => terminalText(name).length));
21
- for (const connector of connectors) {
22
- this.log(`${terminalText(connector.name).padEnd(nameWidth)} ${terminalText(connector.status)}`);
20
+ else {
21
+ const lines = renderTable(connectors, [
22
+ { header: 'Title', maxWidth: 30, shrinkPriority: 1, value: ({ name }) => name },
23
+ { header: 'Status', shrinkPriority: 2, value: ({ status }) => status },
24
+ {
25
+ header: 'Source',
26
+ maxWidth: 50,
27
+ minWidth: 20,
28
+ shrinkPriority: 0,
29
+ value: ({ source }) => `${source.host}:${source.port}/${source.database}`,
30
+ },
31
+ ], process.stdout.columns ?? 120);
32
+ for (const line of lines)
33
+ this.log(line);
23
34
  }
24
35
  }
25
36
  return { connectors };
@@ -1,7 +1,7 @@
1
1
  import { loadSession } from '../../auth/credentials.js';
2
2
  import { ArdentCommand } from '../../command.js';
3
3
  import { listMemberships, listReadableInvitations } from '../../members/client.js';
4
- import { terminalText } from '../../terminal.js';
4
+ import { renderTable } from '../../table.js';
5
5
  export default class MemberList extends ArdentCommand {
6
6
  static description = 'List organization members and pending invitations';
7
7
  static helpGroup = 'Members';
@@ -17,22 +17,27 @@ export default class MemberList extends ArdentCommand {
17
17
  if (!this.jsonEnabled()) {
18
18
  const rows = [
19
19
  ...memberships.map((membership) => ({
20
- email: terminalText(membership.email),
21
- role: terminalText(membership.role_display_name),
20
+ email: membership.email,
21
+ role: membership.role_display_name,
22
22
  status: 'Active',
23
23
  })),
24
24
  ...readableInvitations.map((invitation) => ({
25
- email: terminalText(invitation.email),
26
- role: terminalText(invitation.role_display_name),
25
+ email: invitation.email,
26
+ role: invitation.role_display_name,
27
27
  status: 'Pending',
28
28
  })),
29
29
  ];
30
30
  if (rows.length === 0)
31
31
  this.log('No members found.');
32
- const emailWidth = Math.max(0, ...rows.map(({ email }) => email.length));
33
- const roleWidth = Math.max(0, ...rows.map(({ role }) => role.length));
34
- for (const row of rows)
35
- this.log(`${row.email.padEnd(emailWidth)} ${row.role.padEnd(roleWidth)} ${row.status}`);
32
+ else {
33
+ const lines = renderTable(rows, [
34
+ { header: 'Email', maxWidth: 40, minWidth: 12, shrinkPriority: 0, value: ({ email }) => email },
35
+ { header: 'Role', maxWidth: 20, shrinkPriority: 1, value: ({ role }) => role },
36
+ { header: 'Status', shrinkPriority: 2, value: ({ status }) => status },
37
+ ], process.stdout.columns ?? 120);
38
+ for (const line of lines)
39
+ this.log(line);
40
+ }
36
41
  if (invitations === undefined)
37
42
  this.log('Pending invitations are not visible with this login.');
38
43
  }
@@ -3,7 +3,7 @@ import { loadSession } from '../../auth/credentials.js';
3
3
  import { ArdentCommand } from '../../command.js';
4
4
  import { listProjects } from '../../projects/client.js';
5
5
  import { replaceProjectCache } from '../../projects/context.js';
6
- import { terminalText } from '../../terminal.js';
6
+ import { renderTable } from '../../table.js';
7
7
  export default class ProjectList extends ArdentCommand {
8
8
  static description = 'List projects';
9
9
  static helpGroup = 'Projects';
@@ -18,10 +18,23 @@ export default class ProjectList extends ArdentCommand {
18
18
  if (!this.jsonEnabled()) {
19
19
  if (projects.length === 0)
20
20
  this.log('No projects found.');
21
- for (const project of projects) {
22
- const name = terminalText(project.name);
23
- const line = project.id === refreshedSession.selectedProject?.id ? `● ${name}` : ` ${name}`;
24
- this.log(project.id === refreshedSession.selectedProject?.id ? ux.colorize('green', line) : line);
21
+ else {
22
+ const lines = renderTable(projects, [
23
+ {
24
+ header: 'Title',
25
+ maxWidth: 30,
26
+ shrinkPriority: 0,
27
+ value: ({ id, name }) => `${id === refreshedSession.selectedProject?.id ? '●' : ' '} ${name}`,
28
+ },
29
+ ], process.stdout.columns ?? 120);
30
+ const selectedIndex = projects.findIndex(({ id }) => id === refreshedSession.selectedProject?.id);
31
+ if (selectedIndex >= 0) {
32
+ const lineIndex = selectedIndex + 3;
33
+ const value = lines[lineIndex].slice(2, -2).trimEnd();
34
+ lines[lineIndex] = `${lines[lineIndex].slice(0, 2)}${ux.colorize('green', value)}${lines[lineIndex].slice(value.length + 2)}`;
35
+ }
36
+ for (const line of lines)
37
+ this.log(line);
25
38
  }
26
39
  }
27
40
  return { projects };
@@ -1,28 +1,45 @@
1
1
  import { Args } from '@oclif/core';
2
2
  import { loadSession } from '../../auth/credentials.js';
3
3
  import { ArdentCommand } from '../../command.js';
4
- import { cachedProjectNamed, selectProject } from '../../projects/context.js';
4
+ import { listProjects } from '../../projects/client.js';
5
+ import { cachedProjectNamed, replaceProjectCache, selectProject } from '../../projects/context.js';
5
6
  import { terminalText } from '../../terminal.js';
6
7
  export default class ProjectSwitch extends ArdentCommand {
7
8
  static args = {
8
9
  name: Args.string({ description: 'Project name', required: true }),
9
10
  };
10
- static description = 'Switch to a project from the local project list';
11
+ static description = 'Switch to a project in the logged-in organization';
11
12
  static helpGroup = 'Projects';
12
13
  async run() {
13
14
  const { args } = await this.parse(ProjectSwitch);
14
15
  const name = args.name.trim();
15
16
  if (!name)
16
17
  this.error('Project name cannot be empty.');
17
- const session = await loadSession(this.config.configDir);
18
+ let session = await loadSession(this.config.configDir);
18
19
  if (!session?.organization)
19
20
  this.error('You must be logged in to run this command. Run ardent-beta login.');
20
- const project = cachedProjectNamed(session, name);
21
- if (!project)
22
- this.error(`Project ${JSON.stringify(name)} not found. Run ardent-beta project list.`);
21
+ // A cached name switches without a request, which is what makes this cheap
22
+ // enough to run constantly. A miss is not an answer though: the cache only
23
+ // holds what the last `project list` returned, so a project created since
24
+ // then reported that it did not exist. Only the organization's list can say
25
+ // that, and it is already the authority `project delete` defers to.
26
+ let project = cachedProjectNamed(session, name);
27
+ if (!project) {
28
+ const projects = await listProjects(session.token, session.organization.id);
29
+ // Refused before anything is written. replaceProjectCache is not a read:
30
+ // it persists, it drops the connector and branch context when the
31
+ // selection moves, and it adopts the sole remaining project when the
32
+ // selected one is gone. Running it first would let a mistyped name exit
33
+ // non-zero having quietly selected a different project underneath.
34
+ project = projects.find((candidate) => candidate.name === name);
35
+ if (!project) {
36
+ this.error(`Project ${JSON.stringify(name)} not found in organization ${JSON.stringify(session.organization.name)}.`);
37
+ }
38
+ session = await replaceProjectCache(this.config.configDir, session, projects);
39
+ }
23
40
  await selectProject(this.config.configDir, session, project);
24
41
  if (!this.jsonEnabled())
25
42
  this.log(`✓ Switched to project ${terminalText(project.name)}`);
26
- return project;
43
+ return { id: project.id, name: project.name };
27
44
  }
28
45
  }
@@ -0,0 +1,8 @@
1
+ export interface TableColumn<Row> {
2
+ header: string;
3
+ maxWidth?: number;
4
+ minWidth?: number;
5
+ shrinkPriority?: number;
6
+ value: (row: Row) => string;
7
+ }
8
+ export declare function renderTable<Row>(rows: Row[], columns: TableColumn<Row>[], terminalWidth?: number): string[];
package/dist/table.js ADDED
@@ -0,0 +1,53 @@
1
+ import stringWidth from 'string-width';
2
+ import { terminalText } from './terminal.js';
3
+ const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
4
+ export function renderTable(rows, columns, terminalWidth = 120) {
5
+ const headers = columns.map(({ header }) => terminalText(header));
6
+ const values = rows.map((row) => columns.map(({ value }) => terminalText(value(row))));
7
+ const widths = columns.map((column, index) => {
8
+ const headerWidth = stringWidth(headers[index]);
9
+ const naturalWidth = Math.max(headerWidth, ...values.map((row) => stringWidth(row[index])));
10
+ return Math.min(naturalWidth, Math.max(headerWidth, column.maxWidth ?? naturalWidth));
11
+ });
12
+ const preferredMinimumWidths = columns.map((column, index) => Math.min(widths[index], Math.max(stringWidth(headers[index]), column.minWidth ?? stringWidth(headers[index]))));
13
+ const frameWidth = columns.length * 3 + 1;
14
+ const availableWidth = Number.isFinite(terminalWidth) ? Math.floor(terminalWidth) : 120;
15
+ const narrowestTableWidth = frameWidth + columns.length;
16
+ let overflow = widths.reduce((total, width) => total + width, frameWidth) - Math.max(narrowestTableWidth, availableWidth);
17
+ const shrinkOrder = columns
18
+ .map((column, index) => ({ index, priority: column.shrinkPriority ?? index }))
19
+ .sort((left, right) => left.priority - right.priority);
20
+ for (const minimumWidths of [preferredMinimumWidths, columns.map(() => 1)]) {
21
+ for (const { index } of shrinkOrder) {
22
+ const reduction = Math.min(Math.max(0, overflow), widths[index] - minimumWidths[index]);
23
+ widths[index] -= reduction;
24
+ overflow -= reduction;
25
+ }
26
+ }
27
+ const border = (left, divider, right) => `${left}${widths.map((width) => '─'.repeat(width + 2)).join(divider)}${right}`;
28
+ const cell = (value, width) => {
29
+ let display = value;
30
+ if (stringWidth(value) > width) {
31
+ const contentWidth = width - stringWidth('…');
32
+ let truncatedWidth = 0;
33
+ display = '';
34
+ for (const { segment } of graphemeSegmenter.segment(value)) {
35
+ const segmentWidth = stringWidth(segment);
36
+ if (truncatedWidth + segmentWidth > contentWidth)
37
+ break;
38
+ display += segment;
39
+ truncatedWidth += segmentWidth;
40
+ }
41
+ display += '…';
42
+ }
43
+ return display + ' '.repeat(width - stringWidth(display));
44
+ };
45
+ const line = (row) => `│ ${row.map((value, index) => cell(value, widths[index])).join(' │ ')} │`;
46
+ return [
47
+ border('┌', '┬', '┐'),
48
+ line(headers),
49
+ border('├', '┼', '┤'),
50
+ ...values.map(line),
51
+ border('└', '┴', '┘'),
52
+ ];
53
+ }
@@ -546,7 +546,7 @@
546
546
  "required": true
547
547
  }
548
548
  },
549
- "description": "Switch to a project from the local project list",
549
+ "description": "Switch to a project in the logged-in organization",
550
550
  "flags": {
551
551
  "json": {
552
552
  "description": "Format output as json.",
@@ -574,5 +574,5 @@
574
574
  ]
575
575
  }
576
576
  },
577
- "version": "0.0.13"
577
+ "version": "0.0.15"
578
578
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ardent-cli-beta",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "The Ardent command-line interface",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -41,7 +41,8 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@oclif/core": "^5.0.0",
44
- "@oclif/plugin-autocomplete": "^4.0.0"
44
+ "@oclif/plugin-autocomplete": "^4.0.0",
45
+ "string-width": "^4.2.3"
45
46
  },
46
47
  "devDependencies": {
47
48
  "@types/node": "^22.0.0",