clever-tools 5.0.2 → 5.1.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clever-tools",
3
- "version": "5.0.2",
3
+ "version": "5.1.0",
4
4
  "description": "Command Line Interface for Clever Cloud.",
5
5
  "keywords": [
6
6
  "cli",
@@ -36,6 +36,7 @@ These options are available for all commands:
36
36
  |[`clever env`](./env/env.docs.md)|Manage environment variables of an application|
37
37
  |[`clever features`](./features/features.docs.md)|Manage Clever Tools experimental features|
38
38
  |[`clever help`](./help/help.docs.md)|Display help about the Clever Cloud CLI|
39
+ |[`clever instances`](./instances/instances.docs.md)|List instances of an application|
39
40
  |[`clever k8s`](./k8s/k8s.docs.md)|Manage Kubernetes clusters|
40
41
  |[`clever keycloak`](./keycloak/keycloak.docs.md)|Manage Clever Cloud Keycloak services|
41
42
  |[`clever kv`](./kv/kv.docs.md)|Send a raw command to a Materia KV or Redis® add-on|
@@ -48,6 +48,7 @@ export const drainCommand = defineCommand({
48
48
  const formattedDrains = drains.map((drain) => {
49
49
  return {
50
50
  ID: drain.id,
51
+ Kind: drain.kind,
51
52
  Status: drain.status.status,
52
53
  'Execution status': drain.execution.status,
53
54
  URL: drain.recipient.url,
@@ -6,6 +6,7 @@ import { Logger } from '../../logger.js';
6
6
  import { createLogDrain, resolveDrainResource } from '../../models/drain.js';
7
7
  import { addonIdOrRealIdOption, aliasOption, appIdOrNameOption } from '../global.options.js';
8
8
  import { drainUrlArg } from './drain.args.js';
9
+ import { drainKindOption } from './drain.options.js';
9
10
 
10
11
  export const drainCreateBetterstackCommand = defineCommand({
11
12
  description: 'Create a Better Stack drain',
@@ -18,16 +19,17 @@ export const drainCreateBetterstackCommand = defineCommand({
18
19
  aliases: ['t'],
19
20
  placeholder: 'source-token',
20
21
  }),
22
+ kind: drainKindOption,
21
23
  alias: aliasOption,
22
24
  appIdOrName: appIdOrNameOption,
23
25
  addonIdOrRealId: addonIdOrRealIdOption,
24
26
  },
25
27
  args: [drainUrlArg],
26
28
  async handler(options, url) {
27
- const { alias, appIdOrName, addonIdOrRealId, sourceToken } = options;
29
+ const { kind, alias, appIdOrName, addonIdOrRealId, sourceToken } = options;
28
30
  const { ownerId, resourceId } = await resolveDrainResource(alias, appIdOrName, addonIdOrRealId);
29
31
 
30
- const drain = await createLogDrain('BETTERSTACK', ownerId, resourceId, url, { sourceToken });
32
+ const drain = await createLogDrain('BETTERSTACK', kind, ownerId, resourceId, url, { sourceToken });
31
33
 
32
34
  Logger.printSuccess(
33
35
  `Better Stack drain ${styleText(['bold', 'green'], drain.id)} has been successfully created and enabled!`,
@@ -4,21 +4,23 @@ import { Logger } from '../../logger.js';
4
4
  import { createLogDrain, resolveDrainResource } from '../../models/drain.js';
5
5
  import { addonIdOrRealIdOption, aliasOption, appIdOrNameOption } from '../global.options.js';
6
6
  import { drainUrlArg } from './drain.args.js';
7
+ import { drainKindOption } from './drain.options.js';
7
8
 
8
9
  export const drainCreateDatadogCommand = defineCommand({
9
10
  description: 'Create a Datadog drain',
10
11
  since: '0.9.0',
11
12
  options: {
13
+ kind: drainKindOption,
12
14
  alias: aliasOption,
13
15
  appIdOrName: appIdOrNameOption,
14
16
  addonIdOrRealId: addonIdOrRealIdOption,
15
17
  },
16
18
  args: [drainUrlArg],
17
19
  async handler(options, url) {
18
- const { alias, appIdOrName, addonIdOrRealId } = options;
20
+ const { kind, alias, appIdOrName, addonIdOrRealId } = options;
19
21
  const { ownerId, resourceId } = await resolveDrainResource(alias, appIdOrName, addonIdOrRealId);
20
22
 
21
- const drain = await createLogDrain('DATADOG', ownerId, resourceId, url);
23
+ const drain = await createLogDrain('DATADOG', kind, ownerId, resourceId, url);
22
24
 
23
25
  Logger.printSuccess(
24
26
  `Datadog drain ${styleText(['bold', 'green'], drain.id)} has been successfully created and enabled!`,
@@ -6,7 +6,7 @@ import { styleText } from '../../lib/style-text.js';
6
6
  import { Logger } from '../../logger.js';
7
7
  import { createLogDrain, resolveDrainResource } from '../../models/drain.js';
8
8
  import { addonIdOrRealIdOption, aliasOption, appIdOrNameOption } from '../global.options.js';
9
- import { drainPasswordOption, drainUsernameOption } from './drain.options.js';
9
+ import { drainKindOption, drainPasswordOption, drainUsernameOption } from './drain.options.js';
10
10
 
11
11
  export const drainCreateElasticsearchCommand = defineCommand({
12
12
  description: 'Create an Elasticsearch drain',
@@ -21,6 +21,7 @@ export const drainCreateElasticsearchCommand = defineCommand({
21
21
  }),
22
22
  username: drainUsernameOption,
23
23
  password: drainPasswordOption,
24
+ kind: drainKindOption,
24
25
  alias: aliasOption,
25
26
  appIdOrName: appIdOrNameOption,
26
27
  addonIdOrRealId: addonIdOrRealIdOption,
@@ -36,10 +37,10 @@ export const drainCreateElasticsearchCommand = defineCommand({
36
37
  }),
37
38
  ],
38
39
  async handler(options, url) {
39
- const { alias, appIdOrName, addonIdOrRealId, index, username, password } = options;
40
+ const { kind, alias, appIdOrName, addonIdOrRealId, index, username, password } = options;
40
41
  const { ownerId, resourceId } = await resolveDrainResource(alias, appIdOrName, addonIdOrRealId);
41
42
 
42
- const drain = await createLogDrain('ELASTICSEARCH', ownerId, resourceId, url, { index, username, password });
43
+ const drain = await createLogDrain('ELASTICSEARCH', kind, ownerId, resourceId, url, { index, username, password });
43
44
 
44
45
  Logger.printSuccess(
45
46
  `Elasticsearch drain ${styleText(['bold', 'green'], drain.id)} has been successfully created and enabled!`,
@@ -6,6 +6,7 @@ import { Logger } from '../../logger.js';
6
6
  import { createLogDrain, resolveDrainResource } from '../../models/drain.js';
7
7
  import { addonIdOrRealIdOption, aliasOption, appIdOrNameOption } from '../global.options.js';
8
8
  import { drainUrlArg } from './drain.args.js';
9
+ import { drainKindOption } from './drain.options.js';
9
10
 
10
11
  export const drainCreateNewRelicCommand = defineCommand({
11
12
  description: 'Create a New Relic drain',
@@ -18,16 +19,17 @@ export const drainCreateNewRelicCommand = defineCommand({
18
19
  aliases: ['k'],
19
20
  placeholder: 'api-key',
20
21
  }),
22
+ kind: drainKindOption,
21
23
  alias: aliasOption,
22
24
  appIdOrName: appIdOrNameOption,
23
25
  addonIdOrRealId: addonIdOrRealIdOption,
24
26
  },
25
27
  args: [drainUrlArg],
26
28
  async handler(options, url) {
27
- const { alias, appIdOrName, addonIdOrRealId, apiKey } = options;
29
+ const { kind, alias, appIdOrName, addonIdOrRealId, apiKey } = options;
28
30
  const { ownerId, resourceId } = await resolveDrainResource(alias, appIdOrName, addonIdOrRealId);
29
31
 
30
- const drain = await createLogDrain('NEWRELIC', ownerId, resourceId, url, { apiKey });
32
+ const drain = await createLogDrain('NEWRELIC', kind, ownerId, resourceId, url, { apiKey });
31
33
 
32
34
  Logger.printSuccess(
33
35
  `New Relic drain ${styleText(['bold', 'green'], drain.id)} has been successfully created and enabled!`,
@@ -4,23 +4,24 @@ import { Logger } from '../../logger.js';
4
4
  import { createLogDrain, resolveDrainResource } from '../../models/drain.js';
5
5
  import { addonIdOrRealIdOption, aliasOption, appIdOrNameOption } from '../global.options.js';
6
6
  import { drainUrlArg } from './drain.args.js';
7
- import { drainSdParamsOption } from './drain.options.js';
7
+ import { drainKindOption, drainSdParamsOption } from './drain.options.js';
8
8
 
9
9
  export const drainCreateOvhTcpCommand = defineCommand({
10
10
  description: 'Create an OVH TCP drain',
11
11
  since: '0.9.0',
12
12
  options: {
13
13
  rfc5424StructuredDataParameters: drainSdParamsOption,
14
+ kind: drainKindOption,
14
15
  alias: aliasOption,
15
16
  appIdOrName: appIdOrNameOption,
16
17
  addonIdOrRealId: addonIdOrRealIdOption,
17
18
  },
18
19
  args: [drainUrlArg],
19
20
  async handler(options, url) {
20
- const { alias, appIdOrName, addonIdOrRealId, rfc5424StructuredDataParameters } = options;
21
+ const { kind, alias, appIdOrName, addonIdOrRealId, rfc5424StructuredDataParameters } = options;
21
22
  const { ownerId, resourceId } = await resolveDrainResource(alias, appIdOrName, addonIdOrRealId);
22
23
 
23
- const drain = await createLogDrain('OVH_TCP', ownerId, resourceId, url, { rfc5424StructuredDataParameters });
24
+ const drain = await createLogDrain('OVH_TCP', kind, ownerId, resourceId, url, { rfc5424StructuredDataParameters });
24
25
 
25
26
  Logger.printSuccess(
26
27
  `OVH TCP drain ${styleText(['bold', 'green'], drain.id)} has been successfully created and enabled!`,
@@ -4,7 +4,7 @@ import { Logger } from '../../logger.js';
4
4
  import { createLogDrain, resolveDrainResource } from '../../models/drain.js';
5
5
  import { addonIdOrRealIdOption, aliasOption, appIdOrNameOption } from '../global.options.js';
6
6
  import { drainUrlArg } from './drain.args.js';
7
- import { drainPasswordOption, drainUsernameOption } from './drain.options.js';
7
+ import { drainKindOption, drainPasswordOption, drainUsernameOption } from './drain.options.js';
8
8
 
9
9
  export const drainCreateRawHttpCommand = defineCommand({
10
10
  description: 'Create a raw HTTP drain',
@@ -12,16 +12,17 @@ export const drainCreateRawHttpCommand = defineCommand({
12
12
  options: {
13
13
  username: drainUsernameOption,
14
14
  password: drainPasswordOption,
15
+ kind: drainKindOption,
15
16
  alias: aliasOption,
16
17
  appIdOrName: appIdOrNameOption,
17
18
  addonIdOrRealId: addonIdOrRealIdOption,
18
19
  },
19
20
  args: [drainUrlArg],
20
21
  async handler(options, url) {
21
- const { alias, appIdOrName, addonIdOrRealId, username, password } = options;
22
+ const { kind, alias, appIdOrName, addonIdOrRealId, username, password } = options;
22
23
  const { ownerId, resourceId } = await resolveDrainResource(alias, appIdOrName, addonIdOrRealId);
23
24
 
24
- const drain = await createLogDrain('RAW_HTTP', ownerId, resourceId, url, { username, password });
25
+ const drain = await createLogDrain('RAW_HTTP', kind, ownerId, resourceId, url, { username, password });
25
26
 
26
27
  Logger.printSuccess(
27
28
  `Raw HTTP drain ${styleText(['bold', 'green'], drain.id)} has been successfully created and enabled!`,
@@ -6,6 +6,7 @@ import { Logger } from '../../logger.js';
6
6
  import { createLogDrain, resolveDrainResource } from '../../models/drain.js';
7
7
  import { addonIdOrRealIdOption, aliasOption, appIdOrNameOption } from '../global.options.js';
8
8
  import { drainUrlArg } from './drain.args.js';
9
+ import { drainKindOption } from './drain.options.js';
9
10
 
10
11
  export const drainCreateSplunkCommand = defineCommand({
11
12
  description: 'Create a Splunk HEC drain',
@@ -38,16 +39,17 @@ export const drainCreateSplunkCommand = defineCommand({
38
39
  description: 'TLS verification mode, use `trustful` to accept a self-signed certificate',
39
40
  placeholder: 'tls-verification',
40
41
  }),
42
+ kind: drainKindOption,
41
43
  alias: aliasOption,
42
44
  appIdOrName: appIdOrNameOption,
43
45
  addonIdOrRealId: addonIdOrRealIdOption,
44
46
  },
45
47
  args: [drainUrlArg],
46
48
  async handler(options, url) {
47
- const { alias, appIdOrName, addonIdOrRealId, token, index, sourcetype, tlsVerification } = options;
49
+ const { kind, alias, appIdOrName, addonIdOrRealId, token, index, sourcetype, tlsVerification } = options;
48
50
  const { ownerId, resourceId } = await resolveDrainResource(alias, appIdOrName, addonIdOrRealId);
49
51
 
50
- const drain = await createLogDrain('SPLUNK', ownerId, resourceId, url, {
52
+ const drain = await createLogDrain('SPLUNK', kind, ownerId, resourceId, url, {
51
53
  token,
52
54
  index,
53
55
  sourcetype,
@@ -4,23 +4,26 @@ import { Logger } from '../../logger.js';
4
4
  import { createLogDrain, resolveDrainResource } from '../../models/drain.js';
5
5
  import { addonIdOrRealIdOption, aliasOption, appIdOrNameOption } from '../global.options.js';
6
6
  import { drainUrlArg } from './drain.args.js';
7
- import { drainSdParamsOption } from './drain.options.js';
7
+ import { drainKindOption, drainSdParamsOption } from './drain.options.js';
8
8
 
9
9
  export const drainCreateSyslogTcpCommand = defineCommand({
10
10
  description: 'Create a Syslog TCP drain',
11
11
  since: '0.9.0',
12
12
  options: {
13
13
  rfc5424StructuredDataParameters: drainSdParamsOption,
14
+ kind: drainKindOption,
14
15
  alias: aliasOption,
15
16
  appIdOrName: appIdOrNameOption,
16
17
  addonIdOrRealId: addonIdOrRealIdOption,
17
18
  },
18
19
  args: [drainUrlArg],
19
20
  async handler(options, url) {
20
- const { alias, appIdOrName, addonIdOrRealId, rfc5424StructuredDataParameters } = options;
21
+ const { kind, alias, appIdOrName, addonIdOrRealId, rfc5424StructuredDataParameters } = options;
21
22
  const { ownerId, resourceId } = await resolveDrainResource(alias, appIdOrName, addonIdOrRealId);
22
23
 
23
- const drain = await createLogDrain('SYSLOG_TCP', ownerId, resourceId, url, { rfc5424StructuredDataParameters });
24
+ const drain = await createLogDrain('SYSLOG_TCP', kind, ownerId, resourceId, url, {
25
+ rfc5424StructuredDataParameters,
26
+ });
24
27
 
25
28
  Logger.printSuccess(
26
29
  `Syslog TCP drain ${styleText(['bold', 'green'], drain.id)} has been successfully created and enabled!`,
@@ -4,23 +4,26 @@ import { Logger } from '../../logger.js';
4
4
  import { createLogDrain, resolveDrainResource } from '../../models/drain.js';
5
5
  import { addonIdOrRealIdOption, aliasOption, appIdOrNameOption } from '../global.options.js';
6
6
  import { drainUrlArg } from './drain.args.js';
7
- import { drainSdParamsOption } from './drain.options.js';
7
+ import { drainKindOption, drainSdParamsOption } from './drain.options.js';
8
8
 
9
9
  export const drainCreateSyslogUdpCommand = defineCommand({
10
10
  description: 'Create a Syslog UDP drain',
11
11
  since: '0.9.0',
12
12
  options: {
13
13
  rfc5424StructuredDataParameters: drainSdParamsOption,
14
+ kind: drainKindOption,
14
15
  alias: aliasOption,
15
16
  appIdOrName: appIdOrNameOption,
16
17
  addonIdOrRealId: addonIdOrRealIdOption,
17
18
  },
18
19
  args: [drainUrlArg],
19
20
  async handler(options, url) {
20
- const { alias, appIdOrName, addonIdOrRealId, rfc5424StructuredDataParameters } = options;
21
+ const { kind, alias, appIdOrName, addonIdOrRealId, rfc5424StructuredDataParameters } = options;
21
22
  const { ownerId, resourceId } = await resolveDrainResource(alias, appIdOrName, addonIdOrRealId);
22
23
 
23
- const drain = await createLogDrain('SYSLOG_UDP', ownerId, resourceId, url, { rfc5424StructuredDataParameters });
24
+ const drain = await createLogDrain('SYSLOG_UDP', kind, ownerId, resourceId, url, {
25
+ rfc5424StructuredDataParameters,
26
+ });
24
27
 
25
28
  Logger.printSuccess(
26
29
  `Syslog UDP drain ${styleText(['bold', 'green'], drain.id)} has been successfully created and enabled!`,
@@ -70,6 +70,7 @@ clever drain create betterstack --source-token <source-token> <drain-url> [optio
70
70
  |`--addon` `<addon-id>`|Add-on ID or real ID|
71
71
  |`-a`, `--alias` `<alias>`|Short name for the application|
72
72
  |`--app` `<app-id\|app-name>`|Application to manage by its ID (or name, if unambiguous)|
73
+ |`--kind` `<kind>`|Kind of logs sent to the drain (LOG, ACCESSLOG) (default: LOG)|
73
74
 
74
75
  ### Examples
75
76
 
@@ -100,6 +101,7 @@ clever drain create datadog <drain-url> [options]
100
101
  |`--addon` `<addon-id>`|Add-on ID or real ID|
101
102
  |`-a`, `--alias` `<alias>`|Short name for the application|
102
103
  |`--app` `<app-id\|app-name>`|Application to manage by its ID (or name, if unambiguous)|
104
+ |`--kind` `<kind>`|Kind of logs sent to the drain (LOG, ACCESSLOG) (default: LOG)|
103
105
 
104
106
  ### Examples
105
107
 
@@ -136,6 +138,7 @@ clever drain create elasticsearch --index-prefix <index-prefix> <drain-url> [opt
136
138
  |`--addon` `<addon-id>`|Add-on ID or real ID|
137
139
  |`-a`, `--alias` `<alias>`|Short name for the application|
138
140
  |`--app` `<app-id\|app-name>`|Application to manage by its ID (or name, if unambiguous)|
141
+ |`--kind` `<kind>`|Kind of logs sent to the drain (LOG, ACCESSLOG) (default: LOG)|
139
142
  |`-p`, `--password` `<password>`|Basic auth password|
140
143
  |`-u`, `--username` `<username>`|Basic auth username|
141
144
 
@@ -171,6 +174,7 @@ clever drain create newrelic --api-key <api-key> <drain-url> [options]
171
174
  |`--addon` `<addon-id>`|Add-on ID or real ID|
172
175
  |`-a`, `--alias` `<alias>`|Short name for the application|
173
176
  |`--app` `<app-id\|app-name>`|Application to manage by its ID (or name, if unambiguous)|
177
+ |`--kind` `<kind>`|Kind of logs sent to the drain (LOG, ACCESSLOG) (default: LOG)|
174
178
 
175
179
  ### Examples
176
180
 
@@ -201,6 +205,7 @@ clever drain create ovh-tcp <drain-url> [options]
201
205
  |`--addon` `<addon-id>`|Add-on ID or real ID|
202
206
  |`-a`, `--alias` `<alias>`|Short name for the application|
203
207
  |`--app` `<app-id\|app-name>`|Application to manage by its ID (or name, if unambiguous)|
208
+ |`--kind` `<kind>`|Kind of logs sent to the drain (LOG, ACCESSLOG) (default: LOG)|
204
209
  |`-s`, `--sd-params` `<sd-params>`|RFC5424 structured data parameters, e.g.: `token=\"REDACTED\"`|
205
210
 
206
211
  ### Examples
@@ -232,6 +237,7 @@ clever drain create raw-http <drain-url> [options]
232
237
  |`--addon` `<addon-id>`|Add-on ID or real ID|
233
238
  |`-a`, `--alias` `<alias>`|Short name for the application|
234
239
  |`--app` `<app-id\|app-name>`|Application to manage by its ID (or name, if unambiguous)|
240
+ |`--kind` `<kind>`|Kind of logs sent to the drain (LOG, ACCESSLOG) (default: LOG)|
235
241
  |`-p`, `--password` `<password>`|Basic auth password|
236
242
  |`-u`, `--username` `<username>`|Basic auth username|
237
243
 
@@ -267,6 +273,7 @@ clever drain create splunk --hec-token <hec-token> <drain-url> [options]
267
273
  |`-a`, `--alias` `<alias>`|Short name for the application|
268
274
  |`--app` `<app-id\|app-name>`|Application to manage by its ID (or name, if unambiguous)|
269
275
  |`--index` `<index>`|Optional target index, the HEC token's own index is used if not set|
276
+ |`--kind` `<kind>`|Kind of logs sent to the drain (LOG, ACCESSLOG) (default: LOG)|
270
277
  |`--sourcetype` `<sourcetype>`|Optional sourcetype, the HEC token's own sourcetype is used if not set|
271
278
  |`--tls-verification` `<tls-verification>`|TLS verification mode, use `trustful` to accept a self-signed certificate (default, trustful)|
272
279
 
@@ -291,6 +298,7 @@ clever drain create syslog-tcp <drain-url> [options]
291
298
  |`--addon` `<addon-id>`|Add-on ID or real ID|
292
299
  |`-a`, `--alias` `<alias>`|Short name for the application|
293
300
  |`--app` `<app-id\|app-name>`|Application to manage by its ID (or name, if unambiguous)|
301
+ |`--kind` `<kind>`|Kind of logs sent to the drain (LOG, ACCESSLOG) (default: LOG)|
294
302
  |`-s`, `--sd-params` `<sd-params>`|RFC5424 structured data parameters, e.g.: `token=\"REDACTED\"`|
295
303
 
296
304
  ### Examples
@@ -323,6 +331,7 @@ clever drain create syslog-udp <drain-url> [options]
323
331
  |`--addon` `<addon-id>`|Add-on ID or real ID|
324
332
  |`-a`, `--alias` `<alias>`|Short name for the application|
325
333
  |`--app` `<app-id\|app-name>`|Application to manage by its ID (or name, if unambiguous)|
334
+ |`--kind` `<kind>`|Kind of logs sent to the drain (LOG, ACCESSLOG) (default: LOG)|
326
335
  |`-s`, `--sd-params` `<sd-params>`|RFC5424 structured data parameters, e.g.: `token=\"REDACTED\"`|
327
336
 
328
337
  ### Examples
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { defineOption } from '../../lib/define-option.js';
3
+ import { DRAIN_KINDS } from '../../models/drain.js';
3
4
 
4
5
  export const drainUsernameOption = defineOption({
5
6
  name: 'username',
@@ -24,3 +25,10 @@ export const drainSdParamsOption = defineOption({
24
25
  aliases: ['s'],
25
26
  placeholder: 'sd-params',
26
27
  });
28
+
29
+ export const drainKindOption = defineOption({
30
+ name: 'kind',
31
+ schema: z.enum(DRAIN_KINDS).default('LOG'),
32
+ description: 'Kind of logs sent to the drain',
33
+ placeholder: 'kind',
34
+ });
@@ -72,6 +72,7 @@ import { featuresEnableCommand } from './features/features.enable.command.js';
72
72
  import { featuresInfoCommand } from './features/features.info.command.js';
73
73
  import { featuresListCommand } from './features/features.list.command.js';
74
74
  import { helpCommand } from './help/help.command.js';
75
+ import { instancesCommand } from './instances/instances.command.js';
75
76
  import { k8sActivityCommand } from './k8s/k8s.activity.command.js';
76
77
  import { k8sAddPersistentStorageCommand } from './k8s/k8s.add-persistent-storage.command.js';
77
78
  import { k8sCommand } from './k8s/k8s.command.js';
@@ -328,6 +329,7 @@ export const globalCommands = {
328
329
  },
329
330
  ],
330
331
  help: helpCommand,
332
+ instances: instancesCommand,
331
333
  k8s: [
332
334
  k8sCommand,
333
335
  {
@@ -0,0 +1,108 @@
1
+ import { z } from 'zod';
2
+ import { formatTable } from '../../format-table.js';
3
+ import { defineCommand } from '../../lib/define-command.js';
4
+ import { defineOption } from '../../lib/define-option.js';
5
+ import { styleText } from '../../lib/style-text.js';
6
+ import { Logger } from '../../logger.js';
7
+ import * as Application from '../../models/application.js';
8
+ import { listInstances } from '../../models/instances.js';
9
+ import { date } from '../../parsers.js';
10
+ import { aliasOption, appIdOrNameOption, humanJsonOutputFormatOption } from '../global.options.js';
11
+
12
+ export const instancesCommand = defineCommand({
13
+ description: 'List instances of an application',
14
+ since: '5.1.0',
15
+ options: {
16
+ all: defineOption({
17
+ name: 'all',
18
+ schema: z.boolean().default(false),
19
+ description: 'List instances in any state, including deleted ones (default: only running instances)',
20
+ }),
21
+ since: defineOption({
22
+ name: 'after',
23
+ schema: z.string().transform(date).optional(),
24
+ description:
25
+ 'List instances that existed after this date/time, in any state (ISO8601 date, positive number in seconds or duration, e.g.: 1h)',
26
+ aliases: ['since'],
27
+ placeholder: 'after',
28
+ }),
29
+ until: defineOption({
30
+ name: 'before',
31
+ schema: z.string().transform(date).optional(),
32
+ description:
33
+ 'List instances that existed before this date/time, in any state (ISO8601 date, positive number in seconds or duration, e.g.: 1h)',
34
+ aliases: ['until'],
35
+ placeholder: 'before',
36
+ }),
37
+ deploymentId: defineOption({
38
+ name: 'deployment-id',
39
+ schema: z.string().optional(),
40
+ description: 'List instances created by this deployment, in any state',
41
+ placeholder: 'deployment-id',
42
+ }),
43
+ limit: defineOption({
44
+ name: 'limit',
45
+ schema: z.coerce.number().int().min(1).max(1000).default(100),
46
+ description: 'Maximum number of instances to list, keeping the most recent ones (1 to 1000)',
47
+ placeholder: 'limit',
48
+ }),
49
+ alias: aliasOption,
50
+ app: appIdOrNameOption,
51
+ format: humanJsonOutputFormatOption,
52
+ },
53
+ async handler(options) {
54
+ const { alias, app: appIdOrName, all, since, until, deploymentId, limit, format } = options;
55
+ if (since != null && until != null && since > until) {
56
+ throw new Error('--after must be earlier than or equal to --before');
57
+ }
58
+ const { ownerId, appId } = await Application.resolveId(appIdOrName, alias);
59
+
60
+ // Date and deployment filters also include past instances
61
+ const hasFilters = since != null || until != null || deploymentId != null;
62
+ const onlyRunning = !all && !hasFilters;
63
+
64
+ const instances = await listInstances({
65
+ ownerId,
66
+ appId,
67
+ onlyRunning,
68
+ since,
69
+ until,
70
+ deploymentId,
71
+ limit,
72
+ });
73
+
74
+ switch (format) {
75
+ case 'json': {
76
+ Logger.printJson(instances);
77
+ break;
78
+ }
79
+ case 'human':
80
+ default: {
81
+ if (instances.length === 0) {
82
+ Logger.println(
83
+ styleText(
84
+ 'blue',
85
+ onlyRunning
86
+ ? 'No running instances found for this application'
87
+ : hasFilters
88
+ ? 'No instances found matching the filters'
89
+ : 'No instances found for this application',
90
+ ),
91
+ );
92
+ return;
93
+ }
94
+
95
+ const headers = ['CREATED', 'STATE', '#', 'FLAVOR', 'INSTANCE ID', 'DEPLOYMENT ID'];
96
+ const rows = instances.map((instance) => [
97
+ `${instance.createdAt.substring(0, 19).replace('T', ' ')}Z`,
98
+ instance.state,
99
+ instance.isBuildVm ? 'build' : String(instance.instanceNumber ?? '?'),
100
+ instance.flavor ?? '',
101
+ instance.id,
102
+ instance.deploymentId,
103
+ ]);
104
+ Logger.println(formatTable([headers, ...rows]));
105
+ }
106
+ }
107
+ },
108
+ });
@@ -0,0 +1,22 @@
1
+ # 📖 `clever instances` command reference
2
+
3
+ ## ➡️ `clever instances` <kbd>Since 5.1.0</kbd>
4
+
5
+ List instances of an application
6
+
7
+ ```bash
8
+ clever instances [options]
9
+ ```
10
+
11
+ ### ⚙️ Options
12
+
13
+ |Name|Description|
14
+ |---|---|
15
+ |`--after`, `--since` `<after>`|List instances that existed after this date/time, in any state (ISO8601 date, positive number in seconds or duration, e.g.: 1h)|
16
+ |`-a`, `--alias` `<alias>`|Short name for the application|
17
+ |`--all`|List instances in any state, including deleted ones (default: only running instances)|
18
+ |`--app` `<app-id\|app-name>`|Application to manage by its ID (or name, if unambiguous)|
19
+ |`--before`, `--until` `<before>`|List instances that existed before this date/time, in any state (ISO8601 date, positive number in seconds or duration, e.g.: 1h)|
20
+ |`--deployment-id` `<deployment-id>`|List instances created by this deployment, in any state|
21
+ |`-F`, `--format` `<format>`|Output format (human, json) (default: human)|
22
+ |`--limit` `<limit>`|Maximum number of instances to list, keeping the most recent ones (1 to 1000) (default: 100)|
@@ -1,6 +1,7 @@
1
1
  import { getAllEnvVars } from '@clevercloud/client/esm/api/v2/addon.js';
2
2
  import Redis from 'ioredis';
3
3
  import { z } from 'zod';
4
+ import { ambiguousNameError } from '../../lib/ambiguous-name-error.js';
4
5
  import { defineArgument } from '../../lib/define-argument.js';
5
6
  import { defineCommand } from '../../lib/define-command.js';
6
7
  import { styleText } from '../../lib/style-text.js';
@@ -70,10 +71,12 @@ export const kvCommand = defineCommand({
70
71
  }
71
72
 
72
73
  if (addons.length > 1) {
73
- const formattedAddons = addons
74
- .map(({ addonId, ownerId }) => `\n${styleText('grey', `- ${addonId} (${ownerId})`)}`)
75
- .join('');
76
- throw new Error(`Several add-ons found for '${addonIdOrRealIdOrName}', use ID instead:${formattedAddons}`);
74
+ const candidates = addons.map(({ name, addonId, ownerId }) => ({
75
+ name,
76
+ id: addonId,
77
+ details: `owner ${ownerId}`,
78
+ }));
79
+ throw ambiguousNameError(addonIdOrRealIdOrName, candidates);
77
80
  }
78
81
 
79
82
  const { addonId, ownerId } = addons[0];
@@ -1,4 +1,3 @@
1
- import { getAllInstances } from '@clevercloud/client/esm/api/v2/application.js';
2
1
  import { spawn } from 'node:child_process';
3
2
  import { randomUUID } from 'node:crypto';
4
3
  import { z } from 'zod';
@@ -6,10 +5,13 @@ import { config } from '../../config/config.js';
6
5
  import { defineCommand } from '../../lib/define-command.js';
7
6
  import { defineOption } from '../../lib/define-option.js';
8
7
  import { selectAnswer } from '../../lib/prompts.js';
8
+ import { styleText } from '../../lib/style-text.js';
9
9
  import * as Application from '../../models/application.js';
10
- import { sendToApi } from '../../models/send-to-api.js';
10
+ import { listInstances } from '../../models/instances.js';
11
11
  import { aliasOption, appIdOrNameOption } from '../global.options.js';
12
12
 
13
+ /** @typedef {import('../../models/instances.js').Instance} Instance */
14
+
13
15
  export const sshCommand = defineCommand({
14
16
  description: 'Connect to running instances through SSH',
15
17
  since: '0.7.0',
@@ -28,33 +30,62 @@ export const sshCommand = defineCommand({
28
30
  aliases: ['c'],
29
31
  placeholder: 'command',
30
32
  }),
33
+ instance: defineOption({
34
+ name: 'instance',
35
+ // Most specific first: z.union keeps the first match and an ID accepts any string
36
+ schema: z
37
+ .union([z.literal('any'), z.string().regex(/^\d+$/).transform(Number).pipe(z.int()), z.string().min(1)])
38
+ .optional(),
39
+ description:
40
+ 'Instance to connect to, by ID or number, or `any` (skips interactive selection). Build VMs are only picked by ID, or by `any` when no other instance is running',
41
+ placeholder: 'instance-id|number|any',
42
+ }),
31
43
  alias: aliasOption,
32
44
  app: appIdOrNameOption,
33
45
  },
34
46
  args: [],
35
47
  async handler(options) {
36
- const { alias, app: appIdOrName, identityFile, command } = options;
48
+ const { alias, app: appIdOrName, identityFile, command, instance } = options;
37
49
  const { appId, ownerId } = await Application.resolveId(appIdOrName, alias);
38
50
 
39
- const instances = await getAllInstances({ id: ownerId, appId }).then(sendToApi);
51
+ const instances = await listInstances({ ownerId, appId, onlyRunning: true });
40
52
 
41
53
  if (instances.length === 0) {
42
54
  throw new Error('No running instances found for this application');
43
55
  }
44
56
 
57
+ // Build VMs also carry number 0, keep them last
58
+ // Instances have no number at the very beginning of booting
59
+ // Numbers restart at 0 on each deployment, instances sharing a number are ordered newest first
60
+ const sortedInstances = instances.toSorted((a, b) => {
61
+ return (
62
+ Number(a.isBuildVm) - Number(b.isBuildVm) ||
63
+ (a.instanceNumber ?? Infinity) - (b.instanceNumber ?? Infinity) ||
64
+ b.createdAt.localeCompare(a.createdAt)
65
+ );
66
+ });
67
+
68
+ if (instance == null && sortedInstances.length > 1 && !process.stdin.isTTY) {
69
+ throw new Error(`Multiple instances are running, pick one with --instance:\n${formatInstances(sortedInstances)}`);
70
+ }
71
+
45
72
  let sshTarget;
46
- if (instances.length === 1) {
47
- sshTarget = instances[0].id;
48
- } else if (process.stdin.isTTY) {
49
- const choices = instances
50
- .sort((a, b) => a.instanceNumber - b.instanceNumber)
51
- .map((inst) => ({
52
- name: `${inst.displayName} - Instance ${inst.instanceNumber} - ${inst.state} (${inst.id})`,
53
- value: inst.id,
54
- }));
55
- sshTarget = await selectAnswer('Select an instance:', choices);
73
+ if (instance != null) {
74
+ const selectedInstance = selectInstance(sortedInstances, instance);
75
+ if (selectedInstance == null) {
76
+ throw new Error(
77
+ `No instance ${styleText('red', String(instance))} on this application, pick one with --instance:\n${formatInstances(sortedInstances)}`,
78
+ );
79
+ }
80
+ sshTarget = selectedInstance.id;
81
+ } else if (sortedInstances.length === 1) {
82
+ sshTarget = sortedInstances[0].id;
56
83
  } else {
57
- throw new Error('Multiple instances are running. Cannot select in non-interactive mode.');
84
+ const choices = sortedInstances.map((i) => ({
85
+ name: `${i.name ?? '?'} - ${i.isBuildVm ? 'Build instance' : `Instance ${i.instanceNumber ?? '?'}`} - ${i.state} (${i.id})`,
86
+ value: i.id,
87
+ }));
88
+ sshTarget = await selectAnswer('Select an instance:', choices);
58
89
  }
59
90
 
60
91
  const sshParams = [];
@@ -126,3 +157,45 @@ export const sshCommand = defineCommand({
126
157
  process.exit(exitCode);
127
158
  },
128
159
  });
160
+
161
+ /**
162
+ * Pick the instance the caller asked for, by ID, by number, or `any`, or nothing when none match.
163
+ *
164
+ * Numbers are not unique: while a deployment rolls, the instance going away and the one coming up
165
+ * carry the same number. `UP` ones are preferred among them, the most recent first, and `any` prefers
166
+ * an `UP` one over the lowest number. Build VMs are also numbered 0, so a number never picks them,
167
+ * and `any` only falls back to them when no other instance is running.
168
+ *
169
+ * @param {Array<Instance>} sortedInstances - build VMs last, then sorted by instance number, most recent first
170
+ * @param {string | number | 'any'} wanted - an instance ID, an instance number or `any`
171
+ * @returns {Instance | null}
172
+ */
173
+ function selectInstance(sortedInstances, wanted) {
174
+ const runtimeInstances = sortedInstances.filter((i) => !i.isBuildVm);
175
+ if (wanted === 'any') {
176
+ return getReadiestInstance(runtimeInstances) ?? getReadiestInstance(sortedInstances);
177
+ }
178
+ if (typeof wanted === 'number') {
179
+ return getReadiestInstance(runtimeInstances.filter((i) => i.instanceNumber === wanted));
180
+ }
181
+ return sortedInstances.find((i) => i.id === wanted) ?? null;
182
+ }
183
+
184
+ /**
185
+ * The first serving instance, or the first one when none is serving.
186
+ * @param {Array<Instance>} sortedInstances - sorted by instance number, then most recent first
187
+ * @returns {Instance | null}
188
+ */
189
+ function getReadiestInstance(sortedInstances) {
190
+ return sortedInstances.find((i) => i.state === 'UP') ?? sortedInstances[0] ?? null;
191
+ }
192
+
193
+ /**
194
+ * One line per instance, to help the caller pick one with `--instance`.
195
+ * @param {Array<Instance>} instances
196
+ * @returns {string}
197
+ */
198
+ function formatInstances(instances) {
199
+ const lines = instances.map((i) => ` - ${i.isBuildVm ? 'build' : (i.instanceNumber ?? '?')} ${i.id} (${i.state})`);
200
+ return styleText('grey', lines.join('\n'));
201
+ }
@@ -16,3 +16,13 @@ clever ssh [options]
16
16
  |`--app` `<app-id\|app-name>`|Application to manage by its ID (or name, if unambiguous)|
17
17
  |`-c`, `--command` `<command>`|Execute a command on the remote instance and exit|
18
18
  |`-i`, `--identity-file` `<identity-file>`|SSH identity file|
19
+ |`--instance` `<instance-id\|number\|any>`|Instance to connect to, by ID or number, or `any` (skips interactive selection). Build VMs are only picked by ID, or by `any` when no other instance is running|
20
+
21
+ ### Examples
22
+
23
+ ```bash
24
+ clever ssh --instance 0 -c hostname # instance number 0
25
+ clever ssh --instance any -c hostname # any running instance, a build VM only if nothing else runs
26
+ clever instances # during a deployment, the build VM shows as `build`
27
+ clever ssh --instance <build-vm-id> # connect to a build VM
28
+ ```
@@ -0,0 +1,15 @@
1
+ import { styleText } from './style-text.js';
2
+
3
+ /**
4
+ * Builds the error thrown when a name matches several resources, listing them so the user can pick one by ID
5
+ * @param {string} name - The name that matched several resources
6
+ * @param {Array<{ name: string, id: string, details?: string }>} candidates - The resources matching the name, with optional details to tell them apart
7
+ * @param {string} [idLabel] - How the identifier to use instead is called (e.g. "real ID", "key")
8
+ * @returns {Error}
9
+ */
10
+ export function ambiguousNameError(name, candidates, idLabel = 'ID') {
11
+ const list = candidates
12
+ .map((c) => ` - ${c.name} ${styleText('grey', `(${[c.id, c.details].filter(Boolean).join(', ')})`)}`)
13
+ .join('\n');
14
+ return new Error(`Ambiguous name ${styleText('red', name)}, use the ${idLabel} instead:\n${list}`);
15
+ }
package/src/lib/k8s.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import dedent from 'dedent';
2
+ import { ambiguousNameError } from './ambiguous-name-error.js';
2
3
  import { ask, confirm, selectAnswer } from './prompts.js';
3
4
  import { styleText } from './style-text.js';
4
5
 
@@ -244,10 +245,7 @@ export async function getClusterIdFromAddonIdOrName(addonIdOrName, ownerId) {
244
245
  throw new Error(`No Kubernetes cluster found with the name ${styleText('red', name)}`);
245
246
  }
246
247
  if (matches.length > 1) {
247
- const listing = matches.map((c) => `- ${c.name} (${c.id})`).join('\n');
248
- throw new Error(
249
- `Multiple Kubernetes clusters found with the name ${styleText('red', name)}, use the ID instead:\n${styleText('grey', listing)}`,
250
- );
248
+ throw ambiguousNameError(name, matches);
251
249
  }
252
250
  return matches[0].id;
253
251
  } else {
@@ -436,10 +434,7 @@ async function resolveNodeGroupId(ownerId, clusterId, nodeGroupIdOrName) {
436
434
  throw new Error(`No node group found with name ${styleText('red', nodeGroupIdOrName)}`);
437
435
  }
438
436
  if (matches.length > 1) {
439
- const listing = matches.map((ng) => `- ${ng.name} (${ng.id})`).join('\n');
440
- throw new Error(
441
- `Multiple node groups found with the name ${styleText('red', nodeGroupIdOrName)}, use the ID instead:\n${styleText('grey', listing)}`,
442
- );
437
+ throw ambiguousNameError(nodeGroupIdOrName, matches);
443
438
  }
444
439
  return matches[0].id;
445
440
  }
@@ -17,16 +17,19 @@ export async function resolveDrainResource(alias, appIdOrName, addonIdOrRealId)
17
17
  return { ownerId, resourceId: appId };
18
18
  }
19
19
 
20
+ export const DRAIN_KINDS = /** @type {const} */ (['LOG', 'ACCESSLOG']);
21
+
20
22
  /**
21
- * Creates a log drain, the recipient options that are not set are left out of the payload.
23
+ * Creates a drain, the recipient options that are not set are left out of the payload.
22
24
  * @param {string} type - Drain type, as expected by the API
25
+ * @param {typeof DRAIN_KINDS[number]} kind - Kind of logs sent to the drain
23
26
  * @param {string} ownerId
24
27
  * @param {string} resourceId
25
28
  * @param {string} url - Drain URL
26
29
  * @param {Record<string, unknown>} [recipientOptions] - Extra recipient fields, `null` and `undefined` ones are ignored
27
30
  */
28
- export function createLogDrain(type, ownerId, resourceId, url, recipientOptions = {}) {
29
- const body = { kind: 'LOG', recipient: { type, url } };
31
+ export function createLogDrain(type, kind, ownerId, resourceId, url, recipientOptions = {}) {
32
+ const body = { kind, recipient: { type, url } };
30
33
 
31
34
  for (const key in recipientOptions) {
32
35
  if (recipientOptions[key] != null) {
@@ -72,6 +75,7 @@ function formatThroughput(bytesPerSecond) {
72
75
  export function formatDrain(rawDrain) {
73
76
  const drainDetails = [
74
77
  ['ID', rawDrain.id],
78
+ ['Kind', rawDrain.kind],
75
79
  ['Status', rawDrain.status.status],
76
80
  ['Execution status', rawDrain.execution.status],
77
81
  ['URL', rawDrain.recipient.url],
@@ -0,0 +1,75 @@
1
+ import { getAllApplicationInstances } from '@clevercloud/client/esm/api/v4/instance.js';
2
+ import { toDate } from '../lib/date-utils.js';
3
+ import { sendToApi } from './send-to-api.js';
4
+
5
+ // States in which the VM is running
6
+ const RUNNING_STATES = [
7
+ 'BOOTING',
8
+ 'STARTING',
9
+ 'READY',
10
+ 'BUILDING',
11
+ 'DEPLOYING',
12
+ 'UP',
13
+ 'TASK_IN_PROGRESS',
14
+ 'MIGRATION_IN_PROGRESS',
15
+ 'STOPPING',
16
+ ];
17
+
18
+ /**
19
+ * @typedef {object} Instance
20
+ * @property {number} [instanceNumber] - Position of the instance within its deployment, starting at 0 (build VMs are also numbered 0), missing at the very beginning of booting
21
+ * @property {string} id
22
+ * @property {string|null} name
23
+ * @property {string} state
24
+ * @property {string|null} flavor
25
+ * @property {boolean} isBuildVm
26
+ * @property {string} deploymentId
27
+ * @property {string} createdAt - ISO 8601 date
28
+ * @property {string|null} deletedAt - ISO 8601 date
29
+ */
30
+
31
+ /**
32
+ * Lists the instances of an application, sorted by creation date (oldest first).
33
+ * The API returns the most recent instances first, so `limit` keeps the N most recent ones.
34
+ * @param {object} params
35
+ * @param {string} params.ownerId
36
+ * @param {string} params.appId
37
+ * @param {boolean} [params.onlyRunning] - Only keep instances whose VM is running
38
+ * @param {Date} [params.since] - Only keep instances deleted at or after this date, or not deleted yet
39
+ * @param {Date} [params.until] - Only keep instances created at or before this date
40
+ * @param {string} [params.deploymentId] - Only keep instances created by this deployment
41
+ * @param {number} [params.limit]
42
+ * @returns {Promise<Array<Instance>>}
43
+ */
44
+ export async function listInstances({ ownerId, appId, onlyRunning, since, until, deploymentId, limit }) {
45
+ const rawInstances = await getAllApplicationInstances({
46
+ ownerId,
47
+ applicationId: appId,
48
+ // The API rejects a repeated query parameter but accepts a comma-separated list
49
+ includeState: onlyRunning ? RUNNING_STATES.join(',') : undefined,
50
+ since: since?.toISOString(),
51
+ until: until?.toISOString(),
52
+ deploymentId,
53
+ limit,
54
+ }).then(sendToApi);
55
+
56
+ return rawInstances.map(normalizeInstance).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
57
+ }
58
+
59
+ /**
60
+ * @param {object} rawInstance - Instance as returned by the v4 orchestration API
61
+ * @returns {Instance}
62
+ */
63
+ function normalizeInstance(rawInstance) {
64
+ return {
65
+ instanceNumber: rawInstance.index,
66
+ id: rawInstance.id,
67
+ name: rawInstance.name ?? null,
68
+ state: rawInstance.state,
69
+ flavor: rawInstance.flavor ?? null,
70
+ isBuildVm: rawInstance.isBuildVm,
71
+ deploymentId: rawInstance.deploymentId,
72
+ createdAt: toDate(rawInstance.creationDate).toISOString(),
73
+ deletedAt: toDate(rawInstance.deletionDate)?.toISOString() ?? null,
74
+ };
75
+ }
package/src/models/ng.js CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  } from '@clevercloud/client/esm/api/v4/network-group.js';
8
8
  import crypto from 'node:crypto';
9
9
  import { searchNetworkGroupOrResource } from '../clever-client/ng.js';
10
+ import { ambiguousNameError } from '../lib/ambiguous-name-error.js';
10
11
  import { styleText } from '../lib/style-text.js';
11
12
  import { Logger } from '../logger.js';
12
13
  import { getOwnerIdFromOrgIdOrName } from './ids-resolver.js';
@@ -189,8 +190,8 @@ export async function searchNgOrResource(idOrLabel, orgaIdOrName, type = 'all',
189
190
  }
190
191
 
191
192
  if (filtered.length > 1 && type !== 'all') {
192
- throw new Error(`Multiple resources found for ${styleText('red', query)}, use ID instead:
193
- ${filtered.map((f) => ` • ${f.id} ${styleText('grey', `(${f.domainName || f.label} - ${f.type})`)}`).join('\n')}`);
193
+ const candidates = filtered.map((f) => ({ name: f.domainName || f.label, id: f.id, details: f.type }));
194
+ throw ambiguousNameError(query, candidates);
194
195
  }
195
196
 
196
197
  // Deduplicate results
@@ -1,5 +1,6 @@
1
1
  import { get as getOauthConsumer } from '@clevercloud/client/esm/api/v2/oauth-consumer.js';
2
2
  import { getSummary } from '@clevercloud/client/esm/api/v2/user.js';
3
+ import { ambiguousNameError } from '../lib/ambiguous-name-error.js';
3
4
  import { promptCheckbox } from '../lib/prompts.js';
4
5
  import { styleText } from '../lib/style-text.js';
5
6
  import { findOauthConsumersByKeyOrName } from './ids-resolver.js';
@@ -67,8 +68,8 @@ export async function resolveOauthConsumer(keyOrName) {
67
68
  }
68
69
 
69
70
  if (candidates.length > 1) {
70
- const list = candidates.map((c) => ` - ${c.name} ${styleText('grey', `(${c.key})`)}`).join('\n');
71
- throw new Error(`Ambiguous name ${styleText('red', keyOrName)}, use the key instead:\n${list}`);
71
+ const consumers = candidates.map((c) => ({ name: c.name, id: c.key }));
72
+ throw ambiguousNameError(keyOrName, consumers, 'key');
72
73
  }
73
74
 
74
75
  const { ownerId, key } = candidates[0];
@@ -1,5 +1,5 @@
1
- import dedent from 'dedent';
2
1
  import { getOperator } from '../clever-client/operators.js';
2
+ import { ambiguousNameError } from '../lib/ambiguous-name-error.js';
3
3
  import { styleText } from '../lib/style-text.js';
4
4
  import { findAddonsByNameOrId } from './ids-resolver.js';
5
5
  import { sendToApi } from './send-to-api.js';
@@ -36,10 +36,8 @@ export async function getSingleRealId(operatorIdOrName) {
36
36
  }
37
37
 
38
38
  if (operators.length > 1) {
39
- throw new Error(dedent`
40
- Ambiguous name ${styleText('red', name)}, use the real ID instead:
41
- ${styleText('grey', operators.map((otoroshi) => `- ${otoroshi.name} (${otoroshi.realId})`).join('\n'))}
42
- `);
39
+ const candidates = operators.map((o) => ({ name: o.name, id: o.realId }));
40
+ throw ambiguousNameError(name, candidates, 'real ID');
43
41
  }
44
42
 
45
43
  return operators[0].realId;
package/src/parsers.js CHANGED
@@ -138,11 +138,10 @@ export function durationInSeconds(durationStr = '') {
138
138
  try {
139
139
  return parseSimpleDuration(durationStr);
140
140
  } catch {
141
- const n = Number.parseInt(durationStr);
142
- if (isNaN(n) || n < 0) {
141
+ if (!/^\d+$/.test(durationStr)) {
143
142
  throw new Error(errorMessage);
144
143
  }
145
- return n;
144
+ return Number(durationStr);
146
145
  }
147
146
  }
148
147
 
@@ -165,6 +164,7 @@ function parseSimpleDuration(durationStr) {
165
164
  const d = ISO8601.parse(isoDuration);
166
165
  return ISO8601.toSeconds(d);
167
166
  }
167
+ throw new Error(`Unknown duration unit in "${durationStr}"`);
168
168
  }
169
169
 
170
170
  // Network Groups parsers