@nocobase/cli 2.1.0-alpha.40 → 2.1.0-alpha.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -8
- package/README.zh-CN.md +20 -8
- package/bin/run.js +9 -37
- package/dist/commands/app/destroy.js +225 -0
- package/dist/commands/app/down.js +24 -254
- package/dist/commands/app/restart.js +12 -5
- package/dist/commands/app/shared.js +122 -0
- package/dist/commands/app/start.js +41 -59
- package/dist/commands/app/stop.js +72 -26
- package/dist/commands/app/upgrade.js +312 -429
- package/dist/commands/config/delete.js +8 -1
- package/dist/commands/config/get.js +5 -0
- package/dist/commands/config/set.js +10 -1
- package/dist/commands/db/ps.js +5 -18
- package/dist/commands/db/start.js +23 -8
- package/dist/commands/env/add.js +66 -6
- package/dist/commands/env/auth.js +86 -27
- package/dist/commands/env/info.js +52 -8
- package/dist/commands/env/list.js +6 -7
- package/dist/commands/env/shared.js +41 -3
- package/dist/commands/env/status.js +10 -7
- package/dist/commands/init.js +214 -136
- package/dist/commands/install.js +364 -294
- package/dist/commands/license/plugins/shared.js +9 -3
- package/dist/commands/license/plugins/sync.js +54 -25
- package/dist/commands/self/update.js +58 -1
- package/dist/commands/source/dev.js +5 -5
- package/dist/commands/source/download.js +29 -25
- package/dist/generated/command-registry.js +3 -2
- package/dist/lib/api-client.js +6 -0
- package/dist/lib/api-command-compat.js +641 -0
- package/dist/lib/app-health.js +31 -18
- package/dist/lib/app-managed-resources.js +1 -1
- package/dist/lib/app-runtime.js +2 -2
- package/dist/lib/auth-store.js +56 -19
- package/dist/lib/backup.js +3 -3
- package/dist/lib/bootstrap.js +14 -8
- package/dist/lib/cli-config.js +137 -4
- package/dist/lib/cli-entry-error.js +44 -0
- package/dist/lib/cli-locale.js +19 -7
- package/dist/lib/db-connection-check.js +61 -0
- package/dist/lib/docker-log-stream.js +45 -0
- package/dist/lib/env-auth.js +98 -11
- package/dist/lib/env-config.js +3 -2
- package/dist/lib/env-guard.js +1 -1
- package/dist/lib/generated-command.js +17 -0
- package/dist/lib/prompt-validators.js +23 -5
- package/dist/lib/prompt-web-ui.js +143 -19
- package/dist/lib/run-npm.js +133 -23
- package/dist/lib/skills-manager.js +80 -4
- package/dist/lib/source-publish.js +4 -1
- package/dist/lib/startup-update.js +74 -50
- package/dist/lib/ui.js +4 -1
- package/dist/locale/en-US.json +92 -5
- package/dist/locale/zh-CN.json +92 -5
- package/package.json +34 -2
package/dist/lib/cli-locale.js
CHANGED
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
import { readFileSync } from 'node:fs';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { resolveCliHomeDir } from './cli-home.js';
|
|
12
13
|
export const SUPPORTED_CLI_LOCALES = ['en-US', 'zh-CN'];
|
|
13
14
|
export const CLI_LOCALE_FLAG_OPTIONS = [...SUPPORTED_CLI_LOCALES];
|
|
14
15
|
export const CLI_LOCALE_FLAG_DESCRIPTION = 'Language for CLI prompts and the local setup UI.';
|
|
15
16
|
const DEFAULT_CLI_LOCALE = 'en-US';
|
|
16
17
|
const localeCache = {};
|
|
17
|
-
function normalizeCliLocale(value) {
|
|
18
|
+
export function normalizeCliLocale(value) {
|
|
18
19
|
const raw = String(value ?? '').trim();
|
|
19
20
|
if (!raw) {
|
|
20
21
|
return undefined;
|
|
@@ -28,6 +29,17 @@ function normalizeCliLocale(value) {
|
|
|
28
29
|
}
|
|
29
30
|
return undefined;
|
|
30
31
|
}
|
|
32
|
+
function readConfiguredCliLocale() {
|
|
33
|
+
try {
|
|
34
|
+
const configPath = path.join(resolveCliHomeDir(), 'config.json');
|
|
35
|
+
const content = readFileSync(configPath, 'utf8');
|
|
36
|
+
const parsed = JSON.parse(content);
|
|
37
|
+
return normalizeCliLocale(parsed.settings?.locale === undefined ? undefined : String(parsed.settings.locale));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
31
43
|
function loadLocaleMessages(locale) {
|
|
32
44
|
if (localeCache[locale]) {
|
|
33
45
|
return localeCache[locale];
|
|
@@ -65,9 +77,11 @@ function interpolateTemplate(template, values) {
|
|
|
65
77
|
return value === undefined || value === null ? '' : String(value);
|
|
66
78
|
});
|
|
67
79
|
}
|
|
68
|
-
export function detectCliLocale() {
|
|
80
|
+
export function detectCliLocale(configuredLocale) {
|
|
81
|
+
const resolvedConfiguredLocale = configuredLocale ?? readConfiguredCliLocale();
|
|
69
82
|
const candidates = [
|
|
70
83
|
process.env.NB_LOCALE,
|
|
84
|
+
resolvedConfiguredLocale,
|
|
71
85
|
process.env.LC_ALL,
|
|
72
86
|
process.env.LC_MESSAGES,
|
|
73
87
|
process.env.LANG,
|
|
@@ -81,8 +95,8 @@ export function detectCliLocale() {
|
|
|
81
95
|
}
|
|
82
96
|
return DEFAULT_CLI_LOCALE;
|
|
83
97
|
}
|
|
84
|
-
export function resolveCliLocale(preferred) {
|
|
85
|
-
return normalizeCliLocale(preferred) ?? detectCliLocale();
|
|
98
|
+
export function resolveCliLocale(preferred, options) {
|
|
99
|
+
return normalizeCliLocale(preferred) ?? detectCliLocale(options?.configuredLocale);
|
|
86
100
|
}
|
|
87
101
|
export function applyCliLocale(preferred) {
|
|
88
102
|
const locale = resolveCliLocale(preferred);
|
|
@@ -111,9 +125,7 @@ export function localeText(key, values, fallback) {
|
|
|
111
125
|
};
|
|
112
126
|
}
|
|
113
127
|
export function isLocalizedTextDef(value) {
|
|
114
|
-
return Boolean(value
|
|
115
|
-
&& typeof value === 'object'
|
|
116
|
-
&& typeof value.key === 'string');
|
|
128
|
+
return Boolean(value && typeof value === 'object' && typeof value.key === 'string');
|
|
117
129
|
}
|
|
118
130
|
export function resolveLocalizedText(text, options) {
|
|
119
131
|
if (text === undefined) {
|
|
@@ -10,6 +10,7 @@ import { translateCli } from "./cli-locale.js";
|
|
|
10
10
|
import { validateTcpPort } from "./prompt-validators.js";
|
|
11
11
|
const DB_CONNECTION_TIMEOUT_MS = 5_000;
|
|
12
12
|
const externalDbValidationCache = new Map();
|
|
13
|
+
const mysqlLowerCaseTableNamesCache = new Map();
|
|
13
14
|
function trimPromptValue(value) {
|
|
14
15
|
return String(value ?? '').trim();
|
|
15
16
|
}
|
|
@@ -117,6 +118,36 @@ async function checkMysqlFamilyConnection(config) {
|
|
|
117
118
|
await Promise.resolve(connection.end()).catch(() => undefined);
|
|
118
119
|
}
|
|
119
120
|
}
|
|
121
|
+
async function readMysqlFamilyLowerCaseTableNames(config) {
|
|
122
|
+
const { default: mysql } = await import('mysql2/promise');
|
|
123
|
+
const connection = await mysql.createConnection({
|
|
124
|
+
host: config.host,
|
|
125
|
+
port: config.port,
|
|
126
|
+
user: config.user,
|
|
127
|
+
password: config.password,
|
|
128
|
+
database: config.database,
|
|
129
|
+
connectTimeout: DB_CONNECTION_TIMEOUT_MS,
|
|
130
|
+
});
|
|
131
|
+
try {
|
|
132
|
+
const [rows] = await connection.query(`SHOW VARIABLES LIKE 'lower_case_table_names'`);
|
|
133
|
+
if (!Array.isArray(rows)) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
for (const row of rows) {
|
|
137
|
+
if (!row || typeof row !== 'object') {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const value = String(row.Value ?? '').trim();
|
|
141
|
+
if (value === '0' || value === '1' || value === '2') {
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
await Promise.resolve(connection.end()).catch(() => undefined);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
120
151
|
async function performExternalDbConnectionCheck(config) {
|
|
121
152
|
try {
|
|
122
153
|
switch (config.dialect) {
|
|
@@ -146,6 +177,19 @@ export async function checkExternalDbConnection(config) {
|
|
|
146
177
|
externalDbValidationCache.set(cacheKey, pending);
|
|
147
178
|
return await pending;
|
|
148
179
|
}
|
|
180
|
+
async function readMysqlLowerCaseTableNamesMode(config) {
|
|
181
|
+
if (config.dialect !== 'mysql' && config.dialect !== 'mariadb') {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
const cacheKey = buildValidationCacheKey(config);
|
|
185
|
+
const cached = mysqlLowerCaseTableNamesCache.get(cacheKey);
|
|
186
|
+
if (cached) {
|
|
187
|
+
return await cached;
|
|
188
|
+
}
|
|
189
|
+
const pending = readMysqlFamilyLowerCaseTableNames(config);
|
|
190
|
+
mysqlLowerCaseTableNamesCache.set(cacheKey, pending);
|
|
191
|
+
return await pending;
|
|
192
|
+
}
|
|
149
193
|
export async function validateExternalDbConfig(values) {
|
|
150
194
|
const config = readExternalDbConnectionConfig(values);
|
|
151
195
|
if (!config) {
|
|
@@ -153,6 +197,23 @@ export async function validateExternalDbConfig(values) {
|
|
|
153
197
|
}
|
|
154
198
|
return await checkExternalDbConnection(config);
|
|
155
199
|
}
|
|
200
|
+
export async function validateMysqlLowerCaseTableNamesCompatibility(values) {
|
|
201
|
+
const config = readExternalDbConnectionConfig(values);
|
|
202
|
+
if (!config || (config.dialect !== 'mysql' && config.dialect !== 'mariadb')) {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const mode = await readMysqlLowerCaseTableNamesMode(config);
|
|
207
|
+
if (mode === '1' && values.dbUnderscored !== true) {
|
|
208
|
+
return translateCli('validators.dbConnection.lowerCaseTableNamesRequiresUnderscored');
|
|
209
|
+
}
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
return formatDbConnectionError(config, error);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
156
216
|
export function clearExternalDbValidationCache() {
|
|
157
217
|
externalDbValidationCache.clear();
|
|
218
|
+
mysqlLowerCaseTableNamesCache.clear();
|
|
158
219
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
const DEFAULT_DOCKER_LOG_TAIL = 50;
|
|
11
|
+
export function startDockerLogFollower(containerName, options) {
|
|
12
|
+
const tail = Math.max(0, options?.tail ?? DEFAULT_DOCKER_LOG_TAIL);
|
|
13
|
+
const child = spawn('docker', ['logs', '--tail', String(tail), '--follow', containerName], {
|
|
14
|
+
stdio: 'inherit',
|
|
15
|
+
});
|
|
16
|
+
let settled = false;
|
|
17
|
+
let resolveClosed;
|
|
18
|
+
const closed = new Promise((resolve) => {
|
|
19
|
+
resolveClosed = resolve;
|
|
20
|
+
});
|
|
21
|
+
const settle = () => {
|
|
22
|
+
if (!settled) {
|
|
23
|
+
settled = true;
|
|
24
|
+
resolveClosed();
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
child.once('error', settle);
|
|
28
|
+
child.once('close', settle);
|
|
29
|
+
return {
|
|
30
|
+
stop: async () => {
|
|
31
|
+
if (settled) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
if (!child.kill()) {
|
|
36
|
+
settle();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
settle();
|
|
41
|
+
}
|
|
42
|
+
await closed;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
package/dist/lib/env-auth.js
CHANGED
|
@@ -34,6 +34,28 @@ export function getOauthResource(issuerOrBaseUrl) {
|
|
|
34
34
|
export function getDefaultOauthScope() {
|
|
35
35
|
return DEFAULT_OAUTH_SCOPE;
|
|
36
36
|
}
|
|
37
|
+
function formatApiAuthError(prefix, data, fallbackStatus) {
|
|
38
|
+
if (typeof data === 'string' && data.trim()) {
|
|
39
|
+
return `${prefix}: ${data}`;
|
|
40
|
+
}
|
|
41
|
+
const errors = Array.isArray(data?.errors) ? data.errors : [];
|
|
42
|
+
if (errors.length > 0) {
|
|
43
|
+
const details = errors
|
|
44
|
+
.map((entry) => {
|
|
45
|
+
const code = entry?.code ? `[${entry.code}] ` : '';
|
|
46
|
+
return `${code}${entry?.message ?? 'Authentication failed.'}`;
|
|
47
|
+
})
|
|
48
|
+
.join('; ');
|
|
49
|
+
return `${prefix}: ${details}`;
|
|
50
|
+
}
|
|
51
|
+
if (typeof data?.error?.message === 'string' && data.error.message.trim()) {
|
|
52
|
+
return `${prefix}: ${data.error.message}`;
|
|
53
|
+
}
|
|
54
|
+
if (typeof fallbackStatus === 'number') {
|
|
55
|
+
return `${prefix}: HTTP ${fallbackStatus}`;
|
|
56
|
+
}
|
|
57
|
+
return prefix;
|
|
58
|
+
}
|
|
37
59
|
export function isOauthAccessTokenExpired(auth, now = Date.now()) {
|
|
38
60
|
if (!auth.expiresAt) {
|
|
39
61
|
return false;
|
|
@@ -607,19 +629,19 @@ async function createLoopbackServer(state) {
|
|
|
607
629
|
});
|
|
608
630
|
let resolveWaiter;
|
|
609
631
|
let rejectWaiter;
|
|
610
|
-
const waitForCode = () => new Promise((
|
|
632
|
+
const waitForCode = () => new Promise((resolve, reject) => {
|
|
611
633
|
resolveWaiter = (code) => {
|
|
612
634
|
void close();
|
|
613
|
-
|
|
635
|
+
resolve(code);
|
|
614
636
|
};
|
|
615
637
|
rejectWaiter = (error) => {
|
|
616
638
|
void close();
|
|
617
|
-
|
|
639
|
+
reject(error);
|
|
618
640
|
};
|
|
619
641
|
});
|
|
620
642
|
const close = async () => {
|
|
621
|
-
await new Promise((
|
|
622
|
-
server.close(() =>
|
|
643
|
+
await new Promise((resolve) => {
|
|
644
|
+
server.close(() => resolve());
|
|
623
645
|
});
|
|
624
646
|
};
|
|
625
647
|
server.on('error', (error) => {
|
|
@@ -750,7 +772,7 @@ export async function resolveAccessToken(options) {
|
|
|
750
772
|
}
|
|
751
773
|
const baseUrl = options.baseUrl ?? env.baseUrl;
|
|
752
774
|
if (!baseUrl) {
|
|
753
|
-
throw new Error(`Env "${envName}" is missing a base URL.
|
|
775
|
+
throw new Error(`Env "${envName}" is missing a base URL. Update it with \`nb env update ${envName} --api-base-url <url>\` first.`);
|
|
754
776
|
}
|
|
755
777
|
printVerbose(`Refreshing OAuth session for env "${envName}"`);
|
|
756
778
|
return refreshOauthAccessToken({
|
|
@@ -771,10 +793,72 @@ export async function resolveServerRequestTarget(options) {
|
|
|
771
793
|
scope: options.scope,
|
|
772
794
|
});
|
|
773
795
|
if (!baseUrl) {
|
|
774
|
-
throw new Error(
|
|
796
|
+
throw new Error([
|
|
797
|
+
env ? `Env "${envName}" is missing a base URL.` : `Env "${envName}" is not configured.`,
|
|
798
|
+
env
|
|
799
|
+
? `Use --api-base-url or update env "${envName}" with \`nb env update ${envName} --api-base-url <url>\`.`
|
|
800
|
+
: `Use --api-base-url or run \`nb init --ui --env ${envName}\` first.`,
|
|
801
|
+
].join('\n'));
|
|
775
802
|
}
|
|
776
803
|
return { baseUrl, token };
|
|
777
804
|
}
|
|
805
|
+
export async function authenticateEnvWithBasic(options) {
|
|
806
|
+
const envName = options.envName ?? (await getCurrentEnvName({ scope: options.scope }));
|
|
807
|
+
const env = await getEnv(envName, { scope: options.scope });
|
|
808
|
+
const baseUrl = env?.baseUrl;
|
|
809
|
+
if (!baseUrl) {
|
|
810
|
+
throw new Error([
|
|
811
|
+
env
|
|
812
|
+
? `Environment "${envName}" does not have an API base URL yet.`
|
|
813
|
+
: `Environment "${envName}" has not been set up yet.`,
|
|
814
|
+
env
|
|
815
|
+
? `Run \`nb env update ${envName} --api-base-url <url>\` to finish setting it up.`
|
|
816
|
+
: `Run \`nb init --ui --env ${envName}\` first.`,
|
|
817
|
+
]
|
|
818
|
+
.filter(Boolean)
|
|
819
|
+
.join('\n'));
|
|
820
|
+
}
|
|
821
|
+
const loginUrl = `${normalizeBaseUrl(baseUrl)}/auth:signIn`;
|
|
822
|
+
let response;
|
|
823
|
+
try {
|
|
824
|
+
response = await fetchWithOauthRetry(loginUrl, {
|
|
825
|
+
method: 'POST',
|
|
826
|
+
headers: {
|
|
827
|
+
accept: 'application/json',
|
|
828
|
+
'content-type': 'application/json',
|
|
829
|
+
'x-authenticator': 'basic',
|
|
830
|
+
},
|
|
831
|
+
body: JSON.stringify({
|
|
832
|
+
account: options.username,
|
|
833
|
+
password: options.password,
|
|
834
|
+
}),
|
|
835
|
+
}, {
|
|
836
|
+
operation: 'Signing in with basic credentials',
|
|
837
|
+
onRetry: (message) => updateTask(message),
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
catch (error) {
|
|
841
|
+
throw new Error(formatOauthFetchFailure('Failed to sign in with basic credentials.', {
|
|
842
|
+
envName,
|
|
843
|
+
baseUrl,
|
|
844
|
+
url: loginUrl,
|
|
845
|
+
rawMessage: error?.message,
|
|
846
|
+
}));
|
|
847
|
+
}
|
|
848
|
+
const data = await parseJsonResponse(response);
|
|
849
|
+
if (!response.ok) {
|
|
850
|
+
throw new Error(formatApiAuthError('Basic sign-in failed', data, response.status));
|
|
851
|
+
}
|
|
852
|
+
const token = typeof data?.data?.token === 'string' && data.data.token.trim()
|
|
853
|
+
? data.data.token.trim()
|
|
854
|
+
: typeof data?.token === 'string' && data.token.trim()
|
|
855
|
+
? data.token.trim()
|
|
856
|
+
: '';
|
|
857
|
+
if (!token) {
|
|
858
|
+
throw new Error('Basic sign-in succeeded but no token was returned.');
|
|
859
|
+
}
|
|
860
|
+
return token;
|
|
861
|
+
}
|
|
778
862
|
export async function authenticateEnvWithOauth(options) {
|
|
779
863
|
const envName = options.envName ?? (await getCurrentEnvName({ scope: options.scope }));
|
|
780
864
|
const env = await getEnv(envName, { scope: options.scope });
|
|
@@ -785,8 +869,8 @@ export async function authenticateEnvWithOauth(options) {
|
|
|
785
869
|
? `Environment "${envName}" does not have an API base URL yet.`
|
|
786
870
|
: `Environment "${envName}" has not been set up yet.`,
|
|
787
871
|
env
|
|
788
|
-
? `Run \`nb env
|
|
789
|
-
: `Run \`nb env
|
|
872
|
+
? `Run \`nb env update ${envName} --api-base-url <url>\` to finish setting it up.`
|
|
873
|
+
: `Run \`nb init --ui --env ${envName}\` first.`,
|
|
790
874
|
]
|
|
791
875
|
.filter(Boolean)
|
|
792
876
|
.join('\n'));
|
|
@@ -833,10 +917,13 @@ export async function authenticateEnvWithOauth(options) {
|
|
|
833
917
|
const code = await new Promise((resolve, reject) => {
|
|
834
918
|
const timeout = setTimeout(() => reject(new Error(`OAuth sign-in timed out after 5 minutes. Run \`nb env auth ${envName}\` to try again.`)), OAUTH_LOGIN_TIMEOUT_MS);
|
|
835
919
|
timeout.unref?.();
|
|
836
|
-
callback
|
|
920
|
+
callback
|
|
921
|
+
.waitForCode()
|
|
922
|
+
.then((value) => {
|
|
837
923
|
clearTimeout(timeout);
|
|
838
924
|
resolve(value);
|
|
839
|
-
}
|
|
925
|
+
})
|
|
926
|
+
.catch((error) => {
|
|
840
927
|
clearTimeout(timeout);
|
|
841
928
|
reject(error);
|
|
842
929
|
});
|
package/dist/lib/env-config.js
CHANGED
|
@@ -19,6 +19,7 @@ const STRING_ENV_CONFIG_KEYS = [
|
|
|
19
19
|
'appPort',
|
|
20
20
|
'appKey',
|
|
21
21
|
'timezone',
|
|
22
|
+
'authUsername',
|
|
22
23
|
'dbDialect',
|
|
23
24
|
'builtinDbImage',
|
|
24
25
|
'dbHost',
|
|
@@ -83,11 +84,11 @@ export function buildStoredEnvConfig(input) {
|
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
const authType = trimConfigValue(input.authType);
|
|
86
|
-
if (authType === 'token' || authType === 'oauth') {
|
|
87
|
+
if (authType === 'basic' || authType === 'token' || authType === 'oauth') {
|
|
87
88
|
envConfig.authType = authType;
|
|
88
89
|
}
|
|
89
90
|
const accessToken = trimConfigValue(input.accessToken);
|
|
90
|
-
if (authType === 'token' && accessToken) {
|
|
91
|
+
if ((authType === 'basic' || authType === 'token') && accessToken) {
|
|
91
92
|
envConfig.accessToken = accessToken;
|
|
92
93
|
}
|
|
93
94
|
return envConfig;
|
package/dist/lib/env-guard.js
CHANGED
|
@@ -43,7 +43,7 @@ export async function ensureCrossEnvConfirmed(options) {
|
|
|
43
43
|
}
|
|
44
44
|
const currentEnv = normalizeEnvName(await getCurrentEnvName());
|
|
45
45
|
const interactiveTerminal = isInteractiveTerminal();
|
|
46
|
-
const bypassInteractivePrompt =
|
|
46
|
+
const bypassInteractivePrompt = Boolean(options.yes);
|
|
47
47
|
if (!currentEnv || currentEnv === requestedEnv || bypassInteractivePrompt) {
|
|
48
48
|
return true;
|
|
49
49
|
}
|
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { Command, Flags } from '@oclif/core';
|
|
18
18
|
import { executeApiRequest } from './api-client.js';
|
|
19
|
+
import { findApiCommandCompatViolation, formatApiCommandCompatViolation } from './api-command-compat.js';
|
|
19
20
|
import { ensureCrossEnvConfirmed } from './env-guard.js';
|
|
20
21
|
import { applyPostProcessor } from './post-processors.js';
|
|
22
|
+
import { readInstalledManagedSkillsVersion } from './skills-manager.js';
|
|
21
23
|
import { registerPostProcessors } from '../post-processors/index.js';
|
|
22
24
|
function buildParameterFlag(parameter, options) {
|
|
23
25
|
const hints = [parameter.in];
|
|
@@ -142,6 +144,7 @@ export function createGeneratedFlags(operation) {
|
|
|
142
144
|
}
|
|
143
145
|
export class GeneratedApiCommand extends Command {
|
|
144
146
|
static operation;
|
|
147
|
+
static runtimeVersion;
|
|
145
148
|
async run() {
|
|
146
149
|
registerPostProcessors();
|
|
147
150
|
const ctor = this.constructor;
|
|
@@ -154,7 +157,21 @@ export class GeneratedApiCommand extends Command {
|
|
|
154
157
|
if (!confirmed) {
|
|
155
158
|
return;
|
|
156
159
|
}
|
|
160
|
+
const cliVersion = String(this.config.pjson.version ?? '').trim();
|
|
161
|
+
const skillsVersion = await readInstalledManagedSkillsVersion();
|
|
162
|
+
const compatViolation = findApiCommandCompatViolation({
|
|
163
|
+
packageJson: this.config.pjson,
|
|
164
|
+
commandId: ctor.operation.commandId,
|
|
165
|
+
cliVersion,
|
|
166
|
+
appVersion: ctor.runtimeVersion,
|
|
167
|
+
skillsVersion,
|
|
168
|
+
});
|
|
169
|
+
if (compatViolation) {
|
|
170
|
+
this.error(formatApiCommandCompatViolation(compatViolation));
|
|
171
|
+
}
|
|
157
172
|
const response = await executeApiRequest({
|
|
173
|
+
cliVersion,
|
|
174
|
+
skillsVersion,
|
|
158
175
|
envName: flags.env,
|
|
159
176
|
baseUrl: flags['api-base-url'],
|
|
160
177
|
role: flags.role,
|
|
@@ -13,6 +13,7 @@ const API_BASE_URL_EXAMPLE = 'http://localhost:13000/api';
|
|
|
13
13
|
const ENV_KEY_PATTERN = /^[A-Za-z0-9]+$/;
|
|
14
14
|
const TCP_PORT_EXAMPLE = '13000';
|
|
15
15
|
const API_BASE_URL_REQUEST_TIMEOUT_MS = 5_000;
|
|
16
|
+
const TCP_PORT_PROBE_HOSTS = ['127.0.0.1', '0.0.0.0', '::1'];
|
|
16
17
|
function buildHealthCheckUrl(apiBaseUrl) {
|
|
17
18
|
return `${apiBaseUrl.replace(/\/+$/, '')}/__health_check`;
|
|
18
19
|
}
|
|
@@ -119,7 +120,7 @@ export function validateTcpPort(value) {
|
|
|
119
120
|
}
|
|
120
121
|
return undefined;
|
|
121
122
|
}
|
|
122
|
-
async function
|
|
123
|
+
async function canListenOnTcpPortHost(port, host) {
|
|
123
124
|
return await new Promise((resolve) => {
|
|
124
125
|
const server = net.createServer();
|
|
125
126
|
const resolveAndCleanup = (result) => {
|
|
@@ -131,15 +132,28 @@ async function canListenOnTcpPort(port) {
|
|
|
131
132
|
resolve(result);
|
|
132
133
|
}
|
|
133
134
|
};
|
|
134
|
-
server.once('error', () => {
|
|
135
|
+
server.once('error', (error) => {
|
|
136
|
+
if (error.code === 'EADDRNOTAVAIL' || error.code === 'EAFNOSUPPORT') {
|
|
137
|
+
resolveAndCleanup(undefined);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
135
140
|
resolveAndCleanup(false);
|
|
136
141
|
});
|
|
137
142
|
server.once('listening', () => {
|
|
138
143
|
resolveAndCleanup(true);
|
|
139
144
|
});
|
|
140
|
-
server.listen(port,
|
|
145
|
+
server.listen(port, host);
|
|
141
146
|
});
|
|
142
147
|
}
|
|
148
|
+
async function canListenOnTcpPort(port) {
|
|
149
|
+
for (const host of TCP_PORT_PROBE_HOSTS) {
|
|
150
|
+
const result = await canListenOnTcpPortHost(port, host);
|
|
151
|
+
if (result === false) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
143
157
|
function parseDockerPublishedTcpPorts(output) {
|
|
144
158
|
const ports = new Set();
|
|
145
159
|
for (const line of output.split(/\r?\n/)) {
|
|
@@ -210,9 +224,10 @@ async function allocateAvailableTcpPort() {
|
|
|
210
224
|
}
|
|
211
225
|
export async function findAvailableTcpPort() {
|
|
212
226
|
const dockerPorts = await getDockerPublishedTcpPorts();
|
|
213
|
-
for (let attempt = 0; attempt <
|
|
227
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
214
228
|
const candidate = await allocateAvailableTcpPort();
|
|
215
|
-
|
|
229
|
+
const port = Number.parseInt(candidate, 10);
|
|
230
|
+
if (!dockerPorts.has(port) && (await canListenOnTcpPort(port))) {
|
|
216
231
|
return candidate;
|
|
217
232
|
}
|
|
218
233
|
}
|
|
@@ -228,6 +243,9 @@ export async function validateAvailableTcpPort(value) {
|
|
|
228
243
|
return formatError;
|
|
229
244
|
}
|
|
230
245
|
const port = parseTcpPort(raw);
|
|
246
|
+
if (port === undefined) {
|
|
247
|
+
return translateCli('validators.tcpPort.invalid', { example: TCP_PORT_EXAMPLE });
|
|
248
|
+
}
|
|
231
249
|
const dockerPorts = await getDockerPublishedTcpPorts();
|
|
232
250
|
if (dockerPorts.has(port)) {
|
|
233
251
|
return translateCli('validators.tcpPort.alreadyInUseByDocker', { port });
|