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

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,7 +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
+ import { applyCliLocale, localeText, resolveCliLocale, translateCli, } from "../lib/cli-locale.js";
19
19
  import { findAvailableTcpPort, validateAvailableTcpPort, validateTcpPort, validateEnvKey, } from "../lib/prompt-validators.js";
20
20
  import { formatMissingManagedAppEnvMessage } from '../lib/app-runtime.js';
21
21
  import { run, runNocoBaseCommand } from '../lib/run-npm.js';
@@ -40,6 +40,12 @@ const DEFAULT_INSTALL_BUILTIN_DB_IMAGES = {
40
40
  mariadb: 'mariadb:11',
41
41
  kingbase: 'registry.cn-shanghai.aliyuncs.com/nocobase/kingbase:v009r001c001b0030_single_x86',
42
42
  };
43
+ const DEFAULT_INSTALL_BUILTIN_DB_IMAGES_ZH_CN = {
44
+ postgres: 'registry.cn-shanghai.aliyuncs.com/nocobase/postgres:16',
45
+ mysql: 'registry.cn-shanghai.aliyuncs.com/nocobase/mysql:8',
46
+ mariadb: 'registry.cn-shanghai.aliyuncs.com/nocobase/mariadb:11',
47
+ kingbase: 'registry.cn-shanghai.aliyuncs.com/nocobase/kingbase:v009r001c001b0030_single_x86',
48
+ };
43
49
  const DEFAULT_INSTALL_DB_DATABASE = 'nocobase';
44
50
  const DEFAULT_INSTALL_DB_USER = 'nocobase';
45
51
  const DEFAULT_INSTALL_DB_PASSWORD = 'nocobase';
@@ -141,9 +147,12 @@ export function defaultDbPortForDialect(value) {
141
147
  }
142
148
  function defaultBuiltinDbImageForDialect(value) {
143
149
  const dialect = String(value ?? 'postgres').trim();
150
+ const defaults = resolveCliLocale(process.env.NB_LOCALE) === 'zh-CN'
151
+ ? DEFAULT_INSTALL_BUILTIN_DB_IMAGES_ZH_CN
152
+ : DEFAULT_INSTALL_BUILTIN_DB_IMAGES;
144
153
  return supportsBuiltinDbDialect(dialect)
145
- ? DEFAULT_INSTALL_BUILTIN_DB_IMAGES[dialect]
146
- : DEFAULT_INSTALL_BUILTIN_DB_IMAGES.postgres;
154
+ ? defaults[dialect]
155
+ : defaults.postgres;
147
156
  }
148
157
  function defaultDbDatabaseForDialect(value) {
149
158
  return String(value ?? '').trim() === 'kingbase'
@@ -254,6 +263,10 @@ export default class Install extends Command {
254
263
  description: 'Resume a previous unfinished setup for this env using the saved workspace env config',
255
264
  default: false,
256
265
  }),
266
+ verbose: Flags.boolean({
267
+ description: 'Show detailed command output',
268
+ default: false,
269
+ }),
257
270
  env: Flags.string({
258
271
  char: 'e',
259
272
  description: 'App/env name to create or update. Defaults app paths to ./<envName>/source/ and ./<envName>/storage/.',
@@ -804,15 +817,15 @@ export default class Install extends Command {
804
817
  };
805
818
  }
806
819
  /**
807
- * When install runs {@link Download.prompts} after app prompts, align language and
808
- * output directory defaults with the app settings collected earlier in the flow.
820
+ * When install runs {@link Download.prompts} after app prompts, align the download
821
+ * output directory with app settings, while Docker registry defaults follow the CLI locale.
809
822
  */
810
823
  static buildDownloadPromptOptionsForInstall(appResults, envName) {
811
824
  const appRoot = String(appResults.appRootPath ?? '').trim() || defaultInstallAppRootPath(envName);
812
825
  const lang = String(appResults.lang ?? DEFAULT_INSTALL_LANG).trim() || DEFAULT_INSTALL_LANG;
813
826
  const initialValues = {
814
827
  lang,
815
- dockerRegistry: defaultDockerRegistryForLang(lang),
828
+ dockerRegistry: defaultDockerRegistryForLang(process.env.NB_LOCALE),
816
829
  outputDir: appRoot,
817
830
  };
818
831
  const values = {
@@ -970,7 +983,7 @@ export default class Install extends Command {
970
983
  ? dbHostInput
971
984
  : containerName);
972
985
  if (dbDialect === 'postgres') {
973
- const image = String(params.builtinDbImage ?? '').trim() || DEFAULT_INSTALL_BUILTIN_DB_IMAGES.postgres;
986
+ const image = String(params.builtinDbImage ?? '').trim() || defaultBuiltinDbImageForDialect(dbDialect);
974
987
  const dataDir = path.resolve(params.storagePath, 'db', 'postgres');
975
988
  const args = [
976
989
  'run',
@@ -1014,7 +1027,7 @@ export default class Install extends Command {
1014
1027
  };
1015
1028
  }
1016
1029
  if (dbDialect === 'mysql') {
1017
- const image = String(params.builtinDbImage ?? '').trim() || DEFAULT_INSTALL_BUILTIN_DB_IMAGES.mysql;
1030
+ const image = String(params.builtinDbImage ?? '').trim() || defaultBuiltinDbImageForDialect(dbDialect);
1018
1031
  const dataDir = path.resolve(params.storagePath, 'db', 'mysql');
1019
1032
  const dbUser = String(params.dbUser ?? DEFAULT_INSTALL_DB_USER).trim() || DEFAULT_INSTALL_DB_USER;
1020
1033
  const dbDatabase = String(params.dbDatabase ?? defaultDbDatabase).trim() || defaultDbDatabase;
@@ -1060,7 +1073,7 @@ export default class Install extends Command {
1060
1073
  };
1061
1074
  }
1062
1075
  if (dbDialect === 'mariadb') {
1063
- const image = String(params.builtinDbImage ?? '').trim() || DEFAULT_INSTALL_BUILTIN_DB_IMAGES.mariadb;
1076
+ const image = String(params.builtinDbImage ?? '').trim() || defaultBuiltinDbImageForDialect(dbDialect);
1064
1077
  const dataDir = path.resolve(params.storagePath, 'db', 'mariadb');
1065
1078
  const dbUser = String(params.dbUser ?? DEFAULT_INSTALL_DB_USER).trim() || DEFAULT_INSTALL_DB_USER;
1066
1079
  const dbDatabase = String(params.dbDatabase ?? defaultDbDatabase).trim() || defaultDbDatabase;
@@ -1106,7 +1119,7 @@ export default class Install extends Command {
1106
1119
  };
1107
1120
  }
1108
1121
  if (dbDialect === 'kingbase') {
1109
- const image = String(params.builtinDbImage ?? '').trim() || DEFAULT_INSTALL_BUILTIN_DB_IMAGES.kingbase;
1122
+ const image = String(params.builtinDbImage ?? '').trim() || defaultBuiltinDbImageForDialect(dbDialect);
1110
1123
  const dataDir = path.resolve(params.storagePath, 'db', 'kingbase');
1111
1124
  const dbUser = String(params.dbUser ?? DEFAULT_INSTALL_DB_USER).trim() || DEFAULT_INSTALL_DB_USER;
1112
1125
  const dbDatabase = String(params.dbDatabase ?? defaultDbDatabase).trim() || defaultDbDatabase;
@@ -1194,6 +1207,7 @@ export default class Install extends Command {
1194
1207
  async removeDockerContainer(name) {
1195
1208
  await run('docker', ['rm', '-f', name], {
1196
1209
  errorName: 'docker rm',
1210
+ stdio: 'ignore',
1197
1211
  });
1198
1212
  }
1199
1213
  async removeDockerContainerIfForced(params) {
@@ -1225,7 +1239,7 @@ export default class Install extends Command {
1225
1239
  }
1226
1240
  return env;
1227
1241
  }
1228
- async ensureBuiltinDbContainer(plan) {
1242
+ async ensureBuiltinDbContainer(plan, options) {
1229
1243
  const exists = await this.dockerContainerExists(plan.containerName);
1230
1244
  if (exists) {
1231
1245
  p.log.info(`Built-in ${plan.dbDialect} container already exists: ${plan.containerName}`);
@@ -1234,6 +1248,7 @@ export default class Install extends Command {
1234
1248
  await mkdir(plan.dataDir, { recursive: true });
1235
1249
  await run('docker', plan.args, {
1236
1250
  errorName: 'docker run',
1251
+ stdio: options?.stdio ?? 'ignore',
1237
1252
  });
1238
1253
  }
1239
1254
  async startBuiltinDb(params) {
@@ -1265,13 +1280,15 @@ export default class Install extends Command {
1265
1280
  throw new Error(`Built-in ${plan.dbDialect} needs host port ${plan.dbPort}, but ${portError}`);
1266
1281
  }
1267
1282
  }
1268
- await this.ensureBuiltinDbContainer(plan);
1283
+ await this.ensureBuiltinDbContainer(plan, {
1284
+ stdio: params.commandStdio ?? 'ignore',
1285
+ });
1269
1286
  p.log.info(`Built-in ${plan.dbDialect} database is ready at ${plan.dbHost}:${plan.dbPort}`);
1270
1287
  return plan;
1271
1288
  }
1272
1289
  static buildDockerAppPlan(params) {
1273
1290
  const dockerRegistry = String(downloadResultsValue(params.downloadResults, 'dockerRegistry') ?? '').trim()
1274
- || defaultDockerRegistryForLang(params.appResults.lang);
1291
+ || defaultDockerRegistryForLang(process.env.NB_LOCALE);
1275
1292
  const version = String(downloadResultsValue(params.downloadResults, 'version') ?? '').trim() || 'latest';
1276
1293
  const appPort = String(params.appResults.appPort ?? DEFAULT_INSTALL_APP_PORT).trim() || DEFAULT_INSTALL_APP_PORT;
1277
1294
  const storagePath = path.resolve(String(params.appResults.storagePath ?? '').trim()
@@ -1319,7 +1336,7 @@ export default class Install extends Command {
1319
1336
  args,
1320
1337
  };
1321
1338
  }
1322
- async ensureDockerAppContainer(plan) {
1339
+ async ensureDockerAppContainer(plan, options) {
1323
1340
  const exists = await this.dockerContainerExists(plan.containerName);
1324
1341
  if (exists) {
1325
1342
  p.log.info(`App container already exists: ${plan.containerName}`);
@@ -1328,6 +1345,7 @@ export default class Install extends Command {
1328
1345
  await mkdir(plan.storagePath, { recursive: true });
1329
1346
  await run('docker', plan.args, {
1330
1347
  errorName: 'docker run',
1348
+ stdio: options?.stdio ?? 'ignore',
1331
1349
  });
1332
1350
  return 'created';
1333
1351
  }
@@ -1350,7 +1368,9 @@ export default class Install extends Command {
1350
1368
  displayName: 'app container',
1351
1369
  force: params.force,
1352
1370
  });
1353
- const containerState = await this.ensureDockerAppContainer(plan);
1371
+ const containerState = await this.ensureDockerAppContainer(plan, {
1372
+ stdio: params.commandStdio ?? 'ignore',
1373
+ });
1354
1374
  if (containerState === 'existing') {
1355
1375
  const env = await this.inspectDockerContainerEnv(plan.containerName);
1356
1376
  plan.appKey = env.APP_KEY || plan.appKey;
@@ -1365,8 +1385,11 @@ export default class Install extends Command {
1365
1385
  argv.push(flag, text);
1366
1386
  }
1367
1387
  }
1368
- static buildDownloadArgvFromResults(results) {
1388
+ static buildDownloadArgvFromResults(results, options) {
1369
1389
  const argv = ['-y', '--no-intro'];
1390
+ if (options?.verbose) {
1391
+ argv.push('--verbose');
1392
+ }
1370
1393
  Install.pushDownloadArgIfValue(argv, '--source', results.source);
1371
1394
  Install.pushDownloadArgIfValue(argv, '--version', results.version);
1372
1395
  Install.pushDownloadArgIfValue(argv, '--output-dir', results.outputDir);
@@ -1401,10 +1424,24 @@ export default class Install extends Command {
1401
1424
  || defaultInstallAppRootPath(params.envName);
1402
1425
  return path.resolve(process.cwd(), outputDir);
1403
1426
  }
1427
+ commandStdio(verbose) {
1428
+ return verbose ? 'inherit' : 'ignore';
1429
+ }
1430
+ async downloadManagedSource(params) {
1431
+ const argv = Install.buildDownloadArgvFromResults(params.downloadResults, {
1432
+ verbose: params.verbose,
1433
+ });
1434
+ const source = String(params.downloadResults.source ?? '').trim();
1435
+ p.log.step(source === 'docker'
1436
+ ? 'Downloading Docker image'
1437
+ : 'Downloading local NocoBase app files');
1438
+ return await this.config.runCommand('download', argv);
1439
+ }
1404
1440
  async downloadLocalApp(params) {
1405
- const argv = Install.buildDownloadArgvFromResults(params.downloadResults);
1406
- p.log.step('Downloading local NocoBase app files');
1407
- const result = await this.config.runCommand('download', argv);
1441
+ const result = await this.downloadManagedSource({
1442
+ downloadResults: params.downloadResults,
1443
+ verbose: params.verbose,
1444
+ });
1408
1445
  const projectRoot = Install.resolveLocalProjectRoot({
1409
1446
  envName: params.envName,
1410
1447
  appResults: params.appResults,
@@ -1458,6 +1495,7 @@ export default class Install extends Command {
1458
1495
  await runNocoBaseCommand(['pm2', 'kill'], {
1459
1496
  cwd: params.projectRoot,
1460
1497
  env,
1498
+ stdio: params.commandStdio ?? 'ignore',
1461
1499
  });
1462
1500
  }
1463
1501
  catch (error) {
@@ -1468,6 +1506,7 @@ export default class Install extends Command {
1468
1506
  await runNocoBaseCommand(args, {
1469
1507
  cwd: params.projectRoot,
1470
1508
  env,
1509
+ stdio: params.commandStdio ?? 'ignore',
1471
1510
  });
1472
1511
  p.log.info(`Local app is starting at http://127.0.0.1:${env.APP_PORT}`);
1473
1512
  return {
@@ -1743,6 +1782,7 @@ export default class Install extends Command {
1743
1782
  const parsed = {
1744
1783
  ...flags,
1745
1784
  };
1785
+ const commandStdio = this.commandStdio(parsed.verbose);
1746
1786
  if (!parsed['no-intro']) {
1747
1787
  p.intro('Set Up NocoBase');
1748
1788
  }
@@ -1769,6 +1809,7 @@ export default class Install extends Command {
1769
1809
  downloadResults,
1770
1810
  dbResults,
1771
1811
  force: parsed.force,
1812
+ commandStdio,
1772
1813
  });
1773
1814
  dbResults.dbHost = builtinDbPlan.dbHost;
1774
1815
  dbResults.dbPort = builtinDbPlan.dbPort;
@@ -1781,6 +1822,10 @@ export default class Install extends Command {
1781
1822
  let localAppPlan;
1782
1823
  if (Boolean(appResults.fetchSource)) {
1783
1824
  if (source === 'docker') {
1825
+ await this.downloadManagedSource({
1826
+ downloadResults,
1827
+ verbose: parsed.verbose,
1828
+ });
1784
1829
  dockerAppPlan = await this.installDockerApp({
1785
1830
  envName,
1786
1831
  workspaceName,
@@ -1790,6 +1835,7 @@ export default class Install extends Command {
1790
1835
  rootResults,
1791
1836
  builtinDbPlan,
1792
1837
  force: parsed.force,
1838
+ commandStdio,
1793
1839
  });
1794
1840
  appResults.appKey = dockerAppPlan.appKey;
1795
1841
  appResults.timeZone = dockerAppPlan.timeZone;
@@ -1799,6 +1845,7 @@ export default class Install extends Command {
1799
1845
  envName,
1800
1846
  appResults,
1801
1847
  downloadResults,
1848
+ verbose: parsed.verbose,
1802
1849
  });
1803
1850
  localAppPlan = await this.startLocalApp({
1804
1851
  envName,
@@ -1807,6 +1854,7 @@ export default class Install extends Command {
1807
1854
  appResults,
1808
1855
  dbResults,
1809
1856
  rootResults,
1857
+ commandStdio,
1810
1858
  });
1811
1859
  appResults.appKey = localAppPlan.appKey;
1812
1860
  appResults.timeZone = localAppPlan.timeZone;
@@ -0,0 +1,71 @@
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 { Command, Flags } from '@oclif/core';
10
+ import { getRecommendedSelfUpdateCommand, inspectSelfStatus, } from '../../lib/self-manager.js';
11
+ import { printInfo, renderTable } from '../../lib/ui.js';
12
+ export default class SelfCheck extends Command {
13
+ static summary = 'Check the installed NocoBase CLI version and self-update support';
14
+ static description = 'Inspect the current NocoBase CLI install, resolve the latest version for the selected channel, and report whether automatic self-update is supported.';
15
+ static examples = [
16
+ '<%= config.bin %> <%= command.id %>',
17
+ '<%= config.bin %> <%= command.id %> --channel beta',
18
+ '<%= config.bin %> <%= command.id %> --json',
19
+ ];
20
+ static flags = {
21
+ channel: Flags.string({
22
+ description: 'Release channel to compare against. Defaults to the current CLI channel.',
23
+ options: ['auto', 'latest', 'beta', 'alpha'],
24
+ default: 'auto',
25
+ }),
26
+ json: Flags.boolean({
27
+ description: 'Output the result as JSON',
28
+ default: false,
29
+ }),
30
+ };
31
+ async run() {
32
+ const { flags } = await this.parse(SelfCheck);
33
+ const status = await inspectSelfStatus({
34
+ channel: flags.channel,
35
+ });
36
+ if (flags.json) {
37
+ this.log(JSON.stringify({
38
+ ok: true,
39
+ kind: 'self',
40
+ packageName: status.packageName,
41
+ currentVersion: status.currentVersion,
42
+ latestVersion: status.latestVersion,
43
+ channel: status.channel,
44
+ updateAvailable: status.updateAvailable,
45
+ installMethod: status.installMethod,
46
+ updatable: status.updatable,
47
+ updateBlockedReason: status.updateBlockedReason,
48
+ recommendedCommand: getRecommendedSelfUpdateCommand(status),
49
+ registryError: status.registryError,
50
+ }, null, 2));
51
+ return;
52
+ }
53
+ this.log(renderTable(['Field', 'Value'], [
54
+ ['Current version', status.currentVersion || 'unknown'],
55
+ ['Latest version', status.latestVersion || 'unknown'],
56
+ ['Channel', status.channel],
57
+ ['Install method', status.installMethod],
58
+ ['Auto-update', status.updatable ? 'supported' : 'not supported'],
59
+ ['Update available', status.updateAvailable ? 'yes' : 'no'],
60
+ ]));
61
+ if (status.updateAvailable && status.updatable) {
62
+ printInfo('Run `nb self update` to update the CLI.');
63
+ }
64
+ else if (status.updateAvailable && status.updateBlockedReason) {
65
+ printInfo(status.updateBlockedReason);
66
+ }
67
+ if (status.registryError) {
68
+ printInfo(`Version check warning: ${status.registryError}`);
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,20 @@
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 { Command, loadHelpClass } from '@oclif/core';
10
+ export default class Self extends Command {
11
+ static summary = 'Inspect or update the NocoBase CLI itself';
12
+ async run() {
13
+ await this.parse(Self);
14
+ const Help = await loadHelpClass(this.config);
15
+ await new Help(this.config, this.config.pjson.oclif.helpOptions ?? this.config.pjson.helpOptions).showHelp([
16
+ this.id ?? 'self',
17
+ ...this.argv,
18
+ ]);
19
+ }
20
+ }
@@ -0,0 +1,76 @@
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 { Command, Flags } from '@oclif/core';
10
+ import { confirmAction } from '../../lib/ui.js';
11
+ import { formatSelfUpdateUnavailableMessage, formatUnsupportedSelfUpdateMessage, inspectSelfStatus, updateSelf, } from '../../lib/self-manager.js';
12
+ export default class SelfUpdate extends Command {
13
+ static summary = 'Update the globally installed NocoBase CLI';
14
+ static description = 'Update the current NocoBase CLI install when it is managed by a standard global npm install.';
15
+ static examples = [
16
+ '<%= config.bin %> <%= command.id %>',
17
+ '<%= config.bin %> <%= command.id %> --yes',
18
+ '<%= config.bin %> <%= command.id %> --channel alpha --json',
19
+ ];
20
+ static flags = {
21
+ channel: Flags.string({
22
+ description: 'Release channel to update to. Defaults to the current CLI channel.',
23
+ options: ['auto', 'latest', 'beta', 'alpha'],
24
+ default: 'auto',
25
+ }),
26
+ yes: Flags.boolean({
27
+ char: 'y',
28
+ description: 'Skip the update confirmation prompt',
29
+ default: false,
30
+ }),
31
+ json: Flags.boolean({
32
+ description: 'Output the result as JSON',
33
+ default: false,
34
+ }),
35
+ };
36
+ async run() {
37
+ const { flags } = await this.parse(SelfUpdate);
38
+ const status = await inspectSelfStatus({
39
+ channel: flags.channel,
40
+ });
41
+ if (!status.updatable) {
42
+ this.error(formatUnsupportedSelfUpdateMessage(status));
43
+ }
44
+ if (!status.latestVersion && status.registryError) {
45
+ this.error(formatSelfUpdateUnavailableMessage(status));
46
+ }
47
+ if (!flags.yes && status.updateAvailable) {
48
+ const confirmed = await confirmAction(`Update ${status.packageName} from ${status.currentVersion} to ${status.latestVersion}?`, { defaultValue: false });
49
+ if (!confirmed) {
50
+ this.log('Skipped CLI update.');
51
+ return;
52
+ }
53
+ }
54
+ const result = await updateSelf({
55
+ channel: flags.channel,
56
+ });
57
+ if (flags.json) {
58
+ this.log(JSON.stringify({
59
+ ok: true,
60
+ kind: 'self',
61
+ action: result.action,
62
+ packageName: result.status.packageName,
63
+ packageSpec: result.packageSpec,
64
+ channel: result.status.channel,
65
+ fromVersion: result.status.currentVersion,
66
+ toVersion: result.targetVersion,
67
+ }, null, 2));
68
+ return;
69
+ }
70
+ if (result.action === 'noop') {
71
+ this.log(`NocoBase CLI is already up to date at ${result.status.currentVersion}.`);
72
+ return;
73
+ }
74
+ this.log(`Updated NocoBase CLI from ${result.status.currentVersion} using ${result.packageSpec}${result.targetVersion ? ` (latest ${result.status.channel} resolves to ${result.targetVersion})` : ''}.`);
75
+ }
76
+ }
@@ -0,0 +1,63 @@
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 { Command, Flags } from '@oclif/core';
10
+ import { inspectSkillsStatus } from '../../lib/skills-manager.js';
11
+ import { printInfo, renderTable } from '../../lib/ui.js';
12
+ export default class SkillsCheck extends Command {
13
+ static summary = 'Check the NocoBase AI coding skills installed for this workspace';
14
+ static description = 'Inspect the current workspace for NocoBase AI coding skills and report whether they are managed by the CLI and whether an update is available.';
15
+ static examples = [
16
+ '<%= config.bin %> <%= command.id %>',
17
+ '<%= config.bin %> <%= command.id %> --json',
18
+ ];
19
+ static flags = {
20
+ json: Flags.boolean({
21
+ description: 'Output the result as JSON',
22
+ default: false,
23
+ }),
24
+ };
25
+ async run() {
26
+ const { flags } = await this.parse(SkillsCheck);
27
+ const status = await inspectSkillsStatus();
28
+ if (flags.json) {
29
+ this.log(JSON.stringify({
30
+ ok: true,
31
+ kind: 'skills',
32
+ workspaceRoot: status.workspaceRoot,
33
+ installed: status.installed,
34
+ managedByNb: status.managedByNb,
35
+ sourcePackage: status.sourcePackage,
36
+ installedSkillNames: status.installedSkillNames,
37
+ installedRef: status.installedRef,
38
+ latestRef: status.latestRef,
39
+ updateAvailable: status.updateAvailable,
40
+ recommendedCommand: status.installed ? 'nb skills update --yes' : 'nb skills install --yes',
41
+ registryError: status.registryError,
42
+ }, null, 2));
43
+ return;
44
+ }
45
+ this.log(renderTable(['Field', 'Value'], [
46
+ ['Workspace', status.workspaceRoot],
47
+ ['Installed', status.installed ? 'yes' : 'no'],
48
+ ['Managed by nb', status.managedByNb ? 'yes' : 'no'],
49
+ ['Installed skills', status.installedSkillNames.length ? status.installedSkillNames.join(', ') : '(none)'],
50
+ ['Update available', status.updateAvailable === null ? 'unknown' : status.updateAvailable ? 'yes' : 'no'],
51
+ ]));
52
+ if (!status.installed) {
53
+ printInfo('Run `nb skills install` to install the NocoBase AI coding skills for this workspace.');
54
+ return;
55
+ }
56
+ if (status.updateAvailable) {
57
+ printInfo('Run `nb skills update` to refresh the NocoBase AI coding skills for this workspace.');
58
+ }
59
+ if (status.registryError) {
60
+ printInfo(`Update check warning: ${status.registryError}`);
61
+ }
62
+ }
63
+ }
@@ -0,0 +1,20 @@
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 { Command, loadHelpClass } from '@oclif/core';
10
+ export default class Skills extends Command {
11
+ static summary = 'Inspect or synchronize NocoBase AI coding skills for this workspace';
12
+ async run() {
13
+ await this.parse(Skills);
14
+ const Help = await loadHelpClass(this.config);
15
+ await new Help(this.config, this.config.pjson.oclif.helpOptions ?? this.config.pjson.helpOptions).showHelp([
16
+ this.id ?? 'skills',
17
+ ...this.argv,
18
+ ]);
19
+ }
20
+ }
@@ -0,0 +1,58 @@
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 { Command, Flags } from '@oclif/core';
10
+ import { confirmAction } from '../../lib/ui.js';
11
+ import { installNocoBaseSkills } from '../../lib/skills-manager.js';
12
+ export default class SkillsInstall extends Command {
13
+ static summary = 'Install the NocoBase AI coding skills for this workspace';
14
+ static description = 'Install the NocoBase AI coding skills for the current workspace. If they are already installed, this command does not update them.';
15
+ static examples = [
16
+ '<%= config.bin %> <%= command.id %>',
17
+ '<%= config.bin %> <%= command.id %> --yes',
18
+ '<%= config.bin %> <%= command.id %> --json',
19
+ ];
20
+ static flags = {
21
+ yes: Flags.boolean({
22
+ char: 'y',
23
+ description: 'Skip the install confirmation prompt',
24
+ default: false,
25
+ }),
26
+ json: Flags.boolean({
27
+ description: 'Output the result as JSON',
28
+ default: false,
29
+ }),
30
+ };
31
+ async run() {
32
+ const { flags } = await this.parse(SkillsInstall);
33
+ if (!flags.yes) {
34
+ const confirmed = await confirmAction('Install the NocoBase AI coding skills for this workspace?', { defaultValue: true });
35
+ if (!confirmed) {
36
+ this.log('Skipped skills install.');
37
+ return;
38
+ }
39
+ }
40
+ const result = await installNocoBaseSkills();
41
+ if (flags.json) {
42
+ this.log(JSON.stringify({
43
+ ok: true,
44
+ kind: 'skills',
45
+ action: result.action,
46
+ workspaceRoot: result.status.workspaceRoot,
47
+ installedSkillNames: result.status.installedSkillNames,
48
+ installedRef: result.status.installedRef,
49
+ }, null, 2));
50
+ return;
51
+ }
52
+ if (result.action === 'noop') {
53
+ this.log('NocoBase AI coding skills are already installed for this workspace. Run `nb skills update` to refresh them.');
54
+ return;
55
+ }
56
+ this.log('Installed the NocoBase AI coding skills for this workspace.');
57
+ }
58
+ }
@@ -0,0 +1,58 @@
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 { Command, Flags } from '@oclif/core';
10
+ import { confirmAction } from '../../lib/ui.js';
11
+ import { updateNocoBaseSkills } from '../../lib/skills-manager.js';
12
+ export default class SkillsUpdate extends Command {
13
+ static summary = 'Update the NocoBase AI coding skills for this workspace';
14
+ static description = 'Refresh the NocoBase AI coding skills for the current workspace. This command only updates an existing nocobase/skills install.';
15
+ static examples = [
16
+ '<%= config.bin %> <%= command.id %>',
17
+ '<%= config.bin %> <%= command.id %> --yes',
18
+ '<%= config.bin %> <%= command.id %> --json',
19
+ ];
20
+ static flags = {
21
+ yes: Flags.boolean({
22
+ char: 'y',
23
+ description: 'Skip the update confirmation prompt',
24
+ default: false,
25
+ }),
26
+ json: Flags.boolean({
27
+ description: 'Output the result as JSON',
28
+ default: false,
29
+ }),
30
+ };
31
+ async run() {
32
+ const { flags } = await this.parse(SkillsUpdate);
33
+ if (!flags.yes) {
34
+ const confirmed = await confirmAction('Update the NocoBase AI coding skills for this workspace?', { defaultValue: true });
35
+ if (!confirmed) {
36
+ this.log('Skipped skills update.');
37
+ return;
38
+ }
39
+ }
40
+ const result = await updateNocoBaseSkills();
41
+ if (flags.json) {
42
+ this.log(JSON.stringify({
43
+ ok: true,
44
+ kind: 'skills',
45
+ action: result.action,
46
+ workspaceRoot: result.status.workspaceRoot,
47
+ installedSkillNames: result.status.installedSkillNames,
48
+ installedRef: result.status.installedRef,
49
+ }, null, 2));
50
+ return;
51
+ }
52
+ if (result.action === 'noop') {
53
+ this.log('NocoBase AI coding skills are already up to date for this workspace.');
54
+ return;
55
+ }
56
+ this.log('Updated the NocoBase AI coding skills for this workspace.');
57
+ }
58
+ }