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

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.
@@ -1,53 +1,198 @@
1
- import { Command, Flags } from '@oclif/core';
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { Args, Command, Flags } from '@oclif/core';
10
+ import * as p from '@clack/prompts';
2
11
  import { upsertEnv } from '../../lib/auth-store.js';
3
12
  import { formatCliHomeScope } from '../../lib/cli-home.js';
4
- import { isInteractiveTerminal, printVerbose, promptText, setVerboseMode } from '../../lib/ui.js';
13
+ import { isInteractiveTerminal, printVerbose, setVerboseMode } from '../../lib/ui.js';
5
14
  export default class EnvAdd extends Command {
6
- static summary = 'Add or update a NocoBase environment';
7
- static id = 'env add';
15
+ static summary = 'Save a named NocoBase API endpoint (token or OAuth), then switch the CLI to use it';
16
+ static examples = [
17
+ '<%= config.bin %> <%= command.id %>',
18
+ '<%= config.bin %> <%= command.id %> local',
19
+ '<%= config.bin %> <%= command.id %> local --scope project --api-base-url http://localhost:13000/api --auth-type oauth',
20
+ ];
21
+ static args = {
22
+ name: Args.string({
23
+ description: 'Label for this environment (optional first argument; in a TTY, prompted when omitted; required when not using a TTY)',
24
+ required: false,
25
+ }),
26
+ };
8
27
  static flags = {
28
+ env: Flags.string({
29
+ char: 'e',
30
+ hidden: true,
31
+ deprecated: true,
32
+ description: 'Environment name (same as the optional positional argument; for compatibility with -e/--env on other commands)',
33
+ }),
9
34
  verbose: Flags.boolean({
10
- description: 'Show detailed progress output',
35
+ description: 'Print detailed progress while writing config',
11
36
  default: false,
12
37
  }),
13
- name: Flags.string({
14
- description: 'Environment name',
15
- default: 'default',
16
- }),
17
38
  scope: Flags.string({
18
39
  char: 's',
19
- description: 'Config scope',
40
+ description: 'Where to store env config: project (.nocobase in the repo) or global (user-level); prompted in a TTY when omitted',
20
41
  options: ['project', 'global'],
21
42
  }),
22
- 'base-url': Flags.string({
23
- description: 'NocoBase API base URL, for example http://localhost:13000/api',
43
+ 'api-base-url': Flags.string({
44
+ char: 'u',
45
+ aliases: ['base-url'],
46
+ description: 'Root URL for HTTP API calls, including the /api prefix (e.g. http://localhost:13000/api); prompted in a TTY when omitted',
24
47
  }),
25
- token: Flags.string({
48
+ 'auth-type': Flags.string({
49
+ char: 'a',
50
+ description: 'Authentication: token (API key) or oauth (browser login via `nb env auth`); prompted in a TTY when omitted',
51
+ options: ['token', 'oauth'],
52
+ }),
53
+ 'access-token': Flags.string({
26
54
  char: 't',
27
- description: 'API key',
55
+ aliases: ['token'],
56
+ description: 'API key or access token when using --auth-type token (prompted in a TTY when omitted)',
28
57
  }),
29
58
  };
59
+ exitCancelled() {
60
+ p.cancel('Cancelled.');
61
+ this.exit(0);
62
+ }
30
63
  async run() {
31
- const { flags } = await this.parse(EnvAdd);
64
+ const { args, flags } = await this.parse(EnvAdd);
32
65
  setVerboseMode(flags.verbose);
33
- const name = flags.name || 'default';
34
- const scope = flags.scope;
35
- const baseUrl = flags['base-url'] ||
36
- (isInteractiveTerminal()
37
- ? await promptText('Base URL', { defaultValue: 'http://localhost:13000/api' })
38
- : '');
39
- if (Object.keys(flags).includes('token') && !flags.token) {
40
- flags.token = isInteractiveTerminal() ? await promptText('API key (optional)', { secret: true }) : '';
41
- if (!flags.token) {
42
- this.error('API key cannot be empty if --token flag is provided without a value.');
43
- }
44
- }
45
- const token = flags.token;
46
- if (!baseUrl) {
47
- this.error('Missing base URL. Pass `--base-url <url>` or run in a TTY to enter it interactively.');
48
- }
49
- printVerbose(`Saving env "${name}" with base URL ${baseUrl}`);
50
- await upsertEnv(name, baseUrl, token, { scope });
51
- this.log(`Saved env "${name}" and set it as current${scope ? ` in ${formatCliHomeScope(scope)} scope` : ''}.`);
66
+ const nameArg = args.name?.trim();
67
+ const nameFlag = flags.env?.trim() || undefined;
68
+ if (nameArg && nameFlag && nameArg !== nameFlag) {
69
+ this.error(`Environment name was given both as the argument ("${nameArg}") and as --env ("${nameFlag}"); use only one.`);
70
+ }
71
+ let name = nameArg || nameFlag || undefined;
72
+ let scope = flags.scope;
73
+ let baseUrl = flags['api-base-url'] ?? flags['base-url'];
74
+ let authType = flags['auth-type'];
75
+ const interactive = isInteractiveTerminal();
76
+ if (!interactive) {
77
+ const missing = [];
78
+ if (!name?.trim()) {
79
+ missing.push('<name> (first argument or --env)');
80
+ }
81
+ if (!scope) {
82
+ missing.push('--scope');
83
+ }
84
+ if (!baseUrl) {
85
+ missing.push('--api-base-url');
86
+ }
87
+ if (!authType) {
88
+ missing.push('--auth-type');
89
+ }
90
+ if (missing.length > 0) {
91
+ this.error(`Non-interactive mode requires: ${missing.join(', ')}. Example: nb env add -e local --scope project --api-base-url http://localhost:13000/api --auth-type oauth`);
92
+ }
93
+ }
94
+ else {
95
+ if (!name?.trim()) {
96
+ const answer = await p.text({
97
+ message: 'Environment name',
98
+ placeholder: 'default',
99
+ defaultValue: 'default',
100
+ });
101
+ if (p.isCancel(answer)) {
102
+ this.exitCancelled();
103
+ }
104
+ name = answer;
105
+ }
106
+ if (!scope) {
107
+ const answer = await p.select({
108
+ message: 'Where should this env be stored?',
109
+ options: [
110
+ { value: 'project', label: 'Project', hint: '.nocobase in this repo' },
111
+ { value: 'global', label: 'Global', hint: 'user-level config' },
112
+ ],
113
+ initialValue: 'project',
114
+ });
115
+ if (p.isCancel(answer)) {
116
+ this.exitCancelled();
117
+ }
118
+ scope = answer;
119
+ }
120
+ if (!baseUrl) {
121
+ const answer = await p.text({
122
+ message: 'API base URL',
123
+ placeholder: 'http://localhost:13000/api',
124
+ defaultValue: 'http://localhost:13000/api',
125
+ });
126
+ if (p.isCancel(answer)) {
127
+ this.exitCancelled();
128
+ }
129
+ baseUrl = answer;
130
+ }
131
+ if (!authType) {
132
+ const answer = await p.select({
133
+ message: 'How do you want to authenticate?',
134
+ options: [
135
+ { value: 'oauth', label: 'OAuth (browser login)', hint: 'runs nb env auth after save' },
136
+ { value: 'token', label: 'API token / API key' },
137
+ ],
138
+ initialValue: 'oauth',
139
+ });
140
+ if (p.isCancel(answer)) {
141
+ this.exitCancelled();
142
+ }
143
+ authType = answer;
144
+ }
145
+ }
146
+ const accessTokenKeys = ['access-token', 'token'];
147
+ const accessTokenFlagPresent = accessTokenKeys.some((key) => Object.prototype.hasOwnProperty.call(flags, key));
148
+ if (accessTokenFlagPresent && !flags['access-token'] && !flags['token']) {
149
+ if (!interactive) {
150
+ this.error('When passing --access-token (or --token) without a value, run in a TTY or provide the token as the flag value.');
151
+ }
152
+ const prompted = await p.password({
153
+ message: 'Access token / API key',
154
+ validate: (value) => (value.trim() ? undefined : 'Token cannot be empty'),
155
+ });
156
+ if (p.isCancel(prompted)) {
157
+ this.exitCancelled();
158
+ }
159
+ flags['access-token'] = prompted;
160
+ }
161
+ let token = flags['access-token'] ?? flags['token'];
162
+ if (!name?.trim()) {
163
+ this.error('Environment name cannot be empty.');
164
+ }
165
+ if (!baseUrl?.trim()) {
166
+ this.error('API base URL cannot be empty.');
167
+ }
168
+ name = name.trim();
169
+ baseUrl = baseUrl.trim();
170
+ if (authType === 'token') {
171
+ if (!token && interactive) {
172
+ const answer = await p.password({
173
+ message: 'Access token / API key',
174
+ validate: (value) => (value.trim() ? undefined : 'Token cannot be empty'),
175
+ });
176
+ if (p.isCancel(answer)) {
177
+ this.exitCancelled();
178
+ }
179
+ token = answer;
180
+ }
181
+ if (!token?.trim()) {
182
+ this.error('Auth type token requires an access token. Pass `--access-token`, or run in a TTY to enter it.');
183
+ }
184
+ printVerbose(`Saving env "${name}" with API base URL ${baseUrl} (token auth)`);
185
+ await upsertEnv(name, { baseUrl, accessToken: token }, { scope });
186
+ this.log(`Saved env "${name}" and set it as current${scope ? ` in ${formatCliHomeScope(scope)} scope` : ''}.`);
187
+ return;
188
+ }
189
+ printVerbose(`Saving env "${name}" with API base URL ${baseUrl} (OAuth next)`);
190
+ await upsertEnv(name, { baseUrl }, { scope });
191
+ this.log(`Saved env "${name}"${scope ? ` in ${formatCliHomeScope(scope)} scope` : ''}. Starting OAuth login (\`nb env auth ${name}\`).`);
192
+ const authArgv = [name];
193
+ if (scope) {
194
+ authArgv.push('-s', scope);
195
+ }
196
+ await this.config.runCommand('env:auth', authArgv);
52
197
  }
53
198
  }
@@ -1,14 +1,33 @@
1
- import { Command, Flags } from '@oclif/core';
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { Args, Command, Flags } from '@oclif/core';
10
+ import { getCurrentEnvName } from '../../lib/auth-store.js';
2
11
  import { formatCliHomeScope } from '../../lib/cli-home.js';
3
12
  import { authenticateEnvWithOauth } from '../../lib/env-auth.js';
4
13
  import { failTask, startTask, succeedTask } from '../../lib/ui.js';
5
14
  export default class EnvAuth extends Command {
6
15
  static summary = 'Authenticate an environment with OAuth';
7
- static id = 'env auth';
16
+ static examples = [
17
+ '<%= config.bin %> <%= command.id %> prod',
18
+ ];
19
+ static args = {
20
+ name: Args.string({
21
+ description: 'Environment name (omit to use the current env)',
22
+ required: true,
23
+ }),
24
+ };
8
25
  static flags = {
9
26
  env: Flags.string({
10
27
  char: 'e',
11
- description: 'Environment name',
28
+ hidden: true,
29
+ deprecated: true,
30
+ description: 'Environment name (same as the optional positional argument; for compatibility with -e/--env on other commands)',
12
31
  }),
13
32
  scope: Flags.string({
14
33
  char: 's',
@@ -17,13 +36,19 @@ export default class EnvAuth extends Command {
17
36
  }),
18
37
  };
19
38
  async run() {
20
- const { flags } = await this.parse(EnvAuth);
39
+ const { args, flags } = await this.parse(EnvAuth);
21
40
  const scope = flags.scope;
22
- const envLabel = flags.env ?? 'current';
41
+ const nameArg = args.name?.trim();
42
+ const nameFlag = flags.env?.trim() || undefined;
43
+ if (nameArg && nameFlag && nameArg !== nameFlag) {
44
+ this.error(`Environment name was given both as the argument ("${nameArg}") and as --env ("${nameFlag}"); use only one.`);
45
+ }
46
+ const envName = nameArg || nameFlag || undefined;
47
+ const envLabel = envName ?? (await getCurrentEnvName({ scope }));
23
48
  startTask(`Authenticating env: ${envLabel}${scope ? ` (${formatCliHomeScope(scope)})` : ''}`);
24
49
  try {
25
50
  await authenticateEnvWithOauth({
26
- envName: flags.env,
51
+ envName,
27
52
  scope,
28
53
  });
29
54
  succeedTask(`Authenticated env "${envLabel}" with OAuth${scope ? ` in ${formatCliHomeScope(scope)} scope` : ''}.`);
@@ -1,10 +1,20 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
1
9
  import { Command, Flags } from '@oclif/core';
2
10
  import { listEnvs } from '../../lib/auth-store.js';
3
11
  import { formatCliHomeScope } from '../../lib/cli-home.js';
4
12
  import { renderTable } from '../../lib/ui.js';
5
13
  export default class EnvList extends Command {
6
14
  static summary = 'List configured environments';
7
- static id = 'env list';
15
+ static examples = [
16
+ '<%= config.bin %> <%= command.id %>',
17
+ ];
8
18
  static flags = {
9
19
  scope: Flags.string({
10
20
  char: 's',
@@ -19,7 +29,7 @@ export default class EnvList extends Command {
19
29
  const names = Object.keys(envs).sort();
20
30
  if (!names.length) {
21
31
  this.log(`No envs configured${scope ? ` in ${formatCliHomeScope(scope)} scope` : ''}.`);
22
- this.log('Run `nb env add --name <name> --base-url <url>` to add one.');
32
+ this.log('Run `nb env add <name> --base-url <url>` to add one.');
23
33
  return;
24
34
  }
25
35
  const rows = names.map((name) => {
@@ -1,10 +1,21 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
1
9
  import { Args, Command, Flags } from '@oclif/core';
2
10
  import { getCurrentEnvName, removeEnv } from '../../lib/auth-store.js';
3
11
  import { formatCliHomeScope } from '../../lib/cli-home.js';
4
12
  import { confirmAction, isInteractiveTerminal, printVerbose, setVerboseMode } from '../../lib/ui.js';
5
13
  export default class EnvRemove extends Command {
6
- static id = 'env remove';
7
14
  static summary = 'Remove a configured environment';
15
+ static examples = [
16
+ '<%= config.bin %> <%= command.id %> staging',
17
+ '<%= config.bin %> <%= command.id %> staging -f',
18
+ ];
8
19
  static flags = {
9
20
  force: Flags.boolean({
10
21
  char: 'f',
@@ -1,22 +1,36 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
1
9
  import path from 'node:path';
2
10
  import { fileURLToPath } from 'node:url';
3
- import { Command, Flags } from '@oclif/core';
11
+ import { Args, Command, Flags } from '@oclif/core';
12
+ import { getCurrentEnvName } from '../../lib/auth-store.js';
4
13
  import { updateEnvRuntime } from '../../lib/bootstrap.js';
5
14
  import { formatCliHomeScope } from '../../lib/cli-home.js';
6
15
  import { failTask, startTask, succeedTask } from '../../lib/ui.js';
7
16
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
17
  export default class EnvUpdate extends Command {
9
18
  static summary = 'Refresh an environment runtime from swagger:get and persist connection overrides';
10
- static id = 'env update';
19
+ static examples = [
20
+ '<%= config.bin %> <%= command.id %>',
21
+ '<%= config.bin %> <%= command.id %> prod',
22
+ ];
23
+ static args = {
24
+ name: Args.string({
25
+ description: 'Environment name (omit to use the current env)',
26
+ required: false,
27
+ }),
28
+ };
11
29
  static flags = {
12
30
  verbose: Flags.boolean({
13
31
  description: 'Show detailed progress output',
14
32
  default: false,
15
33
  }),
16
- env: Flags.string({
17
- char: 'e',
18
- description: 'Environment name',
19
- }),
20
34
  scope: Flags.string({
21
35
  char: 's',
22
36
  description: 'Config scope',
@@ -34,13 +48,14 @@ export default class EnvUpdate extends Command {
34
48
  }),
35
49
  };
36
50
  async run() {
37
- const { flags } = await this.parse(EnvUpdate);
51
+ const { args, flags } = await this.parse(EnvUpdate);
38
52
  const scope = flags.scope;
39
- const envLabel = flags.env ?? 'current';
53
+ const envName = args.name;
54
+ const envLabel = envName ?? (await getCurrentEnvName({ scope }));
40
55
  startTask(`Updating env runtime: ${envLabel}${scope ? ` (${formatCliHomeScope(scope)})` : ''}`);
41
56
  try {
42
57
  const runtime = await updateEnvRuntime({
43
- envName: flags.env,
58
+ envName,
44
59
  scope,
45
60
  baseUrl: flags['base-url'],
46
61
  role: flags.role,
@@ -1,9 +1,19 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
1
9
  import { Args, Command, Flags } from '@oclif/core';
2
10
  import { setCurrentEnv } from '../../lib/auth-store.js';
3
11
  import { formatCliHomeScope } from '../../lib/cli-home.js';
4
12
  export default class EnvUse extends Command {
5
13
  static summary = 'Switch the current environment';
6
- static id = 'env use';
14
+ static examples = [
15
+ '<%= config.bin %> <%= command.id %> local',
16
+ ];
7
17
  static flags = {
8
18
  scope: Flags.string({
9
19
  char: 's',
@@ -0,0 +1,186 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { Command, Flags } from '@oclif/core';
10
+ import * as p from '@clack/prompts';
11
+ import pc from 'picocolors';
12
+ import { stdin as stdinStream, stdout as stdoutStream } from 'node:process';
13
+ import { buildEnvAddArgv, InitWizardCancelledError, runInitBrowserWizard, } from "../lib/init-browser-wizard.js";
14
+ import { run } from "../lib/run-npm.js";
15
+ 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):
18
+
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).
22
+
23
+ Internal ordering: (skills?) → (already have an app? → env add | install only).
24
+
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.
26
+
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\`.`;
28
+ static examples = [
29
+ '<%= config.bin %> <%= command.id %>',
30
+ '<%= 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',
33
+ ];
34
+ static flags = {
35
+ yes: Flags.boolean({
36
+ char: 'y',
37
+ description: 'Skip all prompts',
38
+ default: false,
39
+ }),
40
+ ui: Flags.boolean({
41
+ description: 'Open a browser-based setup wizard (local HTTP server; not valid with --yes)',
42
+ default: false,
43
+ }),
44
+ 'ui-host': Flags.string({
45
+ description: 'Bind address for the --ui wizard HTTP server (default 0.0.0.0; only with --ui)',
46
+ }),
47
+ 'ui-port': Flags.integer({
48
+ description: 'TCP port for the --ui wizard; 0 = OS-assigned ephemeral port (default 0; only with --ui)',
49
+ min: 0,
50
+ max: 65535,
51
+ }),
52
+ };
53
+ async run() {
54
+ const { flags } = await this.parse(Init);
55
+ if (flags.ui && flags.yes) {
56
+ this.error('--ui cannot be used with --yes.');
57
+ }
58
+ if (!flags.ui &&
59
+ (flags['ui-host'] !== undefined || flags['ui-port'] !== undefined)) {
60
+ this.error('--ui-host and --ui-port require --ui.');
61
+ }
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')}`);
67
+ }
68
+ else {
69
+ this.log('nb init — browser wizard');
70
+ }
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
+ 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
+ }
99
+ }
100
+ 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;
111
+ }
112
+ }
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';
142
+ }
143
+ 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).');
148
+ }
149
+ if (installSkills) {
150
+ try {
151
+ p.log.step('Installing NocoBase agent skills (npx -y skills add nocobase/skills)');
152
+ await run('npx', ['-y', 'skills', 'add', 'nocobase/skills', '-y']);
153
+ }
154
+ catch (error) {
155
+ const message = error instanceof Error ? error.message : String(error);
156
+ p.outro(pc.red(`Skills install failed: ${message}`));
157
+ this.error(message);
158
+ }
159
+ }
160
+ else {
161
+ p.log.info('Skipped NocoBase agent skills install.');
162
+ }
163
+ try {
164
+ // oclif explicit registry keys use `:` (e.g. `env:add`); users still type `nb env add`.
165
+ if (hasNocobase) {
166
+ 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);
173
+ }
174
+ else {
175
+ p.log.step('Running nb install');
176
+ await this.config.runCommand('install', skipInstallPrompts ? ['-e', 'local', '-y'] : []);
177
+ }
178
+ }
179
+ catch (error) {
180
+ const message = error instanceof Error ? error.message : String(error);
181
+ p.outro(pc.red(message));
182
+ this.error(message);
183
+ }
184
+ p.outro('Workspace init finished.');
185
+ }
186
+ }