@nocobase/cli 2.1.0-alpha.20 → 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.
Files changed (42) hide show
  1. package/README.md +256 -89
  2. package/README.zh-CN.md +332 -0
  3. package/bin/run.js +21 -2
  4. package/dist/commands/build.js +7 -1
  5. package/dist/commands/db/logs.js +85 -0
  6. package/dist/commands/db/ps.js +60 -0
  7. package/dist/commands/db/shared.js +81 -0
  8. package/dist/commands/db/start.js +55 -7
  9. package/dist/commands/db/stop.js +70 -0
  10. package/dist/commands/dev.js +112 -21
  11. package/dist/commands/down.js +193 -0
  12. package/dist/commands/download.js +633 -183
  13. package/dist/commands/env/add.js +260 -131
  14. package/dist/commands/env/auth.js +9 -8
  15. package/dist/commands/init.js +723 -103
  16. package/dist/commands/install.js +1702 -565
  17. package/dist/commands/logs.js +90 -0
  18. package/dist/commands/pm/disable.js +35 -3
  19. package/dist/commands/pm/enable.js +35 -3
  20. package/dist/commands/pm/list.js +37 -4
  21. package/dist/commands/prompts-stages.js +150 -0
  22. package/dist/commands/prompts-test.js +181 -0
  23. package/dist/commands/ps.js +116 -0
  24. package/dist/commands/start.js +171 -15
  25. package/dist/commands/stop.js +90 -0
  26. package/dist/commands/upgrade.js +559 -11
  27. package/dist/lib/api-client.js +49 -5
  28. package/dist/lib/app-runtime.js +142 -0
  29. package/dist/lib/auth-store.js +44 -3
  30. package/dist/lib/bootstrap.js +7 -3
  31. package/dist/lib/cli-locale.js +115 -0
  32. package/dist/lib/env-auth.js +427 -82
  33. package/dist/lib/prompt-catalog.js +574 -0
  34. package/dist/lib/prompt-validators.js +185 -0
  35. package/dist/lib/prompt-web-ui.js +2061 -0
  36. package/dist/lib/run-npm.js +71 -7
  37. package/dist/lib/runtime-generator.js +12 -1
  38. package/dist/locale/en-US.json +282 -0
  39. package/dist/locale/zh-CN.json +282 -0
  40. package/package.json +5 -4
  41. package/dist/commands/restart.js +0 -32
  42. package/dist/lib/init-browser-wizard.js +0 -431
@@ -11,279 +11,729 @@ import { Command, Flags } from '@oclif/core';
11
11
  import * as p from '@clack/prompts';
12
12
  import path from 'node:path';
13
13
  import { stdin as stdinStream, stdout as stdoutStream } from 'node:process';
14
+ import { runPromptCatalog, } from "../lib/prompt-catalog.js";
15
+ import { applyCliLocale, CLI_LOCALE_FLAG_DESCRIPTION, CLI_LOCALE_FLAG_OPTIONS, localeText, } from "../lib/cli-locale.js";
14
16
  import { run } from "../lib/run-npm.js";
17
+ import { printVerbose, setVerboseMode, startTask, stopTask, updateTask } from '../lib/ui.js';
18
+ const DEFAULT_DOCKER_REGISTRY = 'nocobase/nocobase';
19
+ const DEFAULT_DOCKER_REGISTRY_ZH_CN = 'registry.cn-shanghai.aliyuncs.com/nocobase/nocobase';
20
+ const DEFAULT_DOCKER_PLATFORM = 'auto';
21
+ const downloadText = (key, values) => localeText(`commands.download.${key}`, values);
22
+ function defaultOutputDirForVersion(versionTag) {
23
+ const safe = versionTag.replace(/[/\\]/g, '-');
24
+ return `./nocobase-${safe}`;
25
+ }
26
+ async function pathExists(target) {
27
+ try {
28
+ await fsp.access(target);
29
+ return true;
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ }
35
+ export function defaultDockerRegistryForLang(lang) {
36
+ return String(lang ?? '').trim() === 'zh-CN'
37
+ ? DEFAULT_DOCKER_REGISTRY_ZH_CN
38
+ : DEFAULT_DOCKER_REGISTRY;
39
+ }
40
+ function argvHasToken(argv, tokens) {
41
+ return tokens.some((t) => argv.includes(t));
42
+ }
43
+ function gitRefForVersion(versionSpec) {
44
+ const versionToRef = {
45
+ latest: 'main',
46
+ beta: 'next',
47
+ alpha: 'develop',
48
+ };
49
+ return versionToRef[versionSpec] || versionSpec;
50
+ }
51
+ /** `build-dts` only applies when `build` is true and source is npm/git. */
52
+ function normalizeBuildDts(source, build, wantDts) {
53
+ if (source !== 'npm' && source !== 'git') {
54
+ return false;
55
+ }
56
+ return Boolean(build && wantDts);
57
+ }
58
+ function downloadSourceLabel(source) {
59
+ switch (source) {
60
+ case 'docker':
61
+ return 'Docker image';
62
+ case 'npm':
63
+ return 'npm package';
64
+ case 'git':
65
+ return 'Git repository';
66
+ default:
67
+ return source;
68
+ }
69
+ }
70
+ function normalizeDockerPlatform(value) {
71
+ const text = String(value ?? '').trim();
72
+ if (text === 'linux/amd64' || text === 'linux/arm64') {
73
+ return text;
74
+ }
75
+ return DEFAULT_DOCKER_PLATFORM;
76
+ }
77
+ function dockerPlatformArg(value) {
78
+ const platform = normalizeDockerPlatform(value);
79
+ if (platform === 'auto') {
80
+ return undefined;
81
+ }
82
+ return platform;
83
+ }
84
+ const EXTERNAL_COMMAND_LOADING_DELAY_MS = 8_000;
85
+ const EXTERNAL_COMMAND_LOADING_UPDATE_MS = 15_000;
15
86
  export default class Download extends Command {
16
- static description = 'Scaffold or fetch NocoBase: npm (create-nocobase-app), docker (image pull), or git (shallow clone).';
87
+ _flags;
88
+ preparationTaskActive = false;
89
+ static description = 'Scaffold or fetch NocoBase from npm, Docker, or Git. `--version` is the shared version input: package version for npm, image tag for Docker, and git ref for Git.';
17
90
  static examples = [
18
91
  '<%= config.bin %> <%= command.id %>',
19
- '<%= config.bin %> <%= command.id %> -y --source npm --version latest',
20
- '<%= config.bin %> <%= command.id %> --source npm --version latest',
21
- '<%= config.bin %> <%= command.id %> --source npm --version latest --output-dir=./app',
22
- '<%= config.bin %> <%= command.id %> --source docker --version latest --docker-registry=nocobase/nocobase',
23
- '<%= config.bin %> <%= command.id %> --source git --version latest --git-url=https://github.com/nocobase/nocobase.git',
92
+ '<%= config.bin %> <%= command.id %> -y --source npm --version alpha',
93
+ '<%= config.bin %> <%= command.id %> -y --source npm --version alpha --no-build',
94
+ '<%= config.bin %> <%= command.id %> --source npm --version alpha',
95
+ '<%= config.bin %> <%= command.id %> --source npm --version alpha --output-dir=./app',
96
+ '<%= config.bin %> <%= command.id %> --source docker --version alpha --docker-registry=nocobase/nocobase --docker-platform=linux/arm64',
97
+ '<%= config.bin %> <%= command.id %> -y --source docker --version alpha --docker-save -o ./docker-images',
98
+ '<%= config.bin %> <%= command.id %> --source git --version alpha --git-url=git@github.com:nocobase/nocobase.git',
99
+ '<%= config.bin %> <%= command.id %> --source git --version fix/cli-v2',
100
+ '<%= config.bin %> <%= command.id %> -y --source git --version fix/cli-v2 --no-build',
101
+ '<%= config.bin %> <%= command.id %> -y --source npm --version alpha --build-dts',
102
+ '<%= config.bin %> <%= command.id %> -y --source npm --version alpha --npm-registry=https://registry.npmmirror.com',
24
103
  ];
25
104
  static flags = {
26
105
  yes: Flags.boolean({
27
106
  char: 'y',
28
- description: 'Skip interactive prompts; use flags only (non-TTY implies -y)',
107
+ description: 'Use defaults and skip interactive prompts.',
108
+ default: false,
109
+ }),
110
+ verbose: Flags.boolean({
111
+ description: 'Show detailed command output',
112
+ default: false,
113
+ }),
114
+ locale: Flags.string({
115
+ description: CLI_LOCALE_FLAG_DESCRIPTION,
116
+ options: CLI_LOCALE_FLAG_OPTIONS,
117
+ }),
118
+ 'no-intro': Flags.boolean({
119
+ hidden: true,
120
+ description: 'Skip command intro when invoked by another CLI command',
29
121
  default: false,
30
122
  }),
31
123
  source: Flags.string({
32
124
  char: 's',
33
- description: 'Distribution: npm runs create-nocobase-app, docker runs docker pull, git clones the repository',
125
+ description: 'How to get NocoBase: Docker image, npm package, or Git repository.',
34
126
  options: ['docker', 'npm', 'git'],
35
127
  required: false,
36
128
  }),
37
129
  version: Flags.string({
38
130
  char: 'v',
39
- description: 'npm: dist-tag or version for create-nocobase-app; docker: image tag; git: branch or tag (latest→main, beta→next, alpha→develop); default: latest',
131
+ description: 'Shared version input. For npm this is the package version, for Docker the image tag, and for Git a git ref such as a branch name (for example: alpha, beta, latest, fix/cli-v2).',
40
132
  }),
41
133
  replace: Flags.boolean({
42
134
  char: 'r',
43
- description: 'npm/git: delete the target project directory if it exists, then scaffold or clone again; docker: ignored',
135
+ description: 'Replace the target directory if it already exists.',
44
136
  default: false,
45
137
  }),
46
- 'dev': Flags.boolean({
47
- description: 'npm: install devDependencies in create-nocobase-app and run a non-production yarn install; git/docker: ignored',
138
+ 'dev-dependencies': Flags.boolean({
139
+ char: 'D',
140
+ allowNo: true,
141
+ description: 'Install development dependencies for npm/git source installs.',
48
142
  default: false,
49
143
  }),
50
144
  'output-dir': Flags.string({
51
145
  char: 'o',
52
- description: 'npm/git: output directory (relative to cwd); default ./nocobase-<version> using the same value as --version; docker: ignored',
146
+ description: 'Download target directory, or Docker tarball directory when --docker-save is enabled.',
53
147
  }),
54
148
  'git-url': Flags.string({
55
- description: 'git: remote URL to clone (default: https://github.com/nocobase/nocobase.git)',
149
+ description: 'Git repository URL to clone when --source git is used.',
56
150
  }),
57
151
  'docker-registry': Flags.string({
58
- description: 'docker: image reference without tag (default: nocobase/nocobase); use -v for the tag, e.g. ghcr.io/nocobase/nocobase',
152
+ description: 'Docker registry to pull when --source docker is used; combine it with --version as the image tag.',
153
+ }),
154
+ 'docker-platform': Flags.string({
155
+ description: 'Docker image platform to pull; use auto to let Docker choose.',
156
+ options: ['auto', 'linux/amd64', 'linux/arm64'],
157
+ }),
158
+ 'docker-save': Flags.boolean({
159
+ allowNo: true,
160
+ description: 'Also save the pulled Docker image as a tarball.',
161
+ default: false,
162
+ }),
163
+ 'npm-registry': Flags.string({
164
+ description: 'npm registry for npm/git downloads and dependency installation.',
59
165
  }),
166
+ 'build': Flags.boolean({
167
+ allowNo: true,
168
+ description: 'Build npm/git source after dependencies are installed.',
169
+ default: true,
170
+ }),
171
+ 'build-dts': Flags.boolean({
172
+ description: 'Generate TypeScript declaration files during the npm/git build.',
173
+ default: false,
174
+ }),
175
+ };
176
+ static prompts = {
177
+ source: {
178
+ type: 'select',
179
+ message: downloadText('prompts.source.message'),
180
+ options: [
181
+ { value: 'npm', label: downloadText('prompts.source.npmLabel') },
182
+ { value: 'git', label: downloadText('prompts.source.gitLabel') },
183
+ { value: 'docker', label: downloadText('prompts.source.dockerLabel') },
184
+ ],
185
+ yesInitialValue: 'docker',
186
+ initialValue: 'docker',
187
+ required: true,
188
+ },
189
+ version: {
190
+ type: 'text',
191
+ message: downloadText('prompts.version.message'),
192
+ placeholder: downloadText('prompts.version.placeholder'),
193
+ initialValue: 'alpha',
194
+ yesInitialValue: 'alpha',
195
+ required: true,
196
+ },
197
+ dockerRegistry: {
198
+ type: 'text',
199
+ message: downloadText('prompts.dockerRegistry.message'),
200
+ placeholder: downloadText('prompts.dockerRegistry.placeholder'),
201
+ initialValue: (values) => defaultDockerRegistryForLang(values.lang),
202
+ yesInitialValue: DEFAULT_DOCKER_REGISTRY,
203
+ required: true,
204
+ hidden: (values) => values.source !== 'docker',
205
+ },
206
+ dockerPlatform: {
207
+ type: 'select',
208
+ message: downloadText('prompts.dockerPlatform.message'),
209
+ options: [
210
+ {
211
+ value: 'auto',
212
+ label: downloadText('prompts.dockerPlatform.autoLabel'),
213
+ hint: downloadText('prompts.dockerPlatform.autoHint'),
214
+ },
215
+ { value: 'linux/amd64', label: 'linux/amd64' },
216
+ { value: 'linux/arm64', label: 'linux/arm64' },
217
+ ],
218
+ initialValue: DEFAULT_DOCKER_PLATFORM,
219
+ yesInitialValue: DEFAULT_DOCKER_PLATFORM,
220
+ required: true,
221
+ hidden: (values) => values.source !== 'docker',
222
+ },
223
+ dockerSave: {
224
+ type: 'boolean',
225
+ message: downloadText('prompts.dockerSave.message'),
226
+ initialValue: false,
227
+ hidden: (values) => values.source !== 'docker',
228
+ },
229
+ gitUrl: {
230
+ type: 'text',
231
+ message: downloadText('prompts.gitUrl.message'),
232
+ placeholder: downloadText('prompts.gitUrl.placeholder'),
233
+ initialValue: 'https://github.com/nocobase/nocobase.git',
234
+ yesInitialValue: 'https://github.com/nocobase/nocobase.git',
235
+ required: true,
236
+ hidden: (values) => values.source !== 'git',
237
+ },
238
+ outputDir: {
239
+ type: 'text',
240
+ message: downloadText('prompts.outputDir.message'),
241
+ placeholder: downloadText('prompts.outputDir.placeholder'),
242
+ initialValue: (values) => defaultOutputDirForVersion(String(values.version ?? 'latest').trim() || 'latest'),
243
+ required: true,
244
+ hidden: (values) => {
245
+ const s = values.source;
246
+ if (s === 'npm' || s === 'git') {
247
+ return false;
248
+ }
249
+ if (s === 'docker') {
250
+ return !values.dockerSave;
251
+ }
252
+ return true;
253
+ },
254
+ },
255
+ npmRegistry: {
256
+ type: 'text',
257
+ message: downloadText('prompts.npmRegistry.message'),
258
+ placeholder: downloadText('prompts.npmRegistry.placeholder'),
259
+ initialValue: '',
260
+ hidden: (values) => values.source !== 'npm' && values.source !== 'git',
261
+ },
262
+ replace: {
263
+ type: 'boolean',
264
+ message: downloadText('prompts.replace.message'),
265
+ initialValue: false,
266
+ hidden: (values) => Download.hideOutputDirAndReplaceSteps(values),
267
+ },
268
+ devDependencies: {
269
+ type: 'boolean',
270
+ message: downloadText('prompts.devDependencies.message'),
271
+ initialValue: false,
272
+ hidden: (values) => values.source !== 'npm',
273
+ },
274
+ build: {
275
+ type: 'boolean',
276
+ message: downloadText('prompts.build.message'),
277
+ initialValue: true,
278
+ yesInitialValue: true,
279
+ hidden: () => true,
280
+ },
281
+ buildDts: {
282
+ type: 'boolean',
283
+ message: downloadText('prompts.buildDts.message'),
284
+ initialValue: false,
285
+ hidden: (values) => values.source !== 'git',
286
+ },
60
287
  };
288
+ /** When true, `outputDir` / `replace` prompts are skipped (same condition for both). */
289
+ static hideOutputDirAndReplaceSteps(values) {
290
+ const s = values.source;
291
+ if (s === 'npm' || s === 'git') {
292
+ return false;
293
+ }
294
+ if (s === 'docker') {
295
+ return !values.dockerSave;
296
+ }
297
+ return true;
298
+ }
61
299
  resolveOutputDir(flags) {
62
300
  const explicit = flags['output-dir'];
63
301
  if (explicit) {
64
302
  return explicit;
65
303
  }
66
- const tag = flags.version || 'latest';
67
- const safe = tag.replace(/[/\\]/g, '-');
68
- return `./nocobase-${safe}`;
304
+ return defaultOutputDirForVersion(flags.version || 'latest');
69
305
  }
70
- defaultOutputDir(versionTag) {
71
- const safe = versionTag.replace(/[/\\]/g, '-');
72
- return `./nocobase-${safe}`;
306
+ async ensureOutputDirAvailable(outputDir, replace) {
307
+ const outputAbs = path.resolve(process.cwd(), outputDir);
308
+ if (replace) {
309
+ await fsp.rm(outputAbs, { recursive: true, force: true });
310
+ return outputAbs;
311
+ }
312
+ if (await pathExists(outputAbs)) {
313
+ this.error(`Download target already exists: ${outputDir}. Use --replace to remove it before continuing.`);
314
+ }
315
+ return outputAbs;
316
+ }
317
+ dockerTarPath(flags, outputAbs) {
318
+ const image = flags['docker-registry'] ?? DEFAULT_DOCKER_REGISTRY;
319
+ const tag = flags.version ?? 'latest';
320
+ const safeBase = `${image.replace(/[/:]/g, '-')}-${tag.replace(/[/\\]/g, '-')}`;
321
+ return path.join(outputAbs, `${safeBase}.tar`);
73
322
  }
74
323
  /**
75
- * When stdin/stdout are TTY and not `-y`, prompt for any missing download options.
324
+ * Defaults for prompts only. Keys present in **`preset`** are omitted so `runPromptCatalog` uses
325
+ * **`values`** (preset) alone for those steps — no duplicate prefill for skipped prompts.
76
326
  */
77
- async resolveDownloadFlags(flags) {
78
- const interactive = Boolean(stdinStream.isTTY && stdoutStream.isTTY && !flags.yes);
79
- let source = flags.source?.trim();
80
- if (source === '') {
81
- source = undefined;
82
- }
83
- let version = flags.version?.trim() || undefined;
84
- let replace = flags.replace;
85
- let dev = flags['dev'];
86
- let outputDir = flags['output-dir']?.trim() || undefined;
87
- if (outputDir === '') {
88
- outputDir = undefined;
89
- }
90
- let gitUrl = flags['git-url']?.trim() || undefined;
91
- if (gitUrl === '') {
92
- gitUrl = undefined;
93
- }
94
- let dockerRegistry = flags['docker-registry']?.trim() || undefined;
95
- if (dockerRegistry === '') {
96
- dockerRegistry = undefined;
97
- }
98
- if (!interactive) {
99
- if (!source) {
100
- this.error('Distribution is required (--source npm|git|docker). Use a terminal for interactive setup, or pass -s/--source.');
101
- }
102
- const v = version || 'latest';
103
- return {
104
- source,
105
- version: v,
106
- replace,
107
- 'dev': dev,
108
- 'output-dir': outputDir,
109
- 'git-url': gitUrl,
110
- 'docker-registry': dockerRegistry,
111
- };
112
- }
113
- p.intro('nb download');
114
- if (!source) {
115
- const src = await p.select({
116
- message: 'How do you want to get NocoBase?',
117
- options: [
118
- { value: 'npm', label: 'npm — create-nocobase-app' },
119
- { value: 'git', label: 'git — shallow clone' },
120
- { value: 'docker', label: 'docker — pull image' },
121
- ],
122
- initialValue: 'npm',
123
- });
124
- if (p.isCancel(src)) {
125
- p.cancel('Download cancelled.');
126
- this.exit(0);
127
- }
128
- source = src;
327
+ buildInitialValuesFromParsed(flags, preset) {
328
+ const initialValues = {};
329
+ const source = flags.source?.trim();
330
+ if (source) {
331
+ initialValues.source = source;
129
332
  }
130
- if (version === undefined) {
131
- const verAns = await p.text({
132
- message: 'Version / dist-tag / image tag / branch alias (-v)',
133
- placeholder: 'latest',
134
- initialValue: 'latest',
135
- });
136
- if (p.isCancel(verAns)) {
137
- p.cancel('Download cancelled.');
138
- this.exit(0);
139
- }
140
- version = verAns.trim() || 'latest';
141
- }
142
- const versionResolved = version || 'latest';
143
- if (source === 'docker') {
144
- if (dockerRegistry === undefined) {
145
- const reg = await p.text({
146
- message: 'Docker image without tag (--docker-registry)',
147
- placeholder: 'nocobase/nocobase',
148
- initialValue: 'nocobase/nocobase',
149
- });
150
- if (p.isCancel(reg)) {
151
- p.cancel('Download cancelled.');
152
- this.exit(0);
153
- }
154
- dockerRegistry = reg.trim() || 'nocobase/nocobase';
155
- }
333
+ if (flags.version !== undefined) {
334
+ initialValues.version = flags.version.trim() || 'latest';
156
335
  }
157
- if (source === 'git') {
158
- if (gitUrl === undefined) {
159
- const urlAns = await p.text({
160
- message: 'Git remote URL (--git-url)',
161
- placeholder: 'https://github.com/nocobase/nocobase.git',
162
- initialValue: 'https://github.com/nocobase/nocobase.git',
163
- });
164
- if (p.isCancel(urlAns)) {
165
- p.cancel('Download cancelled.');
166
- this.exit(0);
167
- }
168
- gitUrl = urlAns.trim() || 'https://github.com/nocobase/nocobase.git';
336
+ initialValues.replace = flags.replace;
337
+ initialValues.devDependencies = flags['dev-dependencies'];
338
+ initialValues.build = flags.build;
339
+ initialValues.buildDts = flags['build-dts'];
340
+ if (flags['output-dir'] !== undefined) {
341
+ initialValues.outputDir = flags['output-dir']?.trim() ?? '';
342
+ }
343
+ if (flags['git-url'] !== undefined) {
344
+ initialValues.gitUrl = flags['git-url']?.trim() ?? '';
345
+ }
346
+ if (flags['docker-registry'] !== undefined) {
347
+ initialValues.dockerRegistry = String(flags['docker-registry'] ?? '').trim();
348
+ }
349
+ initialValues.dockerPlatform = normalizeDockerPlatform(flags['docker-platform']);
350
+ initialValues.dockerSave = flags['docker-save'];
351
+ if (flags['npm-registry'] !== undefined) {
352
+ initialValues.npmRegistry =
353
+ typeof flags['npm-registry'] === 'string' ? flags['npm-registry'] : '';
354
+ }
355
+ for (const key of Object.keys(preset)) {
356
+ if (Object.prototype.hasOwnProperty.call(initialValues, key)) {
357
+ delete initialValues[key];
169
358
  }
170
359
  }
171
- if (source === 'npm' || source === 'git') {
172
- if (outputDir === undefined) {
173
- const initialOut = this.defaultOutputDir(versionResolved);
174
- const outAns = await p.text({
175
- message: 'Output directory relative to cwd (-o)',
176
- placeholder: initialOut,
177
- initialValue: initialOut,
178
- });
179
- if (p.isCancel(outAns)) {
180
- p.cancel('Download cancelled.');
181
- this.exit(0);
182
- }
183
- outputDir = outAns.trim() || initialOut;
360
+ return initialValues;
361
+ }
362
+ /**
363
+ * Preset `values` for `runPromptCatalog`: any key here skips that prompt and fixes the result.
364
+ * Keys not listed are resolved interactively (TTY) or from catalog defaults / `initialValues` (non-TTY / `-y`).
365
+ */
366
+ buildPresetValuesFromFlags(flags) {
367
+ const preset = {};
368
+ const argv = process.argv.slice(2);
369
+ if (flags.source !== undefined && String(flags.source).trim() !== '') {
370
+ preset.source = String(flags.source).trim();
371
+ }
372
+ if (flags.version !== undefined) {
373
+ preset.version = String(flags.version).trim() || 'latest';
374
+ }
375
+ if (flags['docker-registry'] !== undefined) {
376
+ const v = String(flags['docker-registry'] ?? '').trim();
377
+ if (v) {
378
+ preset.dockerRegistry = v;
184
379
  }
185
- const replaceAns = await p.confirm({
186
- message: 'Delete existing output directory if present, then retry? (--replace)',
187
- initialValue: replace,
188
- });
189
- if (p.isCancel(replaceAns)) {
190
- p.cancel('Download cancelled.');
191
- this.exit(0);
380
+ }
381
+ if (argvHasToken(argv, ['--docker-platform'])) {
382
+ preset.dockerPlatform = normalizeDockerPlatform(flags['docker-platform']);
383
+ }
384
+ if (flags['output-dir'] !== undefined) {
385
+ const v = flags['output-dir']?.trim();
386
+ if (v) {
387
+ preset.outputDir = v;
192
388
  }
193
- replace = replaceAns;
194
389
  }
195
- if (source === 'npm') {
196
- const devAns = await p.confirm({
197
- message: 'Install devDependencies and run a non-production yarn install? (--dev)',
198
- initialValue: dev,
199
- });
200
- if (p.isCancel(devAns)) {
201
- p.cancel('Download cancelled.');
202
- this.exit(0);
390
+ if (flags['git-url'] !== undefined) {
391
+ const v = flags['git-url']?.trim();
392
+ if (v) {
393
+ preset.gitUrl = v;
203
394
  }
204
- dev = devAns;
205
395
  }
396
+ if (flags['npm-registry'] !== undefined) {
397
+ preset.npmRegistry = typeof flags['npm-registry'] === 'string' ? flags['npm-registry'] : '';
398
+ }
399
+ if (argvHasToken(argv, ['--replace', '-r'])) {
400
+ preset.replace = flags.replace;
401
+ }
402
+ if (argvHasToken(argv, ['--dev-dependencies', '--no-dev-dependencies', '-D'])) {
403
+ preset.devDependencies = flags['dev-dependencies'];
404
+ }
405
+ if (argvHasToken(argv, ['--docker-save', '--no-docker-save'])) {
406
+ preset.dockerSave = flags['docker-save'];
407
+ }
408
+ if (argvHasToken(argv, ['--build', '--no-build'])) {
409
+ preset.build = flags.build;
410
+ }
411
+ if (argvHasToken(argv, ['--build-dts', '--no-build-dts'])) {
412
+ preset.buildDts = flags['build-dts'];
413
+ }
414
+ return preset;
415
+ }
416
+ resolveEffectiveBuild(source, results, flags) {
417
+ if (source === 'npm' && !Boolean(results.devDependencies)) {
418
+ return false;
419
+ }
420
+ if (source === 'npm' || source === 'git') {
421
+ return results.build !== undefined ? Boolean(results.build) : flags.build;
422
+ }
423
+ return flags.build;
424
+ }
425
+ mapCatalogResultsToResolved(results, flags) {
426
+ const source = String(results.source);
427
+ const version = String(results.version ?? '').trim() || 'latest';
428
+ const devDependencies = source === 'npm' ? Boolean(results.devDependencies) : undefined;
429
+ const effectiveBuild = this.resolveEffectiveBuild(source, results, flags);
430
+ const buildDtsWant = results.buildDts !== undefined ? Boolean(results.buildDts) : flags['build-dts'];
431
+ const outputDir = results.outputDir !== undefined
432
+ ? String(results.outputDir).trim() || undefined
433
+ : flags['output-dir']?.trim() || undefined;
434
+ const replace = results.replace !== undefined ? Boolean(results.replace) : flags.replace;
435
+ const gitUrl = results.gitUrl !== undefined
436
+ ? String(results.gitUrl).trim() || undefined
437
+ : flags['git-url']?.trim() || undefined;
438
+ const dockerRegistry = results.dockerRegistry !== undefined
439
+ ? String(results.dockerRegistry).trim() || undefined
440
+ : flags['docker-registry']?.trim() || undefined;
441
+ const dockerPlatform = source === 'docker'
442
+ ? normalizeDockerPlatform(results.dockerPlatform !== undefined
443
+ ? results.dockerPlatform
444
+ : flags['docker-platform'])
445
+ : undefined;
446
+ const dockerSave = source === 'docker'
447
+ ? results.dockerSave !== undefined
448
+ ? Boolean(results.dockerSave)
449
+ : flags['docker-save']
450
+ : false;
451
+ const npmRegistryRaw = results.npmRegistry !== undefined
452
+ ? String(results.npmRegistry)
453
+ : flags['npm-registry'] ?? '';
454
+ const npmRegistry = npmRegistryRaw.trim() || undefined;
206
455
  return {
207
456
  source,
208
- version: versionResolved,
457
+ version,
209
458
  replace,
210
- 'dev': dev,
459
+ ...(source === 'npm' ? { 'dev-dependencies': devDependencies } : {}),
460
+ 'build': effectiveBuild,
461
+ 'build-dts': normalizeBuildDts(source, effectiveBuild, buildDtsWant),
211
462
  'output-dir': outputDir,
212
463
  'git-url': gitUrl,
213
464
  'docker-registry': dockerRegistry,
465
+ ...(source === 'docker' ? { 'docker-platform': dockerPlatform } : {}),
466
+ ...(source === 'docker' ? { 'docker-save': dockerSave } : {}),
467
+ ...(npmRegistry ? { 'npm-registry': npmRegistry } : {}),
214
468
  };
215
469
  }
470
+ async resolveDownloadFlags(flags) {
471
+ const nonInteractive = !stdinStream.isTTY || !stdoutStream.isTTY || flags.yes;
472
+ if (nonInteractive && !flags.source?.trim()) {
473
+ this.error('Download source is required in non-interactive mode. Use --source npm, --source git, or --source docker.');
474
+ }
475
+ const presetValues = this.buildPresetValuesFromFlags(flags);
476
+ const initialValues = this.buildInitialValuesFromParsed(flags, presetValues);
477
+ const results = await runPromptCatalog(Download.prompts, {
478
+ initialValues,
479
+ values: presetValues,
480
+ yes: flags.yes,
481
+ command: this,
482
+ hooks: {
483
+ onCancel: () => {
484
+ p.cancel('Download cancelled.');
485
+ this.exit(0);
486
+ },
487
+ onMissingNonInteractive: (message) => {
488
+ this.error(message);
489
+ },
490
+ },
491
+ });
492
+ const source = String(results.source ?? '').trim();
493
+ if (!source || !['docker', 'npm', 'git'].includes(source)) {
494
+ this.error('Download source is required. Choose npm, git, or docker.');
495
+ }
496
+ if (flags['docker-save'] && source !== 'docker') {
497
+ this.error('--docker-save is only available when --source docker is selected.');
498
+ }
499
+ return this.mapCatalogResultsToResolved(results, flags);
500
+ }
501
+ npmRegistryUrl(flags) {
502
+ const url = flags['npm-registry']?.trim();
503
+ return url || undefined;
504
+ }
505
+ npmRegistryEnv(flags) {
506
+ const url = this.npmRegistryUrl(flags);
507
+ if (!url) {
508
+ return undefined;
509
+ }
510
+ return { npm_config_registry: url };
511
+ }
512
+ isVerbose() {
513
+ const flags = this._flags;
514
+ return Boolean(flags?.verbose);
515
+ }
516
+ commandStdio() {
517
+ return this.isVerbose() ? 'inherit' : 'ignore';
518
+ }
519
+ formatCommandForLog(name, args, cwd) {
520
+ const quotedArgs = args.map((arg) => (/\s/.test(arg) ? JSON.stringify(arg) : arg));
521
+ const commandLine = [name, ...quotedArgs].join(' ');
522
+ return cwd ? `${commandLine} (cwd: ${cwd})` : commandLine;
523
+ }
524
+ async runExternalCommand(name, args, options) {
525
+ const cwd = options?.cwd;
526
+ printVerbose(`Running command: ${this.formatCommandForLog(name, args, cwd)}`);
527
+ let loadingStarted = false;
528
+ let loadingTimer;
529
+ let updateTimer;
530
+ let elapsedSeconds = 0;
531
+ if (!this.isVerbose() && options?.loadingMessage) {
532
+ loadingTimer = setTimeout(() => {
533
+ loadingStarted = true;
534
+ elapsedSeconds = Math.floor(EXTERNAL_COMMAND_LOADING_DELAY_MS / 1000);
535
+ startTask(`${options.loadingMessage}. Please wait...`);
536
+ updateTimer = setInterval(() => {
537
+ elapsedSeconds += Math.floor(EXTERNAL_COMMAND_LOADING_UPDATE_MS / 1000);
538
+ updateTask(`${options.loadingMessage}. Still working... (${elapsedSeconds}s elapsed)`);
539
+ }, EXTERNAL_COMMAND_LOADING_UPDATE_MS);
540
+ }, EXTERNAL_COMMAND_LOADING_DELAY_MS);
541
+ }
542
+ try {
543
+ await run(name, args, {
544
+ ...options,
545
+ stdio: this.commandStdio(),
546
+ });
547
+ }
548
+ finally {
549
+ if (loadingTimer) {
550
+ clearTimeout(loadingTimer);
551
+ }
552
+ if (updateTimer) {
553
+ clearInterval(updateTimer);
554
+ }
555
+ if (loadingStarted) {
556
+ stopTask();
557
+ }
558
+ }
559
+ }
560
+ startPreparationTask(message) {
561
+ if (this.isVerbose()) {
562
+ p.log.step(message);
563
+ return;
564
+ }
565
+ this.preparationTaskActive = true;
566
+ startTask(message);
567
+ }
568
+ finishPreparationTask() {
569
+ if (!this.preparationTaskActive) {
570
+ return;
571
+ }
572
+ this.preparationTaskActive = false;
573
+ stopTask();
574
+ }
575
+ runOptionsWithCwd(cwd, registryEnv) {
576
+ if (registryEnv) {
577
+ return { cwd, env: registryEnv };
578
+ }
579
+ return { cwd };
580
+ }
581
+ buildCommandArgv(projectRoot, flags) {
582
+ const argv = ['--cwd', projectRoot];
583
+ if (!flags['build-dts']) {
584
+ argv.push('--no-dts');
585
+ }
586
+ return argv;
587
+ }
216
588
  async downloadFromDocker(flags) {
217
- const image = flags['docker-registry'] ?? 'nocobase/nocobase';
589
+ const image = flags['docker-registry'] ?? DEFAULT_DOCKER_REGISTRY;
218
590
  const tag = flags.version ?? 'latest';
219
- await run('docker', ['pull', `${image}:${tag}`]);
591
+ const imageRef = `${image}:${tag}`;
592
+ const platform = dockerPlatformArg(flags['docker-platform']);
593
+ const pullArgs = ['pull'];
594
+ if (platform) {
595
+ pullArgs.push('--platform', platform);
596
+ }
597
+ pullArgs.push(imageRef);
598
+ this.finishPreparationTask();
599
+ p.log.step(`Pulling Docker image ${imageRef}`);
600
+ await this.runExternalCommand('docker', pullArgs, {
601
+ errorName: 'docker pull',
602
+ loadingMessage: 'Pulling the Docker image',
603
+ });
604
+ p.log.info(`Docker image is ready: ${imageRef}`);
605
+ if (!flags['docker-save']) {
606
+ return;
607
+ }
608
+ const outputDir = this.resolveOutputDir(flags);
609
+ const outAbs = flags.replace
610
+ ? path.resolve(process.cwd(), outputDir)
611
+ : await this.ensureOutputDirAvailable(outputDir, false);
612
+ if (flags.replace) {
613
+ await fsp.rm(outAbs, { recursive: true, force: true });
614
+ }
615
+ await fsp.mkdir(outAbs, { recursive: true });
616
+ const tarPath = this.dockerTarPath(flags, outAbs);
617
+ p.log.step(`Saving Docker image tarball to ${tarPath}`);
618
+ await this.runExternalCommand('docker', ['save', '-o', tarPath, imageRef], {
619
+ errorName: 'docker save',
620
+ loadingMessage: 'Saving the Docker image tarball',
621
+ });
622
+ p.log.info(`Docker image tarball saved: ${tarPath}`);
220
623
  }
221
624
  async downloadFromNpm(flags) {
222
625
  const versionSpec = flags.version || 'latest';
223
626
  const outputDir = this.resolveOutputDir(flags);
224
627
  const projectRoot = path.resolve(process.cwd(), outputDir);
225
- const npxArgs = ['-y', `create-nocobase-app@${versionSpec}`, outputDir];
226
- if (!flags['dev']) {
628
+ await this.ensureOutputDirAvailable(outputDir, flags.replace);
629
+ const parentDir = path.dirname(projectRoot);
630
+ const appName = path.basename(projectRoot);
631
+ const npxArgs = ['-y', `create-nocobase-app@${versionSpec}`, appName];
632
+ if (!flags['dev-dependencies']) {
227
633
  npxArgs.push('--skip-dev-dependencies');
228
634
  }
229
- if (flags.replace) {
230
- await fsp.rm(projectRoot, { recursive: true, force: true });
231
- }
232
- await run('npx', npxArgs);
635
+ await fsp.mkdir(parentDir, { recursive: true });
636
+ const registryEnv = this.npmRegistryEnv(flags);
637
+ this.finishPreparationTask();
638
+ p.log.step(`Creating NocoBase app "${appName}" from npm`);
639
+ await this.runExternalCommand('npx', npxArgs, {
640
+ ...this.runOptionsWithCwd(parentDir, registryEnv),
641
+ errorName: 'npx create-nocobase-app',
642
+ loadingMessage: 'Creating the app scaffold',
643
+ });
233
644
  const installArgs = ['install'];
234
- if (!flags['dev']) {
645
+ if (!flags['dev-dependencies']) {
235
646
  installArgs.push('--production');
236
647
  }
237
- await run('yarn', installArgs, { cwd: projectRoot });
648
+ p.log.step(`Installing dependencies in ${projectRoot}`);
649
+ await this.runExternalCommand('yarn', installArgs, {
650
+ ...this.runOptionsWithCwd(projectRoot, registryEnv),
651
+ errorName: 'yarn install',
652
+ loadingMessage: 'Installing dependencies',
653
+ });
654
+ if (flags.build && flags['dev-dependencies']) {
655
+ p.log.step(`Building app in ${projectRoot}`);
656
+ await this.config.runCommand('build', [
657
+ ...this.buildCommandArgv(projectRoot, flags),
658
+ ...(this.isVerbose() ? ['--verbose'] : []),
659
+ ]);
660
+ }
661
+ p.log.info(`NocoBase app is ready at ${projectRoot}`);
238
662
  return projectRoot;
239
663
  }
240
664
  async downloadFromGit(flags) {
241
665
  const repoUrl = flags['git-url'] ?? 'https://github.com/nocobase/nocobase.git';
242
666
  const versionSpec = flags.version || 'latest';
243
667
  const outputDir = this.resolveOutputDir(flags);
244
- const versionToRef = {
245
- 'latest': 'main',
246
- 'beta': 'next',
247
- 'alpha': 'develop',
248
- };
249
- if (flags.replace) {
250
- await fsp.rm(path.resolve(process.cwd(), outputDir), { recursive: true, force: true });
251
- }
252
- const branch = versionToRef[versionSpec] || versionSpec;
668
+ await this.ensureOutputDirAvailable(outputDir, flags.replace);
669
+ const branch = gitRefForVersion(versionSpec);
253
670
  const gitArgs = ['clone'];
254
671
  gitArgs.push('--branch', branch);
255
672
  gitArgs.push('--depth', '1', repoUrl, outputDir);
256
- await run('git', gitArgs);
673
+ this.finishPreparationTask();
674
+ p.log.step(branch === versionSpec
675
+ ? `Cloning NocoBase from ${repoUrl} (${branch})`
676
+ : `Cloning NocoBase from ${repoUrl} (${branch}, resolved from ${versionSpec})`);
677
+ await this.runExternalCommand('git', gitArgs, {
678
+ errorName: 'git clone',
679
+ loadingMessage: 'Cloning the repository',
680
+ });
257
681
  const projectRoot = path.resolve(process.cwd(), outputDir);
258
- await run('yarn', ['install'], { cwd: projectRoot });
682
+ const registryEnv = this.npmRegistryEnv(flags);
683
+ p.log.step(`Installing dependencies in ${projectRoot}`);
684
+ await this.runExternalCommand('yarn', ['install'], {
685
+ ...this.runOptionsWithCwd(projectRoot, registryEnv),
686
+ errorName: 'yarn install',
687
+ loadingMessage: 'Installing dependencies',
688
+ });
689
+ if (flags.build) {
690
+ p.log.step(`Building app in ${projectRoot}`);
691
+ await this.config.runCommand('build', [
692
+ ...this.buildCommandArgv(projectRoot, flags),
693
+ ...(this.isVerbose() ? ['--verbose'] : []),
694
+ ]);
695
+ }
696
+ p.log.info(`NocoBase app is ready at ${projectRoot}`);
259
697
  return projectRoot;
260
698
  }
261
- /**
262
- * @returns Final resolved flags and, for npm/git, the absolute project directory.
263
- */
264
699
  async download() {
265
700
  const { flags } = await this.parse(Download);
701
+ this._flags = flags;
702
+ applyCliLocale(this._flags.locale);
703
+ setVerboseMode(Boolean(flags.verbose));
704
+ if (!flags['no-intro']) {
705
+ p.intro('Get NocoBase');
706
+ }
266
707
  const resolved = await this.resolveDownloadFlags(flags);
267
- switch (resolved.source) {
268
- case 'npm': {
269
- const projectRoot = await this.downloadFromNpm(resolved);
270
- return { resolved, projectRoot };
271
- }
272
- case 'docker': {
273
- await this.downloadFromDocker(resolved);
274
- return { resolved, projectRoot: undefined };
275
- }
276
- case 'git': {
277
- const projectRoot = await this.downloadFromGit(resolved);
278
- return { resolved, projectRoot };
708
+ const source = resolved.source;
709
+ this.startPreparationTask(`Preparing download from ${downloadSourceLabel(source)}`);
710
+ try {
711
+ switch (source) {
712
+ case 'npm': {
713
+ const projectRoot = await this.downloadFromNpm(resolved);
714
+ return { resolved, projectRoot };
715
+ }
716
+ case 'docker': {
717
+ await this.downloadFromDocker(resolved);
718
+ return { resolved, projectRoot: undefined };
719
+ }
720
+ case 'git': {
721
+ const projectRoot = await this.downloadFromGit(resolved);
722
+ return { resolved, projectRoot };
723
+ }
724
+ default:
725
+ this.error(`Unsupported download source: ${resolved.source}`);
279
726
  }
280
- default:
281
- this.error(`Invalid --source: ${resolved.source}`);
727
+ }
728
+ finally {
729
+ this.finishPreparationTask();
282
730
  }
283
731
  }
284
732
  async run() {
285
733
  try {
286
- return await this.download();
734
+ const result = await this.download();
735
+ p.outro(`Download completed via ${downloadSourceLabel(result.resolved.source)}.`);
736
+ return result;
287
737
  }
288
738
  catch (error) {
289
739
  const message = error instanceof Error ? error.message : String(error);