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

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