@nocobase/cli 2.1.0-beta.36 → 2.1.0-beta.37

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.
@@ -8,7 +8,8 @@
8
8
  */
9
9
  import { promises as fs } from 'node:fs';
10
10
  import path from 'node:path';
11
- import { resolveCliHomeDir, resolveConfiguredEnvPath, resolveEnvRelativePath, } from './cli-home.js';
11
+ import { resolveCliHomeDir, resolveConfiguredEnvPath, resolveEnvRelativePath } from './cli-home.js';
12
+ import { normalizeCliLocale } from './cli-locale.js';
12
13
  import { cleanupCurrentSessionAfterEnvRemoval, resolveEffectiveCurrentEnv, setSessionCurrentEnv, } from './session-store.js';
13
14
  function normalizeStoredEnvKind(value) {
14
15
  const kind = String(value ?? '').trim();
@@ -24,13 +25,20 @@ function normalizeOptionalString(value) {
24
25
  const normalized = String(value ?? '').trim();
25
26
  return normalized || undefined;
26
27
  }
28
+ function normalizeOptionalCliLocale(value) {
29
+ const normalized = normalizeOptionalString(value);
30
+ if (!normalized) {
31
+ return undefined;
32
+ }
33
+ return normalizeCliLocale(normalized);
34
+ }
27
35
  export function readEnvApiBaseUrl(config) {
28
36
  if (!config) {
29
37
  return undefined;
30
38
  }
31
- return (normalizeOptionalString(config.apiBaseUrl)
32
- ?? normalizeOptionalString(config.baseUrl)
33
- ?? normalizeOptionalString(config.apibaseUrl));
39
+ return (normalizeOptionalString(config.apiBaseUrl) ??
40
+ normalizeOptionalString(config.baseUrl) ??
41
+ normalizeOptionalString(config.apibaseUrl));
34
42
  }
35
43
  export function resolveEnvKind(config) {
36
44
  if (!config) {
@@ -70,9 +78,11 @@ function normalizeEnvConfigEntry(entry) {
70
78
  }
71
79
  function normalizeAuthConfig(config) {
72
80
  const settings = config.settings ?? {};
81
+ const locale = normalizeOptionalCliLocale(settings.locale);
73
82
  return {
74
83
  name: config.name || config.dockerResourcePrefix,
75
84
  settings: {
85
+ ...(locale ? { locale } : {}),
76
86
  ...(settings.license?.pkgUrl ? { license: { pkgUrl: normalizeOptionalString(settings.license.pkgUrl) } } : {}),
77
87
  ...(settings.docker?.network || settings.docker?.containerPrefix
78
88
  ? {
@@ -84,8 +94,19 @@ function normalizeAuthConfig(config) {
84
94
  },
85
95
  }
86
96
  : {}),
97
+ ...(settings.bin?.docker || settings.bin?.git || settings.bin?.yarn
98
+ ? {
99
+ bin: {
100
+ ...(settings.bin?.docker ? { docker: normalizeOptionalString(settings.bin.docker) } : {}),
101
+ ...(settings.bin?.git ? { git: normalizeOptionalString(settings.bin.git) } : {}),
102
+ ...(settings.bin?.yarn ? { yarn: normalizeOptionalString(settings.bin.yarn) } : {}),
103
+ },
104
+ }
105
+ : {}),
87
106
  },
88
- lastEnv: config.lastEnv || config.currentEnv || 'default',
107
+ lastEnv: config.lastEnv ||
108
+ config.currentEnv ||
109
+ 'default',
89
110
  envs: Object.fromEntries(Object.entries(config.envs || {}).map(([envName, entry]) => [envName, normalizeEnvConfigEntry(entry) ?? {}])),
90
111
  };
91
112
  }
@@ -222,10 +243,11 @@ export class Env {
222
243
  export async function getEnv(envName, options = {}) {
223
244
  const { config: snapshot, ...loadOptions } = options;
224
245
  const config = snapshot ?? (await loadAuthConfig(loadOptions));
225
- const resolved = envName?.trim() || (await resolveEffectiveCurrentEnv(Object.keys(config.envs).sort(), {
226
- scope: loadOptions.scope,
227
- lastEnv: config.lastEnv,
228
- }));
246
+ const resolved = envName?.trim() ||
247
+ (await resolveEffectiveCurrentEnv(Object.keys(config.envs).sort(), {
248
+ scope: loadOptions.scope,
249
+ lastEnv: config.lastEnv,
250
+ }));
229
251
  const envConfig = config.envs[resolved];
230
252
  if (!envConfig) {
231
253
  return undefined;
@@ -260,37 +282,40 @@ async function writeEnv(envName, updater, options = {}) {
260
282
  await saveAuthConfig(config, options);
261
283
  }
262
284
  function normalizeConfiguredAuthType(value) {
263
- return value === 'token' || value === 'oauth' ? value : undefined;
285
+ return value === 'basic' || value === 'token' || value === 'oauth' ? value : undefined;
264
286
  }
265
287
  export function resolveConfiguredAuthType(config) {
266
288
  return normalizeConfiguredAuthType(config?.authType) ?? normalizeConfiguredAuthType(config?.auth?.type);
267
289
  }
268
290
  export async function upsertEnv(envName, config, options = {}) {
269
291
  await writeEnv(envName, (previous) => {
270
- const { apiBaseUrl: _apiBaseUrl, baseUrl: _baseUrl, apibaseUrl: _legacyApiBaseUrl, accessToken, authType, ...rest } = config;
292
+ const { apiBaseUrl: _apiBaseUrl, baseUrl: _baseUrl, apibaseUrl: _legacyApiBaseUrl, accessToken, authType, authUsername, ...rest } = config;
271
293
  const nextApiBaseUrl = readEnvApiBaseUrl(config);
272
294
  const previousApiBaseUrl = readEnvApiBaseUrl(previous);
273
295
  const baseUrlChanged = previousApiBaseUrl !== nextApiBaseUrl;
274
296
  const previousAuthType = resolveConfiguredAuthType(previous);
275
297
  const requestedAuthType = normalizeConfiguredAuthType(authType);
276
298
  const nextAuthType = requestedAuthType ?? (accessToken ? 'token' : previousAuthType);
299
+ const nextAuthUsername = nextAuthType === 'basic' ? normalizeOptionalString(authUsername) ?? previous?.authUsername : undefined;
277
300
  const nextAuth = accessToken
278
301
  ? {
279
302
  type: 'token',
280
303
  accessToken,
281
304
  }
282
- : nextAuthType === 'token' || baseUrlChanged || previous?.auth?.type === 'token'
283
- ? undefined
284
- : previous?.auth;
305
+ : nextAuthType === 'oauth' && !baseUrlChanged && previous?.auth?.type === 'oauth'
306
+ ? previous.auth
307
+ : undefined;
285
308
  const authChanged = !areAuthConfigsEquivalent(previous?.auth, nextAuth);
286
309
  const authTypeChanged = previousAuthType !== nextAuthType;
310
+ const authUsernameChanged = previous?.authUsername !== nextAuthUsername;
287
311
  return {
288
312
  ...previous,
289
313
  apiBaseUrl: nextApiBaseUrl,
290
314
  authType: nextAuthType,
315
+ authUsername: nextAuthUsername,
291
316
  auth: nextAuth,
292
317
  ...rest,
293
- runtime: baseUrlChanged || authChanged || authTypeChanged ? undefined : previous?.runtime,
318
+ runtime: baseUrlChanged || authChanged || authTypeChanged || authUsernameChanged ? undefined : previous?.runtime,
294
319
  };
295
320
  }, options);
296
321
  }
@@ -302,22 +327,25 @@ export async function updateEnvConnection(envName, updates, options = {}) {
302
327
  const previousAuthType = resolveConfiguredAuthType(previous);
303
328
  const requestedAuthType = normalizeConfiguredAuthType(updates.authType);
304
329
  const nextAuthType = requestedAuthType ?? (updates.accessToken ? 'token' : previousAuthType);
330
+ const nextAuthUsername = nextAuthType === 'basic' ? normalizeOptionalString(updates.authUsername) ?? previous?.authUsername : undefined;
305
331
  const nextAuth = updates.accessToken
306
332
  ? {
307
333
  type: 'token',
308
334
  accessToken: updates.accessToken,
309
335
  }
310
- : nextAuthType === 'token' || baseUrlChanged || previous?.auth?.type === 'token'
311
- ? undefined
312
- : previous?.auth;
336
+ : nextAuthType === 'oauth' && !baseUrlChanged && previous?.auth?.type === 'oauth'
337
+ ? previous.auth
338
+ : undefined;
313
339
  const authChanged = !areAuthConfigsEquivalent(previous?.auth, nextAuth);
314
340
  const authTypeChanged = previousAuthType !== nextAuthType;
341
+ const authUsernameChanged = previous?.authUsername !== nextAuthUsername;
315
342
  return {
316
343
  ...previous,
317
344
  ...(nextApiBaseUrl !== undefined ? { apiBaseUrl: nextApiBaseUrl } : {}),
318
345
  authType: nextAuthType,
346
+ authUsername: nextAuthUsername,
319
347
  auth: nextAuth,
320
- runtime: baseUrlChanged || authChanged || authTypeChanged ? undefined : previous?.runtime,
348
+ runtime: baseUrlChanged || authChanged || authTypeChanged || authUsernameChanged ? undefined : previous?.runtime,
321
349
  };
322
350
  }, options);
323
351
  }
@@ -8,13 +8,21 @@
8
8
  */
9
9
  import { loadExactAuthConfig, saveAuthConfig } from './auth-store.js';
10
10
  import { resolveDefaultConfigScope } from './cli-home.js';
11
+ import { CLI_LOCALE_FLAG_OPTIONS, normalizeCliLocale, resolveCliLocale } from './cli-locale.js';
11
12
  export const DEFAULT_LICENSE_PKG_URL = 'https://pkg.nocobase.com/';
12
13
  export const DEFAULT_DOCKER_NETWORK = 'nocobase';
13
14
  export const DEFAULT_DOCKER_CONTAINER_PREFIX = 'nb';
15
+ export const DEFAULT_DOCKER_BIN = 'docker';
16
+ export const DEFAULT_GIT_BIN = 'git';
17
+ export const DEFAULT_YARN_BIN = 'yarn';
14
18
  export const SUPPORTED_CLI_CONFIG_KEYS = [
19
+ 'locale',
15
20
  'license.pkg-url',
16
21
  'docker.network',
17
22
  'docker.container-prefix',
23
+ 'bin.docker',
24
+ 'bin.git',
25
+ 'bin.yarn',
18
26
  ];
19
27
  function trimValue(value) {
20
28
  const text = String(value ?? '').trim();
@@ -36,11 +44,16 @@ export function assertSupportedCliConfigKey(value) {
36
44
  }
37
45
  function cloneSettings(config) {
38
46
  return {
47
+ ...(config.settings?.locale ? { locale: trimValue(config.settings.locale) } : {}),
39
48
  license: config.settings?.license ? { ...config.settings.license } : undefined,
40
49
  docker: config.settings?.docker ? { ...config.settings.docker } : undefined,
50
+ bin: config.settings?.bin ? { ...config.settings.bin } : undefined,
41
51
  };
42
52
  }
43
53
  function pruneSettings(config) {
54
+ if (config.settings && !trimValue(config.settings.locale)) {
55
+ delete config.settings.locale;
56
+ }
44
57
  const license = config.settings?.license;
45
58
  if (license && !trimValue(license.pkgUrl)) {
46
59
  delete config.settings?.license;
@@ -49,34 +62,56 @@ function pruneSettings(config) {
49
62
  if (docker && !trimValue(docker.network) && !trimValue(docker.containerPrefix)) {
50
63
  delete config.settings?.docker;
51
64
  }
52
- if (config.settings
53
- && !config.settings.license
54
- && !config.settings.docker) {
65
+ const bin = config.settings?.bin;
66
+ if (bin && !trimValue(bin.docker) && !trimValue(bin.git) && !trimValue(bin.yarn)) {
67
+ delete config.settings?.bin;
68
+ }
69
+ if (config.settings &&
70
+ !config.settings.locale &&
71
+ !config.settings.license &&
72
+ !config.settings.docker &&
73
+ !config.settings.bin) {
55
74
  delete config.settings;
56
75
  }
57
76
  }
58
77
  export function getExplicitCliConfigValue(config, key) {
59
78
  switch (key) {
79
+ case 'locale':
80
+ return trimValue(config.settings?.locale);
60
81
  case 'license.pkg-url':
61
82
  return trimValue(config.settings?.license?.pkgUrl);
62
83
  case 'docker.network':
63
84
  return trimValue(config.settings?.docker?.network);
64
85
  case 'docker.container-prefix':
65
86
  return trimValue(config.settings?.docker?.containerPrefix);
87
+ case 'bin.docker':
88
+ return trimValue(config.settings?.bin?.docker);
89
+ case 'bin.git':
90
+ return trimValue(config.settings?.bin?.git);
91
+ case 'bin.yarn':
92
+ return trimValue(config.settings?.bin?.yarn);
66
93
  }
67
94
  }
68
95
  export function getEffectiveCliConfigValue(config, key) {
69
96
  const explicit = getExplicitCliConfigValue(config, key);
70
- if (explicit) {
97
+ if (explicit && key !== 'locale') {
71
98
  return explicit;
72
99
  }
73
100
  switch (key) {
101
+ case 'locale':
102
+ return resolveCliLocale(undefined, { configuredLocale: trimValue(config.settings?.locale) });
74
103
  case 'license.pkg-url':
75
104
  return DEFAULT_LICENSE_PKG_URL;
76
105
  case 'docker.network':
77
106
  return trimValue(config.name) || DEFAULT_DOCKER_NETWORK;
78
107
  case 'docker.container-prefix':
79
108
  return trimValue(config.name) || DEFAULT_DOCKER_CONTAINER_PREFIX;
109
+ case 'bin.docker':
110
+ return DEFAULT_DOCKER_BIN;
111
+ case 'bin.git':
112
+ return DEFAULT_GIT_BIN;
113
+ case 'bin.yarn':
114
+ return DEFAULT_YARN_BIN;
80
115
  }
81
116
  }
82
117
  export function normalizeCliConfigValue(key, value) {
@@ -87,6 +122,13 @@ export function normalizeCliConfigValue(key, value) {
87
122
  if (key === 'license.pkg-url') {
88
123
  return normalized.replace(/\/+$/, '') + '/';
89
124
  }
125
+ if (key === 'locale') {
126
+ const locale = normalizeCliLocale(normalized);
127
+ if (!locale) {
128
+ throw new Error(`Config key "${key}" must be one of: ${CLI_LOCALE_FLAG_OPTIONS.join(', ')}`);
129
+ }
130
+ return locale;
131
+ }
90
132
  return normalized;
91
133
  }
92
134
  export async function loadCliConfig(options = {}) {
@@ -113,6 +155,9 @@ export async function setCliConfigValue(key, value, options = {}) {
113
155
  const normalized = normalizeCliConfigValue(key, value);
114
156
  config.settings = cloneSettings(config);
115
157
  switch (key) {
158
+ case 'locale':
159
+ config.settings.locale = normalized;
160
+ break;
116
161
  case 'license.pkg-url':
117
162
  config.settings.license = {
118
163
  ...(config.settings.license ?? {}),
@@ -131,6 +176,24 @@ export async function setCliConfigValue(key, value, options = {}) {
131
176
  containerPrefix: normalized,
132
177
  };
133
178
  break;
179
+ case 'bin.docker':
180
+ config.settings.bin = {
181
+ ...(config.settings.bin ?? {}),
182
+ docker: normalized,
183
+ };
184
+ break;
185
+ case 'bin.git':
186
+ config.settings.bin = {
187
+ ...(config.settings.bin ?? {}),
188
+ git: normalized,
189
+ };
190
+ break;
191
+ case 'bin.yarn':
192
+ config.settings.bin = {
193
+ ...(config.settings.bin ?? {}),
194
+ yarn: normalized,
195
+ };
196
+ break;
134
197
  }
135
198
  pruneSettings(config);
136
199
  await saveAuthConfig(config, scope);
@@ -145,6 +208,9 @@ export async function deleteCliConfigValue(key, options = {}) {
145
208
  }
146
209
  config.settings = cloneSettings(config);
147
210
  switch (key) {
211
+ case 'locale':
212
+ delete config.settings.locale;
213
+ break;
148
214
  case 'license.pkg-url':
149
215
  if (config.settings.license) {
150
216
  delete config.settings.license.pkgUrl;
@@ -160,6 +226,21 @@ export async function deleteCliConfigValue(key, options = {}) {
160
226
  delete config.settings.docker.containerPrefix;
161
227
  }
162
228
  break;
229
+ case 'bin.docker':
230
+ if (config.settings.bin) {
231
+ delete config.settings.bin.docker;
232
+ }
233
+ break;
234
+ case 'bin.git':
235
+ if (config.settings.bin) {
236
+ delete config.settings.bin.git;
237
+ }
238
+ break;
239
+ case 'bin.yarn':
240
+ if (config.settings.bin) {
241
+ delete config.settings.bin.yarn;
242
+ }
243
+ break;
163
244
  }
164
245
  pruneSettings(config);
165
246
  await saveAuthConfig(config, scope);
@@ -174,3 +255,17 @@ export async function resolveDockerContainerPrefix(options = {}) {
174
255
  export async function resolveLicensePkgUrlFromConfig(options = {}) {
175
256
  return await getCliConfigValue('license.pkg-url', options);
176
257
  }
258
+ const CONFIGURABLE_COMMAND_KEYS = {
259
+ docker: 'bin.docker',
260
+ git: 'bin.git',
261
+ yarn: 'bin.yarn',
262
+ };
263
+ export function isConfigurableCommandName(value) {
264
+ return Object.prototype.hasOwnProperty.call(CONFIGURABLE_COMMAND_KEYS, value);
265
+ }
266
+ export async function resolveConfiguredCommandName(commandName, options = {}) {
267
+ if (!isConfigurableCommandName(commandName)) {
268
+ return commandName;
269
+ }
270
+ return await getCliConfigValue(CONFIGURABLE_COMMAND_KEYS[commandName], options);
271
+ }
@@ -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
  }
@@ -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;
@@ -775,6 +797,63 @@ export async function resolveServerRequestTarget(options) {
775
797
  }
776
798
  return { baseUrl, token };
777
799
  }
800
+ export async function authenticateEnvWithBasic(options) {
801
+ const envName = options.envName ?? (await getCurrentEnvName({ scope: options.scope }));
802
+ const env = await getEnv(envName, { scope: options.scope });
803
+ const baseUrl = env?.baseUrl;
804
+ if (!baseUrl) {
805
+ throw new Error([
806
+ env
807
+ ? `Environment "${envName}" does not have an API base URL yet.`
808
+ : `Environment "${envName}" has not been set up yet.`,
809
+ env
810
+ ? `Run \`nb env add ${envName} --api-base-url <url>\` to finish setting it up.`
811
+ : `Run \`nb env add ${envName}\` first.`,
812
+ ]
813
+ .filter(Boolean)
814
+ .join('\n'));
815
+ }
816
+ const loginUrl = `${normalizeBaseUrl(baseUrl)}/auth:signIn`;
817
+ let response;
818
+ try {
819
+ response = await fetchWithOauthRetry(loginUrl, {
820
+ method: 'POST',
821
+ headers: {
822
+ accept: 'application/json',
823
+ 'content-type': 'application/json',
824
+ 'x-authenticator': 'basic',
825
+ },
826
+ body: JSON.stringify({
827
+ account: options.username,
828
+ password: options.password,
829
+ }),
830
+ }, {
831
+ operation: 'Signing in with basic credentials',
832
+ onRetry: (message) => updateTask(message),
833
+ });
834
+ }
835
+ catch (error) {
836
+ throw new Error(formatOauthFetchFailure('Failed to sign in with basic credentials.', {
837
+ envName,
838
+ baseUrl,
839
+ url: loginUrl,
840
+ rawMessage: error?.message,
841
+ }));
842
+ }
843
+ const data = await parseJsonResponse(response);
844
+ if (!response.ok) {
845
+ throw new Error(formatApiAuthError('Basic sign-in failed', data, response.status));
846
+ }
847
+ const token = typeof data?.data?.token === 'string' && data.data.token.trim()
848
+ ? data.data.token.trim()
849
+ : typeof data?.token === 'string' && data.token.trim()
850
+ ? data.token.trim()
851
+ : '';
852
+ if (!token) {
853
+ throw new Error('Basic sign-in succeeded but no token was returned.');
854
+ }
855
+ return token;
856
+ }
778
857
  export async function authenticateEnvWithOauth(options) {
779
858
  const envName = options.envName ?? (await getCurrentEnvName({ scope: options.scope }));
780
859
  const env = await getEnv(envName, { scope: options.scope });
@@ -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;