clever-tools 4.10.0 → 5.0.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/bin/clever.js +5 -10
- package/package.json +27 -27
- package/src/clever-client/drains.js +17 -0
- package/src/commands/accesslogs/accesslogs.command.js +35 -18
- package/src/commands/accesslogs/accesslogs.docs.md +1 -1
- package/src/commands/addon/addon.docs.md +2 -2
- package/src/commands/config-provider/config-provider.docs.md +5 -5
- package/src/commands/create/create.command.js +8 -1
- package/src/commands/database/database.docs.md +2 -2
- package/src/commands/deploy/deploy.docs.md +7 -6
- package/src/commands/drain/drain.args.js +6 -0
- package/src/commands/drain/drain.check.command.js +43 -0
- package/src/commands/drain/drain.create.betterstack.command.js +36 -0
- package/src/commands/drain/drain.create.command.js +0 -118
- package/src/commands/drain/drain.create.datadog.command.js +27 -0
- package/src/commands/drain/drain.create.elasticsearch.command.js +48 -0
- package/src/commands/drain/drain.create.newrelic.command.js +36 -0
- package/src/commands/drain/drain.create.ovh-tcp.command.js +29 -0
- package/src/commands/drain/drain.create.raw-http.command.js +30 -0
- package/src/commands/drain/drain.create.splunk.command.js +61 -0
- package/src/commands/drain/drain.create.syslog-tcp.command.js +29 -0
- package/src/commands/drain/drain.create.syslog-udp.command.js +29 -0
- package/src/commands/drain/drain.docs.md +296 -7
- package/src/commands/drain/drain.options.js +26 -0
- package/src/commands/features/features.list.command.js +6 -13
- package/src/commands/global.commands.js +25 -1
- package/src/commands/global.options.js +8 -0
- package/src/commands/k8s/k8s.docs.md +17 -17
- package/src/commands/keycloak/keycloak.docs.md +11 -11
- package/src/commands/kv/kv.docs.md +1 -1
- package/src/commands/link/link.docs.md +1 -1
- package/src/commands/login/login.command.js +11 -1
- package/src/commands/matomo/matomo.docs.md +6 -6
- package/src/commands/metabase/metabase.docs.md +9 -9
- package/src/commands/ng/ng.docs.md +10 -10
- package/src/commands/oauth-consumers/oauth-consumers.docs.md +4 -4
- package/src/commands/otoroshi/otoroshi.docs.md +13 -13
- package/src/commands/profile/profile.switch.command.js +5 -1
- package/src/commands/service/service.docs.md +4 -4
- package/src/commands/ssh/ssh.command.js +7 -1
- package/src/config/features.js +61 -29
- package/src/format-table.js +14 -10
- package/src/initial-setup.js +13 -0
- package/src/initial-update-notifier.js +2 -5
- package/src/lib/access-logs-clf.js +52 -0
- package/src/lib/access-logs-transport.js +29 -0
- package/src/lib/cliparse-patched.js +19 -1
- 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 +39 -12
- package/src/models/git-isomorphic.js +16 -1
- package/src/models/git-system.js +2 -2
- package/src/models/git.js +19 -1
- package/src/models/oauth-consumer.js +1 -1
- package/src/models/send-to-api.js +15 -0
|
@@ -31,7 +31,7 @@ clever service link-addon <addon-id|addon-name> [options]
|
|
|
31
31
|
|
|
32
32
|
|Name|Description|
|
|
33
33
|
|---|---|
|
|
34
|
-
|`addon-id
|
|
34
|
+
|`addon-id\|addon-name`|Add-on ID (or name, if unambiguous)|
|
|
35
35
|
|
|
36
36
|
### ⚙️ Options
|
|
37
37
|
|
|
@@ -52,7 +52,7 @@ clever service link-app <app-id|app-name> [options]
|
|
|
52
52
|
|
|
53
53
|
|Name|Description|
|
|
54
54
|
|---|---|
|
|
55
|
-
|`app-id
|
|
55
|
+
|`app-id\|app-name`|Application ID (or name, if unambiguous)|
|
|
56
56
|
|
|
57
57
|
### ⚙️ Options
|
|
58
58
|
|
|
@@ -73,7 +73,7 @@ clever service unlink-addon <addon-id|addon-name> [options]
|
|
|
73
73
|
|
|
74
74
|
|Name|Description|
|
|
75
75
|
|---|---|
|
|
76
|
-
|`addon-id
|
|
76
|
+
|`addon-id\|addon-name`|Add-on ID (or name, if unambiguous)|
|
|
77
77
|
|
|
78
78
|
### ⚙️ Options
|
|
79
79
|
|
|
@@ -94,7 +94,7 @@ clever service unlink-app <app-id|app-name> [options]
|
|
|
94
94
|
|
|
95
95
|
|Name|Description|
|
|
96
96
|
|---|---|
|
|
97
|
-
|`app-id
|
|
97
|
+
|`app-id\|app-name`|Application ID (or name, if unambiguous)|
|
|
98
98
|
|
|
99
99
|
### ⚙️ Options
|
|
100
100
|
|
|
@@ -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) => ({
|
|
@@ -58,12 +58,18 @@ export const sshCommand = defineCommand({
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
const sshParams = [];
|
|
61
|
+
// Clever Cloud SSH only accepts key auth. Disable password fallback so a missing
|
|
62
|
+
// or unregistered key fails fast instead of prompting for a password that cannot work.
|
|
63
|
+
sshParams.push('-o', 'PreferredAuthentications=publickey', '-o', 'PasswordAuthentication=no');
|
|
61
64
|
// -t: force PTY allocation (SSH skips it by default because appId is passed as a command for gateway routing)
|
|
62
65
|
if (command == null) {
|
|
63
66
|
sshParams.push('-t');
|
|
64
67
|
}
|
|
65
68
|
if (identityFile != null) {
|
|
69
|
+
// -i adds the key to the ones ssh may offer, but doesn't guarantee it's the one used
|
|
66
70
|
sshParams.push('-i', identityFile);
|
|
71
|
+
// IdentitiesOnly forces ssh to only offer this key, ignoring agent/default ones
|
|
72
|
+
sshParams.push('-o', 'IdentitiesOnly=yes');
|
|
67
73
|
}
|
|
68
74
|
sshParams.push(config.SSH_GATEWAY, sshTarget);
|
|
69
75
|
|
package/src/config/features.js
CHANGED
|
@@ -1,26 +1,40 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import z from 'zod';
|
|
3
|
-
import {
|
|
3
|
+
import { readJsonSync, writeJson } from '../lib/fs.js';
|
|
4
4
|
import { Logger } from '../logger.js';
|
|
5
5
|
import { config } from './config.js';
|
|
6
6
|
import { getConfigPath } from './paths.js';
|
|
7
7
|
|
|
8
8
|
const EXPERIMENTAL_FEATURES_FILEPATH = getConfigPath('clever-tools-experimental-features.json');
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {object} ExperimentalFeature
|
|
12
|
+
* @property {'beta'|'stable'} status
|
|
13
|
+
* @property {boolean} defaultValue - Value used when the feature is not explicitly set by the user
|
|
14
|
+
* @property {string} description
|
|
15
|
+
* @property {string} [instructions]
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** @type {Record<string, ExperimentalFeature>} */
|
|
10
19
|
export const EXPERIMENTAL_FEATURES = {
|
|
11
20
|
'system-git': {
|
|
12
|
-
status: '
|
|
13
|
-
|
|
21
|
+
status: 'stable',
|
|
22
|
+
defaultValue: true,
|
|
23
|
+
description: 'Use system git instead of the pure JS implementation for git operations',
|
|
14
24
|
instructions: dedent`
|
|
15
|
-
This feature switches from the
|
|
16
|
-
the git installed on your system.
|
|
25
|
+
This feature switches from the pure JS implementation to using
|
|
26
|
+
the git installed on your system. It is enabled by default since v5.0.0.
|
|
17
27
|
|
|
18
28
|
Requirements:
|
|
19
29
|
- git must be installed and available in your PATH
|
|
30
|
+
|
|
31
|
+
Disable it to fall back to the previous pure JS implementation:
|
|
32
|
+
clever features disable system-git
|
|
20
33
|
`,
|
|
21
34
|
},
|
|
22
35
|
k8s: {
|
|
23
36
|
status: 'beta',
|
|
37
|
+
defaultValue: false,
|
|
24
38
|
description: 'Deploy and manage Kubernetes clusters on Clever Cloud',
|
|
25
39
|
instructions: dedent`
|
|
26
40
|
- Create a Kubernetes cluster:
|
|
@@ -48,6 +62,7 @@ export const EXPERIMENTAL_FEATURES = {
|
|
|
48
62
|
},
|
|
49
63
|
kv: {
|
|
50
64
|
status: 'beta',
|
|
65
|
+
defaultValue: false,
|
|
51
66
|
description:
|
|
52
67
|
'Send commands to databases such as Materia KV or Redis® directly from Clever Tools, without other dependencies',
|
|
53
68
|
instructions: dedent`
|
|
@@ -64,6 +79,7 @@ export const EXPERIMENTAL_FEATURES = {
|
|
|
64
79
|
},
|
|
65
80
|
ng: {
|
|
66
81
|
status: 'beta',
|
|
82
|
+
defaultValue: false,
|
|
67
83
|
description: 'Manage Network Groups to manage applications, add-ons, external peers through a WireGuard network',
|
|
68
84
|
instructions: dedent`
|
|
69
85
|
- Create a Network Group:
|
|
@@ -92,6 +108,7 @@ export const EXPERIMENTAL_FEATURES = {
|
|
|
92
108
|
},
|
|
93
109
|
operators: {
|
|
94
110
|
status: 'beta',
|
|
111
|
+
defaultValue: false,
|
|
95
112
|
description: 'Manage operators and their features such as Keycloak, Matomo, Metabase, Otoroshi',
|
|
96
113
|
instructions: dedent`
|
|
97
114
|
clever keycloak
|
|
@@ -116,52 +133,67 @@ const FeaturesConfigSchema = z
|
|
|
116
133
|
*/
|
|
117
134
|
|
|
118
135
|
/**
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* @returns {Promise<FeaturesConfig>} The features configuration object
|
|
136
|
+
* The features explicitly set by the user, loaded synchronously at startup.
|
|
137
|
+
* @type {FeaturesConfig}
|
|
122
138
|
*/
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
139
|
+
let userFeatures = loadFeatures();
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Reads and parses the features file.
|
|
143
|
+
* Returns an empty object if the file doesn't exist or is invalid.
|
|
144
|
+
* @returns {FeaturesConfig} The features configuration object
|
|
145
|
+
*/
|
|
146
|
+
function loadFeatures() {
|
|
147
|
+
Logger.debug(`Load features configuration from ${EXPERIMENTAL_FEATURES_FILEPATH}`);
|
|
148
|
+
const rawFeatures = readJsonSync(EXPERIMENTAL_FEATURES_FILEPATH);
|
|
149
|
+
if (rawFeatures == null) {
|
|
150
|
+
return {};
|
|
151
|
+
}
|
|
152
|
+
const parsed = FeaturesConfigSchema.safeParse(rawFeatures);
|
|
153
|
+
if (!parsed.success) {
|
|
154
|
+
Logger.info(`Invalid features format in ${EXPERIMENTAL_FEATURES_FILEPATH}`);
|
|
137
155
|
return {};
|
|
138
156
|
}
|
|
157
|
+
return parsed.data;
|
|
139
158
|
}
|
|
140
159
|
|
|
141
160
|
/**
|
|
142
161
|
* Sets an experimental feature to the specified value.
|
|
143
|
-
* Creates the configuration directory and features file if they don't exist
|
|
162
|
+
* Creates the configuration directory and features file if they don't exist,
|
|
163
|
+
* then reloads the in-memory configuration.
|
|
144
164
|
* @param {string} feature - The name of the feature to set
|
|
145
165
|
* @param {boolean} value - The value to set for the feature
|
|
146
166
|
* @returns {Promise<void>}
|
|
147
167
|
* @throws {Error} If the features file cannot be written
|
|
148
168
|
*/
|
|
149
169
|
export async function setFeature(feature, value) {
|
|
150
|
-
const
|
|
151
|
-
const newFeatures = { ...currentFeatures, [feature]: value };
|
|
170
|
+
const newFeatures = { ...userFeatures, [feature]: value };
|
|
152
171
|
try {
|
|
153
172
|
await writeJson(EXPERIMENTAL_FEATURES_FILEPATH, newFeatures, { mode: 0o700 });
|
|
154
173
|
} catch (error) {
|
|
155
174
|
throw new Error(`Cannot write experimental features configuration to ${EXPERIMENTAL_FEATURES_FILEPATH}`);
|
|
156
175
|
}
|
|
176
|
+
userFeatures = loadFeatures();
|
|
157
177
|
}
|
|
158
178
|
|
|
159
179
|
/**
|
|
160
180
|
* Checks if an experimental feature is enabled.
|
|
181
|
+
* Falls back to the feature's `defaultValue` when the user hasn't set it explicitly.
|
|
161
182
|
* @param {string} feature - The name of the feature to check
|
|
162
|
-
* @returns {
|
|
183
|
+
* @returns {boolean} True if the feature is enabled
|
|
184
|
+
*/
|
|
185
|
+
export function isFeatureEnabled(feature) {
|
|
186
|
+
return userFeatures[feature] ?? EXPERIMENTAL_FEATURES[feature]?.defaultValue ?? false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Lists all experimental features with their current status.
|
|
191
|
+
* @returns {Array<{ id: string, status: string, description: string, instructions?: string, enabled: boolean }>}
|
|
163
192
|
*/
|
|
164
|
-
export
|
|
165
|
-
|
|
166
|
-
|
|
193
|
+
export function getAllFeatures() {
|
|
194
|
+
return Object.entries(EXPERIMENTAL_FEATURES).map(([id, feature]) => ({
|
|
195
|
+
id,
|
|
196
|
+
...feature,
|
|
197
|
+
enabled: isFeatureEnabled(id),
|
|
198
|
+
}));
|
|
167
199
|
}
|
package/src/format-table.js
CHANGED
|
@@ -36,16 +36,20 @@ export const formatTable = (data, columnWidth = []) => {
|
|
|
36
36
|
return data
|
|
37
37
|
.map((row) => row.map((cell) => String(cell)))
|
|
38
38
|
.map((row) => {
|
|
39
|
-
return
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
return (
|
|
40
|
+
row
|
|
41
|
+
.map((cell, index) => {
|
|
42
|
+
const isLastColumn = index === row.length - 1;
|
|
43
|
+
if (isLastColumn) {
|
|
44
|
+
return cell;
|
|
45
|
+
}
|
|
46
|
+
const rightPaddingLength = columnSizes[index] - stringLength(cell) ?? 0;
|
|
47
|
+
return cell + ' '.repeat(rightPaddingLength);
|
|
48
|
+
})
|
|
49
|
+
.join(SEPARATOR)
|
|
50
|
+
// empty trailing cells would otherwise pad the line with meaningless spaces
|
|
51
|
+
.trimEnd()
|
|
52
|
+
);
|
|
49
53
|
})
|
|
50
54
|
.join('\n');
|
|
51
55
|
};
|
package/src/initial-setup.js
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
|
+
import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici';
|
|
1
2
|
import { hasParam } from './lib/has-param.js';
|
|
2
3
|
|
|
4
|
+
// Node's global fetch() ignores the http_proxy/https_proxy environment variables,
|
|
5
|
+
// unlike most CLI tools (curl, kubectl, s3cmd…). When such a variable is set, install
|
|
6
|
+
// an EnvHttpProxyAgent as the global dispatcher so every fetch() (API calls, update
|
|
7
|
+
// checks…) routes through the proxy and honors no_proxy for exclusions. We only swap
|
|
8
|
+
// the dispatcher when a proxy is actually configured, leaving the default behavior
|
|
9
|
+
// untouched for everyone else.
|
|
10
|
+
const hasProxyConfig =
|
|
11
|
+
process.env.http_proxy || process.env.https_proxy || process.env.HTTP_PROXY || process.env.HTTPS_PROXY;
|
|
12
|
+
if (hasProxyConfig) {
|
|
13
|
+
setGlobalDispatcher(new EnvHttpProxyAgent());
|
|
14
|
+
}
|
|
15
|
+
|
|
3
16
|
// These need to be set before Logger and other stuffs
|
|
4
17
|
if (hasParam('-v') || hasParam('--verbose')) {
|
|
5
18
|
process.env.CLEVER_VERBOSE = '1';
|
|
@@ -5,14 +5,11 @@ import { hasParam } from './lib/has-param.js';
|
|
|
5
5
|
// These need to be set before Logger and other stuffs
|
|
6
6
|
const updateNotifierExplicitFalse = hasParam('--no-update-notifier') || hasParam('--update-notifier', 'false');
|
|
7
7
|
if (!updateNotifierExplicitFalse) {
|
|
8
|
+
const docsUrl = 'https://github.com/CleverCloud/clever-tools/blob/master/docs/update.md';
|
|
8
9
|
updateNotifierModule({
|
|
9
10
|
pkg,
|
|
10
|
-
tagsUrl: 'https://api.github.com/repos/CleverCloud/clever-tools/tags',
|
|
11
11
|
}).notify({
|
|
12
12
|
isGlobal: true,
|
|
13
|
-
|
|
14
|
-
const docsUrl = 'https://github.com/CleverCloud/clever-tools/tree/master/docs#how-to-use-clever-tools';
|
|
15
|
-
return `\nPlease follow this link to update your clever-tools:\n${docsUrl}`;
|
|
16
|
-
},
|
|
13
|
+
message: `Update available {currentVersion} -> {latestVersion}\nPlease follow this link to update your clever-tools:\n${docsUrl}`,
|
|
17
14
|
});
|
|
18
15
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport used by the connection an access log describes.
|
|
3
|
+
* @typedef {'HTTP'|'TCP'|'SSH'} AccessLogTransport
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Guess the transport of an access log.
|
|
8
|
+
*
|
|
9
|
+
* The v4 access log payload has no explicit transport field yet, so it is inferred from the
|
|
10
|
+
* sections the API did send:
|
|
11
|
+
*
|
|
12
|
+
* - an `http` section is only emitted for connections that went through the HTTP reverse proxy,
|
|
13
|
+
* - TCP redirections go through the TCP proxy, which assigns a `requestId` but no `http` section,
|
|
14
|
+
* - direct SSH connections to the instance go through neither, so they carry neither field.
|
|
15
|
+
*
|
|
16
|
+
* Drop this function once the API exposes the transport itself.
|
|
17
|
+
*
|
|
18
|
+
* @param {object} log an access log, as emitted by `ApplicationAccessLogStream`
|
|
19
|
+
* @returns {AccessLogTransport}
|
|
20
|
+
*/
|
|
21
|
+
export function guessAccessLogTransport(log) {
|
|
22
|
+
if (log.http != null) {
|
|
23
|
+
return 'HTTP';
|
|
24
|
+
}
|
|
25
|
+
if (log.requestId != null) {
|
|
26
|
+
return 'TCP';
|
|
27
|
+
}
|
|
28
|
+
return 'SSH';
|
|
29
|
+
}
|
|
@@ -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', ... }) },
|
|
@@ -161,7 +179,7 @@ function wrapParser(parser) {
|
|
|
161
179
|
// instead of cliparse.autocomplete.words(array)
|
|
162
180
|
function wrapComplete(complete) {
|
|
163
181
|
return (word) => {
|
|
164
|
-
// Support both functions and static arrays (e.g. complete:
|
|
182
|
+
// Support both functions and static arrays (e.g. complete: ['human', 'json'])
|
|
165
183
|
const result = typeof complete === 'function' ? complete(word) : complete;
|
|
166
184
|
return Promise.resolve(result).then(cliparse.autocomplete.words);
|
|
167
185
|
};
|
|
@@ -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
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { createDrain } from '../clever-client/drains.js';
|
|
1
2
|
import * as Application from './application.js';
|
|
2
3
|
import { resolveAddon } from './ids-resolver.js';
|
|
4
|
+
import { sendToApi } from './send-to-api.js';
|
|
3
5
|
|
|
4
6
|
export async function resolveDrainResource(alias, appIdOrName, addonIdOrRealId) {
|
|
5
7
|
if (addonIdOrRealId != null && (appIdOrName != null || alias != null)) {
|
|
@@ -15,17 +17,37 @@ export async function resolveDrainResource(alias, appIdOrName, addonIdOrRealId)
|
|
|
15
17
|
return { ownerId, resourceId: appId };
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
20
|
+
/**
|
|
21
|
+
* Creates a log drain, the recipient options that are not set are left out of the payload.
|
|
22
|
+
* @param {string} type - Drain type, as expected by the API
|
|
23
|
+
* @param {string} ownerId
|
|
24
|
+
* @param {string} resourceId
|
|
25
|
+
* @param {string} url - Drain URL
|
|
26
|
+
* @param {Record<string, unknown>} [recipientOptions] - Extra recipient fields, `null` and `undefined` ones are ignored
|
|
27
|
+
*/
|
|
28
|
+
export function createLogDrain(type, ownerId, resourceId, url, recipientOptions = {}) {
|
|
29
|
+
const body = { kind: 'LOG', recipient: { type, url } };
|
|
30
|
+
|
|
31
|
+
for (const key in recipientOptions) {
|
|
32
|
+
if (recipientOptions[key] != null) {
|
|
33
|
+
body.recipient[key] = recipientOptions[key];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return createDrain({ ownerId, resourceId, body }).then(sendToApi);
|
|
38
|
+
}
|
|
27
39
|
|
|
28
|
-
export const
|
|
40
|
+
export const DRAIN_TYPE_LABELS = {
|
|
41
|
+
BETTERSTACK: 'Better Stack',
|
|
42
|
+
DATADOG: 'Datadog',
|
|
43
|
+
ELASTICSEARCH: 'Elasticsearch',
|
|
44
|
+
NEWRELIC: 'New Relic',
|
|
45
|
+
OVH_TCP: 'OVH TCP',
|
|
46
|
+
RAW_HTTP: 'Raw HTTP',
|
|
47
|
+
SPLUNK: 'Splunk',
|
|
48
|
+
SYSLOG_TCP: 'Syslog TCP',
|
|
49
|
+
SYSLOG_UDP: 'Syslog UDP',
|
|
50
|
+
};
|
|
29
51
|
|
|
30
52
|
function formatRate(messagesPerSecond) {
|
|
31
53
|
if (messagesPerSecond < 1) {
|
|
@@ -48,14 +70,19 @@ function formatThroughput(bytesPerSecond) {
|
|
|
48
70
|
}
|
|
49
71
|
|
|
50
72
|
export function formatDrain(rawDrain) {
|
|
51
|
-
const drainType = DRAIN_TYPES[rawDrain.recipient.type];
|
|
52
73
|
const drainDetails = [
|
|
53
74
|
['ID', rawDrain.id],
|
|
54
75
|
['Status', rawDrain.status.status],
|
|
55
76
|
['Execution status', rawDrain.execution.status],
|
|
56
77
|
['URL', rawDrain.recipient.url],
|
|
57
|
-
['Type',
|
|
78
|
+
['Type', DRAIN_TYPE_LABELS[rawDrain.recipient.type]],
|
|
58
79
|
['Custom index', rawDrain.recipient.index],
|
|
80
|
+
['Sourcetype', rawDrain.recipient.sourcetype],
|
|
81
|
+
// DEFAULT is the implicit norm, only a relaxed verification is worth showing
|
|
82
|
+
[
|
|
83
|
+
'TLS verification',
|
|
84
|
+
rawDrain.recipient.tlsVerification === 'TRUSTFUL' ? 'Trustful (certificate not verified)' : null,
|
|
85
|
+
],
|
|
59
86
|
['SD parameters', rawDrain.recipient.rfc5424StructuredDataParameters],
|
|
60
87
|
['Message output rate', formatRate(rawDrain.backlog.msgRateOut)],
|
|
61
88
|
['Message throughput', formatThroughput(rawDrain.backlog.msgThroughputOut)],
|
|
@@ -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-system.js
CHANGED
|
@@ -167,8 +167,8 @@ export class GitSystem extends Git {
|
|
|
167
167
|
class GitNotFoundError extends Error {
|
|
168
168
|
constructor() {
|
|
169
169
|
super(
|
|
170
|
-
'
|
|
171
|
-
'Either install git or
|
|
170
|
+
'git was not found in your PATH\n' +
|
|
171
|
+
'Either install git, or fall back to the previous JS implementation with: clever features disable system-git',
|
|
172
172
|
);
|
|
173
173
|
this.name = 'GitNotFoundError';
|
|
174
174
|
}
|