ardent-cli-beta 0.0.17 → 0.0.19

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 (54) hide show
  1. package/NOTICE +4 -0
  2. package/README.md +96 -18
  3. package/dist/api/client.d.ts +6 -1
  4. package/dist/api/client.js +44 -5
  5. package/dist/auth/credentials.d.ts +1 -0
  6. package/dist/auth/credentials.js +44 -4
  7. package/dist/branches/client.d.ts +3 -1
  8. package/dist/branches/client.js +41 -4
  9. package/dist/branches/connection.d.ts +21 -0
  10. package/dist/branches/connection.js +143 -0
  11. package/dist/branches/credentials.d.ts +18 -0
  12. package/dist/branches/credentials.js +188 -0
  13. package/dist/branches/drivers.d.ts +6 -0
  14. package/dist/branches/drivers.js +77 -0
  15. package/dist/branches/env.d.ts +1 -0
  16. package/dist/branches/env.js +55 -0
  17. package/dist/branches/poll.d.ts +3 -0
  18. package/dist/branches/poll.js +40 -0
  19. package/dist/branches/psql.d.ts +14 -0
  20. package/dist/branches/psql.js +159 -0
  21. package/dist/branches/recovery.d.ts +19 -0
  22. package/dist/branches/recovery.js +211 -0
  23. package/dist/command.d.ts +1 -0
  24. package/dist/command.js +10 -0
  25. package/dist/commands/branch/connect.d.ts +12 -0
  26. package/dist/commands/branch/connect.js +87 -0
  27. package/dist/commands/branch/create.d.ts +9 -1
  28. package/dist/commands/branch/create.js +73 -15
  29. package/dist/commands/branch/recover.d.ts +11 -0
  30. package/dist/commands/branch/recover.js +37 -0
  31. package/dist/commands/branch/suspend.d.ts +11 -0
  32. package/dist/commands/branch/suspend.js +37 -0
  33. package/dist/commands/branch/switch.js +1 -1
  34. package/dist/commands/branch/wake.d.ts +11 -0
  35. package/dist/commands/branch/wake.js +37 -0
  36. package/dist/commands/connector/create.d.ts +2 -0
  37. package/dist/commands/connector/create.js +20 -2
  38. package/dist/commands/connector/delete.d.ts +10 -0
  39. package/dist/commands/connector/delete.js +62 -0
  40. package/dist/commands/connector/status.d.ts +10 -0
  41. package/dist/commands/connector/status.js +28 -0
  42. package/dist/commands/connector/switch.d.ts +12 -0
  43. package/dist/commands/connector/switch.js +45 -0
  44. package/dist/commands/login.js +9 -2
  45. package/dist/commands/project/switch.js +1 -1
  46. package/dist/connectors/client.d.ts +8 -4
  47. package/dist/connectors/client.js +85 -15
  48. package/dist/members/client.js +5 -5
  49. package/dist/projects/client.js +4 -4
  50. package/dist/terminal.d.ts +1 -0
  51. package/dist/terminal.js +7 -0
  52. package/oclif.manifest.json +319 -2
  53. package/openapi.json +1970 -0
  54. package/package.json +8 -2
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { readFile } from 'node:fs/promises';
2
3
  import { Args, Flags, ux } from '@oclif/core';
3
4
  import { loadSession } from '../../auth/credentials.js';
4
5
  import { ArdentCommand } from '../../command.js';
@@ -14,6 +15,8 @@ export default class ConnectorCreate extends ArdentCommand {
14
15
  static flags = {
15
16
  environment: Flags.string({ description: 'Environment ID (defaults to Ardent Cloud)' }),
16
17
  name: Flags.string({ char: 'n', description: 'Connector name' }),
18
+ 'tls-client-cert': Flags.string({ description: 'PEM client certificate file (requires sslmode=require in the URL)' }),
19
+ 'tls-client-key': Flags.string({ description: 'PEM client private-key file (requires sslmode=require in the URL)' }),
17
20
  };
18
21
  async run() {
19
22
  const { args, flags } = await this.parse(ConnectorCreate);
@@ -32,6 +35,21 @@ export default class ConnectorCreate extends ArdentCommand {
32
35
  const environmentID = flags.environment?.trim();
33
36
  if (flags.environment !== undefined && !environmentID)
34
37
  this.error('Environment ID cannot be empty.');
38
+ if (Boolean(flags['tls-client-cert']) !== Boolean(flags['tls-client-key']))
39
+ this.error('--tls-client-cert and --tls-client-key must be supplied together.');
40
+ let tlsClientCertificate = '';
41
+ let tlsClientKey = '';
42
+ if (flags['tls-client-cert'] && flags['tls-client-key']) {
43
+ try {
44
+ ;
45
+ [tlsClientCertificate, tlsClientKey] = await Promise.all([readFile(flags['tls-client-cert'], 'utf8'), readFile(flags['tls-client-key'], 'utf8')]);
46
+ }
47
+ catch {
48
+ this.error('Could not read the TLS client certificate pair.');
49
+ }
50
+ if (tlsClientCertificate.length > 64 * 1024 || tlsClientKey.length > 64 * 1024)
51
+ this.error('TLS client certificate files must each be at most 64 KiB.');
52
+ }
35
53
  const interactive = !this.jsonEnabled() && ux.action.type === 'spinner';
36
54
  if (interactive) {
37
55
  ux.action.start(`${terminalText(name)} in ${terminalText(session.selectedProject.name)}`, 'registering');
@@ -39,7 +57,7 @@ export default class ConnectorCreate extends ArdentCommand {
39
57
  else if (!this.jsonEnabled()) {
40
58
  this.log(`Registering connector ${terminalText(name)} for project ${terminalText(session.selectedProject.name)}...`);
41
59
  }
42
- const registration = await createConnector(session.token, session.organization.id, session.selectedProject.id, environmentID, name, args.url);
60
+ const registration = await createConnector(session.token, session.organization.id, session.selectedProject.id, environmentID, name, args.url, tlsClientCertificate, tlsClientKey);
43
61
  if (!interactive) {
44
62
  if (!this.jsonEnabled()) {
45
63
  this.log(`Connector registration started (${terminalText(registration.connector_id)}).`);
@@ -50,7 +68,7 @@ export default class ConnectorCreate extends ArdentCommand {
50
68
  ux.action.status = status;
51
69
  });
52
70
  ux.action.stop(ux.colorize('green', '✓ registered'));
53
- this.log('Credential delivery and replication readiness are still pending.');
71
+ this.log('Replication readiness is still pending.');
54
72
  return { connector_id: registration.connector_id, status: 'registered' };
55
73
  }
56
74
  }
@@ -0,0 +1,10 @@
1
+ import { ArdentCommand } from '../../command.js';
2
+ import { type ConnectorStatus } from '../../connectors/client.js';
3
+ export default class ConnectorDelete extends ArdentCommand {
4
+ static args: {
5
+ name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
6
+ };
7
+ static description: string;
8
+ static helpGroup: string;
9
+ run(): Promise<ConnectorStatus>;
10
+ }
@@ -0,0 +1,62 @@
1
+ import { Args, ux } from '@oclif/core';
2
+ import { loadSession, updateSession } from '../../auth/credentials.js';
3
+ import { ArdentCommand } from '../../command.js';
4
+ import { deleteConnector, listConnectors, waitForConnectorDeletion } from '../../connectors/client.js';
5
+ import { terminalText } from '../../terminal.js';
6
+ export default class ConnectorDelete extends ArdentCommand {
7
+ static args = { name: Args.string({ description: 'Connector name', required: true }) };
8
+ static description = 'Delete a connector in the selected project after its branches are deleted';
9
+ static helpGroup = 'Connectors';
10
+ async run() {
11
+ const { args } = await this.parse(ConnectorDelete);
12
+ const name = args.name.trim();
13
+ if (!name)
14
+ this.error('Connector name cannot be empty.');
15
+ const session = await loadSession(this.config.configDir);
16
+ if (!session?.organization)
17
+ this.error('You must be logged in to run this command. Run ardent-beta login.');
18
+ if (!session.selectedProject)
19
+ this.error('No project selected. Run ardent-beta project list, then ardent-beta project switch <name>.');
20
+ const connectors = await listConnectors(session.token, session.organization.id, session.selectedProject.id);
21
+ const connector = connectors.find((candidate) => candidate.name === name);
22
+ if (!connector)
23
+ this.error(`Connector ${terminalText(JSON.stringify(name))} not found in the selected project.`);
24
+ const interactive = !this.jsonEnabled() && ux.action.type === 'spinner';
25
+ const target = `${terminalText(connector.name)} (${terminalText(connector.id)}) in project ${terminalText(session.selectedProject.name)}`;
26
+ if (interactive)
27
+ ux.action.start(`Deleting connector ${target}`);
28
+ else if (!this.jsonEnabled())
29
+ this.log(`Deleting connector ${target}...`);
30
+ try {
31
+ await deleteConnector(session.token, connector.id);
32
+ if (!interactive) {
33
+ if (!this.jsonEnabled()) {
34
+ this.log(`Connector deletion started (${terminalText(connector.id)}).`);
35
+ this.log(`Run ardent-beta connector status ${terminalText(connector.id)} to check its state.`);
36
+ }
37
+ return { connector_id: connector.id, status: 'deleting' };
38
+ }
39
+ const result = await waitForConnectorDeletion(session.token, connector.id, (status) => { ux.action.status = status; });
40
+ await updateSession(this.config.configDir, (current) => {
41
+ if (!current || current.token !== session.token || current.organization?.id !== session.organization?.id || current.selectedProject?.id !== session.selectedProject?.id)
42
+ return;
43
+ const updated = { ...current, connectors: current.connectors?.filter(({ id }) => id !== connector.id) };
44
+ if (updated.selectedConnector?.id === connector.id) {
45
+ delete updated.selectedConnector;
46
+ delete updated.branches;
47
+ delete updated.selectedBranch;
48
+ }
49
+ return updated;
50
+ });
51
+ ux.action.stop(ux.colorize('green', '✓ deleted'));
52
+ if (result.failure)
53
+ this.warn(`Source cleanup residue: ${terminalText(result.failure)}. Check the source database for remaining capture objects.`);
54
+ return result;
55
+ }
56
+ catch (error) {
57
+ if (interactive)
58
+ ux.action.stop('failed');
59
+ throw error;
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,10 @@
1
+ import { ArdentCommand } from '../../command.js';
2
+ import { type ConnectorStatus } from '../../connectors/client.js';
3
+ export default class ConnectorStatusCommand extends ArdentCommand {
4
+ static args: {
5
+ id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
6
+ };
7
+ static description: string;
8
+ static helpGroup: string;
9
+ run(): Promise<ConnectorStatus>;
10
+ }
@@ -0,0 +1,28 @@
1
+ import { Args } from '@oclif/core';
2
+ import { loadSession } from '../../auth/credentials.js';
3
+ import { ArdentCommand } from '../../command.js';
4
+ import { getConnectorStatus } from '../../connectors/client.js';
5
+ import { terminalText } from '../../terminal.js';
6
+ export default class ConnectorStatusCommand extends ArdentCommand {
7
+ static args = { id: Args.string({ description: 'Connector ID returned by create or delete', required: true }) };
8
+ static description = 'Show connector registration or deletion status by ID';
9
+ static helpGroup = 'Connectors';
10
+ async run() {
11
+ const { args } = await this.parse(ConnectorStatusCommand);
12
+ const id = args.id.trim();
13
+ if (!id)
14
+ this.error('Connector ID cannot be empty.');
15
+ const session = await loadSession(this.config.configDir);
16
+ if (!session?.organization)
17
+ this.error('You must be logged in to run this command. Run ardent-beta login.');
18
+ const result = await getConnectorStatus(session.token, id);
19
+ if (!this.jsonEnabled()) {
20
+ this.log(`Connector ${terminalText(result.connector_id)}: ${result.status}`);
21
+ if (result.phase)
22
+ this.log(`Phase: ${terminalText(result.phase)}`);
23
+ if (result.failure)
24
+ this.log(`${result.status === 'deleted' ? 'Source cleanup residue' : 'Failure'}: ${terminalText(result.failure)}`);
25
+ }
26
+ return result;
27
+ }
28
+ }
@@ -0,0 +1,12 @@
1
+ import { ArdentCommand } from '../../command.js';
2
+ export default class ConnectorSwitch extends ArdentCommand {
3
+ static args: {
4
+ name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static helpGroup: string;
8
+ run(): Promise<{
9
+ id: string;
10
+ name: string;
11
+ }>;
12
+ }
@@ -0,0 +1,45 @@
1
+ import { Args } from '@oclif/core';
2
+ import { loadSession, updateSession } from '../../auth/credentials.js';
3
+ import { ArdentCommand } from '../../command.js';
4
+ import { listConnectors } from '../../connectors/client.js';
5
+ import { terminalText } from '../../terminal.js';
6
+ export default class ConnectorSwitch extends ArdentCommand {
7
+ static args = { name: Args.string({ description: 'Connector name', required: true }) };
8
+ static description = 'Switch to a connector in the selected project';
9
+ static helpGroup = 'Connectors';
10
+ async run() {
11
+ const { args } = await this.parse(ConnectorSwitch);
12
+ const name = args.name.trim();
13
+ if (!name)
14
+ this.error('Connector name cannot be empty.');
15
+ const session = await loadSession(this.config.configDir);
16
+ if (!session?.organization)
17
+ this.error('You must be logged in to run this command. Run ardent-beta login.');
18
+ if (!session.selectedProject)
19
+ this.error('No project selected. Run ardent-beta project list, then ardent-beta project switch <name>.');
20
+ const connectors = await listConnectors(session.token, session.organization.id, session.selectedProject.id);
21
+ const connector = connectors.find((candidate) => candidate.name === name);
22
+ if (!connector)
23
+ this.error(`Connector ${terminalText(JSON.stringify(name))} not found in the selected project.`);
24
+ // Decide before persisting: a refused switch must preserve the existing target.
25
+ const switched = await updateSession(this.config.configDir, (current) => {
26
+ if (!current || current.token !== session.token || current.organization?.id !== session.organization?.id || current.selectedProject?.id !== session.selectedProject?.id)
27
+ return;
28
+ const updated = {
29
+ ...current,
30
+ connectors: connectors.map(({ id, name, project_id }) => ({ id, name, project_id })),
31
+ selectedConnector: { id: connector.id, name: connector.name, project_id: connector.project_id },
32
+ };
33
+ if (current.selectedConnector?.id !== connector.id) {
34
+ delete updated.branches;
35
+ delete updated.selectedBranch;
36
+ }
37
+ return updated;
38
+ });
39
+ if (!switched)
40
+ this.error('Local context changed during connector discovery. Retry the switch in your current project.');
41
+ if (!this.jsonEnabled())
42
+ this.log(`✓ Switched to connector ${terminalText(connector.name)}`);
43
+ return { id: connector.id, name: connector.name };
44
+ }
45
+ }
@@ -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';
@@ -10,7 +11,13 @@ export default class Login extends ArdentCommand {
10
11
  };
11
12
  async run() {
12
13
  const { flags } = await this.parse(Login);
13
- const organizations = flags.token
14
+ // Oclif discards an empty environment value, so without this an ARDENT_TOKEN
15
+ // that expands to nothing, such as a missing CI secret, would start a
16
+ // 30-minute browser login instead of failing.
17
+ if (flags.token === undefined && process.env.ARDENT_TOKEN !== undefined) {
18
+ this.error('ARDENT_TOKEN is set but empty. Set it to an Ardent API token, or unset it to log in with the browser.');
19
+ }
20
+ const organizations = flags.token !== undefined
14
21
  ? await loginWithToken(this.config.configDir, flags.token)
15
22
  : await loginWithBrowser(this.config.configDir, {
16
23
  onAuthorizationURL: (url, opened) => {
@@ -20,7 +27,7 @@ export default class Login extends ArdentCommand {
20
27
  });
21
28
  this.log('✓ Logged in to Ardent');
22
29
  if (organizations.length === 1)
23
- this.log(` Organization: ${organizations[0].name} (${organizations[0].id})`);
30
+ this.log(` Organization: ${terminalText(organizations[0].name)} (${terminalText(organizations[0].id)})`);
24
31
  else
25
32
  this.log(` Organizations: ${organizations.length}`);
26
33
  return { authenticated: true, organizations };
@@ -33,7 +33,7 @@ export default class ProjectSwitch extends ArdentCommand {
33
33
  // non-zero having quietly selected a different project underneath.
34
34
  project = projects.find((candidate) => candidate.name === name);
35
35
  if (!project) {
36
- this.error(`Project ${JSON.stringify(name)} not found in organization ${JSON.stringify(session.organization.name)}.`);
36
+ this.error(`Project ${JSON.stringify(name)} not found in organization ${terminalText(JSON.stringify(session.organization.name))}.`);
37
37
  }
38
38
  session = await replaceProjectCache(this.config.configDir, session, projects);
39
39
  }
@@ -15,11 +15,15 @@ export interface Connector {
15
15
  export interface ConnectorRegistration {
16
16
  connector_id: string;
17
17
  }
18
- export interface ConnectorRegistrationStatus {
18
+ export interface ConnectorStatus {
19
19
  connector_id: string;
20
20
  failure?: string;
21
- status: 'failed' | 'registered' | 'registering';
21
+ phase?: string;
22
+ status: 'deleted' | 'deleting' | 'failed' | 'halted' | 'registered' | 'registering';
22
23
  }
23
24
  export declare function listConnectors(token: string, organizationID: string, projectID: string): Promise<Connector[]>;
24
- export declare function createConnector(token: string, organizationID: string, projectID: string, environmentID: string | undefined, name: string, url: string): Promise<ConnectorRegistration>;
25
- export declare function waitForConnectorRegistration(token: string, connectorID: string, onStatus?: (status: ConnectorRegistrationStatus['status']) => void): Promise<ConnectorRegistrationStatus>;
25
+ export declare function createConnector(token: string, organizationID: string, projectID: string, environmentID: string | undefined, name: string, url: string, tlsClientCertificate?: string, tlsClientKey?: string): Promise<ConnectorRegistration>;
26
+ export declare function waitForConnectorRegistration(token: string, connectorID: string, onStatus?: (status: ConnectorStatus['status']) => void): Promise<ConnectorStatus>;
27
+ export declare function deleteConnector(token: string, connectorID: string): Promise<ConnectorRegistration>;
28
+ export declare function getConnectorStatus(token: string, connectorID: string, signal?: AbortSignal): Promise<ConnectorStatus>;
29
+ export declare function waitForConnectorDeletion(token: string, connectorID: string, onStatus?: (status: ConnectorStatus['status']) => void): Promise<ConnectorStatus>;
@@ -1,5 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { request } from '../api/client.js';
2
+ import { setTimeout as delay } from 'node:timers/promises';
3
+ import { APIError, request, requestDurableMutation } 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}`);
@@ -11,7 +13,7 @@ export async function listConnectors(token, organizationID, projectID) {
11
13
  }
12
14
  return connectors;
13
15
  }
14
- export async function createConnector(token, organizationID, projectID, environmentID, name, url) {
16
+ export async function createConnector(token, organizationID, projectID, environmentID, name, url, tlsClientCertificate, tlsClientKey) {
15
17
  const requestID = randomUUID();
16
18
  const registration = await request(token, '/connectors', {
17
19
  body: JSON.stringify({
@@ -21,6 +23,7 @@ export async function createConnector(token, organizationID, projectID, environm
21
23
  project_id: projectID,
22
24
  request_id: requestID,
23
25
  url,
26
+ ...(tlsClientCertificate ? { tls_client_certificate: tlsClientCertificate, tls_client_key: tlsClientKey } : {}),
24
27
  }),
25
28
  method: 'POST',
26
29
  });
@@ -29,17 +32,83 @@ export async function createConnector(token, organizationID, projectID, environm
29
32
  return registration;
30
33
  }
31
34
  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.');
35
+ return waitForConnectorStatus(token, connectorID, 'registered', onStatus);
36
+ }
37
+ export async function deleteConnector(token, connectorID) {
38
+ const result = await requestDurableMutation(token, `/connectors/${encodeURIComponent(connectorID)}`, {
39
+ method: 'DELETE',
40
+ }, `ardent-beta connector status ${terminalText(connectorID)}`);
41
+ if (!isConnectorRegistration(result) || result.connector_id !== connectorID) {
42
+ throw new Error(`The Ardent API accepted the connector deletion request but returned no matching connector; completion is unknown. Run ardent-beta connector status ${terminalText(connectorID)} before retrying.`);
43
+ }
44
+ return result;
45
+ }
46
+ export async function getConnectorStatus(token, connectorID, signal) {
47
+ const result = await request(token, `/connectors/${encodeURIComponent(connectorID)}`, { signal });
48
+ if (!isConnectorStatus(result, connectorID))
49
+ throw new Error('Ardent API returned an invalid connector status.');
50
+ return {
51
+ connector_id: result.connector_id,
52
+ status: result.status,
53
+ ...(result.phase === undefined ? {} : { phase: result.phase }),
54
+ ...(result.failure === undefined ? {} : { failure: result.failure }),
55
+ };
56
+ }
57
+ export async function waitForConnectorDeletion(token, connectorID, onStatus) {
58
+ return waitForConnectorStatus(token, connectorID, 'deleted', onStatus);
59
+ }
60
+ async function waitForConnectorStatus(token, connectorID, expected, onStatus) {
61
+ const deadline = new AbortController();
62
+ const timer = setTimeout(() => deadline.abort(), 30 * 60 * 1000);
63
+ try {
64
+ for (;;) {
65
+ const status = await getConnectorStatus(token, connectorID, AbortSignal.any([deadline.signal, AbortSignal.timeout(10_000)]));
66
+ deadline.signal.throwIfAborted();
67
+ onStatus?.(status.status);
68
+ if (status.status === expected)
69
+ return status;
70
+ if (expected === 'deleted') {
71
+ if (status.status !== 'deleting' || status.failure === 'connector_deletion_failed') {
72
+ const reason = status.failure ? `: ${terminalText(status.failure)}` : '';
73
+ throw new Error(`Connector ${terminalText(connectorID)} is ${status.status}${reason}. Deletion is not confirmed.`);
74
+ }
75
+ await delay(2000, undefined, { signal: deadline.signal });
76
+ continue;
77
+ }
78
+ if (status.status === 'failed')
79
+ throw new Error(terminalText(status.failure || 'Connector registration failed.'));
80
+ if (status.status === 'halted') {
81
+ // Not success, and not polled through: docs/REQUIREMENTS.md section 7's
82
+ // "a halt does not clear by being polled".
83
+ const reason = status.failure ? `: ${terminalText(status.failure)}` : '';
84
+ throw new Error(`Connector ${terminalText(connectorID)} is halted${reason}. Run ardent-beta connector list to check its state.`);
85
+ }
86
+ if (status.status === 'deleting' || status.status === 'deleted') {
87
+ const reason = status.failure ? `: ${terminalText(status.failure)}` : '';
88
+ // connector list leaves deleted connectors out, so it can only check a
89
+ // connector that is still deleting.
90
+ const next = status.status === 'deleting' ? 'Run ardent-beta connector list to check its state.' : 'It will not appear in ardent-beta connector list.';
91
+ throw new Error(`Connector ${terminalText(connectorID)} is ${status.status}${reason}. ${next}`);
92
+ }
93
+ await delay(2000, undefined, { signal: deadline.signal });
36
94
  }
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));
95
+ }
96
+ catch (error) {
97
+ if (deadline.signal.aborted) {
98
+ if (expected === 'deleted')
99
+ throw new Error(`Timed out waiting for connector ${terminalText(connectorID)} deletion. Its outcome is unknown. Run ardent-beta connector status ${terminalText(connectorID)} before retrying.`);
100
+ throw new Error(`Timed out waiting for connector ${terminalText(connectorID)} registration. Its outcome is unknown. Run ardent-beta connector list to check its state.`);
101
+ }
102
+ if (expected === 'deleted') {
103
+ if (error instanceof APIError)
104
+ throw new APIError(`${error.message} Run ardent-beta connector status ${terminalText(connectorID)} to confirm the deletion outcome before retrying.`, error.status, error.code);
105
+ const reason = error instanceof Error ? terminalText(error.message) : 'The status request failed.';
106
+ throw new Error(`${reason} Run ardent-beta connector status ${terminalText(connectorID)} to confirm the deletion outcome before retrying.`);
107
+ }
108
+ throw error;
109
+ }
110
+ finally {
111
+ clearTimeout(timer);
43
112
  }
44
113
  }
45
114
  function isConnector(value, projectID) {
@@ -70,11 +139,12 @@ function isConnectorRegistration(value) {
70
139
  const registration = value;
71
140
  return typeof registration.connector_id === 'string' && registration.connector_id.length > 0;
72
141
  }
73
- function isConnectorRegistrationStatus(value, connectorID) {
142
+ function isConnectorStatus(value, connectorID) {
74
143
  if (typeof value !== 'object' || value === null)
75
144
  return false;
76
145
  const status = value;
77
146
  return (status.connector_id === connectorID &&
78
- (status.status === 'registering' || status.status === 'registered' || status.status === 'failed') &&
79
- (status.failure === undefined || typeof status.failure === 'string'));
147
+ (status.status === 'registering' || status.status === 'registered' || status.status === 'failed' || status.status === 'halted' || status.status === 'deleting' || status.status === 'deleted') &&
148
+ (status.failure === undefined || typeof status.failure === 'string') &&
149
+ (status.phase === undefined || typeof status.phase === 'string'));
80
150
  }
@@ -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
  }
@@ -1 +1,2 @@
1
1
  export declare function terminalText(value: string): string;
2
+ export declare function shellWord(value: string): string;
package/dist/terminal.js CHANGED
@@ -7,3 +7,10 @@ export function terminalText(value) {
7
7
  return `\\u${codePoint.toString(16).padStart(4, '0')}`;
8
8
  });
9
9
  }
10
+ // shellWord renders a value as one POSIX shell word, for a command a reader is
11
+ // meant to copy. Anything beyond a conservative safe set is single-quoted.
12
+ export function shellWord(value) {
13
+ if (/^[A-Za-z0-9_.,:\/@%+=-]+$/.test(value))
14
+ return value;
15
+ return `'${value.replaceAll("'", "'\\''")}'`;
16
+ }