@nocobase/cli 2.1.0-alpha.21 → 2.1.0-alpha.22

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.
@@ -15,6 +15,7 @@ import { mkdir } from 'node:fs/promises';
15
15
  import path from 'node:path';
16
16
  import { exit } from 'node:process';
17
17
  import { runPromptCatalog, } from "../lib/prompt-catalog.js";
18
+ import { applyCliLocale, localeText, translateCli, } from "../lib/cli-locale.js";
18
19
  import { findAvailableTcpPort, validateAvailableTcpPort, validateTcpPort, validateEnvKey, } from "../lib/prompt-validators.js";
19
20
  import { formatMissingManagedAppEnvMessage } from '../lib/app-runtime.js';
20
21
  import { run, runNocoBaseCommand } from '../lib/run-npm.js';
@@ -33,11 +34,17 @@ const DEFAULT_INSTALL_DB_PORTS = {
33
34
  mariadb: '3306',
34
35
  kingbase: '54321',
35
36
  };
37
+ const DEFAULT_INSTALL_BUILTIN_DB_IMAGES = {
38
+ postgres: 'postgres:16',
39
+ mysql: 'mysql:8',
40
+ mariadb: 'mariadb:11',
41
+ kingbase: 'registry.cn-shanghai.aliyuncs.com/nocobase/kingbase:v009r001c001b0030_single_x86',
42
+ };
36
43
  const DEFAULT_INSTALL_DB_DATABASE = 'nocobase';
37
44
  const DEFAULT_INSTALL_DB_USER = 'nocobase';
38
45
  const DEFAULT_INSTALL_DB_PASSWORD = 'nocobase';
39
46
  const DEFAULT_INSTALL_ROOT_USERNAME = 'nocobase';
40
- const DEFAULT_INSTALL_ROOT_EMAIL = 'admin@example.com';
47
+ const DEFAULT_INSTALL_ROOT_EMAIL = 'admin@nocobase.com';
41
48
  const DEFAULT_INSTALL_ROOT_PASSWORD = 'admin123';
42
49
  const DEFAULT_INSTALL_ROOT_NICKNAME = 'Super Admin';
43
50
  const CONFIG_SCOPE = 'project';
@@ -117,21 +124,47 @@ const INSTALL_LANGUAGE_OPTIONS = Object.entries(INSTALL_LANGUAGE_CODES).map(([va
117
124
  value,
118
125
  label: `${label} (${value})`,
119
126
  }));
127
+ const installText = (key, values) => localeText(`commands.install.${key}`, values);
120
128
  function argvHasToken(argv, tokens) {
121
129
  return tokens.some((t) => argv.includes(t));
122
130
  }
123
131
  function isInstallDbDialect(value) {
124
132
  return INSTALL_DB_DIALECTS.includes(value);
125
133
  }
134
+ function supportsBuiltinDbDialect(value) {
135
+ const dialect = String(value ?? '').trim();
136
+ return Object.prototype.hasOwnProperty.call(DEFAULT_INSTALL_BUILTIN_DB_IMAGES, dialect);
137
+ }
126
138
  export function defaultDbPortForDialect(value) {
127
139
  const dialect = String(value ?? 'postgres').trim();
128
140
  return DEFAULT_INSTALL_DB_PORTS[isInstallDbDialect(dialect) ? dialect : 'postgres'];
129
141
  }
142
+ function defaultBuiltinDbImageForDialect(value) {
143
+ const dialect = String(value ?? 'postgres').trim();
144
+ return supportsBuiltinDbDialect(dialect)
145
+ ? DEFAULT_INSTALL_BUILTIN_DB_IMAGES[dialect]
146
+ : DEFAULT_INSTALL_BUILTIN_DB_IMAGES.postgres;
147
+ }
148
+ function defaultDbDatabaseForDialect(value) {
149
+ return String(value ?? '').trim() === 'kingbase'
150
+ ? 'kingbase'
151
+ : DEFAULT_INSTALL_DB_DATABASE;
152
+ }
130
153
  function defaultDbHostForBuiltinDb(values) {
131
154
  return Boolean(values.builtinDb)
132
155
  ? DEFAULT_INSTALL_BUILTIN_DB_HOST
133
156
  : DEFAULT_INSTALL_DB_HOST;
134
157
  }
158
+ function validateBuiltinDbEnabled(value, values) {
159
+ if (!Boolean(value)) {
160
+ return undefined;
161
+ }
162
+ const dialect = String(values.dbDialect ?? 'postgres').trim() || 'postgres';
163
+ if (supportsBuiltinDbDialect(dialect)) {
164
+ return undefined;
165
+ }
166
+ return translateCli('commands.install.validation.builtinDbUnsupported', { dialect });
167
+ }
135
168
  function defaultInstallAppRootPath(envName) {
136
169
  const name = String(envName ?? DEFAULT_INSTALL_ENV_NAME).trim() || DEFAULT_INSTALL_ENV_NAME;
137
170
  return `./${name}/source/`;
@@ -264,6 +297,9 @@ export default class Install extends Command {
264
297
  description: 'Database dialect for the app',
265
298
  options: ['postgres', 'mysql', 'mariadb', 'kingbase'],
266
299
  }),
300
+ 'builtin-db-image': Flags.string({
301
+ description: 'Docker image for the built-in database container (default follows the selected database)',
302
+ }),
267
303
  'db-host': Flags.string({
268
304
  description: 'Database host for the app',
269
305
  }),
@@ -289,8 +325,8 @@ export default class Install extends Command {
289
325
  static envPrompts = {
290
326
  env: {
291
327
  type: 'text',
292
- message: 'What would you like to call this app?',
293
- placeholder: DEFAULT_INSTALL_ENV_NAME,
328
+ message: installText('prompts.env.message'),
329
+ placeholder: installText('prompts.env.placeholder'),
294
330
  required: true,
295
331
  validate: validateEnvKey,
296
332
  },
@@ -298,7 +334,7 @@ export default class Install extends Command {
298
334
  static appPrompts = {
299
335
  lang: {
300
336
  type: 'select',
301
- message: 'Which language would you like to use?',
337
+ message: installText('prompts.lang.message'),
302
338
  options: INSTALL_LANGUAGE_OPTIONS,
303
339
  initialValue: DEFAULT_INSTALL_LANG,
304
340
  yesInitialValue: DEFAULT_INSTALL_LANG,
@@ -311,39 +347,33 @@ export default class Install extends Command {
311
347
  // },
312
348
  appRootPath: {
313
349
  type: 'text',
314
- message: 'Where should this app be stored?',
315
- placeholder: './<env>/source/',
350
+ message: installText('prompts.appRootPath.message'),
351
+ placeholder: installText('prompts.appRootPath.placeholder'),
316
352
  initialValue: (values) => defaultInstallAppRootPath(values.env ?? values.appName),
317
353
  },
318
354
  appPort: {
319
355
  type: 'text',
320
- message: 'Which port should this app use?',
321
- placeholder: DEFAULT_INSTALL_APP_PORT,
356
+ message: installText('prompts.appPort.message'),
357
+ placeholder: installText('prompts.appPort.placeholder'),
322
358
  validate: validateAvailableTcpPort,
323
359
  },
324
360
  storagePath: {
325
361
  type: 'text',
326
- message: 'Where should uploads and local files be stored?',
327
- placeholder: './<env>/storage/',
362
+ message: installText('prompts.storagePath.message'),
363
+ placeholder: installText('prompts.storagePath.placeholder'),
328
364
  initialValue: (values) => defaultInstallStoragePath(values.env ?? values.appName),
329
365
  },
330
366
  fetchSource: {
331
367
  type: 'boolean',
332
- message: 'Download NocoBase automatically if the app directory is empty?',
368
+ message: installText('prompts.fetchSource.message'),
333
369
  initialValue: true,
334
370
  yesInitialValue: true,
335
371
  },
336
372
  };
337
373
  static dbPrompts = {
338
- builtinDb: {
339
- type: 'boolean',
340
- message: "Would you like to use the built-in database?",
341
- initialValue: true,
342
- yesInitialValue: true,
343
- },
344
374
  dbDialect: {
345
375
  type: 'select',
346
- message: 'Which database would you like to use?',
376
+ message: installText('prompts.dbDialect.message'),
347
377
  options: [
348
378
  { value: 'postgres', label: 'PostgreSQL' },
349
379
  { value: 'mysql', label: 'MySQL' },
@@ -354,10 +384,26 @@ export default class Install extends Command {
354
384
  yesInitialValue: 'postgres',
355
385
  required: true,
356
386
  },
387
+ builtinDb: {
388
+ type: 'boolean',
389
+ message: installText('prompts.builtinDb.message'),
390
+ initialValue: true,
391
+ yesInitialValue: true,
392
+ validate: validateBuiltinDbEnabled,
393
+ },
394
+ builtinDbImage: {
395
+ type: 'text',
396
+ message: installText('prompts.builtinDbImage.message'),
397
+ placeholder: installText('prompts.builtinDbImage.placeholder'),
398
+ initialValue: (values) => defaultBuiltinDbImageForDialect(values.dbDialect),
399
+ hidden: (values) => !Boolean(values.builtinDb)
400
+ || !supportsBuiltinDbDialect(values.dbDialect),
401
+ required: true,
402
+ },
357
403
  dbHost: {
358
404
  type: 'text',
359
- message: 'What is the database host?',
360
- placeholder: DEFAULT_INSTALL_DB_HOST,
405
+ message: installText('prompts.dbHost.message'),
406
+ placeholder: installText('prompts.dbHost.placeholder'),
361
407
  initialValue: (values) => defaultDbHostForBuiltinDb(values),
362
408
  yesInitialValue: DEFAULT_INSTALL_BUILTIN_DB_HOST,
363
409
  required: true,
@@ -365,8 +411,8 @@ export default class Install extends Command {
365
411
  },
366
412
  dbPort: {
367
413
  type: 'text',
368
- message: 'What is the database port?',
369
- placeholder: DEFAULT_INSTALL_DB_PORTS.postgres,
414
+ message: installText('prompts.dbPort.message'),
415
+ placeholder: installText('prompts.dbPort.placeholder'),
370
416
  initialValue: (values) => defaultDbPortForDialect(values.dbDialect),
371
417
  required: true,
372
418
  validate: validateTcpPort,
@@ -375,21 +421,20 @@ export default class Install extends Command {
375
421
  },
376
422
  dbDatabase: {
377
423
  type: 'text',
378
- message: 'What is the database name?',
379
- initialValue: DEFAULT_INSTALL_DB_DATABASE,
380
- yesInitialValue: DEFAULT_INSTALL_DB_DATABASE,
424
+ message: installText('prompts.dbDatabase.message'),
425
+ initialValue: (values) => defaultDbDatabaseForDialect(values.dbDialect),
381
426
  required: true,
382
427
  },
383
428
  dbUser: {
384
429
  type: 'text',
385
- message: 'What is the database username?',
430
+ message: installText('prompts.dbUser.message'),
386
431
  initialValue: DEFAULT_INSTALL_DB_USER,
387
432
  yesInitialValue: DEFAULT_INSTALL_DB_USER,
388
433
  required: true,
389
434
  },
390
435
  dbPassword: {
391
436
  type: 'password',
392
- message: 'What is the database password?',
437
+ message: installText('prompts.dbPassword.message'),
393
438
  initialValue: DEFAULT_INSTALL_DB_PASSWORD,
394
439
  yesInitialValue: DEFAULT_INSTALL_DB_PASSWORD,
395
440
  required: true,
@@ -398,30 +443,30 @@ export default class Install extends Command {
398
443
  static rootUserPrompts = {
399
444
  rootUsername: {
400
445
  type: 'text',
401
- message: 'Choose the initial admin username',
402
- placeholder: DEFAULT_INSTALL_ROOT_USERNAME,
403
- initialValue: DEFAULT_INSTALL_ROOT_USERNAME,
446
+ message: installText('prompts.rootUsername.message'),
447
+ placeholder: installText('prompts.rootUsername.placeholder'),
404
448
  yesInitialValue: DEFAULT_INSTALL_ROOT_USERNAME,
449
+ required: true,
405
450
  },
406
451
  rootEmail: {
407
452
  type: 'text',
408
- message: 'What is the initial admin email?',
409
- placeholder: DEFAULT_INSTALL_ROOT_EMAIL,
410
- initialValue: DEFAULT_INSTALL_ROOT_EMAIL,
453
+ message: installText('prompts.rootEmail.message'),
454
+ placeholder: installText('prompts.rootEmail.placeholder'),
411
455
  yesInitialValue: DEFAULT_INSTALL_ROOT_EMAIL,
456
+ required: true,
412
457
  },
413
458
  rootPassword: {
414
459
  type: 'password',
415
- message: 'Choose the initial admin password',
416
- initialValue: DEFAULT_INSTALL_ROOT_PASSWORD,
460
+ message: installText('prompts.rootPassword.message'),
417
461
  yesInitialValue: DEFAULT_INSTALL_ROOT_PASSWORD,
462
+ required: true,
418
463
  },
419
464
  rootNickname: {
420
465
  type: 'text',
421
- message: 'What display name should the initial admin use?',
422
- placeholder: DEFAULT_INSTALL_ROOT_NICKNAME,
423
- initialValue: DEFAULT_INSTALL_ROOT_NICKNAME,
466
+ message: installText('prompts.rootNickname.message'),
467
+ placeholder: installText('prompts.rootNickname.placeholder'),
424
468
  yesInitialValue: DEFAULT_INSTALL_ROOT_NICKNAME,
469
+ required: true,
425
470
  },
426
471
  };
427
472
  /**
@@ -520,6 +565,12 @@ export default class Install extends Command {
520
565
  preset.dbDialect = t;
521
566
  }
522
567
  }
568
+ if (flags['builtin-db-image'] !== undefined) {
569
+ const v = String(flags['builtin-db-image'] ?? '').trim();
570
+ if (v) {
571
+ preset.builtinDbImage = v;
572
+ }
573
+ }
523
574
  if (flags['db-host'] !== undefined) {
524
575
  const v = String(flags['db-host'] ?? '').trim();
525
576
  if (v) {
@@ -563,6 +614,7 @@ export default class Install extends Command {
563
614
  return pickPresetKeys(Install.buildPresetValuesFromFlags(flags), [
564
615
  'builtinDb',
565
616
  'dbDialect',
617
+ 'builtinDbImage',
566
618
  'dbHost',
567
619
  'dbPort',
568
620
  'dbDatabase',
@@ -603,6 +655,7 @@ export default class Install extends Command {
603
655
  const dbDatabase = Install.toOptionalPromptString(config.dbDatabase);
604
656
  const dbUser = Install.toOptionalPromptString(config.dbUser);
605
657
  const dbPassword = Install.toOptionalPromptString(config.dbPassword);
658
+ const builtinDbImage = Install.toOptionalPromptString(config.builtinDbImage);
606
659
  const auth = config.auth;
607
660
  const appPreset = {
608
661
  ...(appRootPath ? { appRootPath } : {}),
@@ -630,6 +683,7 @@ export default class Install extends Command {
630
683
  const dbPreset = {
631
684
  ...(typeof config.builtinDb === 'boolean' ? { builtinDb: config.builtinDb } : {}),
632
685
  ...(dbDialect ? { dbDialect } : {}),
686
+ ...(builtinDbImage ? { builtinDbImage } : {}),
633
687
  ...(dbHost ? { dbHost } : {}),
634
688
  ...(dbPort ? { dbPort } : {}),
635
689
  ...(dbDatabase ? { dbDatabase } : {}),
@@ -719,7 +773,7 @@ export default class Install extends Command {
719
773
  if (params.flags['app-port'] === undefined) {
720
774
  initialValues.appPort = await Install.resolveAvailableDefaultPort(DEFAULT_INSTALL_APP_PORT, {
721
775
  label: 'Default app port',
722
- warn: true,
776
+ warn: params.warnOnPortFallback ?? true,
723
777
  });
724
778
  }
725
779
  return initialValues;
@@ -745,7 +799,7 @@ export default class Install extends Command {
745
799
  return {
746
800
  dbPort: await Install.resolveAvailableDefaultPort(defaultPort, {
747
801
  label: `Default ${dialect} port`,
748
- warn: true,
802
+ warn: params.warnOnPortFallback ?? true,
749
803
  }),
750
804
  };
751
805
  }
@@ -900,6 +954,7 @@ export default class Install extends Command {
900
954
  const dbDialect = String(params.dbDialect ?? 'postgres').trim() || 'postgres';
901
955
  const dbPort = String(params.dbPort ?? defaultDbPortForDialect(dbDialect)).trim()
902
956
  || defaultDbPortForDialect(dbDialect);
957
+ const defaultDbDatabase = defaultDbDatabaseForDialect(dbDialect);
903
958
  const networkName = Install.buildBuiltinDbNetworkName(params.envName, params.workspaceName);
904
959
  const containerName = Install.buildBuiltinDbContainerName(params.envName, dbDialect, params.workspaceName);
905
960
  const dbHostInput = String(params.dbHost ?? '').trim();
@@ -915,6 +970,7 @@ export default class Install extends Command {
915
970
  ? dbHostInput
916
971
  : containerName);
917
972
  if (dbDialect === 'postgres') {
973
+ const image = String(params.builtinDbImage ?? '').trim() || DEFAULT_INSTALL_BUILTIN_DB_IMAGES.postgres;
918
974
  const dataDir = path.resolve(params.storagePath, 'db', 'postgres');
919
975
  const args = [
920
976
  'run',
@@ -937,14 +993,14 @@ export default class Install extends Command {
937
993
  if (Install.shouldPublishBuiltinDbPort(params.source)) {
938
994
  args.push('-p', `${dbPort}:5432`);
939
995
  }
940
- args.push('postgres:16', 'postgres', '-c', 'wal_level=logical');
996
+ args.push(image, 'postgres', '-c', 'wal_level=logical');
941
997
  return {
942
998
  source: String(params.source ?? '').trim() || undefined,
943
999
  dbDialect,
944
1000
  dbHost,
945
1001
  dbPort,
946
- dbDatabase: String(params.dbDatabase ?? DEFAULT_INSTALL_DB_DATABASE).trim()
947
- || DEFAULT_INSTALL_DB_DATABASE,
1002
+ dbDatabase: String(params.dbDatabase ?? defaultDbDatabase).trim()
1003
+ || defaultDbDatabase,
948
1004
  dbUser: String(params.dbUser ?? DEFAULT_INSTALL_DB_USER).trim()
949
1005
  || DEFAULT_INSTALL_DB_USER,
950
1006
  dbPassword: String(params.dbPassword ?? DEFAULT_INSTALL_DB_PASSWORD)
@@ -952,14 +1008,16 @@ export default class Install extends Command {
952
1008
  networkName,
953
1009
  containerName,
954
1010
  dataDir,
955
- image: 'postgres:16',
1011
+ builtinDbImage: image,
1012
+ image,
956
1013
  args,
957
1014
  };
958
1015
  }
959
1016
  if (dbDialect === 'mysql') {
1017
+ const image = String(params.builtinDbImage ?? '').trim() || DEFAULT_INSTALL_BUILTIN_DB_IMAGES.mysql;
960
1018
  const dataDir = path.resolve(params.storagePath, 'db', 'mysql');
961
1019
  const dbUser = String(params.dbUser ?? DEFAULT_INSTALL_DB_USER).trim() || DEFAULT_INSTALL_DB_USER;
962
- const dbDatabase = String(params.dbDatabase ?? DEFAULT_INSTALL_DB_DATABASE).trim() || DEFAULT_INSTALL_DB_DATABASE;
1020
+ const dbDatabase = String(params.dbDatabase ?? defaultDbDatabase).trim() || defaultDbDatabase;
963
1021
  const dbPassword = String(params.dbPassword ?? DEFAULT_INSTALL_DB_PASSWORD) || DEFAULT_INSTALL_DB_PASSWORD;
964
1022
  const args = [
965
1023
  'run',
@@ -984,7 +1042,7 @@ export default class Install extends Command {
984
1042
  if (Install.shouldPublishBuiltinDbPort(params.source)) {
985
1043
  args.push('-p', `${dbPort}:3306`);
986
1044
  }
987
- args.push('mysql:8');
1045
+ args.push(image);
988
1046
  return {
989
1047
  source: String(params.source ?? '').trim() || undefined,
990
1048
  dbDialect,
@@ -996,14 +1054,16 @@ export default class Install extends Command {
996
1054
  networkName,
997
1055
  containerName,
998
1056
  dataDir,
999
- image: 'mysql:8',
1057
+ builtinDbImage: image,
1058
+ image,
1000
1059
  args,
1001
1060
  };
1002
1061
  }
1003
1062
  if (dbDialect === 'mariadb') {
1063
+ const image = String(params.builtinDbImage ?? '').trim() || DEFAULT_INSTALL_BUILTIN_DB_IMAGES.mariadb;
1004
1064
  const dataDir = path.resolve(params.storagePath, 'db', 'mariadb');
1005
1065
  const dbUser = String(params.dbUser ?? DEFAULT_INSTALL_DB_USER).trim() || DEFAULT_INSTALL_DB_USER;
1006
- const dbDatabase = String(params.dbDatabase ?? DEFAULT_INSTALL_DB_DATABASE).trim() || DEFAULT_INSTALL_DB_DATABASE;
1066
+ const dbDatabase = String(params.dbDatabase ?? defaultDbDatabase).trim() || defaultDbDatabase;
1007
1067
  const dbPassword = String(params.dbPassword ?? DEFAULT_INSTALL_DB_PASSWORD) || DEFAULT_INSTALL_DB_PASSWORD;
1008
1068
  const args = [
1009
1069
  'run',
@@ -1028,7 +1088,58 @@ export default class Install extends Command {
1028
1088
  if (Install.shouldPublishBuiltinDbPort(params.source)) {
1029
1089
  args.push('-p', `${dbPort}:3306`);
1030
1090
  }
1031
- args.push('mariadb:11');
1091
+ args.push(image);
1092
+ return {
1093
+ source: String(params.source ?? '').trim() || undefined,
1094
+ dbDialect,
1095
+ dbHost,
1096
+ dbPort,
1097
+ dbDatabase,
1098
+ dbUser,
1099
+ dbPassword,
1100
+ networkName,
1101
+ containerName,
1102
+ dataDir,
1103
+ builtinDbImage: image,
1104
+ image,
1105
+ args,
1106
+ };
1107
+ }
1108
+ if (dbDialect === 'kingbase') {
1109
+ const image = String(params.builtinDbImage ?? '').trim() || DEFAULT_INSTALL_BUILTIN_DB_IMAGES.kingbase;
1110
+ const dataDir = path.resolve(params.storagePath, 'db', 'kingbase');
1111
+ const dbUser = String(params.dbUser ?? DEFAULT_INSTALL_DB_USER).trim() || DEFAULT_INSTALL_DB_USER;
1112
+ const dbDatabase = String(params.dbDatabase ?? defaultDbDatabase).trim() || defaultDbDatabase;
1113
+ const dbPassword = String(params.dbPassword ?? DEFAULT_INSTALL_DB_PASSWORD) || DEFAULT_INSTALL_DB_PASSWORD;
1114
+ const args = [
1115
+ 'run',
1116
+ '-d',
1117
+ '--name',
1118
+ containerName,
1119
+ '--restart',
1120
+ 'always',
1121
+ '--network',
1122
+ networkName,
1123
+ '--platform',
1124
+ 'linux/amd64',
1125
+ '--privileged',
1126
+ '-e',
1127
+ 'ENABLE_CI=no',
1128
+ '-e',
1129
+ `DB_USER=${dbUser}`,
1130
+ '-e',
1131
+ `DB_PASSWORD=${dbPassword}`,
1132
+ '-e',
1133
+ 'DB_MODE=pg',
1134
+ '-e',
1135
+ 'NEED_START=yes',
1136
+ '-v',
1137
+ `${dataDir}:/home/kingbase/userdata`,
1138
+ ];
1139
+ if (Install.shouldPublishBuiltinDbPort(params.source)) {
1140
+ args.push('-p', `${dbPort}:54321`);
1141
+ }
1142
+ args.push(image, '/usr/sbin/init');
1032
1143
  return {
1033
1144
  source: String(params.source ?? '').trim() || undefined,
1034
1145
  dbDialect,
@@ -1040,11 +1151,12 @@ export default class Install extends Command {
1040
1151
  networkName,
1041
1152
  containerName,
1042
1153
  dataDir,
1043
- image: 'mariadb:11',
1154
+ builtinDbImage: image,
1155
+ image,
1044
1156
  args,
1045
1157
  };
1046
1158
  }
1047
- throw new Error(`Built-in database does not support "${dbDialect}" yet. Please choose PostgreSQL, MySQL, or MariaDB.`);
1159
+ throw new Error(`Built-in database does not support "${dbDialect}" yet. Please choose PostgreSQL, MySQL, MariaDB, or KingbaseES.`);
1048
1160
  }
1049
1161
  async ensureDockerNetwork(name) {
1050
1162
  p.log.step(`Checking Docker network: ${name}`);
@@ -1138,6 +1250,7 @@ export default class Install extends Command {
1138
1250
  dbDatabase: params.dbResults.dbDatabase,
1139
1251
  dbUser: params.dbResults.dbUser,
1140
1252
  dbPassword: params.dbResults.dbPassword,
1253
+ builtinDbImage: params.dbResults.builtinDbImage,
1141
1254
  });
1142
1255
  p.log.step(`Preparing built-in ${plan.dbDialect} database`);
1143
1256
  await this.ensureDockerNetwork(plan.networkName);
@@ -1519,6 +1632,7 @@ export default class Install extends Command {
1519
1632
  Install.pushArgIfValue(argv, '--timezone', params.appResults.timeZone);
1520
1633
  Install.pushBooleanArgIfSet(argv, '--builtin-db', params.dbResults.builtinDb);
1521
1634
  Install.pushArgIfValue(argv, '--db-dialect', params.dbResults.dbDialect);
1635
+ Install.pushArgIfValue(argv, '--builtin-db-image', params.dbResults.builtinDbImage);
1522
1636
  Install.pushArgIfValue(argv, '--db-host', params.dbResults.dbHost);
1523
1637
  Install.pushArgIfValue(argv, '--db-port', params.dbResults.dbPort);
1524
1638
  Install.pushArgIfValue(argv, '--db-database', params.dbResults.dbDatabase);
@@ -1624,6 +1738,7 @@ export default class Install extends Command {
1624
1738
  }
1625
1739
  async run() {
1626
1740
  const parsedResult = await this.parse(Install);
1741
+ applyCliLocale(parsedResult.flags.locale);
1627
1742
  const flags = parsedResult.flags;
1628
1743
  const parsed = {
1629
1744
  ...flags,
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { Args, Command, Flags } from '@oclif/core';
10
10
  import { runPromptCatalog, } from "../lib/prompt-catalog.js";
11
+ import { applyCliLocale, CLI_LOCALE_FLAG_DESCRIPTION, CLI_LOCALE_FLAG_OPTIONS, } from "../lib/cli-locale.js";
11
12
  import { runPromptCatalogWebUI } from "../lib/prompt-web-ui.js";
12
13
  import PromptsTest from "./prompts-test.js";
13
14
  function buildWebUiStagesFromTestPrompts(c) {
@@ -67,6 +68,10 @@ export default class PromptsStages extends Command {
67
68
  description: 'Accept defaults only in the terminal (no Clack after submit); same semantics as `prompts-test` when used with the submitted or default preset.',
68
69
  default: false,
69
70
  }),
71
+ locale: Flags.string({
72
+ description: CLI_LOCALE_FLAG_DESCRIPTION,
73
+ options: CLI_LOCALE_FLAG_OPTIONS,
74
+ }),
70
75
  file: Flags.string({
71
76
  char: 'f',
72
77
  description: 'Merged into initialValues.file (default for the text prompt when set)',
@@ -96,6 +101,7 @@ export default class PromptsStages extends Command {
96
101
  };
97
102
  async run() {
98
103
  const { args, flags } = await this.parse(PromptsStages);
104
+ applyCliLocale(flags.locale);
99
105
  let presetValues = this.buildPresetValuesFromParsed(args, flags);
100
106
  if (flags.ui) {
101
107
  presetValues = await runPromptCatalogWebUI({
@@ -9,6 +9,7 @@
9
9
  import * as p from '@clack/prompts';
10
10
  import { Args, Command, Flags } from '@oclif/core';
11
11
  import { runPromptCatalog, } from "../lib/prompt-catalog.js";
12
+ import { applyCliLocale, CLI_LOCALE_FLAG_DESCRIPTION, CLI_LOCALE_FLAG_OPTIONS, } from "../lib/cli-locale.js";
12
13
  import { runPromptCatalogWebUI, } from "../lib/prompt-web-ui.js";
13
14
  export default class PromptsTest extends Command {
14
15
  static hidden = true;
@@ -33,6 +34,10 @@ export default class PromptsTest extends Command {
33
34
  description: 'Accept defaults only (no prompts); uses `yesInitialValues` merged over `initialValues`, then per-block `yesInitialValue`. Same as non-TTY.',
34
35
  default: false,
35
36
  }),
37
+ locale: Flags.string({
38
+ description: CLI_LOCALE_FLAG_DESCRIPTION,
39
+ options: CLI_LOCALE_FLAG_OPTIONS,
40
+ }),
36
41
  file: Flags.string({
37
42
  char: 'f',
38
43
  description: 'Merged into initialValues.file (default for the text prompt when set)',
@@ -127,6 +132,7 @@ export default class PromptsTest extends Command {
127
132
  };
128
133
  async run() {
129
134
  const { args, flags } = await this.parse(PromptsTest);
135
+ applyCliLocale(flags.locale);
130
136
  let presetValues = this.buildPresetValuesFromParsed(args, flags);
131
137
  if (flags.ui) {
132
138
  presetValues = await runPromptCatalogWebUI(PromptsTest.prompts, {
@@ -1,5 +1,27 @@
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
+ */
1
9
  import { promises as fs } from 'node:fs';
2
10
  import { resolveServerRequestTarget } from './env-auth.js';
11
+ const CLI_REQUEST_SOURCE_HEADER = 'x-request-source';
12
+ const CLI_REQUEST_SOURCE_VALUE = 'cli';
13
+ function stripUtf8Bom(text) {
14
+ return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
15
+ }
16
+ function parseJsonInput(raw, flagName) {
17
+ const content = stripUtf8Bom(raw);
18
+ try {
19
+ return JSON.parse(content);
20
+ }
21
+ catch (error) {
22
+ throw new Error(`Invalid JSON for --${flagName}: ${error?.message ?? 'parse failed'}`);
23
+ }
24
+ }
3
25
  function normalizeBaseUrl(baseUrl) {
4
26
  return baseUrl.replace(/\/+$/, '');
5
27
  }
@@ -59,6 +81,28 @@ function listProvidedBodyFlags(flags, parameters) {
59
81
  .filter((parameter) => hasParameterValue(flags, parameter))
60
82
  .map((parameter) => `--${parameter.flagName}`);
61
83
  }
84
+ function parseBodyFieldValue(rawValue, parameter) {
85
+ if (rawValue === undefined) {
86
+ return undefined;
87
+ }
88
+ if (parameter.isArray && !parameter.jsonEncoded) {
89
+ return Array.isArray(rawValue) ? rawValue : rawValue ? [rawValue] : undefined;
90
+ }
91
+ if (parameter.jsonEncoded || parameter.type === 'object' || parameter.type === 'array') {
92
+ if (typeof rawValue !== 'string') {
93
+ return rawValue;
94
+ }
95
+ const parsed = parseJsonInput(rawValue, parameter.flagName);
96
+ if (parameter.type === 'array' && !Array.isArray(parsed)) {
97
+ throw new Error(`--${parameter.flagName} must be a JSON array`);
98
+ }
99
+ if (parameter.type === 'object' && (parsed === null || Array.isArray(parsed) || typeof parsed !== 'object')) {
100
+ throw new Error(`--${parameter.flagName} must be a JSON object`);
101
+ }
102
+ return parsed;
103
+ }
104
+ return parseScalarValue(rawValue, parameter.type);
105
+ }
62
106
  export async function parseBody(flags, operation) {
63
107
  const inlineBody = flags.body;
64
108
  const bodyFile = flags['body-file'];
@@ -70,10 +114,10 @@ export async function parseBody(flags, operation) {
70
114
  throw new Error(`Conflicting request body inputs: received ${rawBodyInput} together with body field flags (${providedBodyFlags.join(', ')}). Use either body field flags or --body/--body-file.`);
71
115
  }
72
116
  if (inlineBody) {
73
- return JSON.parse(inlineBody);
117
+ return parseJsonInput(inlineBody, 'body');
74
118
  }
75
119
  if (bodyFile) {
76
- return fs.readFile(bodyFile, 'utf8').then((content) => JSON.parse(content));
120
+ return fs.readFile(bodyFile, 'utf8').then((content) => parseJsonInput(content, 'body-file'));
77
121
  }
78
122
  if (!bodyParameters.length) {
79
123
  return undefined;
@@ -81,9 +125,7 @@ export async function parseBody(flags, operation) {
81
125
  const body = {};
82
126
  for (const parameter of bodyParameters) {
83
127
  const rawValue = flags[parameter.flagName];
84
- const value = parameter.isArray && !parameter.jsonEncoded
85
- ? (Array.isArray(rawValue) ? rawValue : rawValue ? [rawValue] : undefined)
86
- : parseScalarValue(rawValue, parameter.type);
128
+ const value = parseBodyFieldValue(rawValue, parameter);
87
129
  if (parameter.required && (value === undefined || value === '')) {
88
130
  throw new Error(`Missing required body field --${parameter.flagName}`);
89
131
  }
@@ -103,6 +145,7 @@ export async function parseBody(flags, operation) {
103
145
  export async function executeApiRequest(options) {
104
146
  const { baseUrl, token } = await resolveServerRequestTarget(options);
105
147
  const headers = new Headers();
148
+ headers.set(CLI_REQUEST_SOURCE_HEADER, CLI_REQUEST_SOURCE_VALUE);
106
149
  if (token) {
107
150
  headers.set('authorization', `Bearer ${token}`);
108
151
  }
@@ -162,6 +205,7 @@ export async function executeApiRequest(options) {
162
205
  export async function executeRawApiRequest(options) {
163
206
  const { baseUrl, token } = await resolveServerRequestTarget(options);
164
207
  const headers = new Headers();
208
+ headers.set(CLI_REQUEST_SOURCE_HEADER, CLI_REQUEST_SOURCE_VALUE);
165
209
  if (token) {
166
210
  headers.set('authorization', `Bearer ${token}`);
167
211
  }