@nocobase/cli 2.1.0-beta.35 → 2.1.0-beta.37

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 (39) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/bin/run.js +3 -2
  4. package/dist/commands/app/upgrade.js +38 -16
  5. package/dist/commands/backup/create.js +147 -0
  6. package/dist/commands/backup/index.js +20 -0
  7. package/dist/commands/backup/restore.js +105 -0
  8. package/dist/commands/config/delete.js +4 -0
  9. package/dist/commands/config/get.js +4 -0
  10. package/dist/commands/config/set.js +5 -1
  11. package/dist/commands/env/add.js +129 -15
  12. package/dist/commands/env/auth.js +145 -12
  13. package/dist/commands/env/info.js +52 -8
  14. package/dist/commands/env/list.js +2 -2
  15. package/dist/commands/env/shared.js +41 -3
  16. package/dist/commands/init.js +254 -136
  17. package/dist/commands/install.js +447 -272
  18. package/dist/commands/license/activate.js +6 -4
  19. package/dist/commands/source/publish.js +17 -0
  20. package/dist/commands/v1.js +210 -0
  21. package/dist/lib/app-managed-resources.js +20 -1
  22. package/dist/lib/app-runtime.js +13 -4
  23. package/dist/lib/auth-store.js +69 -18
  24. package/dist/lib/backup.js +171 -0
  25. package/dist/lib/bootstrap.js +23 -13
  26. package/dist/lib/cli-config.js +99 -4
  27. package/dist/lib/cli-locale.js +19 -7
  28. package/dist/lib/db-connection-check.js +61 -0
  29. package/dist/lib/env-auth.js +79 -0
  30. package/dist/lib/env-config.js +8 -1
  31. package/dist/lib/prompt-validators.js +23 -5
  32. package/dist/lib/prompt-web-ui.js +143 -19
  33. package/dist/lib/run-npm.js +166 -30
  34. package/dist/lib/skills-manager.js +74 -4
  35. package/dist/lib/source-publish.js +20 -1
  36. package/dist/lib/source-registry.js +2 -2
  37. package/dist/locale/en-US.json +36 -5
  38. package/dist/locale/zh-CN.json +36 -5
  39. package/package.json +6 -3
@@ -21,11 +21,11 @@ async function promptActivationMode() {
21
21
  return await select({
22
22
  message: 'How do you want to activate the license?',
23
23
  choices: [
24
- { value: 'key', name: 'Use an existing license key' },
25
24
  { value: 'online', name: 'Request and activate a license online' },
25
+ { value: 'key', name: 'Use an existing license key' },
26
26
  { value: 'cancel', name: 'Cancel' },
27
27
  ],
28
- default: 'key',
28
+ default: 'online',
29
29
  });
30
30
  }
31
31
  catch {
@@ -70,7 +70,7 @@ async function promptLicenseKeyInput() {
70
70
  return {};
71
71
  }
72
72
  }
73
- async function promptOnlineActivationInput(initial) {
73
+ async function promptOnlineActivationInput(initial, defaultAppName) {
74
74
  let account = String(initial.account ?? '').trim();
75
75
  if (!account) {
76
76
  try {
@@ -107,8 +107,10 @@ async function promptOnlineActivationInput(initial) {
107
107
  let appName = String(initial.appName ?? '').trim();
108
108
  if (!appName) {
109
109
  try {
110
+ const resolvedDefaultAppName = String(defaultAppName ?? '').trim();
110
111
  const answer = await input({
111
112
  message: 'Application name',
113
+ default: resolvedDefaultAppName || undefined,
112
114
  validate: (value) => String(value ?? '').trim() ? true : 'Application name is required.',
113
115
  });
114
116
  appName = String(answer ?? '').trim();
@@ -259,7 +261,7 @@ export default class LicenseActivate extends Command {
259
261
  if (!isInteractiveTerminal()) {
260
262
  this.error('Online activation requires --account, --password, and --desc when not using a TTY.');
261
263
  }
262
- const prompted = await promptOnlineActivationInput(initialOnline);
264
+ const prompted = await promptOnlineActivationInput(initialOnline, runtime.envName);
263
265
  if (!prompted) {
264
266
  return;
265
267
  }
@@ -10,6 +10,11 @@ import { Command, Flags } from '@oclif/core';
10
10
  import { buildSuggestedInitCommand, publishSourceSnapshot } from '../../lib/source-publish.js';
11
11
  import { failTask, printInfo, startTask, succeedTask } from '../../lib/ui.js';
12
12
  function formatPublishFailure(message) {
13
+ if (message.includes('The specified --cwd does not exist:')
14
+ || message.includes('The specified --cwd is not a directory:')
15
+ || message.includes('Couldn\'t find a NocoBase source project from --cwd:')) {
16
+ return message;
17
+ }
13
18
  return [
14
19
  'Couldn\'t publish a source snapshot.',
15
20
  'Check that Docker is running, the target npm registry is reachable, and the current directory is a NocoBase source repo.',
@@ -20,6 +25,8 @@ export default class SourcePublish extends Command {
20
25
  static description = 'Publish the current NocoBase source repo as a snapshot version to an npm registry for install testing.';
21
26
  static examples = [
22
27
  '<%= config.bin %> <%= command.id %> --snapshot',
28
+ '<%= config.bin %> <%= command.id %> --snapshot --no-build',
29
+ '<%= config.bin %> <%= command.id %> --snapshot --build-dts',
23
30
  '<%= config.bin %> <%= command.id %> --snapshot --cwd /path/to/nocobase/source',
24
31
  '<%= config.bin %> <%= command.id %> --snapshot --npm-registry=http://127.0.0.1:4873',
25
32
  '<%= config.bin %> <%= command.id %> --snapshot --json',
@@ -38,6 +45,14 @@ export default class SourcePublish extends Command {
38
45
  description: 'Source repository path. Defaults to the nearest detected NocoBase source root from the current working directory',
39
46
  required: false,
40
47
  }),
48
+ 'no-build': Flags.boolean({
49
+ description: 'Skip building the source repo before snapshot versioning and publish',
50
+ default: false,
51
+ }),
52
+ 'build-dts': Flags.boolean({
53
+ description: 'Generate TypeScript declaration files during the source build',
54
+ default: false,
55
+ }),
41
56
  json: Flags.boolean({
42
57
  description: 'Print the publish result as JSON',
43
58
  default: false,
@@ -59,6 +74,8 @@ export default class SourcePublish extends Command {
59
74
  const result = await publishSourceSnapshot({
60
75
  cwd: flags.cwd,
61
76
  npmRegistry: flags['npm-registry'],
77
+ build: !flags['no-build'],
78
+ buildDts: flags['build-dts'],
62
79
  verbose: flags.verbose,
63
80
  });
64
81
  if (flags.json) {
@@ -0,0 +1,210 @@
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 } from '@oclif/core';
10
+ import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runDockerNocoBaseCommand, runLocalNocoBaseCommand, } from '../lib/app-runtime.js';
11
+ import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../lib/env-guard.js';
12
+ import { announceTargetEnv } from '../lib/ui.js';
13
+ const SILENT_LIKE_PASSTHROUGH_FLAGS = new Set(['--help', '-h', '--silent']);
14
+ const SILENT_RUNTIME_ENV_VARS = {
15
+ LOGGER_SILENT: 'true',
16
+ NODE_NO_WARNINGS: '1',
17
+ };
18
+ const SILENT_STDERR_FILTERS = [
19
+ /^\(node:\d+\) \[DEP0040\] DeprecationWarning: The `punycode` module is deprecated\..*$/,
20
+ /^\(Use `node --trace-deprecation .*$/,
21
+ /^About to overwrite ArrayBuffer\.prototype properties /,
22
+ ];
23
+ function parseBridgeArgv(argv) {
24
+ let requestedEnv;
25
+ let yes = false;
26
+ const passthroughArgs = [];
27
+ for (let index = 0; index < argv.length; index += 1) {
28
+ const token = argv[index];
29
+ if (token === '--') {
30
+ passthroughArgs.push(...argv.slice(index + 1));
31
+ break;
32
+ }
33
+ if (token === '--env') {
34
+ const value = argv[index + 1];
35
+ if (!value || value === '--') {
36
+ throw new Error('Missing value for `--env`.');
37
+ }
38
+ requestedEnv = value.trim() || undefined;
39
+ index += 1;
40
+ continue;
41
+ }
42
+ if (token.startsWith('--env=')) {
43
+ requestedEnv = token.slice('--env='.length).trim() || undefined;
44
+ continue;
45
+ }
46
+ if (token === '-e') {
47
+ const value = argv[index + 1];
48
+ if (!value || value === '--') {
49
+ throw new Error('Missing value for `-e`.');
50
+ }
51
+ requestedEnv = value.trim() || undefined;
52
+ index += 1;
53
+ continue;
54
+ }
55
+ if (token.startsWith('-e') && token.length > 2) {
56
+ requestedEnv = token.slice(2).trim() || undefined;
57
+ continue;
58
+ }
59
+ if (token === '--yes') {
60
+ yes = true;
61
+ continue;
62
+ }
63
+ passthroughArgs.push(...argv.slice(index));
64
+ break;
65
+ }
66
+ return {
67
+ requestedEnv,
68
+ yes,
69
+ passthroughArgs,
70
+ };
71
+ }
72
+ function formatHttpEnvError(envName) {
73
+ return [
74
+ `Can't run \`nb v1\` for "${envName}" yet.`,
75
+ 'This env only has an API connection, so the v1 bridge is not available here.',
76
+ 'Use a local or Docker env instead.',
77
+ ].join('\n');
78
+ }
79
+ function formatSshEnvError(envName) {
80
+ return [
81
+ `Can't run \`nb v1\` for "${envName}" yet.`,
82
+ 'SSH env support is reserved but not implemented yet.',
83
+ 'Use a local or Docker env right now.',
84
+ ].join('\n');
85
+ }
86
+ function hasSilentLikePassthrough(args) {
87
+ return args.some((arg) => SILENT_LIKE_PASSTHROUGH_FLAGS.has(arg));
88
+ }
89
+ function shouldFilterSilentStderrLine(line) {
90
+ const normalized = line.replace(/\r$/, '');
91
+ return SILENT_STDERR_FILTERS.some((pattern) => pattern.test(normalized));
92
+ }
93
+ function createSilentBridgeOptions() {
94
+ let pendingStderr = '';
95
+ const flushBufferedStderr = (force) => {
96
+ while (true) {
97
+ const newlineIndex = pendingStderr.indexOf('\n');
98
+ if (newlineIndex === -1) {
99
+ break;
100
+ }
101
+ const line = pendingStderr.slice(0, newlineIndex);
102
+ pendingStderr = pendingStderr.slice(newlineIndex + 1);
103
+ if (!shouldFilterSilentStderrLine(line)) {
104
+ process.stderr.write(`${line}\n`);
105
+ }
106
+ }
107
+ if (force && pendingStderr) {
108
+ if (!shouldFilterSilentStderrLine(pendingStderr)) {
109
+ process.stderr.write(pendingStderr);
110
+ }
111
+ pendingStderr = '';
112
+ }
113
+ };
114
+ return {
115
+ commandOptions: {
116
+ stdio: 'pipe',
117
+ env: {
118
+ ...SILENT_RUNTIME_ENV_VARS,
119
+ },
120
+ onStdout: (chunk) => {
121
+ process.stdout.write(chunk);
122
+ },
123
+ onStderr: (chunk) => {
124
+ pendingStderr += chunk;
125
+ flushBufferedStderr(false);
126
+ },
127
+ },
128
+ flush: () => {
129
+ flushBufferedStderr(true);
130
+ },
131
+ };
132
+ }
133
+ export default class V1 extends Command {
134
+ static hidden = true;
135
+ static strict = false;
136
+ static summary = 'Forward commands to the selected env through the v1 bridge';
137
+ static description = 'Forward v1-compatible commands to the selected env. Defaults to the current env when `--env` is omitted. Local envs run `nocobase-v1`, and Docker envs run inside the saved app container. Bridge flags (`--env`, `--yes`) must appear before the forwarded command. Use `--` when the forwarded command needs the same flag names.';
138
+ static examples = [
139
+ '<%= config.bin %> <%= command.id %> build',
140
+ '<%= config.bin %> <%= command.id %> --env local pm list',
141
+ '<%= config.bin %> <%= command.id %> --env docker-local -- pm enable @nocobase/plugin-sample --yes',
142
+ ];
143
+ async run() {
144
+ const originalArgv = [...this.argv];
145
+ await this.parse({ strict: false, flags: {}, args: {} }, []);
146
+ this.argv = originalArgv;
147
+ let parsed;
148
+ try {
149
+ parsed = parseBridgeArgv(this.argv);
150
+ }
151
+ catch (error) {
152
+ const message = error instanceof Error ? error.message : String(error);
153
+ this.error(message);
154
+ }
155
+ const { requestedEnv, yes, passthroughArgs } = parsed;
156
+ if (passthroughArgs.length === 0) {
157
+ this.error('Pass at least one v1 command to forward.');
158
+ }
159
+ const explicitEnvSelection = Boolean(requestedEnv && hasExplicitEnvSelection(this.argv));
160
+ if (explicitEnvSelection) {
161
+ const confirmed = await ensureCrossEnvConfirmed({
162
+ command: this,
163
+ requestedEnv,
164
+ yes,
165
+ });
166
+ if (!confirmed) {
167
+ return;
168
+ }
169
+ }
170
+ const runtime = await resolveManagedAppRuntime(requestedEnv);
171
+ if (!runtime) {
172
+ this.error(formatMissingManagedAppEnvMessage(requestedEnv));
173
+ }
174
+ const silentLike = hasSilentLikePassthrough(passthroughArgs);
175
+ const silentBridge = silentLike ? createSilentBridgeOptions() : undefined;
176
+ if (!silentLike) {
177
+ announceTargetEnv(runtime.envName);
178
+ }
179
+ if (runtime.kind === 'local') {
180
+ try {
181
+ await runLocalNocoBaseCommand(runtime, passthroughArgs, silentBridge?.commandOptions);
182
+ }
183
+ catch (error) {
184
+ const message = error instanceof Error ? error.message : String(error);
185
+ this.error(message);
186
+ }
187
+ finally {
188
+ silentBridge?.flush();
189
+ }
190
+ return;
191
+ }
192
+ if (runtime.kind === 'docker') {
193
+ try {
194
+ await runDockerNocoBaseCommand(runtime.containerName, passthroughArgs, silentBridge?.commandOptions);
195
+ }
196
+ catch (error) {
197
+ const message = error instanceof Error ? error.message : String(error);
198
+ this.error(message);
199
+ }
200
+ finally {
201
+ silentBridge?.flush();
202
+ }
203
+ return;
204
+ }
205
+ if (runtime.kind === 'http') {
206
+ this.error(formatHttpEnvError(runtime.envName));
207
+ }
208
+ this.error(formatSshEnvError(runtime.envName));
209
+ }
210
+ }
@@ -32,6 +32,18 @@ function localSourceLabel(source) {
32
32
  function trimValue(value) {
33
33
  return String(value ?? '').trim();
34
34
  }
35
+ function pushOptionalEnvArg(args, key, value) {
36
+ if (typeof value === 'string') {
37
+ if (!value) {
38
+ return;
39
+ }
40
+ args.push('-e', `${key}=${value}`);
41
+ return;
42
+ }
43
+ if (typeof value === 'boolean') {
44
+ args.push('-e', `${key}=${String(value)}`);
45
+ }
46
+ }
35
47
  function normalizeDockerPlatform(value) {
36
48
  const text = trimValue(value);
37
49
  if (!text || text === 'auto') {
@@ -109,6 +121,9 @@ export async function buildSavedDockerRunArgs(runtime) {
109
121
  const dbDatabase = trimValue(config.dbDatabase);
110
122
  const dbUser = trimValue(config.dbUser);
111
123
  const dbPassword = trimValue(config.dbPassword);
124
+ const dbSchema = trimValue(config.dbSchema);
125
+ const dbTablePrefix = trimValue(config.dbTablePrefix);
126
+ const dbUnderscored = typeof config.dbUnderscored === 'boolean' ? config.dbUnderscored : undefined;
112
127
  const dockerRegistry = trimValue(config.dockerRegistry) || DEFAULT_DOCKER_REGISTRY;
113
128
  const version = trimValue(config.downloadVersion) || DEFAULT_DOCKER_VERSION;
114
129
  const imageRef = resolveDockerImageRef(dockerRegistry, version, {
@@ -163,7 +178,11 @@ export async function buildSavedDockerRunArgs(runtime) {
163
178
  if (envFile) {
164
179
  args.push('--env-file', envFile);
165
180
  }
166
- args.push('-e', `APP_KEY=${appKey}`, '-e', `DB_DIALECT=${dbDialect}`, '-e', `DB_HOST=${dbHost}`, '-e', `DB_PORT=${dbPort}`, '-e', `DB_DATABASE=${dbDatabase}`, '-e', `DB_USER=${dbUser}`, '-e', `DB_PASSWORD=${dbPassword}`, '-e', `TZ=${timeZone}`, '-v', `${storagePath}:${DOCKER_APP_STORAGE_DESTINATION}`, imageRef);
181
+ args.push('-e', `APP_KEY=${appKey}`, '-e', `DB_DIALECT=${dbDialect}`, '-e', `DB_HOST=${dbHost}`, '-e', `DB_PORT=${dbPort}`, '-e', `DB_DATABASE=${dbDatabase}`, '-e', `DB_USER=${dbUser}`, '-e', `DB_PASSWORD=${dbPassword}`, '-e', `TZ=${timeZone}`, '-v', `${storagePath}:${DOCKER_APP_STORAGE_DESTINATION}`);
182
+ pushOptionalEnvArg(args, 'DB_SCHEMA', dbSchema || undefined);
183
+ pushOptionalEnvArg(args, 'DB_TABLE_PREFIX', dbTablePrefix || undefined);
184
+ pushOptionalEnvArg(args, 'DB_UNDERSCORED', dbUnderscored);
185
+ args.push(imageRef);
167
186
  return {
168
187
  appPort: appPort || undefined,
169
188
  storagePath,
@@ -119,8 +119,13 @@ export async function runLocalNocoBaseCommand(runtime, args, options) {
119
119
  const envVars = await buildRuntimeEnvVars(runtime);
120
120
  await runNocoBaseCommand(args, {
121
121
  cwd: runtime.projectRoot,
122
- env: envVars,
122
+ env: {
123
+ ...envVars,
124
+ ...options?.env,
125
+ },
123
126
  stdio: options?.stdio,
127
+ onStdout: options?.onStdout,
128
+ onStderr: options?.onStderr,
124
129
  });
125
130
  }
126
131
  export async function dockerContainerExists(containerName) {
@@ -163,9 +168,13 @@ export async function stopDockerContainer(containerName, options) {
163
168
  });
164
169
  return 'stopped';
165
170
  }
166
- export async function runDockerNocoBaseCommand(containerName, args) {
167
- await startDockerContainer(containerName);
168
- await run('docker', ['exec', '-w', DOCKER_APP_WORKDIR, containerName, 'yarn', 'nocobase', ...args], {
171
+ export async function runDockerNocoBaseCommand(containerName, args, options) {
172
+ await startDockerContainer(containerName, { stdio: options?.stdio });
173
+ const dockerEnvArgs = Object.entries(options?.env ?? {}).flatMap(([key, value]) => ['-e', `${key}=${value}`]);
174
+ await run('docker', ['exec', ...dockerEnvArgs, '-w', DOCKER_APP_WORKDIR, containerName, 'yarn', 'nocobase', ...args], {
169
175
  errorName: 'docker exec',
176
+ stdio: options?.stdio,
177
+ onStdout: options?.onStdout,
178
+ onStderr: options?.onStderr,
170
179
  });
171
180
  }
@@ -8,7 +8,8 @@
8
8
  */
9
9
  import { promises as fs } from 'node:fs';
10
10
  import path from 'node:path';
11
- import { resolveCliHomeDir, resolveConfiguredEnvPath, resolveEnvRelativePath, } from './cli-home.js';
11
+ import { resolveCliHomeDir, resolveConfiguredEnvPath, resolveEnvRelativePath } from './cli-home.js';
12
+ import { normalizeCliLocale } from './cli-locale.js';
12
13
  import { cleanupCurrentSessionAfterEnvRemoval, resolveEffectiveCurrentEnv, setSessionCurrentEnv, } from './session-store.js';
13
14
  function normalizeStoredEnvKind(value) {
14
15
  const kind = String(value ?? '').trim();
@@ -24,13 +25,20 @@ function normalizeOptionalString(value) {
24
25
  const normalized = String(value ?? '').trim();
25
26
  return normalized || undefined;
26
27
  }
28
+ function normalizeOptionalCliLocale(value) {
29
+ const normalized = normalizeOptionalString(value);
30
+ if (!normalized) {
31
+ return undefined;
32
+ }
33
+ return normalizeCliLocale(normalized);
34
+ }
27
35
  export function readEnvApiBaseUrl(config) {
28
36
  if (!config) {
29
37
  return undefined;
30
38
  }
31
- return (normalizeOptionalString(config.apiBaseUrl)
32
- ?? normalizeOptionalString(config.baseUrl)
33
- ?? normalizeOptionalString(config.apibaseUrl));
39
+ return (normalizeOptionalString(config.apiBaseUrl) ??
40
+ normalizeOptionalString(config.baseUrl) ??
41
+ normalizeOptionalString(config.apibaseUrl));
34
42
  }
35
43
  export function resolveEnvKind(config) {
36
44
  if (!config) {
@@ -70,9 +78,11 @@ function normalizeEnvConfigEntry(entry) {
70
78
  }
71
79
  function normalizeAuthConfig(config) {
72
80
  const settings = config.settings ?? {};
81
+ const locale = normalizeOptionalCliLocale(settings.locale);
73
82
  return {
74
83
  name: config.name || config.dockerResourcePrefix,
75
84
  settings: {
85
+ ...(locale ? { locale } : {}),
76
86
  ...(settings.license?.pkgUrl ? { license: { pkgUrl: normalizeOptionalString(settings.license.pkgUrl) } } : {}),
77
87
  ...(settings.docker?.network || settings.docker?.containerPrefix
78
88
  ? {
@@ -84,8 +94,19 @@ function normalizeAuthConfig(config) {
84
94
  },
85
95
  }
86
96
  : {}),
97
+ ...(settings.bin?.docker || settings.bin?.git || settings.bin?.yarn
98
+ ? {
99
+ bin: {
100
+ ...(settings.bin?.docker ? { docker: normalizeOptionalString(settings.bin.docker) } : {}),
101
+ ...(settings.bin?.git ? { git: normalizeOptionalString(settings.bin.git) } : {}),
102
+ ...(settings.bin?.yarn ? { yarn: normalizeOptionalString(settings.bin.yarn) } : {}),
103
+ },
104
+ }
105
+ : {}),
87
106
  },
88
- lastEnv: config.lastEnv || config.currentEnv || 'default',
107
+ lastEnv: config.lastEnv ||
108
+ config.currentEnv ||
109
+ 'default',
89
110
  envs: Object.fromEntries(Object.entries(config.envs || {}).map(([envName, entry]) => [envName, normalizeEnvConfigEntry(entry) ?? {}])),
90
111
  };
91
112
  }
@@ -159,6 +180,9 @@ export class Env {
159
180
  get auth() {
160
181
  return this.config.auth;
161
182
  }
183
+ get authType() {
184
+ return resolveConfiguredAuthType(this.config);
185
+ }
162
186
  get runtime() {
163
187
  return this.config.runtime;
164
188
  }
@@ -210,16 +234,20 @@ export class Env {
210
234
  put('DB_DATABASE', this.config.dbDatabase);
211
235
  put('DB_USER', this.config.dbUser);
212
236
  put('DB_PASSWORD', this.config.dbPassword);
237
+ put('DB_SCHEMA', this.config.dbSchema);
238
+ put('DB_TABLE_PREFIX', this.config.dbTablePrefix);
239
+ put('DB_UNDERSCORED', this.config.dbUnderscored);
213
240
  return out;
214
241
  }
215
242
  }
216
243
  export async function getEnv(envName, options = {}) {
217
244
  const { config: snapshot, ...loadOptions } = options;
218
245
  const config = snapshot ?? (await loadAuthConfig(loadOptions));
219
- const resolved = envName?.trim() || (await resolveEffectiveCurrentEnv(Object.keys(config.envs).sort(), {
220
- scope: loadOptions.scope,
221
- lastEnv: config.lastEnv,
222
- }));
246
+ const resolved = envName?.trim() ||
247
+ (await resolveEffectiveCurrentEnv(Object.keys(config.envs).sort(), {
248
+ scope: loadOptions.scope,
249
+ lastEnv: config.lastEnv,
250
+ }));
223
251
  const envConfig = config.envs[resolved];
224
252
  if (!envConfig) {
225
253
  return undefined;
@@ -253,27 +281,41 @@ async function writeEnv(envName, updater, options = {}) {
253
281
  config.envs[envName] = updater(previous);
254
282
  await saveAuthConfig(config, options);
255
283
  }
284
+ function normalizeConfiguredAuthType(value) {
285
+ return value === 'basic' || value === 'token' || value === 'oauth' ? value : undefined;
286
+ }
287
+ export function resolveConfiguredAuthType(config) {
288
+ return normalizeConfiguredAuthType(config?.authType) ?? normalizeConfiguredAuthType(config?.auth?.type);
289
+ }
256
290
  export async function upsertEnv(envName, config, options = {}) {
257
291
  await writeEnv(envName, (previous) => {
258
- const { apiBaseUrl: _apiBaseUrl, baseUrl: _baseUrl, apibaseUrl: _legacyApiBaseUrl, accessToken, ...rest } = config;
292
+ const { apiBaseUrl: _apiBaseUrl, baseUrl: _baseUrl, apibaseUrl: _legacyApiBaseUrl, accessToken, authType, authUsername, ...rest } = config;
259
293
  const nextApiBaseUrl = readEnvApiBaseUrl(config);
260
294
  const previousApiBaseUrl = readEnvApiBaseUrl(previous);
261
295
  const baseUrlChanged = previousApiBaseUrl !== nextApiBaseUrl;
296
+ const previousAuthType = resolveConfiguredAuthType(previous);
297
+ const requestedAuthType = normalizeConfiguredAuthType(authType);
298
+ const nextAuthType = requestedAuthType ?? (accessToken ? 'token' : previousAuthType);
299
+ const nextAuthUsername = nextAuthType === 'basic' ? normalizeOptionalString(authUsername) ?? previous?.authUsername : undefined;
262
300
  const nextAuth = accessToken
263
301
  ? {
264
302
  type: 'token',
265
303
  accessToken,
266
304
  }
267
- : baseUrlChanged || previous?.auth?.type === 'token'
268
- ? undefined
269
- : previous?.auth;
305
+ : nextAuthType === 'oauth' && !baseUrlChanged && previous?.auth?.type === 'oauth'
306
+ ? previous.auth
307
+ : undefined;
270
308
  const authChanged = !areAuthConfigsEquivalent(previous?.auth, nextAuth);
309
+ const authTypeChanged = previousAuthType !== nextAuthType;
310
+ const authUsernameChanged = previous?.authUsername !== nextAuthUsername;
271
311
  return {
272
312
  ...previous,
273
313
  apiBaseUrl: nextApiBaseUrl,
314
+ authType: nextAuthType,
315
+ authUsername: nextAuthUsername,
274
316
  auth: nextAuth,
275
317
  ...rest,
276
- runtime: baseUrlChanged || authChanged ? undefined : previous?.runtime,
318
+ runtime: baseUrlChanged || authChanged || authTypeChanged || authUsernameChanged ? undefined : previous?.runtime,
277
319
  };
278
320
  }, options);
279
321
  }
@@ -282,26 +324,35 @@ export async function updateEnvConnection(envName, updates, options = {}) {
282
324
  const nextApiBaseUrl = readEnvApiBaseUrl(updates) ?? readEnvApiBaseUrl(previous);
283
325
  const previousApiBaseUrl = readEnvApiBaseUrl(previous);
284
326
  const baseUrlChanged = previousApiBaseUrl !== nextApiBaseUrl;
327
+ const previousAuthType = resolveConfiguredAuthType(previous);
328
+ const requestedAuthType = normalizeConfiguredAuthType(updates.authType);
329
+ const nextAuthType = requestedAuthType ?? (updates.accessToken ? 'token' : previousAuthType);
330
+ const nextAuthUsername = nextAuthType === 'basic' ? normalizeOptionalString(updates.authUsername) ?? previous?.authUsername : undefined;
285
331
  const nextAuth = updates.accessToken
286
332
  ? {
287
333
  type: 'token',
288
334
  accessToken: updates.accessToken,
289
335
  }
290
- : baseUrlChanged || previous?.auth?.type === 'token'
291
- ? undefined
292
- : previous?.auth;
336
+ : nextAuthType === 'oauth' && !baseUrlChanged && previous?.auth?.type === 'oauth'
337
+ ? previous.auth
338
+ : undefined;
293
339
  const authChanged = !areAuthConfigsEquivalent(previous?.auth, nextAuth);
340
+ const authTypeChanged = previousAuthType !== nextAuthType;
341
+ const authUsernameChanged = previous?.authUsername !== nextAuthUsername;
294
342
  return {
295
343
  ...previous,
296
344
  ...(nextApiBaseUrl !== undefined ? { apiBaseUrl: nextApiBaseUrl } : {}),
345
+ authType: nextAuthType,
346
+ authUsername: nextAuthUsername,
297
347
  auth: nextAuth,
298
- runtime: baseUrlChanged || authChanged ? undefined : previous?.runtime,
348
+ runtime: baseUrlChanged || authChanged || authTypeChanged || authUsernameChanged ? undefined : previous?.runtime,
299
349
  };
300
350
  }, options);
301
351
  }
302
352
  export async function setEnvOauthSession(envName, auth, options = {}) {
303
353
  await writeEnv(envName, (previous) => ({
304
354
  ...previous,
355
+ authType: 'oauth',
305
356
  auth,
306
357
  runtime: options.preserveRuntime ? previous?.runtime : undefined,
307
358
  }), options);