ardent-cli-beta 0.0.17 → 0.0.18

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.
package/NOTICE ADDED
@@ -0,0 +1,4 @@
1
+ Ardent CLI
2
+ Copyright 2026 ArdentAI, Inc.
3
+
4
+ Licensed under the Apache License, Version 2.0. See LICENSE.
package/README.md CHANGED
@@ -13,7 +13,7 @@ the Ardent frontend and returns the approved API key through a loopback callback
13
13
  bound with PKCE. `ardent-beta login --token <token>` validates and stores an existing
14
14
  API token for CI and automation; `ARDENT_TOKEN` supplies the same flag
15
15
  non-interactively. `ardent-beta status` and `ardent-beta logout` inspect and remove that
16
- local session. The CLI always talks to Ardent's production control plane. Oclif
16
+ local session. The CLI defaults to Ardent's production control plane. Oclif
17
17
  provides help, version, autocomplete, and native JSON command behavior. Resource
18
18
  context is retained from login. Project listing, creation, deletion, and local
19
19
  context switching are implemented. Organization member listing, invitation,
@@ -22,6 +22,14 @@ and registration are implemented; registration follows connector status, while
22
22
  credential delivery and replication readiness remain owed. Connector-scoped branch
23
23
  creation, deletion, listing, and local switching are implemented.
24
24
 
25
+ [openapi.json](openapi.json) describes the HTTP endpoints used by the CLI and
26
+ ships in the npm package. It is a documentation snapshot of the control-plane
27
+ specification from source revision `fbd09a11b9f094ebd13a7d58bda75eb0bb45b016`,
28
+ not proof of deployed compatibility. To refresh it, replace it with the reviewed
29
+ control-plane specification, record its source revision here, and run `make ci`.
30
+ The package test verifies archive contents; it does not detect upstream API
31
+ changes. Ownership and snapshot limits are recorded in [docs/DECISIONS.md](docs/DECISIONS.md).
32
+
25
33
  Set `ARDENT_API_URL` to point the CLI at a development control plane. Without
26
34
  it, the CLI uses `https://production.tryardent.com`.
27
35
 
@@ -63,3 +71,9 @@ context. Create and delete follow the operation in an interactive terminal;
63
71
  non-interactive commands and `--json` return the accepted operation immediately.
64
72
 
65
73
  Before changing this repository, read [ARDENT_CLI_DEVELOPMENT.md](ARDENT_CLI_DEVELOPMENT.md).
74
+
75
+ Running `npm test` or `make ci` requires Node 22+, npm and `tar`. The interactive
76
+ tests also require macOS's system `script`, or Linux's util-linux `script` with
77
+ `-e` and `-c` support, on `PATH`. These are test prerequisites; the installed CLI
78
+ does not invoke `script`. The PTY tests skip other operating systems and fail on
79
+ Linux if the required utility is missing or incompatible.
@@ -2,7 +2,10 @@ export declare const API_URL: string;
2
2
  export declare class APIError extends Error {
3
3
  readonly status: number;
4
4
  readonly code?: string | undefined;
5
- constructor(message: string, status: number, code?: string | undefined);
5
+ readonly workflowID?: string | undefined;
6
+ readonly location?: string | undefined;
7
+ constructor(message: string, status: number, code?: string | undefined, workflowID?: string | undefined, location?: string | undefined);
6
8
  }
9
+ export declare function requestDurableMutation<T>(token: string, path: string, init: RequestInit, refreshCommand: string): Promise<T>;
7
10
  export declare function request<T>(token: string, path: string, init?: RequestInit): Promise<T>;
8
11
  export declare function requestWithoutAuthentication<T>(path: string, init?: RequestInit): Promise<T>;
@@ -1,12 +1,39 @@
1
+ import { terminalText } from '../terminal.js';
1
2
  const DEFAULT_TIMEOUT_MS = 10_000;
3
+ const DURABLE_MUTATION_TIMEOUT_MS = 90_000;
2
4
  export const API_URL = process.env.ARDENT_API_URL || 'https://production.tryardent.com';
3
5
  export class APIError extends Error {
4
6
  status;
5
7
  code;
6
- constructor(message, status, code) {
8
+ workflowID;
9
+ location;
10
+ constructor(message, status, code, workflowID, location) {
7
11
  super(message);
8
12
  this.status = status;
9
13
  this.code = code;
14
+ this.workflowID = workflowID;
15
+ this.location = location;
16
+ }
17
+ }
18
+ export async function requestDurableMutation(token, path, init, refreshCommand) {
19
+ // The API can await its durable workflow for 75 seconds. Allow response transit
20
+ // time too, without replacing an explicit caller deadline or retrying a write.
21
+ try {
22
+ return await request(token, path, {
23
+ ...init,
24
+ signal: init.signal ?? AbortSignal.timeout(DURABLE_MUTATION_TIMEOUT_MS),
25
+ });
26
+ }
27
+ catch (error) {
28
+ if (error instanceof APIError) {
29
+ if (error.status !== 503 || error.code !== 'workflow_result_timeout')
30
+ throw error;
31
+ const workflow = error.workflowID ? ` Workflow: ${terminalText(error.workflowID)}.` : '';
32
+ const location = error.location ? ` Check its status with an authenticated GET ${terminalText(error.location)}.` : '';
33
+ error.message = `The mutation was still running when the API stopped waiting.${workflow}${location} Run ${refreshCommand} to refresh resource state; confirm the workflow outcome before retrying.`;
34
+ throw error;
35
+ }
36
+ throw new Error(`The mutation response was not received completely; completion is unknown and work may still be running. Run ${refreshCommand} to refresh resource state and confirm the outcome before retrying.`, { cause: error });
10
37
  }
11
38
  }
12
39
  export async function request(token, path, init = {}) {
@@ -39,14 +66,15 @@ async function requestJSON(path, init) {
39
66
  }
40
67
  const text = await response.text();
41
68
  if (!response.ok) {
42
- let detail = {};
69
+ let detail;
43
70
  try {
44
71
  detail = JSON.parse(text);
45
72
  }
46
73
  catch {
47
74
  // Plain-text errors still receive the status-based fallback below.
48
75
  }
49
- throw new APIError(detail.message ?? `Ardent API request failed with HTTP ${response.status}.`, response.status, detail.error);
76
+ const fields = typeof detail === 'object' && detail !== null ? detail : {};
77
+ throw new APIError(typeof fields.message === 'string' ? fields.message : `Ardent API request failed with HTTP ${response.status}.`, response.status, typeof fields.error === 'string' ? fields.error : undefined, typeof fields.workflow_id === 'string' ? fields.workflow_id : undefined, response.headers.get('Location') ?? undefined);
50
78
  }
51
79
  if (response.status === 204)
52
80
  return undefined;
package/dist/command.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Command } from '@oclif/core';
2
2
  export declare abstract class ArdentCommand extends Command {
3
3
  static enableJsonFlag: boolean;
4
+ protected catch(error: Error): Promise<void>;
4
5
  }
package/dist/command.js CHANGED
@@ -1,4 +1,14 @@
1
1
  import { Command } from '@oclif/core';
2
+ import { APIError } from './api/client.js';
3
+ import { terminalText } from './terminal.js';
2
4
  export class ArdentCommand extends Command {
3
5
  static enableJsonFlag = true;
6
+ async catch(error) {
7
+ if (error instanceof APIError && !this.jsonEnabled()) {
8
+ const displayError = new APIError(terminalText(error.message), error.status, error.code === undefined ? undefined : terminalText(String(error.code)));
9
+ displayError.stack = error.stack?.replace(error.message, () => displayError.message);
10
+ return super.catch(displayError);
11
+ }
12
+ return super.catch(error);
13
+ }
4
14
  }
@@ -2,6 +2,7 @@ import { Flags } from '@oclif/core';
2
2
  import { loginWithBrowser } from '../auth/browser.js';
3
3
  import { loginWithToken } from '../auth/token.js';
4
4
  import { ArdentCommand } from '../command.js';
5
+ import { terminalText } from '../terminal.js';
5
6
  export default class Login extends ArdentCommand {
6
7
  static description = 'Log in to Ardent';
7
8
  static helpGroup = 'Authentication';
@@ -20,7 +21,7 @@ export default class Login extends ArdentCommand {
20
21
  });
21
22
  this.log('✓ Logged in to Ardent');
22
23
  if (organizations.length === 1)
23
- this.log(` Organization: ${organizations[0].name} (${organizations[0].id})`);
24
+ this.log(` Organization: ${terminalText(organizations[0].name)} (${terminalText(organizations[0].id)})`);
24
25
  else
25
26
  this.log(` Organizations: ${organizations.length}`);
26
27
  return { authenticated: true, organizations };
@@ -18,7 +18,7 @@ export interface ConnectorRegistration {
18
18
  export interface ConnectorRegistrationStatus {
19
19
  connector_id: string;
20
20
  failure?: string;
21
- status: 'failed' | 'registered' | 'registering';
21
+ status: 'deleted' | 'deleting' | 'failed' | 'registered' | 'registering';
22
22
  }
23
23
  export declare function listConnectors(token: string, organizationID: string, projectID: string): Promise<Connector[]>;
24
24
  export declare function createConnector(token: string, organizationID: string, projectID: string, environmentID: string | undefined, name: string, url: string): Promise<ConnectorRegistration>;
@@ -1,5 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { setTimeout as delay } from 'node:timers/promises';
2
3
  import { request } from '../api/client.js';
4
+ import { terminalText } from '../terminal.js';
3
5
  export async function listConnectors(token, organizationID, projectID) {
4
6
  const query = new URLSearchParams({ organization_id: organizationID, project_id: projectID });
5
7
  const connectors = await request(token, `/connectors?${query}`);
@@ -29,17 +31,36 @@ export async function createConnector(token, organizationID, projectID, environm
29
31
  return registration;
30
32
  }
31
33
  export async function waitForConnectorRegistration(token, connectorID, onStatus) {
32
- for (;;) {
33
- const status = await request(token, `/connectors/${encodeURIComponent(connectorID)}`);
34
- if (!isConnectorRegistrationStatus(status, connectorID)) {
35
- throw new Error('Ardent API returned an invalid connector registration status.');
34
+ const deadline = new AbortController();
35
+ const timer = setTimeout(() => deadline.abort(), 30 * 60 * 1000);
36
+ try {
37
+ for (;;) {
38
+ const status = await request(token, `/connectors/${encodeURIComponent(connectorID)}`, {
39
+ signal: AbortSignal.any([deadline.signal, AbortSignal.timeout(10_000)]),
40
+ });
41
+ deadline.signal.throwIfAborted();
42
+ if (!isConnectorRegistrationStatus(status, connectorID)) {
43
+ throw new Error('Ardent API returned an invalid connector registration status.');
44
+ }
45
+ onStatus?.(status.status);
46
+ if (status.status === 'registered')
47
+ return status;
48
+ if (status.status === 'failed')
49
+ throw new Error(terminalText(status.failure || 'Connector registration failed.'));
50
+ if (status.status === 'deleting' || status.status === 'deleted') {
51
+ throw new Error(`Connector ${terminalText(connectorID)} is ${status.status}. Run ardent-beta connector list to check its state.`);
52
+ }
53
+ await delay(2000, undefined, { signal: deadline.signal });
36
54
  }
37
- onStatus?.(status.status);
38
- if (status.status === 'registered')
39
- return status;
40
- if (status.status === 'failed')
41
- throw new Error(status.failure || 'Connector registration failed.');
42
- await new Promise((resolve) => setTimeout(resolve, 2000));
55
+ }
56
+ catch (error) {
57
+ if (deadline.signal.aborted) {
58
+ throw new Error(`Timed out waiting for connector ${terminalText(connectorID)} registration. Its outcome is unknown. Run ardent-beta connector list to check its state.`);
59
+ }
60
+ throw error;
61
+ }
62
+ finally {
63
+ clearTimeout(timer);
43
64
  }
44
65
  }
45
66
  function isConnector(value, projectID) {
@@ -75,6 +96,6 @@ function isConnectorRegistrationStatus(value, connectorID) {
75
96
  return false;
76
97
  const status = value;
77
98
  return (status.connector_id === connectorID &&
78
- (status.status === 'registering' || status.status === 'registered' || status.status === 'failed') &&
99
+ (status.status === 'registering' || status.status === 'registered' || status.status === 'failed' || status.status === 'deleting' || status.status === 'deleted') &&
79
100
  (status.failure === undefined || typeof status.failure === 'string'));
80
101
  }
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { APIError, request } from '../api/client.js';
2
+ import { APIError, request, requestDurableMutation } from '../api/client.js';
3
3
  const sharedReadScopes = [
4
4
  'organizations.read',
5
5
  'organization_memberships.read',
@@ -60,22 +60,22 @@ export async function listReadableInvitations(token, organizationID) {
60
60
  }
61
61
  }
62
62
  export async function inviteMember(token, organizationID, email, role) {
63
- const invitation = await request(token, `/organizations/${encodeURIComponent(organizationID)}/invitations`, {
63
+ const invitation = await requestDurableMutation(token, `/organizations/${encodeURIComponent(organizationID)}/invitations`, {
64
64
  body: JSON.stringify({ email, role_display_name: role, scopes: standardRoleScopes[role] }),
65
65
  headers: { 'Idempotency-Key': randomUUID() },
66
66
  method: 'POST',
67
- });
67
+ }, 'ardent-beta member list');
68
68
  if (!isInvitation(invitation, organizationID))
69
69
  throw new Error('Ardent API returned an invalid invitation response.');
70
70
  return invitation;
71
71
  }
72
72
  export async function deleteMembership(token, organizationID, userID) {
73
- const response = await request(token, `/organizations/${encodeURIComponent(organizationID)}/memberships/${encodeURIComponent(userID)}`, { method: 'DELETE' });
73
+ const response = await requestDurableMutation(token, `/organizations/${encodeURIComponent(organizationID)}/memberships/${encodeURIComponent(userID)}`, { method: 'DELETE' }, 'ardent-beta member list');
74
74
  if (response !== undefined)
75
75
  throw new Error('Ardent API did not confirm member deletion. Run ardent-beta member list before retrying.');
76
76
  }
77
77
  export async function deleteInvitation(token, organizationID, invitationID) {
78
- const response = await request(token, `/organizations/${encodeURIComponent(organizationID)}/invitations/${encodeURIComponent(invitationID)}`, { method: 'DELETE' });
78
+ const response = await requestDurableMutation(token, `/organizations/${encodeURIComponent(organizationID)}/invitations/${encodeURIComponent(invitationID)}`, { method: 'DELETE' }, 'ardent-beta member list');
79
79
  if (response !== undefined)
80
80
  throw new Error('Ardent API did not confirm invitation cancellation. Run ardent-beta member list before retrying.');
81
81
  }
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { request } from '../api/client.js';
2
+ import { request, requestDurableMutation } from '../api/client.js';
3
3
  export async function listProjects(token, organizationID) {
4
4
  const projects = await request(token, `/projects?organization_id=${encodeURIComponent(organizationID)}`);
5
5
  if (!Array.isArray(projects) ||
@@ -11,17 +11,17 @@ export async function listProjects(token, organizationID) {
11
11
  return projects;
12
12
  }
13
13
  export async function createProject(token, organizationID, name) {
14
- const project = await request(token, '/projects', {
14
+ const project = await requestDurableMutation(token, '/projects', {
15
15
  body: JSON.stringify({ name, organization_id: organizationID }),
16
16
  headers: { 'Idempotency-Key': randomUUID() },
17
17
  method: 'POST',
18
- });
18
+ }, 'ardent-beta project list');
19
19
  if (!isProject(project, organizationID))
20
20
  throw new Error('Ardent API returned an invalid project response.');
21
21
  return project;
22
22
  }
23
23
  export async function deleteProject(token, organizationID, projectID) {
24
- const response = await request(token, `/projects/${encodeURIComponent(projectID)}?organization_id=${encodeURIComponent(organizationID)}`, { method: 'DELETE' });
24
+ const response = await requestDurableMutation(token, `/projects/${encodeURIComponent(projectID)}?organization_id=${encodeURIComponent(organizationID)}`, { method: 'DELETE' }, 'ardent-beta project list');
25
25
  if (response !== undefined) {
26
26
  throw new Error('Ardent API did not confirm project deletion. Run ardent-beta project list before retrying.');
27
27
  }
@@ -574,5 +574,5 @@
574
574
  ]
575
575
  }
576
576
  },
577
- "version": "0.0.17"
577
+ "version": "0.0.18"
578
578
  }