clever-tools 4.10.0 → 4.11.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 +1 -1
- package/src/clever-client/drains.js +17 -0
- package/src/commands/accesslogs/accesslogs.command.js +11 -2
- package/src/commands/accesslogs/accesslogs.docs.md +1 -1
- package/src/commands/create/create.command.js +8 -1
- package/src/commands/deploy/deploy.docs.md +1 -0
- package/src/commands/drain/drain.check.command.js +43 -0
- package/src/commands/drain/drain.create.command.js +15 -1
- package/src/commands/drain/drain.docs.md +25 -1
- package/src/commands/global.commands.js +2 -0
- package/src/commands/global.options.js +8 -0
- package/src/commands/profile/profile.switch.command.js +5 -1
- package/src/commands/ssh/ssh.command.js +1 -1
- package/src/lib/access-logs-clf.js +52 -0
- package/src/lib/cliparse-patched.js +18 -0
- package/src/lib/define-command.types.d.ts +7 -2
- package/src/lib/k8s.js +1 -0
- package/src/lib/operator-commands.js +1 -0
- package/src/lib/profile.js +8 -1
- package/src/lib/prompts.js +30 -2
- package/src/models/drain.js +1 -0
- package/src/models/git-isomorphic.js +16 -1
- package/src/models/git.js +18 -0
- package/src/models/oauth-consumer.js +1 -1
- package/src/models/send-to-api.js +15 -0
package/package.json
CHANGED
|
@@ -108,3 +108,20 @@ export function enableDrain(params) {
|
|
|
108
108
|
// no body
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* POST /v4/drains/organisations/{ownerId}/resources/{resourceId}/drains/{drainId}/check
|
|
114
|
+
* @param {Object} params
|
|
115
|
+
* @param {String} params.ownerId
|
|
116
|
+
* @param {String} params.resourceId
|
|
117
|
+
* @param {String} params.drainId
|
|
118
|
+
*/
|
|
119
|
+
export function checkDrain(params) {
|
|
120
|
+
// no multipath for /self or /organisations/{id}
|
|
121
|
+
return Promise.resolve({
|
|
122
|
+
method: 'post',
|
|
123
|
+
url: `/v4/drains/organisations/${params.ownerId}/resources/${params.resourceId}/drains/${params.drainId}/check`,
|
|
124
|
+
headers: { Accept: 'application/json' },
|
|
125
|
+
// no body
|
|
126
|
+
});
|
|
127
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ApplicationAccessLogStream } from '@clevercloud/client/esm/streams/access-logs.js';
|
|
2
2
|
import { formatTable } from '../../format-table.js';
|
|
3
|
+
import { formatClf } from '../../lib/access-logs-clf.js';
|
|
3
4
|
import { defineCommand } from '../../lib/define-command.js';
|
|
4
5
|
import { styleText } from '../../lib/style-text.js';
|
|
5
6
|
import { Logger } from '../../logger.js';
|
|
@@ -8,12 +9,12 @@ import { JsonArray } from '../../models/json-array.js';
|
|
|
8
9
|
import { getHostAndTokens } from '../../models/send-to-api.js';
|
|
9
10
|
import { truncateWithEllipsis } from '../../models/utils.js';
|
|
10
11
|
import {
|
|
12
|
+
accessLogsFormatOption,
|
|
11
13
|
addonIdOrRealIdOption,
|
|
12
14
|
afterOption,
|
|
13
15
|
aliasOption,
|
|
14
16
|
appIdOrNameOption,
|
|
15
17
|
beforeOption,
|
|
16
|
-
logsFormatOption,
|
|
17
18
|
} from '../global.options.js';
|
|
18
19
|
|
|
19
20
|
const THROTTLE_ELEMENTS = 2000;
|
|
@@ -76,7 +77,7 @@ export const accesslogsCommand = defineCommand({
|
|
|
76
77
|
options: {
|
|
77
78
|
alias: aliasOption,
|
|
78
79
|
app: appIdOrNameOption,
|
|
79
|
-
format:
|
|
80
|
+
format: accessLogsFormatOption,
|
|
80
81
|
before: beforeOption,
|
|
81
82
|
after: afterOption,
|
|
82
83
|
addon: addonIdOrRealIdOption,
|
|
@@ -132,6 +133,14 @@ export const accesslogsCommand = defineCommand({
|
|
|
132
133
|
case 'json-stream':
|
|
133
134
|
Logger.printJson(log);
|
|
134
135
|
break;
|
|
136
|
+
case 'clf':
|
|
137
|
+
// when the connection is cut too early, or for TCP redirections, we don't have HTTP section
|
|
138
|
+
if (log.http == null) {
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
Logger.println(formatClf(log));
|
|
143
|
+
break;
|
|
135
144
|
case 'human':
|
|
136
145
|
default:
|
|
137
146
|
// when the connection is cut too early, or for TCP redirections, we don't have HTTP section
|
|
@@ -17,4 +17,4 @@ clever accesslogs [options]
|
|
|
17
17
|
|`-a`, `--alias` `<alias>`|Short name for the application|
|
|
18
18
|
|`--app` `<app-id\|app-name>`|Application to manage by its ID (or name, if unambiguous)|
|
|
19
19
|
|`--before`, `--until` `<before>`|Fetch logs before this date/time (ISO8601 date, positive number in seconds or duration, e.g.: 1h)|
|
|
20
|
-
|`-F`, `--format` `<format>`|Output format (human, json, json-stream) (default: human)|
|
|
20
|
+
|`-F`, `--format` `<format>`|Output format (human, json, json-stream, clf) (default: human)|
|
|
@@ -9,6 +9,7 @@ import { Logger } from '../../logger.js';
|
|
|
9
9
|
import * as AppConfig from '../../models/app_configuration.js';
|
|
10
10
|
import * as Application from '../../models/application.js';
|
|
11
11
|
import { AVAILABLE_ZONES, listAvailableTypes, listAvailableZones } from '../../models/application.js';
|
|
12
|
+
import { LinkedWorktreeNotSupportedError } from '../../models/git-isomorphic.js';
|
|
12
13
|
import { Git } from '../../models/git.js';
|
|
13
14
|
import { aliasCreationOption, humanJsonOutputFormatOption, orgaIdOrNameOption } from '../global.options.js';
|
|
14
15
|
|
|
@@ -54,7 +55,13 @@ async function displayAppCreation(app, alias, github, taskCommand) {
|
|
|
54
55
|
Logger.println(` ${shellCommand('git commit -m "Initial commit"')}`);
|
|
55
56
|
Logger.println();
|
|
56
57
|
} else {
|
|
57
|
-
|
|
58
|
+
// Best-effort hint: if we can't tell because of a linked worktree on the JS backend, don't nag
|
|
59
|
+
const isClean = await git.isGitWorkingDirectoryClean().catch((error) => {
|
|
60
|
+
if (error instanceof LinkedWorktreeNotSupportedError) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
});
|
|
58
65
|
if (!isClean) {
|
|
59
66
|
Logger.println(` ${styleText('yellow', '!')} Commit your changes first:`);
|
|
60
67
|
Logger.println(` ${shellCommand('git add .')}`);
|
|
@@ -28,6 +28,7 @@ Clever Tools uses a current JS implementation for git operations. This works wit
|
|
|
28
28
|
* **HTTP-only**: cannot use SSH-based git protocols
|
|
29
29
|
* **Slow performance** on repositories with rewritten history (rebases, squashes)
|
|
30
30
|
* **Connection timeouts** on large repositories or when pushing big files, due to HTTP-based transfers
|
|
31
|
+
* **No git worktree support**: deploying from a linked git worktree (`git worktree add`) fails with `Could not find HEAD`. The system git backend deploys from worktrees just like from the main working tree
|
|
31
32
|
|
|
32
33
|
If you experience any of these issues, you can enable the **system git backend** which uses the `git` command installed on your system (it must be in your `PATH` environment variable).
|
|
33
34
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { checkDrain } from '../../clever-client/drains.js';
|
|
2
|
+
import { defineCommand } from '../../lib/define-command.js';
|
|
3
|
+
import { styleText } from '../../lib/style-text.js';
|
|
4
|
+
import { Logger } from '../../logger.js';
|
|
5
|
+
import { resolveDrainResource } from '../../models/drain.js';
|
|
6
|
+
import { sendToApi } from '../../models/send-to-api.js';
|
|
7
|
+
import {
|
|
8
|
+
addonIdOrRealIdOption,
|
|
9
|
+
aliasOption,
|
|
10
|
+
appIdOrNameOption,
|
|
11
|
+
humanJsonOutputFormatOption,
|
|
12
|
+
} from '../global.options.js';
|
|
13
|
+
import { drainIdArg } from './drain.args.js';
|
|
14
|
+
|
|
15
|
+
export const drainCheckCommand = defineCommand({
|
|
16
|
+
description: "Check that a drain's recipient is reachable and accepts deliveries",
|
|
17
|
+
since: '4.11.0',
|
|
18
|
+
options: {
|
|
19
|
+
alias: aliasOption,
|
|
20
|
+
appIdOrName: appIdOrNameOption,
|
|
21
|
+
addonIdOrRealId: addonIdOrRealIdOption,
|
|
22
|
+
format: humanJsonOutputFormatOption,
|
|
23
|
+
},
|
|
24
|
+
args: [drainIdArg],
|
|
25
|
+
async handler(options, drainId) {
|
|
26
|
+
const { alias, appIdOrName, addonIdOrRealId, format } = options;
|
|
27
|
+
const { ownerId, resourceId } = await resolveDrainResource(alias, appIdOrName, addonIdOrRealId);
|
|
28
|
+
|
|
29
|
+
const probe = await checkDrain({ ownerId, resourceId, drainId }).then(sendToApi);
|
|
30
|
+
switch (format) {
|
|
31
|
+
case 'json': {
|
|
32
|
+
Logger.printJson({ ok: probe.ok, message: probe.message });
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
case 'human':
|
|
36
|
+
default: {
|
|
37
|
+
const status = probe.ok ? styleText(['bold', 'green'], 'OK') : styleText(['bold', 'red'], 'FAILED');
|
|
38
|
+
Logger.println(`Probe: ${status}`);
|
|
39
|
+
Logger.println(probe.message);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
});
|
|
@@ -34,6 +34,13 @@ export const drainCreateCommand = defineCommand({
|
|
|
34
34
|
aliases: ['k'],
|
|
35
35
|
placeholder: 'api-key',
|
|
36
36
|
}),
|
|
37
|
+
sourceToken: defineOption({
|
|
38
|
+
name: 'source-token',
|
|
39
|
+
schema: z.string().optional(),
|
|
40
|
+
description: 'Source token (for betterstack)',
|
|
41
|
+
aliases: ['t'],
|
|
42
|
+
placeholder: 'source-token',
|
|
43
|
+
}),
|
|
37
44
|
indexPrefix: defineOption({
|
|
38
45
|
name: 'index-prefix',
|
|
39
46
|
schema: z.string().optional(),
|
|
@@ -66,7 +73,7 @@ export const drainCreateCommand = defineCommand({
|
|
|
66
73
|
],
|
|
67
74
|
async handler(options, drainTypeCliCode, url) {
|
|
68
75
|
const { alias, appIdOrName, addonIdOrRealId } = options;
|
|
69
|
-
const { username, password, apiKey, indexPrefix, rfc5424StructuredDataParameters } = options;
|
|
76
|
+
const { username, password, apiKey, sourceToken, indexPrefix, rfc5424StructuredDataParameters } = options;
|
|
70
77
|
|
|
71
78
|
const drainType = Object.values(DRAIN_TYPES).find((drainType) => drainType.cliCode === drainTypeCliCode);
|
|
72
79
|
|
|
@@ -108,6 +115,13 @@ export const drainCreateCommand = defineCommand({
|
|
|
108
115
|
body.recipient.apiKey = apiKey;
|
|
109
116
|
}
|
|
110
117
|
|
|
118
|
+
if (drainTypeCliCode === DRAIN_TYPES.BETTERSTACK.cliCode) {
|
|
119
|
+
if (!sourceToken) {
|
|
120
|
+
throw new Error(`${DRAIN_TYPES.BETTERSTACK.cliCode} drains require a source token (--source-token) to be set`);
|
|
121
|
+
}
|
|
122
|
+
body.recipient.sourceToken = sourceToken;
|
|
123
|
+
}
|
|
124
|
+
|
|
111
125
|
if (
|
|
112
126
|
drainTypeCliCode === DRAIN_TYPES.OVH_TCP.cliCode ||
|
|
113
127
|
drainTypeCliCode === DRAIN_TYPES.SYSLOG_TCP.cliCode ||
|
|
@@ -17,6 +17,29 @@ clever drain [options]
|
|
|
17
17
|
|`--app` `<app-id\|app-name>`|Application to manage by its ID (or name, if unambiguous)|
|
|
18
18
|
|`-F`, `--format` `<format>`|Output format (human, json) (default: human)|
|
|
19
19
|
|
|
20
|
+
## ➡️ `clever drain check` <kbd>Since 4.11.0</kbd>
|
|
21
|
+
|
|
22
|
+
Check that a drain's recipient is reachable and accepts deliveries
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
clever drain check <drain-id> [options]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 📥 Arguments
|
|
29
|
+
|
|
30
|
+
|Name|Description|
|
|
31
|
+
|---|---|
|
|
32
|
+
|`drain-id`|Drain ID|
|
|
33
|
+
|
|
34
|
+
### ⚙️ Options
|
|
35
|
+
|
|
36
|
+
|Name|Description|
|
|
37
|
+
|---|---|
|
|
38
|
+
|`--addon` `<addon-id>`|Add-on ID or real ID|
|
|
39
|
+
|`-a`, `--alias` `<alias>`|Short name for the application|
|
|
40
|
+
|`--app` `<app-id\|app-name>`|Application to manage by its ID (or name, if unambiguous)|
|
|
41
|
+
|`-F`, `--format` `<format>`|Output format (human, json) (default: human)|
|
|
42
|
+
|
|
20
43
|
## ➡️ `clever drain create` <kbd>Since 0.9.0</kbd>
|
|
21
44
|
|
|
22
45
|
Create a drain
|
|
@@ -29,7 +52,7 @@ clever drain create <drain-type> <drain-url> [options]
|
|
|
29
52
|
|
|
30
53
|
|Name|Description|
|
|
31
54
|
|---|---|
|
|
32
|
-
|`drain-type`|Drain type (datadog, elasticsearch, newrelic, ovh-tcp, raw-http, syslog-tcp, syslog-udp)|
|
|
55
|
+
|`drain-type`|Drain type (betterstack, datadog, elasticsearch, newrelic, ovh-tcp, raw-http, syslog-tcp, syslog-udp)|
|
|
33
56
|
|`drain-url`|Drain URL|
|
|
34
57
|
|
|
35
58
|
### ⚙️ Options
|
|
@@ -43,6 +66,7 @@ clever drain create <drain-type> <drain-url> [options]
|
|
|
43
66
|
|`-i`, `--index-prefix` `<index-prefix>`|Optional index prefix (for elasticsearch), `logstash` value is used if not set|
|
|
44
67
|
|`-p`, `--password` `<password>`|Basic auth password (for elasticsearch or raw-http)|
|
|
45
68
|
|`-s`, `--sd-params` `<sd-params>`|RFC5424 structured data parameters (for ovh-tcp), e.g.: `X-OVH-TOKEN=\"REDACTED\"`|
|
|
69
|
+
|`-t`, `--source-token` `<source-token>`|Source token (for betterstack)|
|
|
46
70
|
|`-u`, `--username` `<username>`|Basic auth username (for elasticsearch or raw-http)|
|
|
47
71
|
|
|
48
72
|
## ➡️ `clever drain disable` <kbd>Since 0.9.0</kbd>
|
|
@@ -39,6 +39,7 @@ import { domainFavouriteSetCommand } from './domain/domain.favourite.set.command
|
|
|
39
39
|
import { domainFavouriteUnsetCommand } from './domain/domain.favourite.unset.command.js';
|
|
40
40
|
import { domainOverviewCommand } from './domain/domain.overview.command.js';
|
|
41
41
|
import { domainRmCommand } from './domain/domain.rm.command.js';
|
|
42
|
+
import { drainCheckCommand } from './drain/drain.check.command.js';
|
|
42
43
|
import { drainCommand } from './drain/drain.command.js';
|
|
43
44
|
import { drainCreateCommand } from './drain/drain.create.command.js';
|
|
44
45
|
import { drainDisableCommand } from './drain/drain.disable.command.js';
|
|
@@ -268,6 +269,7 @@ export const globalCommands = {
|
|
|
268
269
|
drain: [
|
|
269
270
|
drainCommand,
|
|
270
271
|
{
|
|
272
|
+
check: drainCheckCommand,
|
|
271
273
|
create: drainCreateCommand,
|
|
272
274
|
disable: drainDisableCommand,
|
|
273
275
|
enable: drainEnableCommand,
|
|
@@ -28,6 +28,14 @@ export const logsFormatOption = defineOption({
|
|
|
28
28
|
placeholder: 'format',
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
+
export const accessLogsFormatOption = defineOption({
|
|
32
|
+
name: 'format',
|
|
33
|
+
schema: z.enum(['human', 'json', 'json-stream', 'clf']).default('human'),
|
|
34
|
+
description: 'Output format',
|
|
35
|
+
aliases: ['F'],
|
|
36
|
+
placeholder: 'format',
|
|
37
|
+
});
|
|
38
|
+
|
|
31
39
|
export const beforeOption = defineOption({
|
|
32
40
|
name: 'before',
|
|
33
41
|
schema: z.string().transform(date).optional(),
|
|
@@ -75,6 +75,10 @@ async function resolveTargetProfile(profiles, requestedAlias) {
|
|
|
75
75
|
name: formatProfile(profile),
|
|
76
76
|
value: profile.alias,
|
|
77
77
|
}));
|
|
78
|
-
const selectedAlias = await selectAnswer(
|
|
78
|
+
const selectedAlias = await selectAnswer(
|
|
79
|
+
'Select a profile:',
|
|
80
|
+
choices,
|
|
81
|
+
'Use --alias <name> to select a profile directly.',
|
|
82
|
+
);
|
|
79
83
|
return profiles.find((profile) => profile.alias === selectedAlias);
|
|
80
84
|
}
|
|
@@ -45,7 +45,7 @@ export const sshCommand = defineCommand({
|
|
|
45
45
|
let sshTarget;
|
|
46
46
|
if (instances.length === 1) {
|
|
47
47
|
sshTarget = instances[0].id;
|
|
48
|
-
} else if (process.
|
|
48
|
+
} else if (process.stdin.isTTY) {
|
|
49
49
|
const choices = instances
|
|
50
50
|
.sort((a, b) => a.instanceNumber - b.instanceNumber)
|
|
51
51
|
.map((inst) => ({
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const CLF_MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Format a `Date` as a Common Log Format timestamp in UTC, e.g. `10/Oct/2000:13:55:36 +0000`.
|
|
5
|
+
* @param {Date} date
|
|
6
|
+
* @returns {string}
|
|
7
|
+
*/
|
|
8
|
+
function formatClfDate(date) {
|
|
9
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
10
|
+
const day = pad(date.getUTCDate());
|
|
11
|
+
const month = CLF_MONTHS[date.getUTCMonth()];
|
|
12
|
+
const year = date.getUTCFullYear();
|
|
13
|
+
const hours = pad(date.getUTCHours());
|
|
14
|
+
const minutes = pad(date.getUTCMinutes());
|
|
15
|
+
const seconds = pad(date.getUTCSeconds());
|
|
16
|
+
return `${day}/${month}/${year}:${hours}:${minutes}:${seconds} +0000`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Escape a value so it can be safely embedded inside a double-quoted CLF field.
|
|
21
|
+
* Backslashes are escaped first, then double-quotes, otherwise a `"` would be escaped
|
|
22
|
+
* twice. Without this, a `"` in the value would close the quoted field early and shift
|
|
23
|
+
* every subsequent column.
|
|
24
|
+
* @param {string} value
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
function escapeClfQuoted(value) {
|
|
28
|
+
return String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Format an HTTP access log as a Common Log Format line.
|
|
33
|
+
*
|
|
34
|
+
* The HTTP protocol version is absent from the v4 access log payload, so the request line is
|
|
35
|
+
* limited to `method path` (no `HTTP/x.y` token). The protocol token is optional in CLF parsers
|
|
36
|
+
* (grok, GoAccess), and emitting a `-` placeholder would actually break their structured parsing.
|
|
37
|
+
*
|
|
38
|
+
* @see https://en.wikipedia.org/wiki/Common_Log_Format
|
|
39
|
+
* @param {object} log an HTTP access log (the `http` section must be present)
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
export function formatClf(log) {
|
|
43
|
+
const host = log.source.ip;
|
|
44
|
+
const ident = '-';
|
|
45
|
+
const authuser = '-';
|
|
46
|
+
const date = formatClfDate(log.date);
|
|
47
|
+
const request = `${log.http.request.method} ${escapeClfQuoted(log.http.request.path)}`;
|
|
48
|
+
const status = log.http.response.statusCode;
|
|
49
|
+
const bytes = log.bytesOut;
|
|
50
|
+
|
|
51
|
+
return `${host} ${ident} ${authuser} [${date}] "${request}" ${status} ${bytes}`;
|
|
52
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import cliparseOriginal from 'cliparse';
|
|
2
|
+
import cliparseArgumentModule from 'cliparse/src/argument.js';
|
|
2
3
|
import cliparseCommandModule from 'cliparse/src/command.js';
|
|
3
4
|
import semver from 'semver';
|
|
4
5
|
import pkg from '../../package.json' with { type: 'json' };
|
|
@@ -33,6 +34,23 @@ cliparseOriginal.command = function (name, options, commandFunction) {
|
|
|
33
34
|
return command;
|
|
34
35
|
};
|
|
35
36
|
|
|
37
|
+
// Patch cliparse.argument.parseList to drop the parse results that succeeded
|
|
38
|
+
// from its error payload. When several positional args are given and only one
|
|
39
|
+
// fails its parser, parseList returns the *whole* result list (successes
|
|
40
|
+
// included) as the error. Downstream, displayErrors prints the valid siblings
|
|
41
|
+
// as `<arg-name>: undefined` (they carry an `.argument` but no `.error`).
|
|
42
|
+
// We keep only the entries that actually failed, fixing the issue at its root.
|
|
43
|
+
// The success path is untouched (we only rewrite the error array), and
|
|
44
|
+
// missing-value errors carry an `.error` ("missing value") so they are kept.
|
|
45
|
+
const originalParseList = cliparseArgumentModule.parseList;
|
|
46
|
+
cliparseArgumentModule.parseList = function (args, providedArguments) {
|
|
47
|
+
const result = originalParseList(args, providedArguments);
|
|
48
|
+
if (Array.isArray(result.error)) {
|
|
49
|
+
return cliparseOriginal.parsers.error(result.error.filter((entry) => entry.error != null));
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
};
|
|
53
|
+
|
|
36
54
|
/**
|
|
37
55
|
* Map options from cliparse format (using option.name) to the original definition keys.
|
|
38
56
|
* For example, { theOptionName: defineOption({ name: 'the-option-name', ... }) },
|
|
@@ -41,8 +41,13 @@ export interface CommandDefinition<
|
|
|
41
41
|
*/
|
|
42
42
|
featureFlag?: string;
|
|
43
43
|
|
|
44
|
-
/**
|
|
45
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Version when this command was introduced (semver format, e.g., '2.1.0').
|
|
46
|
+
* Use `null` for a command not yet released: the release workflow resolves it
|
|
47
|
+
* to the upcoming version (see `scripts/resolve-since.js`). Required on purpose,
|
|
48
|
+
* so a missing value is a type error rather than a silent omission.
|
|
49
|
+
*/
|
|
50
|
+
since: `${number}.${number}.${number}` | null;
|
|
46
51
|
|
|
47
52
|
/** Options (named options like --type, --region, --format). */
|
|
48
53
|
options?: O;
|
package/src/lib/k8s.js
CHANGED
|
@@ -503,6 +503,7 @@ export async function k8sUpdateVersion(orgIdOrName, clusterIdOrName, askedVersio
|
|
|
503
503
|
(await selectAnswer(
|
|
504
504
|
`Which version do you want to update ${styleText('blue', name)} to, current is ${styleText('blue', versions.installed)}?`,
|
|
505
505
|
[...versions.available].reverse(),
|
|
506
|
+
'Use --target <version> to update directly.',
|
|
506
507
|
));
|
|
507
508
|
|
|
508
509
|
if (!versions.available.includes(targetVersion)) {
|
|
@@ -77,6 +77,7 @@ export async function operatorUpdateVersion(provider, askedVersion, addonIdOrNam
|
|
|
77
77
|
(await selectAnswer(
|
|
78
78
|
`Which version do you want to update ${styleText('blue', name)} to, current is ${styleText('blue', versions.installed)}?`,
|
|
79
79
|
versions.available.reverse(),
|
|
80
|
+
'Use --target <version> to update directly.',
|
|
80
81
|
));
|
|
81
82
|
|
|
82
83
|
if (!versions.available.includes(targetVersion)) {
|
package/src/lib/profile.js
CHANGED
|
@@ -59,7 +59,14 @@ export async function getProfileDetails({ profile, isActive }) {
|
|
|
59
59
|
const [user, token] = await Promise.all([
|
|
60
60
|
getUser({}).then(sendWithCredentials),
|
|
61
61
|
getCurrentTokenInfo().then(sendWithCredentials),
|
|
62
|
-
]).catch(() =>
|
|
62
|
+
]).catch((error) => {
|
|
63
|
+
// An expired/invalid token surfaces as a 401: degrade gracefully so the command can report it.
|
|
64
|
+
// Any other failure (TLS, network…) must bubble up instead of being masked as "token invalid".
|
|
65
|
+
if (error?.cause?.response?.status === 401) {
|
|
66
|
+
return [null, null];
|
|
67
|
+
}
|
|
68
|
+
throw error;
|
|
69
|
+
});
|
|
63
70
|
|
|
64
71
|
return {
|
|
65
72
|
id: user?.id ?? profile.userId,
|
package/src/lib/prompts.js
CHANGED
|
@@ -25,11 +25,23 @@ export async function confirmAnswer(message, rejectionMessage, expectedAnswer) {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* @param {string} message
|
|
30
|
+
* @param {Array<unknown>} choices
|
|
31
|
+
* @param {string} [nonInteractiveHint] How to achieve the same result without a prompt (e.g. an option to use)
|
|
32
|
+
*/
|
|
33
|
+
export function selectAnswer(message, choices, nonInteractiveHint) {
|
|
34
|
+
assertInteractiveTerminal(nonInteractiveHint);
|
|
29
35
|
return select({ message, choices }).catch(exitOnPromptError);
|
|
30
36
|
}
|
|
31
37
|
|
|
32
|
-
|
|
38
|
+
/**
|
|
39
|
+
* @param {string} message
|
|
40
|
+
* @param {Array<unknown>} choices
|
|
41
|
+
* @param {string} [nonInteractiveHint] How to achieve the same result without a prompt (e.g. an option to use)
|
|
42
|
+
*/
|
|
43
|
+
export function promptCheckbox(message, choices, nonInteractiveHint) {
|
|
44
|
+
assertInteractiveTerminal(nonInteractiveHint);
|
|
33
45
|
return checkbox({ message, choices }).catch(exitOnPromptError);
|
|
34
46
|
}
|
|
35
47
|
|
|
@@ -50,6 +62,22 @@ export function promptTextOption(option, defaultValue) {
|
|
|
50
62
|
return input({ message, default: defaultValue, validate }).catch(exitOnPromptError);
|
|
51
63
|
}
|
|
52
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Throw an explicit error when no interactive terminal is available.
|
|
67
|
+
*
|
|
68
|
+
* Raw-mode prompts (select, checkbox) need a TTY on stdin. Without one (CI, scripts,
|
|
69
|
+
* pipes, `< /dev/null`), Inquirer reads EOF and throws ExitPromptError, which
|
|
70
|
+
* exitOnPromptError turns into a silent `process.exit(1)` — indistinguishable from a
|
|
71
|
+
* user pressing Ctrl+C. Guarding up front lets us surface a clear message instead.
|
|
72
|
+
* @param {string} [hint] How to achieve the same result without a prompt (e.g. an option to use)
|
|
73
|
+
*/
|
|
74
|
+
function assertInteractiveTerminal(hint) {
|
|
75
|
+
if (!process.stdin.isTTY) {
|
|
76
|
+
const suffix = hint != null ? ` ${hint}` : '';
|
|
77
|
+
throw new Error(`This command requires an interactive terminal.${suffix}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
53
81
|
function exitOnPromptError(error) {
|
|
54
82
|
if (error instanceof Error && error.name === 'ExitPromptError') {
|
|
55
83
|
process.exit(1);
|
package/src/models/drain.js
CHANGED
|
@@ -16,6 +16,7 @@ export async function resolveDrainResource(alias, appIdOrName, addonIdOrRealId)
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export const DRAIN_TYPES = {
|
|
19
|
+
BETTERSTACK: { apiCode: 'BETTERSTACK', cliCode: 'betterstack', label: 'Better Stack' },
|
|
19
20
|
DATADOG: { apiCode: 'DATADOG', cliCode: 'datadog', label: 'Datadog' },
|
|
20
21
|
ELASTICSEARCH: { apiCode: 'ELASTICSEARCH', cliCode: 'elasticsearch', label: 'Elasticsearch' },
|
|
21
22
|
NEWRELIC: { apiCode: 'NEWRELIC', cliCode: 'newrelic', label: 'New Relic' },
|
|
@@ -13,6 +13,9 @@ export class GitIsomorphic extends Git {
|
|
|
13
13
|
|
|
14
14
|
async #getRepo() {
|
|
15
15
|
const dir = await this._getRepoDir();
|
|
16
|
+
if (await this._isLinkedWorktree(dir)) {
|
|
17
|
+
throw new LinkedWorktreeNotSupportedError();
|
|
18
|
+
}
|
|
16
19
|
return { fs, dir, http };
|
|
17
20
|
}
|
|
18
21
|
|
|
@@ -128,7 +131,8 @@ export class GitIsomorphic extends Git {
|
|
|
128
131
|
*/
|
|
129
132
|
async isInsideGitRepo() {
|
|
130
133
|
this._debug('isInsideGitRepo');
|
|
131
|
-
|
|
134
|
+
// Don't go through #getRepo: it rejects linked worktrees, which are still git repos
|
|
135
|
+
return this._getRepoDir()
|
|
132
136
|
.then(() => true)
|
|
133
137
|
.catch(() => false);
|
|
134
138
|
}
|
|
@@ -151,3 +155,14 @@ export class GitIsomorphic extends Git {
|
|
|
151
155
|
return isStatusEmpty;
|
|
152
156
|
}
|
|
153
157
|
}
|
|
158
|
+
|
|
159
|
+
export class LinkedWorktreeNotSupportedError extends Error {
|
|
160
|
+
constructor() {
|
|
161
|
+
super(
|
|
162
|
+
"Linked git worktrees aren't supported by the default JS git backend.\n" +
|
|
163
|
+
'Enable the system git backend (it uses your installed git):\n' +
|
|
164
|
+
' clever features enable system-git',
|
|
165
|
+
);
|
|
166
|
+
this.name = 'LinkedWorktreeNotSupportedError';
|
|
167
|
+
}
|
|
168
|
+
}
|
package/src/models/git.js
CHANGED
|
@@ -66,6 +66,24 @@ export class Git {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Check if the repository is a linked git worktree.
|
|
71
|
+
* In a linked worktree (created with `git worktree add`), the top-level `.git`
|
|
72
|
+
* is a file holding a `gitdir:` pointer instead of a directory.
|
|
73
|
+
* @protected
|
|
74
|
+
* @param {string} [dir] - Repository directory (resolved automatically when omitted)
|
|
75
|
+
* @returns {Promise<boolean>}
|
|
76
|
+
*/
|
|
77
|
+
async _isLinkedWorktree(dir) {
|
|
78
|
+
const repoDir = dir ?? (await this._getRepoDir());
|
|
79
|
+
try {
|
|
80
|
+
const stats = await fs.promises.stat(path.join(repoDir, '.git'));
|
|
81
|
+
return stats.isFile();
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
69
87
|
/**
|
|
70
88
|
* Add a remote to the repository
|
|
71
89
|
* @param {string} remoteName
|
|
@@ -47,7 +47,7 @@ export async function promptRights(existingRights) {
|
|
|
47
47
|
checked: existingRights?.[apiName] ?? false,
|
|
48
48
|
}));
|
|
49
49
|
|
|
50
|
-
const selected = await promptCheckbox('Select rights', choices);
|
|
50
|
+
const selected = await promptCheckbox('Select rights', choices, 'Use --rights <list> to set rights directly.');
|
|
51
51
|
|
|
52
52
|
return rightsFromList(selected);
|
|
53
53
|
}
|
|
@@ -75,6 +75,15 @@ async function executeRequest(requestParams, customConfig = {}) {
|
|
|
75
75
|
.catch(processError);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
// OpenSSL error codes raised when the server certificate chain isn't trusted
|
|
79
|
+
// (corporate TLS-intercepting proxy, private or self-signed CA…).
|
|
80
|
+
const TLS_ERROR_CODES = [
|
|
81
|
+
'SELF_SIGNED_CERT_IN_CHAIN',
|
|
82
|
+
'DEPTH_ZERO_SELF_SIGNED_CERT',
|
|
83
|
+
'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
|
|
84
|
+
'UNABLE_TO_GET_ISSUER_CERT_LOCALLY',
|
|
85
|
+
];
|
|
86
|
+
|
|
78
87
|
export function processError(error) {
|
|
79
88
|
const code = error.code ?? error?.cause?.code;
|
|
80
89
|
if (code === 'EAI_AGAIN') {
|
|
@@ -89,6 +98,12 @@ export function processError(error) {
|
|
|
89
98
|
{ cause: error },
|
|
90
99
|
);
|
|
91
100
|
}
|
|
101
|
+
if (TLS_ERROR_CODES.includes(code)) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`TLS certificate verification failed (${code}). If you're behind a corporate proxy or using a private/self-signed Certificate Authority, trust your CA and follow the "TLS certificates" section of the documentation.`,
|
|
104
|
+
{ cause: error },
|
|
105
|
+
);
|
|
106
|
+
}
|
|
92
107
|
throw error;
|
|
93
108
|
}
|
|
94
109
|
|