ardent-cli-beta 0.0.15 → 0.0.16

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
@@ -63,3 +63,9 @@ context. Create and delete follow the operation in an interactive terminal;
63
63
  non-interactive commands and `--json` return the accepted operation immediately.
64
64
 
65
65
  Before changing this repository, read [ARDENT_CLI_DEVELOPMENT.md](ARDENT_CLI_DEVELOPMENT.md).
66
+
67
+ Running `npm test` or `make ci` requires Node 22+, npm and `tar`. The interactive
68
+ tests also require macOS's system `script`, or Linux's util-linux `script` with
69
+ `-e` and `-c` support, on `PATH`. These are test prerequisites; the installed CLI
70
+ does not invoke `script`. The PTY tests skip other operating systems and fail on
71
+ 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,9 +1,14 @@
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}`);
6
- if (!Array.isArray(connectors) || connectors.some((connector) => !isConnector(connector, projectID))) {
8
+ if (!Array.isArray(connectors) ||
9
+ connectors.some((connector) => !isConnector(connector, projectID)) ||
10
+ new Set(connectors.map(({ id }) => id)).size !== connectors.length ||
11
+ new Set(connectors.map(({ name }) => name)).size !== connectors.length) {
7
12
  throw new Error('Ardent API returned an invalid connectors response.');
8
13
  }
9
14
  return connectors;
@@ -26,17 +31,36 @@ export async function createConnector(token, organizationID, projectID, environm
26
31
  return registration;
27
32
  }
28
33
  export async function waitForConnectorRegistration(token, connectorID, onStatus) {
29
- for (;;) {
30
- const status = await request(token, `/connectors/${encodeURIComponent(connectorID)}`);
31
- if (!isConnectorRegistrationStatus(status, connectorID)) {
32
- 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 });
33
54
  }
34
- onStatus?.(status.status);
35
- if (status.status === 'registered')
36
- return status;
37
- if (status.status === 'failed')
38
- throw new Error(status.failure || 'Connector registration failed.');
39
- 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);
40
64
  }
41
65
  }
42
66
  function isConnector(value, projectID) {
@@ -50,6 +74,7 @@ function isConnector(value, projectID) {
50
74
  typeof connector.environment_id === 'string' &&
51
75
  connector.environment_id.length > 0 &&
52
76
  typeof connector.name === 'string' &&
77
+ connector.name.length > 0 &&
53
78
  typeof connector.status === 'string' &&
54
79
  connector.status.length > 0 &&
55
80
  typeof source === 'object' &&
@@ -71,6 +96,6 @@ function isConnectorRegistrationStatus(value, connectorID) {
71
96
  return false;
72
97
  const status = value;
73
98
  return (status.connector_id === connectorID &&
74
- (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') &&
75
100
  (status.failure === undefined || typeof status.failure === 'string'));
76
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,24 +1,27 @@
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
- if (!Array.isArray(projects) || projects.some((project) => !isProject(project, organizationID))) {
5
+ if (!Array.isArray(projects) ||
6
+ projects.some((project) => !isProject(project, organizationID)) ||
7
+ new Set(projects.map(({ id }) => id)).size !== projects.length ||
8
+ new Set(projects.map(({ name }) => name)).size !== projects.length) {
6
9
  throw new Error('Ardent API returned an invalid projects response.');
7
10
  }
8
11
  return projects;
9
12
  }
10
13
  export async function createProject(token, organizationID, name) {
11
- const project = await request(token, '/projects', {
14
+ const project = await requestDurableMutation(token, '/projects', {
12
15
  body: JSON.stringify({ name, organization_id: organizationID }),
13
16
  headers: { 'Idempotency-Key': randomUUID() },
14
17
  method: 'POST',
15
- });
18
+ }, 'ardent-beta project list');
16
19
  if (!isProject(project, organizationID))
17
20
  throw new Error('Ardent API returned an invalid project response.');
18
21
  return project;
19
22
  }
20
23
  export async function deleteProject(token, organizationID, projectID) {
21
- 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');
22
25
  if (response !== undefined) {
23
26
  throw new Error('Ardent API did not confirm project deletion. Run ardent-beta project list before retrying.');
24
27
  }
@@ -31,6 +34,7 @@ function isProject(value, organizationID) {
31
34
  project.id.length > 0 &&
32
35
  project.organization_id === organizationID &&
33
36
  typeof project.name === 'string' &&
37
+ project.name.length > 0 &&
34
38
  typeof project.created_at === 'string' &&
35
39
  typeof project.updated_at === 'string');
36
40
  }
@@ -574,5 +574,5 @@
574
574
  ]
575
575
  }
576
576
  },
577
- "version": "0.0.15"
577
+ "version": "0.0.16"
578
578
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ardent-cli-beta",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "description": "The Ardent command-line interface",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -10,6 +10,7 @@
10
10
  "main": "./dist/index.js",
11
11
  "types": "./dist/index.d.ts",
12
12
  "files": [
13
+ "NOTICE",
13
14
  "bin/",
14
15
  "dist/",
15
16
  "oclif.manifest.json"
@@ -36,7 +37,7 @@
36
37
  "clean": "node -e \"const {rmSync} = require('node:fs'); for (const path of ['dist', 'tsconfig.tsbuildinfo']) rmSync(path, {force: true, recursive: true})\"",
37
38
  "manifest": "oclif manifest",
38
39
  "prepack": "npm run build && npm run manifest",
39
- "test": "npm run build && npm run manifest && node --test \"test/**/*.test.js\"",
40
+ "test": "npm run build && npm run manifest && node --import ./test/setup.js --test \"test/**/*.test.js\"",
40
41
  "typecheck": "tsc --noEmit"
41
42
  },
42
43
  "dependencies": {