@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
@@ -9,142 +9,357 @@
9
9
  import { Command, Flags } from '@oclif/core';
10
10
  import * as p from '@clack/prompts';
11
11
  import pc from 'picocolors';
12
+ import { existsSync } from 'node:fs';
13
+ import path from 'node:path';
12
14
  import { stdin as stdinStream, stdout as stdoutStream } from 'node:process';
13
- import { buildEnvAddArgv, InitWizardCancelledError, runInitBrowserWizard, } from "../lib/init-browser-wizard.js";
15
+ import { getEnv, upsertEnv } from "../lib/auth-store.js";
16
+ import { runPromptCatalog, } from "../lib/prompt-catalog.js";
17
+ import { applyCliLocale, localeText, translateCli } from "../lib/cli-locale.js";
18
+ import { runPromptCatalogWebUI, } from "../lib/prompt-web-ui.js";
19
+ import { validateApiBaseUrl, validateEnvKey } from "../lib/prompt-validators.js";
14
20
  import { run } from "../lib/run-npm.js";
21
+ import Download from "./download.js";
22
+ import EnvAdd from "./env/add.js";
23
+ import Install, { defaultDbPortForDialect } from "./install.js";
24
+ import _ from 'lodash';
25
+ const DEFAULT_INIT_API_BASE_URL = 'http://localhost:13000/api';
26
+ const DEFAULT_INIT_APP_NAME = 'local';
27
+ const DOWNLOAD_OUTPUT_DIR_PROMPT = Download.prompts.outputDir;
28
+ const CONFIG_SCOPE = 'project';
29
+ const initText = (key, values) => localeText(`commands.init.${key}`, values);
30
+ function withExtraHidden(def, extraHidden) {
31
+ if (def.type === 'run') {
32
+ return def;
33
+ }
34
+ return {
35
+ ...def,
36
+ hidden: (values) => extraHidden(values) || (def.hidden?.(values) ?? false),
37
+ };
38
+ }
39
+ function existingAppOnly(def) {
40
+ return withExtraHidden(def, (values) => values.hasNocobase !== 'yes');
41
+ }
42
+ function newInstallOnly(def) {
43
+ return withExtraHidden(def, (values) => values.hasNocobase !== 'no');
44
+ }
45
+ function downloadInNewInstallOnly(def) {
46
+ return withExtraHidden(def, (values) => values.hasNocobase !== 'no' || values.fetchSource !== true);
47
+ }
48
+ function argvHasToken(argv, tokens) {
49
+ return tokens.some((token) => argv.includes(token));
50
+ }
51
+ function shouldAllowExistingInitEnv() {
52
+ return argvHasToken(process.argv.slice(2), ['--force', '-f']);
53
+ }
54
+ async function validateInitAppName(value) {
55
+ const formatError = validateEnvKey(value);
56
+ if (formatError) {
57
+ return formatError;
58
+ }
59
+ const envName = String(value ?? '').trim();
60
+ if (!envName) {
61
+ return undefined;
62
+ }
63
+ const existingEnv = await getEnv(envName, { scope: 'project' });
64
+ if (existingEnv) {
65
+ if (shouldAllowExistingInitEnv()) {
66
+ return undefined;
67
+ }
68
+ return translateCli('commands.init.validation.envExists', { envName });
69
+ }
70
+ return undefined;
71
+ }
72
+ function highlightInitValidationMessage(message) {
73
+ return message.replace(/Env "([^"]+)"/, (_match, envName) => `Env ${pc.cyan(pc.bold(`"${envName}"`))}`);
74
+ }
75
+ function formatInitValidationMessage(message) {
76
+ return message;
77
+ }
78
+ function formatResumeEnvRequiredMessage() {
79
+ return [
80
+ translateCli('commands.init.messages.resumeEnvRequired'),
81
+ translateCli('commands.init.messages.resumeEnvHelp'),
82
+ ].join('\n');
83
+ }
84
+ function formatSkippedAppNameRequiredMessage() {
85
+ return [
86
+ translateCli('commands.init.messages.appNameRequiredWhenSkipped'),
87
+ translateCli('commands.init.messages.appNameEnvHelp'),
88
+ ].join('\n');
89
+ }
90
+ function initTitle() {
91
+ return translateCli('commands.init.messages.title');
92
+ }
93
+ function logInitUiReady(command, url) {
94
+ p.log.step(translateCli('commands.init.messages.uiReady'));
95
+ p.log.info(translateCli('commands.init.messages.uiReadyHelp'));
96
+ command.log(`URL: ${url}`);
97
+ }
98
+ function logInitUiBrowserOpenFallback() {
99
+ p.log.warn(translateCli('commands.init.messages.uiOpenBrowserFallback'));
100
+ }
15
101
  export default class Init extends Command {
16
- static summary = 'Initialize the NocoBase AI setup environment';
17
- static description = `Initialize the current workspace for NocoBase CLI and agent workflows. You only run nb init; the following runs inside this command (not as separate manual steps):
102
+ static summary = 'Set up NocoBase so coding agents can connect and work with it';
103
+ static description = `Set up NocoBase for coding agents in the current workspace.
104
+
105
+ \`nb init\` prepares a NocoBase environment that coding agents can use. It supports two setup paths:
18
106
 
19
- 1. Optionally install NocoBase agent skills (\`npx -y skills add nocobase/skills\`)—you are prompted when using a TTY.
20
- 2. If you already have a NocoBase application (anywhere): runs \`nb env add\` only (\`nb install\` is skipped).
21
- 3. If not: runs \`nb install\` only (\`nb env add\` is not run afterward; configure the CLI with \`nb env add\` when you need it).
107
+ - Connect an existing NocoBase app and save it as a CLI env.
108
+ - Install a new NocoBase app, then save it as a CLI env.
22
109
 
23
- Internal ordering: (skills?) (already have an app? env add | install only).
110
+ It can also install NocoBase AI coding skills (\`nocobase/skills\`) so agents get the project-specific workflow guidance.
24
111
 
25
- Use \`-y\` / \`--yes\` to skip init prompts (defaults: install skills, then \`nb install\` only—same as choosing the first option, no existing app). When you choose an existing app in a TTY, \`nb env add\` may still prompt for URL and auth.
112
+ If setup was interrupted earlier, use \`--resume\` with an existing env name to continue from the saved workspace config.
26
113
 
27
- Use \`--ui\` to open a **browser** wizard (local HTTP server; default bind \`0.0.0.0\`, random port). Use \`--ui-host\` / \`--ui-port\` to override. The opened URL uses \`127.0.0.1\` when the bind address is all-interfaces. It can collect \`nb env add\` fields when you link an existing app, so the terminal env wizard is skipped. Cannot be combined with \`--yes\`.`;
114
+ Prompt modes:
115
+ - Default: guided prompts in the terminal.
116
+ - \`--ui\`: open the same setup flow in a local browser form.
117
+ - \`-y\`, \`--yes\`: skip prompts. In this mode \`--env <envName>\` is required, and init creates a new local NocoBase app with safe defaults.
118
+
119
+ \`--ui\` cannot be combined with \`--yes\`.`;
28
120
  static examples = [
29
121
  '<%= config.bin %> <%= command.id %>',
122
+ '<%= config.bin %> <%= command.id %> --env app1',
123
+ '<%= config.bin %> <%= command.id %> --env app1 --ui',
30
124
  '<%= config.bin %> <%= command.id %> --ui',
31
- '<%= config.bin %> <%= command.id %> --ui --ui-host 127.0.0.1 --ui-port 3000',
32
- '<%= config.bin %> <%= command.id %> -y',
125
+ '<%= config.bin %> <%= command.id %> --env app1 --yes',
126
+ '<%= config.bin %> <%= command.id %> --env app1 --resume',
127
+ '<%= config.bin %> <%= command.id %> --env app1 --yes --source docker --version alpha',
128
+ '<%= config.bin %> <%= command.id %> --env app1 --yes --source npm --version alpha --app-port 13080',
129
+ '<%= config.bin %> <%= command.id %> --env app1 --yes --source git --version fix/cli-v2',
130
+ '<%= config.bin %> <%= command.id %> --ui --ui-port 3000',
33
131
  ];
132
+ static prompts = {
133
+ appName: {
134
+ type: 'text',
135
+ message: initText('prompts.appName.message'),
136
+ placeholder: initText('prompts.appName.placeholder'),
137
+ required: true,
138
+ validate: validateInitAppName,
139
+ },
140
+ hasNocobase: {
141
+ type: 'select',
142
+ variant: 'radio',
143
+ message: initText('prompts.hasNocobase.message'),
144
+ options: [
145
+ {
146
+ value: 'no',
147
+ label: initText('prompts.hasNocobase.noLabel'),
148
+ },
149
+ {
150
+ value: 'yes',
151
+ label: initText('prompts.hasNocobase.yesLabel'),
152
+ },
153
+ ],
154
+ initialValue: 'no',
155
+ yesInitialValue: 'no',
156
+ required: true,
157
+ },
158
+ installSkills: {
159
+ type: 'boolean',
160
+ message: initText('prompts.installSkills.message'),
161
+ initialValue: true,
162
+ yesInitialValue: true,
163
+ },
164
+ apiBaseUrl: existingAppOnly({
165
+ type: 'text',
166
+ message: initText('prompts.apiBaseUrl.message'),
167
+ placeholder: initText('prompts.apiBaseUrl.placeholder'),
168
+ required: true,
169
+ validate: validateApiBaseUrl,
170
+ }),
171
+ authType: existingAppOnly(EnvAdd.prompts.authType),
172
+ accessToken: existingAppOnly(EnvAdd.prompts.accessToken),
173
+ lang: newInstallOnly(Install.appPrompts.lang),
174
+ appRootPath: newInstallOnly(Install.appPrompts.appRootPath),
175
+ appPort: newInstallOnly(Install.appPrompts.appPort),
176
+ storagePath: newInstallOnly(Install.appPrompts.storagePath),
177
+ fetchSource: newInstallOnly(Install.appPrompts.fetchSource),
178
+ source: downloadInNewInstallOnly(Download.prompts.source),
179
+ version: downloadInNewInstallOnly(Download.prompts.version),
180
+ dockerRegistry: downloadInNewInstallOnly(Download.prompts.dockerRegistry),
181
+ dockerPlatform: downloadInNewInstallOnly(Download.prompts.dockerPlatform),
182
+ dockerSave: downloadInNewInstallOnly(Download.prompts.dockerSave),
183
+ gitUrl: downloadInNewInstallOnly(Download.prompts.gitUrl),
184
+ outputDir: downloadInNewInstallOnly({
185
+ ...DOWNLOAD_OUTPUT_DIR_PROMPT,
186
+ hidden: (values) => {
187
+ const source = String(values.source ?? '').trim();
188
+ if (source === 'npm' || source === 'git') {
189
+ return true;
190
+ }
191
+ return DOWNLOAD_OUTPUT_DIR_PROMPT.hidden?.(values) ?? false;
192
+ },
193
+ initialValue: (values) => {
194
+ const source = String(values.source ?? '').trim();
195
+ if (source === 'npm' || source === 'git') {
196
+ const appRootPath = String(values.appRootPath ?? '').trim();
197
+ if (appRootPath) {
198
+ return appRootPath;
199
+ }
200
+ }
201
+ const initialValue = DOWNLOAD_OUTPUT_DIR_PROMPT.initialValue;
202
+ return typeof initialValue === 'function' ? initialValue(values) : String(initialValue ?? '');
203
+ },
204
+ }),
205
+ npmRegistry: downloadInNewInstallOnly(Download.prompts.npmRegistry),
206
+ replace: downloadInNewInstallOnly(Download.prompts.replace),
207
+ devDependencies: downloadInNewInstallOnly(Download.prompts.devDependencies),
208
+ build: downloadInNewInstallOnly(Download.prompts.build),
209
+ buildDts: downloadInNewInstallOnly(Download.prompts.buildDts),
210
+ dbDialect: newInstallOnly(Install.dbPrompts.dbDialect),
211
+ builtinDb: newInstallOnly(Install.dbPrompts.builtinDb),
212
+ builtinDbImage: newInstallOnly(Install.dbPrompts.builtinDbImage),
213
+ dbHost: newInstallOnly(Install.dbPrompts.dbHost),
214
+ dbPort: newInstallOnly(Install.dbPrompts.dbPort),
215
+ dbDatabase: newInstallOnly(Install.dbPrompts.dbDatabase),
216
+ dbUser: newInstallOnly(Install.dbPrompts.dbUser),
217
+ dbPassword: newInstallOnly(Install.dbPrompts.dbPassword),
218
+ rootUsername: newInstallOnly(Install.rootUserPrompts.rootUsername),
219
+ rootEmail: newInstallOnly(Install.rootUserPrompts.rootEmail),
220
+ rootPassword: newInstallOnly(Install.rootUserPrompts.rootPassword),
221
+ rootNickname: newInstallOnly(Install.rootUserPrompts.rootNickname),
222
+ };
34
223
  static flags = {
35
224
  yes: Flags.boolean({
36
225
  char: 'y',
37
- description: 'Skip all prompts',
226
+ description: 'Skip prompts and create a new local NocoBase app. Requires an env name.',
227
+ default: false,
228
+ }),
229
+ env: Flags.string({
230
+ char: 'e',
231
+ description: 'Env name for this setup. Required with --yes and --resume',
232
+ }),
233
+ 'install-skills': Flags.boolean({
234
+ description: 'Install NocoBase AI coding skills (`nocobase/skills`) for this workspace',
38
235
  default: false,
39
236
  }),
40
237
  ui: Flags.boolean({
41
- description: 'Open a browser-based setup wizard (local HTTP server; not valid with --yes)',
238
+ description: 'Open the guided setup flow in a local browser form (not valid with --yes)',
42
239
  default: false,
43
240
  }),
44
241
  'ui-host': Flags.string({
45
- description: 'Bind address for the --ui wizard HTTP server (default 0.0.0.0; only with --ui)',
242
+ description: 'Host for the local --ui setup server (default: 127.0.0.1)',
46
243
  }),
47
244
  'ui-port': Flags.integer({
48
- description: 'TCP port for the --ui wizard; 0 = OS-assigned ephemeral port (default 0; only with --ui)',
245
+ description: 'Port for the local --ui setup server; 0 lets the OS choose an available port',
49
246
  min: 0,
50
247
  max: 65535,
51
248
  }),
249
+ ..._.omit(Install.flags, ['yes', 'env']),
52
250
  };
53
251
  async run() {
54
- const { flags } = await this.parse(Init);
55
- if (flags.ui && flags.yes) {
252
+ const parsedResult = await this.parse(Init);
253
+ applyCliLocale(parsedResult.flags.locale);
254
+ const flags = parsedResult.flags;
255
+ const normalizedFlags = { ...flags };
256
+ if (normalizedFlags.ui && normalizedFlags.yes) {
56
257
  this.error('--ui cannot be used with --yes.');
57
258
  }
58
- if (!flags.ui &&
59
- (flags['ui-host'] !== undefined || flags['ui-port'] !== undefined)) {
259
+ if (normalizedFlags.ui && normalizedFlags.resume) {
260
+ this.error('--ui cannot be used with --resume.');
261
+ }
262
+ if (!normalizedFlags.ui &&
263
+ (normalizedFlags['ui-host'] !== undefined || normalizedFlags['ui-port'] !== undefined)) {
60
264
  this.error('--ui-host and --ui-port require --ui.');
61
265
  }
62
- const interactive = Boolean(stdinStream.isTTY && stdoutStream.isTTY);
63
- const useBrowserUi = Boolean(flags.ui);
64
- if (useBrowserUi) {
65
- if (interactive) {
66
- p.intro(`${pc.bold('nb init')} ${pc.dim('— browser wizard')}`);
266
+ if (normalizedFlags.resume) {
267
+ const envName = String(normalizedFlags.env ?? '').trim();
268
+ if (!envName) {
269
+ p.log.error(formatResumeEnvRequiredMessage());
270
+ this.exit(1);
67
271
  }
68
- else {
69
- this.log('nb init — browser wizard');
272
+ p.intro(initTitle());
273
+ if (Boolean(normalizedFlags['install-skills'])) {
274
+ try {
275
+ p.log.step('Installing NocoBase agent skills (npx -y skills add nocobase/skills)');
276
+ await run('npx', ['-y', 'skills', 'add', 'nocobase/skills', '-y']);
277
+ }
278
+ catch (error) {
279
+ const message = error instanceof Error ? error.message : String(error);
280
+ p.outro(pc.red(`Skills install failed: ${message}`));
281
+ this.error(message);
282
+ }
70
283
  }
71
- this.log('Your browser should open; complete the form there to continue.');
72
- }
73
- else {
74
- p.intro('Initialize the NocoBase AI setup environment');
75
- }
76
- /** Whether `nb install` / follow-up should avoid terminal prompts (`-y`). */
77
- const skipInstallPrompts = Boolean(flags.yes) || !interactive;
78
- let installSkills = true;
79
- let hasNocobase = false;
80
- /** When set, \`nb env add\` is invoked with these argv (from \`--ui\` form). */
81
- let envAddArgvFromUi;
82
- if (flags.yes) {
83
- p.log.info('Skipping prompts (--yes): will install NocoBase agent skills.');
84
- installSkills = true;
85
- hasNocobase = false;
86
- p.log.info('Skipping prompts (--yes): will run nb install only (same default as "I don\'t have a NocoBase application yet").');
87
- }
88
- else if (useBrowserUi) {
89
284
  try {
90
- const choice = await runInitBrowserWizard((line) => this.log(line), {
91
- bindHost: flags['ui-host']?.trim() || '0.0.0.0',
92
- port: flags['ui-port'] ?? 0,
93
- });
94
- installSkills = choice.installSkills;
95
- hasNocobase = choice.hasNocobase;
96
- if (choice.envAdd) {
97
- envAddArgvFromUi = buildEnvAddArgv(choice.envAdd);
98
- }
285
+ p.log.step(`Resuming setup for env "${envName}" from the saved workspace config`);
286
+ await this.config.runCommand('install', this.buildResumeInstallArgv(normalizedFlags));
99
287
  }
100
288
  catch (error) {
101
- if (error instanceof InitWizardCancelledError) {
102
- if (interactive) {
103
- p.cancel(error.message);
104
- }
105
- else {
106
- this.log(error.message);
107
- }
108
- this.exit(0);
109
- }
110
- throw error;
289
+ const message = error instanceof Error ? error.message : String(error);
290
+ p.outro(pc.red(message));
291
+ this.error(message);
111
292
  }
293
+ p.outro('Workspace init finished.');
294
+ return;
112
295
  }
113
- else if (interactive) {
114
- const skillsAnswer = await p.confirm({
115
- message: 'Install NocoBase agent skills (nocobase/skills) for Cursor / Codex workflows?',
116
- initialValue: true,
117
- });
118
- if (p.isCancel(skillsAnswer)) {
119
- p.cancel('Init cancelled.');
120
- this.exit(0);
121
- }
122
- installSkills = skillsAnswer;
123
- const answer = await p.select({
124
- message: 'Do you already have a NocoBase application?',
125
- options: [
126
- {
127
- value: 'no',
128
- label: "I don't have a NocoBase application yet",
129
- },
130
- {
131
- value: 'yes',
132
- label: 'I already have a NocoBase application',
133
- },
134
- ],
135
- initialValue: 'no',
136
- });
137
- if (p.isCancel(answer)) {
138
- p.cancel('Init cancelled.');
139
- this.exit(0);
140
- }
141
- hasNocobase = answer === 'yes';
296
+ const interactive = Boolean(stdinStream.isTTY && stdoutStream.isTTY);
297
+ const useBrowserUi = Boolean(normalizedFlags.ui);
298
+ let presetValues = this.buildPresetValuesFromFlags(normalizedFlags);
299
+ if (normalizedFlags.yes && !String(presetValues.appName ?? '').trim()) {
300
+ const formatted = formatSkippedAppNameRequiredMessage();
301
+ p.log.error(highlightInitValidationMessage(formatted));
302
+ this.exit(1);
303
+ }
304
+ const appName = String(presetValues.appName ?? '').trim();
305
+ if (useBrowserUi) {
306
+ p.intro(initTitle());
307
+ p.log.info(translateCli('commands.init.messages.uiOpening'));
142
308
  }
143
309
  else {
144
- p.log.warn('Non-interactive terminal: will install NocoBase agent skills (skip is not available without a TTY).');
145
- installSkills = true;
146
- hasNocobase = false;
147
- p.log.warn('Non-interactive terminal: assuming you do not already have a NocoBase app (will run nb install only).');
310
+ p.intro(initTitle());
311
+ if (normalizedFlags.yes) {
312
+ p.log.info(`Prompts skipped (--yes). NocoBase will be installed for env "${appName}" using the provided flags and safe defaults.`);
313
+ }
314
+ else if (!interactive) {
315
+ p.log.warn('No interactive terminal detected. NocoBase will be installed using the provided flags and safe defaults.');
316
+ }
317
+ }
318
+ const dynamicInitialValues = await Init.buildDynamicInitialValuesForInstall(normalizedFlags, presetValues);
319
+ if (useBrowserUi) {
320
+ presetValues = await runPromptCatalogWebUI({
321
+ stages: Init.buildWebUiStages(),
322
+ values: {
323
+ ...dynamicInitialValues,
324
+ ...presetValues,
325
+ },
326
+ host: normalizedFlags['ui-host']?.trim() || '127.0.0.1',
327
+ port: normalizedFlags['ui-port'] ?? 0,
328
+ pageTitle: initText('webUi.pageTitle'),
329
+ documentHeading: initText('webUi.documentHeading'),
330
+ documentHint: initText('webUi.documentHint'),
331
+ onServerStart: ({ url }) => {
332
+ logInitUiReady(this, url);
333
+ },
334
+ onOpenBrowserError: (_url, _err) => {
335
+ logInitUiBrowserOpenFallback();
336
+ },
337
+ });
338
+ }
339
+ const results = await runPromptCatalog(Init.prompts, {
340
+ initialValues: dynamicInitialValues,
341
+ values: presetValues,
342
+ yes: normalizedFlags.yes || useBrowserUi || !interactive,
343
+ hooks: {
344
+ onCancel: () => {
345
+ p.cancel('Init cancelled.');
346
+ this.exit(0);
347
+ },
348
+ onMissingNonInteractive: (message) => {
349
+ const formatted = formatInitValidationMessage(message);
350
+ p.log.error(highlightInitValidationMessage(formatted));
351
+ this.exit(1);
352
+ },
353
+ },
354
+ command: this,
355
+ });
356
+ const installSkills = Boolean(results.installSkills);
357
+ const hasNocobase = results.hasNocobase === 'yes';
358
+ const existingEnv = !hasNocobase
359
+ ? await getEnv(String(results.appName ?? '').trim(), { scope: 'project' })
360
+ : undefined;
361
+ if (existingEnv && Boolean(normalizedFlags.force)) {
362
+ p.log.warn(`Reconfiguring existing env ${pc.cyan(pc.bold(`"${existingEnv.name}"`))} in this workspace because ${pc.bold('--force')} was set. The env config will be updated before install starts, then refreshed again after install succeeds.`);
148
363
  }
149
364
  if (installSkills) {
150
365
  try {
@@ -164,16 +379,13 @@ Use \`--ui\` to open a **browser** wizard (local HTTP server; default bind \`0.0
164
379
  // oclif explicit registry keys use `:` (e.g. `env:add`); users still type `nb env add`.
165
380
  if (hasNocobase) {
166
381
  p.log.step('Running nb env add');
167
- if (useBrowserUi && !envAddArgvFromUi) {
168
- this.error('Browser wizard did not supply env add options.');
169
- }
170
- const envArgv = envAddArgvFromUi ??
171
- (interactive ? ['--scope', 'project'] : ['default', '--scope', 'project']);
172
- await this.config.runCommand('env:add', envArgv);
382
+ await this.config.runCommand('env:add', this.buildEnvAddArgv(results));
173
383
  }
174
384
  else {
385
+ p.log.step('Saving the local env config');
386
+ await this.persistManagedEnvConfig(results);
175
387
  p.log.step('Running nb install');
176
- await this.config.runCommand('install', skipInstallPrompts ? ['-e', 'local', '-y'] : []);
388
+ await this.config.runCommand('install', this.buildInstallArgv(results, normalizedFlags));
177
389
  }
178
390
  }
179
391
  catch (error) {
@@ -183,4 +395,412 @@ Use \`--ui\` to open a **browser** wizard (local HTTP server; default bind \`0.0
183
395
  }
184
396
  p.outro('Workspace init finished.');
185
397
  }
398
+ static async buildDynamicInitialValuesForInstall(flags, presetValues) {
399
+ const out = {};
400
+ if (!Object.prototype.hasOwnProperty.call(presetValues, 'appPort')) {
401
+ const appInitialValues = await Install.buildAppPromptInitialValues({
402
+ envName: String(presetValues.appName ?? '').trim(),
403
+ flags: {
404
+ ...flags,
405
+ 'app-root-path': flags['app-root-path'] ?? '',
406
+ 'storage-path': flags['storage-path'] ?? '',
407
+ },
408
+ warnOnPortFallback: false,
409
+ });
410
+ if (appInitialValues.appPort !== undefined) {
411
+ out.appPort = appInitialValues.appPort;
412
+ }
413
+ }
414
+ const downloadSeed = { ...presetValues };
415
+ if (flags.yes
416
+ && !Object.prototype.hasOwnProperty.call(downloadSeed, 'source')
417
+ && downloadSeed.fetchSource !== false) {
418
+ downloadSeed.source = 'docker';
419
+ }
420
+ const dbInitial = await Install.buildDbPromptInitialValues({
421
+ flags,
422
+ downloadResults: downloadSeed,
423
+ dbPreset: presetValues,
424
+ warnOnPortFallback: false,
425
+ });
426
+ for (const [key, value] of Object.entries(dbInitial)) {
427
+ if (!Object.prototype.hasOwnProperty.call(presetValues, key)) {
428
+ out[key] = value;
429
+ }
430
+ }
431
+ return out;
432
+ }
433
+ static buildWebUiStages() {
434
+ const c = Init.prompts;
435
+ return [
436
+ {
437
+ sectionTitle: initText('webUi.gettingStarted.title'),
438
+ sectionDescription: initText('webUi.gettingStarted.description'),
439
+ catalog: {
440
+ appName: c.appName,
441
+ hasNocobase: c.hasNocobase,
442
+ installSkills: c.installSkills,
443
+ },
444
+ },
445
+ {
446
+ sectionTitle: initText('webUi.connectExistingApp.title'),
447
+ sectionDescription: initText('webUi.connectExistingApp.description'),
448
+ catalog: {
449
+ apiBaseUrl: c.apiBaseUrl,
450
+ authType: c.authType,
451
+ accessToken: c.accessToken,
452
+ },
453
+ },
454
+ {
455
+ sectionTitle: initText('webUi.createNewApp.title'),
456
+ sectionDescription: initText('webUi.createNewApp.description'),
457
+ catalog: {
458
+ lang: c.lang,
459
+ appRootPath: c.appRootPath,
460
+ appPort: c.appPort,
461
+ storagePath: c.storagePath,
462
+ fetchSource: c.fetchSource,
463
+ },
464
+ },
465
+ {
466
+ sectionTitle: initText('webUi.downloadAppFiles.title'),
467
+ sectionDescription: initText('webUi.downloadAppFiles.description'),
468
+ catalog: {
469
+ source: c.source,
470
+ version: c.version,
471
+ dockerRegistry: c.dockerRegistry,
472
+ dockerPlatform: c.dockerPlatform,
473
+ dockerSave: c.dockerSave,
474
+ gitUrl: c.gitUrl,
475
+ outputDir: c.outputDir,
476
+ npmRegistry: c.npmRegistry,
477
+ replace: c.replace,
478
+ devDependencies: c.devDependencies,
479
+ build: c.build,
480
+ buildDts: c.buildDts,
481
+ },
482
+ },
483
+ {
484
+ sectionTitle: initText('webUi.configureDatabase.title'),
485
+ sectionDescription: initText('webUi.configureDatabase.description'),
486
+ catalog: {
487
+ dbDialect: c.dbDialect,
488
+ builtinDb: c.builtinDb,
489
+ builtinDbImage: c.builtinDbImage,
490
+ dbHost: c.dbHost,
491
+ dbPort: c.dbPort,
492
+ dbDatabase: c.dbDatabase,
493
+ dbUser: c.dbUser,
494
+ dbPassword: c.dbPassword,
495
+ },
496
+ },
497
+ {
498
+ sectionTitle: initText('webUi.createAdminAccount.title'),
499
+ sectionDescription: initText('webUi.createAdminAccount.description'),
500
+ catalog: {
501
+ rootUsername: c.rootUsername,
502
+ rootEmail: c.rootEmail,
503
+ rootPassword: c.rootPassword,
504
+ rootNickname: c.rootNickname,
505
+ },
506
+ },
507
+ ];
508
+ }
509
+ buildPresetValuesFromFlags(flags) {
510
+ const preset = {};
511
+ const argv = process.argv.slice(2);
512
+ if (flags.env !== undefined && String(flags.env).trim() !== '') {
513
+ preset.appName = String(flags.env).trim();
514
+ }
515
+ if (flags.lang !== undefined && String(flags.lang).trim() !== '') {
516
+ preset.lang = String(flags.lang).trim();
517
+ }
518
+ if (flags['app-root-path'] !== undefined && String(flags['app-root-path']).trim() !== '') {
519
+ preset.appRootPath = String(flags['app-root-path']).trim();
520
+ }
521
+ if (flags['app-port'] !== undefined && String(flags['app-port']).trim() !== '') {
522
+ preset.appPort = String(flags['app-port']).trim();
523
+ }
524
+ if (flags['storage-path'] !== undefined && String(flags['storage-path']).trim() !== '') {
525
+ preset.storagePath = String(flags['storage-path']).trim();
526
+ }
527
+ if (flags['root-username'] !== undefined) {
528
+ preset.rootUsername = String(flags['root-username'] ?? '').trim();
529
+ }
530
+ if (flags['root-email'] !== undefined) {
531
+ preset.rootEmail = String(flags['root-email'] ?? '').trim();
532
+ }
533
+ if (flags['root-password'] !== undefined) {
534
+ preset.rootPassword = String(flags['root-password'] ?? '');
535
+ }
536
+ if (flags['root-nickname'] !== undefined) {
537
+ preset.rootNickname = String(flags['root-nickname'] ?? '').trim();
538
+ }
539
+ if (flags['db-dialect'] !== undefined && String(flags['db-dialect']).trim() !== '') {
540
+ preset.dbDialect = String(flags['db-dialect']).trim();
541
+ }
542
+ if (flags['builtin-db-image'] !== undefined && String(flags['builtin-db-image']).trim() !== '') {
543
+ preset.builtinDbImage = String(flags['builtin-db-image']).trim();
544
+ }
545
+ if (flags['db-host'] !== undefined && String(flags['db-host']).trim() !== '') {
546
+ preset.dbHost = String(flags['db-host']).trim();
547
+ }
548
+ if (flags['db-port'] !== undefined && String(flags['db-port']).trim() !== '') {
549
+ preset.dbPort = String(flags['db-port']).trim();
550
+ }
551
+ if (flags['db-database'] !== undefined && String(flags['db-database']).trim() !== '') {
552
+ preset.dbDatabase = String(flags['db-database']).trim();
553
+ }
554
+ if (flags['db-user'] !== undefined && String(flags['db-user']).trim() !== '') {
555
+ preset.dbUser = String(flags['db-user']).trim();
556
+ }
557
+ if (flags['db-password'] !== undefined) {
558
+ preset.dbPassword = String(flags['db-password'] ?? '');
559
+ }
560
+ if (argvHasToken(argv, ['--fetch-source'])) {
561
+ preset.fetchSource = Boolean(flags['fetch-source']);
562
+ }
563
+ if (flags.source !== undefined && String(flags.source).trim() !== '') {
564
+ preset.source = String(flags.source).trim();
565
+ }
566
+ if (flags.version !== undefined) {
567
+ preset.version = String(flags.version).trim() || 'latest';
568
+ }
569
+ if (flags['docker-registry'] !== undefined && String(flags['docker-registry']).trim() !== '') {
570
+ preset.dockerRegistry = String(flags['docker-registry']).trim();
571
+ }
572
+ if (flags['docker-platform'] !== undefined && String(flags['docker-platform']).trim() !== '') {
573
+ preset.dockerPlatform = String(flags['docker-platform']).trim();
574
+ }
575
+ if (flags['output-dir'] !== undefined && String(flags['output-dir']).trim() !== '') {
576
+ preset.outputDir = String(flags['output-dir']).trim();
577
+ }
578
+ if (flags['git-url'] !== undefined && String(flags['git-url']).trim() !== '') {
579
+ preset.gitUrl = String(flags['git-url']).trim();
580
+ }
581
+ if (flags['npm-registry'] !== undefined) {
582
+ preset.npmRegistry = String(flags['npm-registry'] ?? '');
583
+ }
584
+ if (argvHasToken(argv, ['--replace', '-r'])) {
585
+ preset.replace = Boolean(flags.replace);
586
+ }
587
+ if (argvHasToken(argv, ['--dev-dependencies', '--no-dev-dependencies', '-D'])) {
588
+ preset.devDependencies = Boolean(flags['dev-dependencies']);
589
+ }
590
+ if (argvHasToken(argv, ['--docker-save', '--no-docker-save'])) {
591
+ preset.dockerSave = Boolean(flags['docker-save']);
592
+ }
593
+ if (argvHasToken(argv, ['--build', '--no-build'])) {
594
+ preset.build = Boolean(flags.build);
595
+ }
596
+ if (argvHasToken(argv, ['--build-dts', '--no-build-dts'])) {
597
+ preset.buildDts = Boolean(flags['build-dts']);
598
+ }
599
+ if (argvHasToken(argv, ['--builtin-db'])) {
600
+ preset.builtinDb = Boolean(flags['builtin-db']);
601
+ }
602
+ if (argvHasToken(argv, ['--install-skills'])) {
603
+ preset.installSkills = Boolean(flags['install-skills']);
604
+ }
605
+ else if (this.hasAgentsDirInCwd()) {
606
+ preset.installSkills = false;
607
+ }
608
+ return preset;
609
+ }
610
+ hasAgentsDirInCwd() {
611
+ return existsSync(path.resolve(process.cwd(), '.agents'));
612
+ }
613
+ async persistManagedEnvConfig(results) {
614
+ const envName = String(results.appName ?? DEFAULT_INIT_APP_NAME).trim() || DEFAULT_INIT_APP_NAME;
615
+ const appPort = String(results.appPort ?? '').trim();
616
+ const source = String(results.source ?? '').trim();
617
+ const version = String(results.version ?? '').trim();
618
+ const dockerRegistry = String(results.dockerRegistry ?? '').trim();
619
+ const dockerPlatform = String(results.dockerPlatform ?? '').trim();
620
+ const gitUrl = String(results.gitUrl ?? '').trim();
621
+ const npmRegistry = String(results.npmRegistry ?? '').trim();
622
+ const appRootPath = String(results.appRootPath ?? '').trim();
623
+ const storagePath = String(results.storagePath ?? '').trim();
624
+ const dbDialect = String(results.dbDialect ?? '').trim();
625
+ const builtinDbImage = String(results.builtinDbImage ?? '').trim();
626
+ const dbHost = String(results.dbHost ?? '').trim();
627
+ const dbPort = String(results.dbPort ?? '').trim();
628
+ const dbDatabase = String(results.dbDatabase ?? '').trim();
629
+ const dbUser = String(results.dbUser ?? '').trim();
630
+ const dbPassword = String(results.dbPassword ?? '');
631
+ await upsertEnv(envName, {
632
+ ...(appPort ? { baseUrl: `http://127.0.0.1:${appPort}/api` } : {}),
633
+ ...(source ? { source } : {}),
634
+ ...(version ? { downloadVersion: version } : {}),
635
+ ...(dockerRegistry ? { dockerRegistry } : {}),
636
+ ...(dockerPlatform ? { dockerPlatform } : {}),
637
+ ...(gitUrl ? { gitUrl } : {}),
638
+ ...(npmRegistry ? { npmRegistry } : {}),
639
+ ...(appRootPath ? { appRootPath } : {}),
640
+ ...(storagePath ? { storagePath } : {}),
641
+ ...(appPort ? { appPort } : {}),
642
+ ...(results.devDependencies !== undefined ? { devDependencies: Boolean(results.devDependencies) } : {}),
643
+ ...(results.build !== undefined ? { build: Boolean(results.build) } : {}),
644
+ ...(results.buildDts !== undefined ? { buildDts: Boolean(results.buildDts) } : {}),
645
+ ...(results.builtinDb !== undefined ? { builtinDb: Boolean(results.builtinDb) } : {}),
646
+ ...(dbDialect ? { dbDialect } : {}),
647
+ ...(builtinDbImage || results.builtinDb === false ? { builtinDbImage: builtinDbImage || undefined } : {}),
648
+ ...(dbHost ? { dbHost } : {}),
649
+ ...(dbPort ? { dbPort } : {}),
650
+ ...(dbDatabase ? { dbDatabase } : {}),
651
+ ...(dbUser ? { dbUser } : {}),
652
+ ...(dbPassword ? { dbPassword } : {}),
653
+ }, { scope: CONFIG_SCOPE });
654
+ }
655
+ buildEnvAddArgv(results) {
656
+ const argv = [String(results.appName ?? DEFAULT_INIT_APP_NAME)];
657
+ argv.push('--no-intro');
658
+ argv.push('--scope', CONFIG_SCOPE);
659
+ argv.push('--api-base-url', String(results.apiBaseUrl ?? DEFAULT_INIT_API_BASE_URL));
660
+ argv.push('--auth-type', String(results.authType ?? 'oauth'));
661
+ if (results.authType === 'token') {
662
+ argv.push('--access-token', String(results.accessToken ?? ''));
663
+ }
664
+ return argv;
665
+ }
666
+ buildInstallArgv(results, flags, options) {
667
+ const argv = ['--no-intro'];
668
+ if (options?.nonInteractive ?? true) {
669
+ argv.unshift('-y');
670
+ }
671
+ const processArgv = process.argv.slice(2);
672
+ const envName = String(results.appName ?? DEFAULT_INIT_APP_NAME).trim() || DEFAULT_INIT_APP_NAME;
673
+ const source = String(results.source ?? '').trim();
674
+ argv.push('--env', envName);
675
+ if (options?.resume) {
676
+ argv.push('--resume');
677
+ }
678
+ const lang = String(results.lang ?? '').trim();
679
+ if (lang) {
680
+ argv.push('--lang', lang);
681
+ }
682
+ const appRootPath = String(results.appRootPath ?? '').trim();
683
+ if (appRootPath) {
684
+ argv.push('--app-root-path', appRootPath);
685
+ }
686
+ const appPort = String(results.appPort ?? '').trim();
687
+ if (appPort && (!flags.yes || argvHasToken(processArgv, ['--app-port']) || appPort !== '13000')) {
688
+ argv.push('--app-port', appPort);
689
+ }
690
+ const storagePath = String(results.storagePath ?? '').trim();
691
+ if (storagePath) {
692
+ argv.push('--storage-path', storagePath);
693
+ }
694
+ if (Boolean(flags.force)) {
695
+ argv.push('--force');
696
+ }
697
+ if (Boolean(results.fetchSource)) {
698
+ argv.push('--fetch-source');
699
+ if (source) {
700
+ argv.push('--source', source);
701
+ }
702
+ const version = String(results.version ?? '').trim();
703
+ if (version) {
704
+ argv.push('--version', version);
705
+ }
706
+ const outputDir = String(results.outputDir ?? '').trim();
707
+ if (outputDir) {
708
+ argv.push('--output-dir', outputDir);
709
+ }
710
+ const gitUrl = String(results.gitUrl ?? '').trim();
711
+ if (gitUrl) {
712
+ argv.push('--git-url', gitUrl);
713
+ }
714
+ const dockerRegistry = String(results.dockerRegistry ?? '').trim();
715
+ if (dockerRegistry) {
716
+ argv.push('--docker-registry', dockerRegistry);
717
+ }
718
+ const dockerPlatform = String(results.dockerPlatform ?? '').trim();
719
+ if (dockerPlatform) {
720
+ argv.push('--docker-platform', dockerPlatform);
721
+ }
722
+ const npmRegistry = String(results.npmRegistry ?? '').trim();
723
+ if (npmRegistry) {
724
+ argv.push('--npm-registry', npmRegistry);
725
+ }
726
+ if (Boolean(results.replace)) {
727
+ argv.push('--replace');
728
+ }
729
+ if (Boolean(results.devDependencies)) {
730
+ argv.push('--dev-dependencies');
731
+ }
732
+ if (Boolean(results.dockerSave)) {
733
+ argv.push('--docker-save');
734
+ }
735
+ if (results.build !== undefined && !Boolean(results.build)) {
736
+ argv.push('--no-build');
737
+ }
738
+ else if (argvHasToken(processArgv, ['--build', '--no-build']) && flags.build === false) {
739
+ argv.push('--no-build');
740
+ }
741
+ if (Boolean(results.buildDts)) {
742
+ argv.push('--build-dts');
743
+ }
744
+ }
745
+ const builtinDb = Boolean(results.builtinDb);
746
+ if (builtinDb) {
747
+ argv.push('--builtin-db');
748
+ }
749
+ const dbDialect = String(results.dbDialect ?? '').trim();
750
+ if (dbDialect) {
751
+ argv.push('--db-dialect', dbDialect);
752
+ }
753
+ const builtinDbImage = String(results.builtinDbImage ?? '').trim();
754
+ if (builtinDb && builtinDbImage) {
755
+ argv.push('--builtin-db-image', builtinDbImage);
756
+ }
757
+ const dbHost = String(results.dbHost ?? '').trim();
758
+ if (dbHost) {
759
+ argv.push('--db-host', dbHost);
760
+ }
761
+ const dbPort = String(results.dbPort ?? '').trim();
762
+ const dbPortWasProvided = argvHasToken(processArgv, ['--db-port']);
763
+ const dockerBuiltinDbPortIsHidden = builtinDb && source === 'docker';
764
+ const dbDefaultPort = defaultDbPortForDialect(dbDialect);
765
+ if (dbPort
766
+ && (!dockerBuiltinDbPortIsHidden || dbPortWasProvided)
767
+ && (!flags.yes || dbPortWasProvided || dbPort !== dbDefaultPort)) {
768
+ argv.push('--db-port', dbPort);
769
+ }
770
+ const dbDatabase = String(results.dbDatabase ?? '').trim();
771
+ if (dbDatabase) {
772
+ argv.push('--db-database', dbDatabase);
773
+ }
774
+ const dbUser = String(results.dbUser ?? '').trim();
775
+ if (dbUser) {
776
+ argv.push('--db-user', dbUser);
777
+ }
778
+ const dbPassword = String(results.dbPassword ?? '');
779
+ if (dbPassword) {
780
+ argv.push('--db-password', dbPassword);
781
+ }
782
+ const rootUsername = String(results.rootUsername ?? '').trim();
783
+ if (rootUsername) {
784
+ argv.push('--root-username', rootUsername);
785
+ }
786
+ const rootEmail = String(results.rootEmail ?? '').trim();
787
+ if (rootEmail) {
788
+ argv.push('--root-email', rootEmail);
789
+ }
790
+ const rootPassword = String(results.rootPassword ?? '');
791
+ if (rootPassword) {
792
+ argv.push('--root-password', rootPassword);
793
+ }
794
+ const rootNickname = String(results.rootNickname ?? '').trim();
795
+ if (rootNickname) {
796
+ argv.push('--root-nickname', rootNickname);
797
+ }
798
+ return argv;
799
+ }
800
+ buildResumeInstallArgv(flags) {
801
+ return this.buildInstallArgv(this.buildPresetValuesFromFlags(flags), flags, {
802
+ nonInteractive: Boolean(flags.yes),
803
+ resume: true,
804
+ });
805
+ }
186
806
  }