ardent-cli-beta 0.0.18 → 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.
- package/README.md +92 -28
- package/dist/api/client.d.ts +2 -0
- package/dist/api/client.js +13 -2
- package/dist/auth/credentials.d.ts +1 -0
- package/dist/auth/credentials.js +44 -4
- package/dist/branches/client.d.ts +3 -1
- package/dist/branches/client.js +41 -4
- package/dist/branches/connection.d.ts +21 -0
- package/dist/branches/connection.js +143 -0
- package/dist/branches/credentials.d.ts +18 -0
- package/dist/branches/credentials.js +188 -0
- package/dist/branches/drivers.d.ts +6 -0
- package/dist/branches/drivers.js +77 -0
- package/dist/branches/env.d.ts +1 -0
- package/dist/branches/env.js +55 -0
- package/dist/branches/poll.d.ts +3 -0
- package/dist/branches/poll.js +40 -0
- package/dist/branches/psql.d.ts +14 -0
- package/dist/branches/psql.js +159 -0
- package/dist/branches/recovery.d.ts +19 -0
- package/dist/branches/recovery.js +211 -0
- package/dist/commands/branch/connect.d.ts +12 -0
- package/dist/commands/branch/connect.js +87 -0
- package/dist/commands/branch/create.d.ts +9 -1
- package/dist/commands/branch/create.js +73 -15
- package/dist/commands/branch/recover.d.ts +11 -0
- package/dist/commands/branch/recover.js +37 -0
- package/dist/commands/branch/suspend.d.ts +11 -0
- package/dist/commands/branch/suspend.js +37 -0
- package/dist/commands/branch/switch.js +1 -1
- package/dist/commands/branch/wake.d.ts +11 -0
- package/dist/commands/branch/wake.js +37 -0
- package/dist/commands/connector/create.d.ts +2 -0
- package/dist/commands/connector/create.js +20 -2
- package/dist/commands/connector/delete.d.ts +10 -0
- package/dist/commands/connector/delete.js +62 -0
- package/dist/commands/connector/status.d.ts +10 -0
- package/dist/commands/connector/status.js +28 -0
- package/dist/commands/connector/switch.d.ts +12 -0
- package/dist/commands/connector/switch.js +45 -0
- package/dist/commands/login.js +7 -1
- package/dist/commands/project/switch.js +1 -1
- package/dist/connectors/client.d.ts +8 -4
- package/dist/connectors/client.js +62 -13
- package/dist/terminal.d.ts +1 -0
- package/dist/terminal.js +7 -0
- package/oclif.manifest.json +319 -2
- package/openapi.json +36 -7
- package/package.json +5 -1
|
@@ -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('
|
|
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
|
+
}
|
package/dist/commands/login.js
CHANGED
|
@@ -11,7 +11,13 @@ export default class Login extends ArdentCommand {
|
|
|
11
11
|
};
|
|
12
12
|
async run() {
|
|
13
13
|
const { flags } = await this.parse(Login);
|
|
14
|
-
|
|
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
|
|
15
21
|
? await loginWithToken(this.config.configDir, flags.token)
|
|
16
22
|
: await loginWithBrowser(this.config.configDir, {
|
|
17
23
|
onAuthorizationURL: (url, opened) => {
|
|
@@ -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
|
|
18
|
+
export interface ConnectorStatus {
|
|
19
19
|
connector_id: string;
|
|
20
20
|
failure?: string;
|
|
21
|
-
|
|
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:
|
|
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,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
3
|
-
import { request } from '../api/client.js';
|
|
3
|
+
import { APIError, request, requestDurableMutation } from '../api/client.js';
|
|
4
4
|
import { terminalText } from '../terminal.js';
|
|
5
5
|
export async function listConnectors(token, organizationID, projectID) {
|
|
6
6
|
const query = new URLSearchParams({ organization_id: organizationID, project_id: projectID });
|
|
@@ -13,7 +13,7 @@ export async function listConnectors(token, organizationID, projectID) {
|
|
|
13
13
|
}
|
|
14
14
|
return connectors;
|
|
15
15
|
}
|
|
16
|
-
export async function createConnector(token, organizationID, projectID, environmentID, name, url) {
|
|
16
|
+
export async function createConnector(token, organizationID, projectID, environmentID, name, url, tlsClientCertificate, tlsClientKey) {
|
|
17
17
|
const requestID = randomUUID();
|
|
18
18
|
const registration = await request(token, '/connectors', {
|
|
19
19
|
body: JSON.stringify({
|
|
@@ -23,6 +23,7 @@ export async function createConnector(token, organizationID, projectID, environm
|
|
|
23
23
|
project_id: projectID,
|
|
24
24
|
request_id: requestID,
|
|
25
25
|
url,
|
|
26
|
+
...(tlsClientCertificate ? { tls_client_certificate: tlsClientCertificate, tls_client_key: tlsClientKey } : {}),
|
|
26
27
|
}),
|
|
27
28
|
method: 'POST',
|
|
28
29
|
});
|
|
@@ -31,32 +32,79 @@ export async function createConnector(token, organizationID, projectID, environm
|
|
|
31
32
|
return registration;
|
|
32
33
|
}
|
|
33
34
|
export async function waitForConnectorRegistration(token, connectorID, onStatus) {
|
|
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) {
|
|
34
61
|
const deadline = new AbortController();
|
|
35
62
|
const timer = setTimeout(() => deadline.abort(), 30 * 60 * 1000);
|
|
36
63
|
try {
|
|
37
64
|
for (;;) {
|
|
38
|
-
const status = await
|
|
39
|
-
signal: AbortSignal.any([deadline.signal, AbortSignal.timeout(10_000)]),
|
|
40
|
-
});
|
|
65
|
+
const status = await getConnectorStatus(token, connectorID, AbortSignal.any([deadline.signal, AbortSignal.timeout(10_000)]));
|
|
41
66
|
deadline.signal.throwIfAborted();
|
|
42
|
-
if (!isConnectorRegistrationStatus(status, connectorID)) {
|
|
43
|
-
throw new Error('Ardent API returned an invalid connector registration status.');
|
|
44
|
-
}
|
|
45
67
|
onStatus?.(status.status);
|
|
46
|
-
if (status.status ===
|
|
68
|
+
if (status.status === expected)
|
|
47
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
|
+
}
|
|
48
78
|
if (status.status === 'failed')
|
|
49
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
|
+
}
|
|
50
86
|
if (status.status === 'deleting' || status.status === 'deleted') {
|
|
51
|
-
|
|
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}`);
|
|
52
92
|
}
|
|
53
93
|
await delay(2000, undefined, { signal: deadline.signal });
|
|
54
94
|
}
|
|
55
95
|
}
|
|
56
96
|
catch (error) {
|
|
57
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.`);
|
|
58
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.`);
|
|
59
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
|
+
}
|
|
60
108
|
throw error;
|
|
61
109
|
}
|
|
62
110
|
finally {
|
|
@@ -91,11 +139,12 @@ function isConnectorRegistration(value) {
|
|
|
91
139
|
const registration = value;
|
|
92
140
|
return typeof registration.connector_id === 'string' && registration.connector_id.length > 0;
|
|
93
141
|
}
|
|
94
|
-
function
|
|
142
|
+
function isConnectorStatus(value, connectorID) {
|
|
95
143
|
if (typeof value !== 'object' || value === null)
|
|
96
144
|
return false;
|
|
97
145
|
const status = value;
|
|
98
146
|
return (status.connector_id === connectorID &&
|
|
99
|
-
(status.status === 'registering' || status.status === 'registered' || status.status === 'failed' || status.status === 'deleting' || status.status === 'deleted') &&
|
|
100
|
-
(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'));
|
|
101
150
|
}
|
package/dist/terminal.d.ts
CHANGED
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
|
+
}
|