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

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.
Files changed (74) hide show
  1. package/README.md +41 -49
  2. package/README.zh-CN.md +38 -45
  3. package/bin/run.js +15 -0
  4. package/dist/commands/app/down.js +260 -0
  5. package/dist/commands/app/logs.js +98 -0
  6. package/dist/commands/app/restart.js +75 -0
  7. package/dist/commands/app/start.js +252 -0
  8. package/dist/commands/app/stop.js +98 -0
  9. package/dist/commands/app/upgrade.js +595 -0
  10. package/dist/commands/build.js +3 -48
  11. package/dist/commands/db/shared.js +19 -5
  12. package/dist/commands/dev.js +3 -140
  13. package/dist/commands/down.js +3 -184
  14. package/dist/commands/download.js +4 -856
  15. package/dist/commands/env/add.js +33 -48
  16. package/dist/commands/env/auth.js +6 -13
  17. package/dist/commands/env/info.js +152 -0
  18. package/dist/commands/env/list.js +27 -18
  19. package/dist/commands/env/remove.js +4 -10
  20. package/dist/commands/env/shared.js +158 -0
  21. package/dist/commands/env/update.js +7 -13
  22. package/dist/commands/env/use.js +5 -13
  23. package/dist/commands/{prompts-stages.js → examples/prompts-stages.js} +3 -3
  24. package/dist/commands/{prompts-test.js → examples/prompts-test.js} +3 -3
  25. package/dist/commands/init.js +270 -64
  26. package/dist/commands/install.js +352 -86
  27. package/dist/commands/logs.js +3 -81
  28. package/dist/commands/plugin/disable.js +64 -0
  29. package/dist/commands/plugin/enable.js +64 -0
  30. package/dist/commands/plugin/list.js +62 -0
  31. package/dist/commands/pm/disable.js +3 -54
  32. package/dist/commands/pm/enable.js +3 -54
  33. package/dist/commands/pm/list.js +3 -45
  34. package/dist/commands/restart.js +12 -0
  35. package/dist/commands/scaffold/migration.js +1 -1
  36. package/dist/commands/scaffold/plugin.js +1 -1
  37. package/dist/commands/self/check.js +1 -1
  38. package/dist/commands/self/update.js +13 -3
  39. package/dist/commands/skills/check.js +11 -5
  40. package/dist/commands/skills/index.js +1 -1
  41. package/dist/commands/skills/install.js +20 -7
  42. package/dist/commands/skills/remove.js +71 -0
  43. package/dist/commands/skills/update.js +27 -7
  44. package/dist/commands/source/build.js +58 -0
  45. package/dist/commands/source/dev.js +157 -0
  46. package/dist/commands/source/download.js +866 -0
  47. package/dist/commands/source/test.js +467 -0
  48. package/dist/commands/start.js +3 -202
  49. package/dist/commands/stop.js +3 -81
  50. package/dist/commands/test.js +3 -457
  51. package/dist/commands/upgrade.js +3 -574
  52. package/dist/help/runtime-help.js +3 -0
  53. package/dist/lib/api-client.js +22 -7
  54. package/dist/lib/app-health.js +126 -0
  55. package/dist/lib/app-managed-resources.js +264 -0
  56. package/dist/lib/app-runtime.js +16 -5
  57. package/dist/lib/auth-store.js +162 -43
  58. package/dist/lib/bootstrap.js +13 -12
  59. package/dist/lib/cli-home.js +38 -6
  60. package/dist/lib/cli-locale.js +15 -1
  61. package/dist/lib/env-auth.js +3 -3
  62. package/dist/lib/env-config.js +80 -0
  63. package/dist/lib/generated-command.js +10 -2
  64. package/dist/lib/http-request.js +49 -0
  65. package/dist/lib/prompt-web-ui.js +13 -6
  66. package/dist/lib/resource-command.js +10 -2
  67. package/dist/lib/runtime-generator.js +1 -1
  68. package/dist/lib/self-manager.js +1 -1
  69. package/dist/lib/skills-manager.js +173 -79
  70. package/dist/lib/startup-update.js +203 -0
  71. package/dist/locale/en-US.json +4 -1
  72. package/dist/locale/zh-CN.json +4 -1
  73. package/package.json +27 -4
  74. package/dist/commands/ps.js +0 -116
@@ -0,0 +1,467 @@
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 { Args, Command, Flags } from '@oclif/core';
10
+ import { spawn } from 'node:child_process';
11
+ import fsp from 'node:fs/promises';
12
+ import path from 'node:path';
13
+ import Install from '../install.js';
14
+ import { defaultWorkspaceName } from '../../lib/app-runtime.js';
15
+ import { findAvailableTcpPort, validateAvailableTcpPort } from '../../lib/prompt-validators.js';
16
+ import { commandSucceeds, resolveProjectCwd, run, runNocoBaseCommand } from '../../lib/run-npm.js';
17
+ import { failTask, printInfo, setVerboseMode, startTask, succeedTask } from '../../lib/ui.js';
18
+ const DEFAULT_DB_HOST = '127.0.0.1';
19
+ const DEFAULT_DB_DATABASE = 'nocobase-test';
20
+ const DEFAULT_DB_USER = 'nocobase';
21
+ const DEFAULT_DB_PASSWORD = 'nocobase';
22
+ const DEFAULT_DB_DIALECT = 'postgres';
23
+ const DEFAULT_TEST_TIMEZONE = 'UTC';
24
+ const DEFAULT_TEST_DB_IMAGES = {
25
+ postgres: 'postgres:16',
26
+ mysql: 'mysql:8',
27
+ mariadb: 'mariadb:11',
28
+ kingbase: 'registry.cn-shanghai.aliyuncs.com/nocobase/kingbase:v009r001c001b0030_single_x86',
29
+ };
30
+ const DEFAULT_TEST_DB_DISTRIBUTOR_PORT = '23450';
31
+ const DEFAULT_TEST_DB_DISTRIBUTOR_PREFIX = {
32
+ postgres: 'test',
33
+ mysql: 'test_',
34
+ mariadb: 'test_',
35
+ };
36
+ const DEFAULT_DB_PORTS = {
37
+ postgres: 5433,
38
+ mysql: 3307,
39
+ mariadb: 3307,
40
+ kingbase: 54322,
41
+ };
42
+ const TCP_PORT_READY_SCRIPT = [
43
+ "const net = require('node:net');",
44
+ "const port = Number(process.argv.at(-1));",
45
+ "const socket = net.createConnection({ host: '127.0.0.1', port });",
46
+ "socket.once('connect', () => { socket.end(); process.exit(0); });",
47
+ "socket.once('error', () => process.exit(1));",
48
+ "setTimeout(() => { socket.destroy(); process.exit(1); }, 200).unref();",
49
+ ].join('\n');
50
+ function inferTestEnv(paths) {
51
+ const first = String(paths[0] ?? '').trim();
52
+ if (!first) {
53
+ return undefined;
54
+ }
55
+ const normalized = first.split('\\').join('/');
56
+ if (normalized.includes('/client/')
57
+ || normalized.includes('/client-v2/')
58
+ || normalized.includes('/flow-engine/')) {
59
+ return 'client-side';
60
+ }
61
+ return 'server-side';
62
+ }
63
+ function trimValue(value) {
64
+ return String(value ?? '').trim();
65
+ }
66
+ function resolveWorkspaceName(cwd) {
67
+ return defaultWorkspaceName(cwd);
68
+ }
69
+ function defaultTestDbPort(dbDialect) {
70
+ return String(DEFAULT_DB_PORTS[dbDialect] ?? DEFAULT_DB_PORTS.postgres);
71
+ }
72
+ function defaultTestDbImage(dbDialect) {
73
+ return DEFAULT_TEST_DB_IMAGES[dbDialect] ?? DEFAULT_TEST_DB_IMAGES.postgres;
74
+ }
75
+ function delay(ms) {
76
+ return new Promise((resolve) => {
77
+ setTimeout(resolve, ms);
78
+ });
79
+ }
80
+ function shouldRunServerTests(params) {
81
+ if (params.server) {
82
+ return true;
83
+ }
84
+ if (params.client) {
85
+ return false;
86
+ }
87
+ return inferTestEnv(params.paths) !== 'client-side';
88
+ }
89
+ function defaultTestDbDistributorPrefix(dbDialect) {
90
+ return DEFAULT_TEST_DB_DISTRIBUTOR_PREFIX[dbDialect];
91
+ }
92
+ function supportsTestDbDistributor(dbDialect) {
93
+ return Boolean(defaultTestDbDistributorPrefix(dbDialect));
94
+ }
95
+ function buildTestDbDistributorEnv(env) {
96
+ if (env.DB_DIALECT === 'mysql' || env.DB_DIALECT === 'mariadb') {
97
+ return {
98
+ ...env,
99
+ DB_APP_USER: env.DB_USER,
100
+ DB_USER: 'root',
101
+ };
102
+ }
103
+ return env;
104
+ }
105
+ async function waitForTcpPortReady(port, timeoutMs = 5000) {
106
+ const deadline = Date.now() + timeoutMs;
107
+ while (Date.now() < deadline) {
108
+ if (await commandSucceeds(process.execPath, ['-e', TCP_PORT_READY_SCRIPT, port])) {
109
+ return;
110
+ }
111
+ await delay(100);
112
+ }
113
+ throw new Error(`Timed out while waiting for the test DB distributor on 127.0.0.1:${port}.`);
114
+ }
115
+ async function stopBackgroundProcess(child) {
116
+ if (child.exitCode !== null || child.killed) {
117
+ return;
118
+ }
119
+ await new Promise((resolve) => {
120
+ const finish = () => {
121
+ clearTimeout(timeout);
122
+ resolve();
123
+ };
124
+ const timeout = setTimeout(finish, 1000);
125
+ child.once('close', finish);
126
+ try {
127
+ child.kill();
128
+ }
129
+ catch {
130
+ finish();
131
+ }
132
+ });
133
+ }
134
+ async function startTestDbDistributor(params) {
135
+ const port = DEFAULT_TEST_DB_DISTRIBUTOR_PORT;
136
+ const prefix = defaultTestDbDistributorPrefix(params.env.DB_DIALECT);
137
+ if (!prefix) {
138
+ throw new Error(`The ${params.env.DB_DIALECT} test DB distributor is not supported.`);
139
+ }
140
+ const portError = await validateAvailableTcpPort(port);
141
+ if (portError) {
142
+ throw new Error(`Host port ${port} is unavailable for the test DB distributor. ${portError}`);
143
+ }
144
+ const distributorEnv = buildTestDbDistributorEnv(params.env);
145
+ const child = spawn(process.execPath, [
146
+ path.resolve(params.cwd, 'node_modules', 'tsx', 'dist', 'cli.mjs'),
147
+ path.resolve(params.cwd, 'packages', 'core', 'test', 'src', 'scripts', 'test-db-creator.ts'),
148
+ ], {
149
+ cwd: params.cwd,
150
+ env: {
151
+ ...process.env,
152
+ ...distributorEnv,
153
+ DB_TEST_DISTRIBUTOR_PORT: port,
154
+ DB_TEST_PREFIX: prefix,
155
+ },
156
+ stdio: params.stdio,
157
+ windowsHide: process.platform === 'win32',
158
+ });
159
+ let childError;
160
+ child.once('error', (error) => {
161
+ childError = error;
162
+ });
163
+ child.once('close', (code, signal) => {
164
+ if (code === 0) {
165
+ return;
166
+ }
167
+ childError = childError ?? new Error(signal
168
+ ? `test DB distributor exited due to signal ${signal}`
169
+ : `test DB distributor exited with code ${code ?? 'unknown'}`);
170
+ });
171
+ try {
172
+ await waitForTcpPortReady(port);
173
+ }
174
+ catch (error) {
175
+ await stopBackgroundProcess(child);
176
+ throw childError ?? error;
177
+ }
178
+ return {
179
+ port,
180
+ prefix,
181
+ stop: async () => {
182
+ await stopBackgroundProcess(child);
183
+ },
184
+ };
185
+ }
186
+ async function ensureDockerNetwork(networkName, options) {
187
+ if (await commandSucceeds('docker', ['network', 'inspect', networkName])) {
188
+ return;
189
+ }
190
+ await run('docker', ['network', 'create', networkName], {
191
+ errorName: 'docker network create',
192
+ stdio: options?.stdio ?? 'ignore',
193
+ });
194
+ }
195
+ async function removeDockerContainerIfExists(containerName, options) {
196
+ if (!(await commandSucceeds('docker', ['container', 'inspect', containerName]))) {
197
+ return 'missing';
198
+ }
199
+ await run('docker', ['rm', '-f', containerName], {
200
+ errorName: 'docker rm',
201
+ stdio: options?.stdio ?? 'ignore',
202
+ });
203
+ return 'removed';
204
+ }
205
+ function formatDbBootstrapFailure(message) {
206
+ return [
207
+ 'Could not prepare the built-in test database.',
208
+ 'The CLI was not able to recreate a clean Docker database for this test run.',
209
+ 'Check Docker status, the selected port, and local storage permissions, then try again.',
210
+ `Details: ${message}`,
211
+ ].join('\n');
212
+ }
213
+ function buildTestDbConfig(params) {
214
+ const dbDialect = trimValue(params.dbDialect) || DEFAULT_DB_DIALECT;
215
+ const workspaceName = resolveWorkspaceName(params.cwd);
216
+ const storagePath = path.join(params.cwd, 'storage', 'test');
217
+ const plan = Install.buildBuiltinDbPlan({
218
+ envName: 'test',
219
+ workspaceName,
220
+ storagePath,
221
+ source: 'test',
222
+ dbDialect,
223
+ dbHost: DEFAULT_DB_HOST,
224
+ dbPort: trimValue(params.dbPort) || defaultTestDbPort(dbDialect),
225
+ dbDatabase: trimValue(params.dbDatabase) || DEFAULT_DB_DATABASE,
226
+ dbUser: trimValue(params.dbUser) || DEFAULT_DB_USER,
227
+ dbPassword: trimValue(params.dbPassword) || DEFAULT_DB_PASSWORD,
228
+ builtinDbImage: trimValue(params.builtinDbImage) || defaultTestDbImage(dbDialect),
229
+ });
230
+ return {
231
+ storagePath,
232
+ containerName: plan.containerName,
233
+ networkName: plan.networkName,
234
+ dataDir: plan.dataDir,
235
+ args: plan.args,
236
+ env: {
237
+ APP_ENV_PATH: '.env',
238
+ STORAGE_PATH: storagePath,
239
+ TZ: DEFAULT_TEST_TIMEZONE,
240
+ DB_DIALECT: plan.dbDialect,
241
+ DB_HOST: plan.dbHost,
242
+ DB_PORT: plan.dbPort,
243
+ DB_DATABASE: plan.dbDatabase,
244
+ DB_USER: plan.dbUser,
245
+ DB_PASSWORD: plan.dbPassword,
246
+ },
247
+ };
248
+ }
249
+ async function prepareTestDatabase(config, options) {
250
+ let nextConfig = config;
251
+ await ensureDockerNetwork(nextConfig.networkName, {
252
+ stdio: options?.stdio,
253
+ });
254
+ await removeDockerContainerIfExists(nextConfig.containerName, {
255
+ stdio: options?.stdio,
256
+ });
257
+ await fsp.rm(nextConfig.storagePath, { recursive: true, force: true });
258
+ const portError = await validateAvailableTcpPort(nextConfig.env.DB_PORT);
259
+ if (portError) {
260
+ if (options?.dbPortExplicit) {
261
+ throw new Error(`Host port ${nextConfig.env.DB_PORT} is unavailable. ${portError}`);
262
+ }
263
+ const fallbackPort = await findAvailableTcpPort();
264
+ printInfo(`Host port ${nextConfig.env.DB_PORT} is unavailable for the test database, so the CLI will use ${fallbackPort} instead.`);
265
+ nextConfig = buildTestDbConfig({
266
+ cwd: path.dirname(path.dirname(nextConfig.storagePath)),
267
+ dbDialect: nextConfig.env.DB_DIALECT,
268
+ dbPort: fallbackPort,
269
+ dbDatabase: nextConfig.env.DB_DATABASE,
270
+ dbUser: nextConfig.env.DB_USER,
271
+ dbPassword: nextConfig.env.DB_PASSWORD,
272
+ builtinDbImage: undefined,
273
+ });
274
+ }
275
+ await fsp.mkdir(nextConfig.dataDir, { recursive: true });
276
+ await run('docker', nextConfig.args, {
277
+ errorName: 'docker run',
278
+ stdio: options?.stdio ?? 'ignore',
279
+ });
280
+ await waitForTcpPortReady(nextConfig.env.DB_PORT, 30_000);
281
+ return nextConfig;
282
+ }
283
+ export default class SourceTest extends Command {
284
+ static hidden = false;
285
+ static args = {
286
+ paths: Args.string({
287
+ description: 'test file paths or globs to pass through',
288
+ multiple: true,
289
+ required: false,
290
+ }),
291
+ };
292
+ static description = 'Run project tests from the selected app directory. Before running tests, the CLI recreates a built-in Docker test database and injects `DB_*` values internally.';
293
+ static examples = [
294
+ '<%= config.bin %> <%= command.id %>',
295
+ '<%= config.bin %> <%= command.id %> --cwd /path/to/app',
296
+ '<%= config.bin %> <%= command.id %> packages/core/server/src/__tests__/foo.test.ts',
297
+ '<%= config.bin %> <%= command.id %> --server --coverage',
298
+ '<%= config.bin %> <%= command.id %> --db-port 5433',
299
+ ];
300
+ static flags = {
301
+ cwd: Flags.string({
302
+ char: 'c',
303
+ description: 'App directory to run tests from. Defaults to the current working directory',
304
+ required: false,
305
+ }),
306
+ watch: Flags.boolean({
307
+ char: 'w',
308
+ description: 'Run Vitest in watch mode',
309
+ default: false,
310
+ }),
311
+ run: Flags.boolean({
312
+ description: 'Run once without watch mode',
313
+ default: false,
314
+ }),
315
+ allowOnly: Flags.boolean({
316
+ description: 'Allow `.only` tests',
317
+ default: false,
318
+ }),
319
+ bail: Flags.boolean({
320
+ description: 'Stop after the first failure',
321
+ default: false,
322
+ }),
323
+ coverage: Flags.boolean({
324
+ description: 'Enable coverage reporting',
325
+ default: false,
326
+ }),
327
+ 'single-thread': Flags.string({
328
+ description: 'Forward single-thread mode to the underlying test runner',
329
+ required: false,
330
+ }),
331
+ server: Flags.boolean({
332
+ description: 'Force server-side test mode',
333
+ default: false,
334
+ }),
335
+ client: Flags.boolean({
336
+ description: 'Force client-side test mode',
337
+ default: false,
338
+ }),
339
+ 'db-clean': Flags.boolean({
340
+ char: 'd',
341
+ description: 'Clean the database before tests when supported by the underlying app command',
342
+ default: false,
343
+ }),
344
+ 'db-dialect': Flags.string({
345
+ description: 'Built-in test database dialect to start',
346
+ options: ['postgres', 'mysql', 'mariadb', 'kingbase'],
347
+ required: false,
348
+ }),
349
+ 'db-image': Flags.string({
350
+ description: 'Built-in test database Docker image to start',
351
+ aliases: ['builtin-db-image'],
352
+ required: false,
353
+ }),
354
+ 'db-port': Flags.string({
355
+ description: 'Host TCP port to publish for the built-in test database',
356
+ required: false,
357
+ }),
358
+ 'db-database': Flags.string({
359
+ description: 'Database name to inject for tests',
360
+ required: false,
361
+ }),
362
+ 'db-user': Flags.string({
363
+ description: 'Database user to inject for tests',
364
+ required: false,
365
+ }),
366
+ 'db-password': Flags.string({
367
+ description: 'Database password to inject for tests',
368
+ required: false,
369
+ }),
370
+ verbose: Flags.boolean({
371
+ description: 'Show raw Docker and test runner output',
372
+ default: false,
373
+ }),
374
+ };
375
+ async run() {
376
+ const { args, flags } = await this.parse(SourceTest);
377
+ setVerboseMode(flags.verbose);
378
+ if (flags.server && flags.client) {
379
+ this.error('Cannot use `--server` and `--client` together.');
380
+ }
381
+ const cwd = resolveProjectCwd(flags.cwd);
382
+ const commandArgs = ['test', ...(args.paths ?? [])];
383
+ if (flags.watch) {
384
+ commandArgs.push('--watch');
385
+ }
386
+ if (flags.run || !flags.watch) {
387
+ commandArgs.push('--run');
388
+ }
389
+ if (flags.allowOnly) {
390
+ commandArgs.push('--allowOnly');
391
+ }
392
+ if (flags.bail) {
393
+ commandArgs.push('--bail');
394
+ }
395
+ if (flags.coverage) {
396
+ commandArgs.push('--coverage');
397
+ }
398
+ if (flags.server) {
399
+ commandArgs.push('--server');
400
+ }
401
+ else if (flags.client) {
402
+ commandArgs.push('--client');
403
+ }
404
+ if (flags['db-clean']) {
405
+ commandArgs.push('--db-clean');
406
+ }
407
+ if (flags['single-thread'] !== undefined) {
408
+ commandArgs.push(`--single-thread=${flags['single-thread']}`);
409
+ }
410
+ else if (!flags.client && !flags.server && inferTestEnv(args.paths ?? []) === 'server-side') {
411
+ commandArgs.push('--single-thread=true');
412
+ }
413
+ startTask('Recreating the built-in test database...');
414
+ let testDbConfig;
415
+ let testDbDistributor;
416
+ try {
417
+ testDbConfig = await prepareTestDatabase(buildTestDbConfig({
418
+ cwd,
419
+ dbDialect: flags['db-dialect'],
420
+ builtinDbImage: flags['db-image'],
421
+ dbPort: flags['db-port'],
422
+ dbDatabase: flags['db-database'],
423
+ dbUser: flags['db-user'],
424
+ dbPassword: flags['db-password'],
425
+ }), {
426
+ stdio: flags.verbose ? 'inherit' : 'ignore',
427
+ dbPortExplicit: Boolean(flags['db-port']),
428
+ });
429
+ if (shouldRunServerTests({
430
+ server: flags.server,
431
+ client: flags.client,
432
+ paths: args.paths ?? [],
433
+ })
434
+ && supportsTestDbDistributor(testDbConfig.env.DB_DIALECT)) {
435
+ testDbDistributor = await startTestDbDistributor({
436
+ cwd,
437
+ env: testDbConfig.env,
438
+ stdio: flags.verbose ? 'inherit' : 'ignore',
439
+ });
440
+ testDbConfig.env.DB_TEST_DISTRIBUTOR_PORT = testDbDistributor.port;
441
+ testDbConfig.env.DB_TEST_PREFIX = testDbDistributor.prefix;
442
+ }
443
+ succeedTask(`The built-in test database is ready at ${testDbConfig.env.DB_HOST}:${testDbConfig.env.DB_PORT}.`);
444
+ printInfo(`Test DB settings: DB_DIALECT=${testDbConfig.env.DB_DIALECT} DB_HOST=${testDbConfig.env.DB_HOST} DB_PORT=${testDbConfig.env.DB_PORT} DB_DATABASE=${testDbConfig.env.DB_DATABASE} DB_USER=${testDbConfig.env.DB_USER}`);
445
+ }
446
+ catch (error) {
447
+ const message = error instanceof Error ? error.message : String(error);
448
+ failTask('Failed to recreate the built-in test database.');
449
+ this.error(formatDbBootstrapFailure(message));
450
+ return;
451
+ }
452
+ try {
453
+ await runNocoBaseCommand(commandArgs, {
454
+ cwd,
455
+ stdio: flags.verbose ? 'inherit' : 'ignore',
456
+ env: testDbConfig.env,
457
+ });
458
+ }
459
+ catch (error) {
460
+ const message = error instanceof Error ? error.message : String(error);
461
+ this.error(message);
462
+ }
463
+ finally {
464
+ await testDbDistributor?.stop();
465
+ }
466
+ }
467
+ }
@@ -6,206 +6,7 @@
6
6
  * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
- import { Command, Flags } from '@oclif/core';
10
- import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runLocalNocoBaseCommand, startDockerContainer, } from '../lib/app-runtime.js';
11
- import { failTask, printInfo, startTask, succeedTask } from '../lib/ui.js';
12
- function argvHasToken(argv, tokens) {
13
- return tokens.some((token) => argv.includes(token));
14
- }
15
- function formatDockerStartFailure(envName, message) {
16
- if (/does not exist/i.test(message)) {
17
- return [
18
- `Can't start NocoBase for "${envName}" yet.`,
19
- 'The saved Docker app for this env could not be found on this machine.',
20
- `Try reinstalling the env, or check whether the container was removed outside the CLI.`,
21
- `Details: ${message}`,
22
- ].join('\n');
23
- }
24
- return [
25
- `Couldn't start NocoBase for "${envName}".`,
26
- 'Check that the Docker runtime for this env is still available, then try again.',
27
- `Details: ${message}`,
28
- ].join('\n');
29
- }
30
- function formatLocalStartFailure(envName, options) {
31
- const sourceLabel = options?.source === 'git'
32
- ? 'the local Git checkout'
33
- : options?.source === 'npm'
34
- ? 'the local npm app'
35
- : 'the local app';
36
- const portHint = options?.port ? ` Expected app port: ${options.port}.` : '';
37
- return [
38
- `Couldn't start NocoBase for "${envName}".`,
39
- `The CLI was not able to start ${sourceLabel} successfully.`,
40
- `Check that the app dependencies, database connection, and local env settings are ready, then try again.${portHint}`,
41
- ].join('\n');
42
- }
43
- function formatAppUrl(port) {
44
- const value = String(port ?? '').trim();
45
- if (!value) {
46
- return undefined;
47
- }
48
- return `http://127.0.0.1:${value}`;
49
- }
50
- async function isAppAlreadyRunning(appUrl) {
51
- if (!appUrl) {
52
- return false;
53
- }
54
- const controller = new AbortController();
55
- const timeout = setTimeout(() => controller.abort(), 1500);
56
- try {
57
- const response = await fetch(`${appUrl}/api/__health_check`, {
58
- signal: controller.signal,
59
- });
60
- const text = await response.text();
61
- return response.ok && text.trim().toLowerCase() === 'ok';
62
- }
63
- catch (_error) {
64
- return false;
65
- }
66
- finally {
67
- clearTimeout(timeout);
68
- }
69
- }
70
- export default class Start extends Command {
71
- static description = 'Start NocoBase for the selected env. Local npm/git installs run the app command, and Docker installs start the saved app container.';
72
- static examples = [
73
- '<%= config.bin %> <%= command.id %>',
74
- '<%= config.bin %> <%= command.id %> --env local',
75
- '<%= config.bin %> <%= command.id %> --env local --quickstart',
76
- '<%= config.bin %> <%= command.id %> --env local --port 12000',
77
- '<%= config.bin %> <%= command.id %> --env local --daemon',
78
- '<%= config.bin %> <%= command.id %> --env local --no-daemon',
79
- '<%= config.bin %> <%= command.id %> --env local --instances 2',
80
- '<%= config.bin %> <%= command.id %> --env local --launch-mode pm2',
81
- '<%= config.bin %> <%= command.id %> --env local --verbose',
82
- '<%= config.bin %> <%= command.id %> --env local-docker',
83
- ];
84
- static flags = {
85
- env: Flags.string({
86
- char: 'e',
87
- description: 'CLI env name to start. Defaults to the current env when omitted',
88
- }),
89
- quickstart: Flags.boolean({ description: 'Quickstart the application', required: false }),
90
- port: Flags.string({ description: 'Port (overrides appPort from env config when set)', char: 'p', required: false }),
91
- daemon: Flags.boolean({
92
- description: 'Run the application as a daemon (default: true; use --no-daemon to stay in the foreground)',
93
- char: 'd',
94
- required: false,
95
- default: true,
96
- allowNo: true,
97
- }),
98
- instances: Flags.integer({ description: 'Number of instances to run', char: 'i', required: false }),
99
- 'launch-mode': Flags.string({ description: 'Launch Mode', required: false, options: ['pm2', 'node'] }),
100
- verbose: Flags.boolean({
101
- description: 'Show raw startup output from the underlying local or Docker command',
102
- default: false,
103
- }),
104
- };
105
- async run() {
106
- const { flags } = await this.parse(Start);
107
- const requestedEnv = flags.env?.trim() || undefined;
108
- const daemonFlagWasProvided = argvHasToken(this.argv, ['--daemon', '--no-daemon']);
109
- const runtime = await resolveManagedAppRuntime(requestedEnv);
110
- const commandStdio = flags.verbose ? 'inherit' : 'ignore';
111
- if (!runtime) {
112
- this.error(formatMissingManagedAppEnvMessage(requestedEnv));
113
- }
114
- if (runtime.kind === 'remote') {
115
- this.error([
116
- `Can't start "${runtime.envName}" from this machine.`,
117
- 'This env only has an API connection, so there is no saved local app or Docker runtime to launch here.',
118
- 'Connect it to a local checkout or reinstall it with npm, git, or Docker if you want CLI-managed start and stop.',
119
- ].join('\n'));
120
- }
121
- if (runtime.kind === 'docker') {
122
- const unsupportedFlags = [
123
- flags.quickstart ? '--quickstart' : undefined,
124
- flags.port ? '--port' : undefined,
125
- daemonFlagWasProvided ? (flags.daemon ? '--daemon' : '--no-daemon') : undefined,
126
- flags.instances !== undefined ? '--instances' : undefined,
127
- flags['launch-mode'] ? '--launch-mode' : undefined,
128
- ].filter(Boolean);
129
- if (unsupportedFlags.length > 0) {
130
- this.error([
131
- `Can't apply ${unsupportedFlags.join(', ')} to "${runtime.envName}".`,
132
- 'This env is managed by Docker, so those options are only available for local npm/git installs.',
133
- `Run \`nb start --env ${runtime.envName}\` to start the saved container, or recreate the env if you need different runtime settings.`,
134
- ].join('\n'));
135
- }
136
- const appUrl = formatAppUrl(runtime.env.appPort === undefined || runtime.env.appPort === null
137
- ? undefined
138
- : String(runtime.env.appPort));
139
- startTask(`Starting NocoBase for "${runtime.envName}"...`);
140
- try {
141
- const state = await startDockerContainer(runtime.containerName, {
142
- stdio: commandStdio,
143
- });
144
- succeedTask(state === 'already-running'
145
- ? `NocoBase is already running for "${runtime.envName}"${appUrl ? ` at ${appUrl}` : ''}.`
146
- : `NocoBase is running for "${runtime.envName}"${appUrl ? ` at ${appUrl}` : ''}.`);
147
- }
148
- catch (error) {
149
- const message = error instanceof Error ? error.message : String(error);
150
- failTask(`Failed to start NocoBase for "${runtime.envName}".`);
151
- this.error(formatDockerStartFailure(runtime.envName, message));
152
- }
153
- return;
154
- }
155
- const npmArgs = ['start'];
156
- if (flags.quickstart) {
157
- npmArgs.push('--quickstart');
158
- }
159
- if (flags.port) {
160
- npmArgs.push('--port', flags.port);
161
- }
162
- else if (runtime.env.appPort !== undefined && runtime.env.appPort !== null && String(runtime.env.appPort).trim() !== '') {
163
- npmArgs.push('--port', String(runtime.env.appPort));
164
- }
165
- if (flags.daemon !== false) {
166
- npmArgs.push('--daemon');
167
- }
168
- if (flags.instances !== undefined) {
169
- npmArgs.push('--instances', flags.instances.toString());
170
- }
171
- if (flags['launch-mode']) {
172
- npmArgs.push('--launch-mode', flags['launch-mode']);
173
- }
174
- const appUrl = formatAppUrl(flags.port
175
- || (runtime.env.appPort !== undefined && runtime.env.appPort !== null
176
- ? String(runtime.env.appPort).trim()
177
- : undefined));
178
- if (await isAppAlreadyRunning(appUrl)) {
179
- if (flags.daemon === false) {
180
- printInfo(`NocoBase is already running for "${runtime.envName}"${appUrl ? ` at ${appUrl}` : ''}. Use \`nb stop --env ${runtime.envName}\` before starting it again in the foreground.`);
181
- }
182
- else {
183
- succeedTask(`NocoBase is already running for "${runtime.envName}"${appUrl ? ` at ${appUrl}` : ''}.`);
184
- }
185
- return;
186
- }
187
- if (flags.daemon === false) {
188
- printInfo(`Starting NocoBase for "${runtime.envName}" in the foreground${appUrl ? ` at ${appUrl}` : ''}. Press Ctrl+C to stop.`);
189
- }
190
- else {
191
- startTask(`Starting NocoBase for "${runtime.envName}" in the background...`);
192
- }
193
- try {
194
- await runLocalNocoBaseCommand(runtime, npmArgs, {
195
- stdio: commandStdio,
196
- });
197
- if (flags.daemon !== false) {
198
- succeedTask(`NocoBase is starting for "${runtime.envName}"${appUrl ? ` at ${appUrl}` : ''}.`);
199
- }
200
- }
201
- catch (error) {
202
- failTask(`Failed to start NocoBase for "${runtime.envName}".`);
203
- this.error(formatLocalStartFailure(runtime.envName, {
204
- port: flags.port || (runtime.env.appPort !== undefined && runtime.env.appPort !== null
205
- ? String(runtime.env.appPort).trim()
206
- : undefined),
207
- source: runtime.source,
208
- }));
209
- }
210
- }
9
+ import AppStart from './app/start.js';
10
+ export default class Start extends AppStart {
11
+ static hidden = true;
211
12
  }