@nocobase/cli 2.1.11-test.9 → 2.1.12-test.1

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.
@@ -1468,8 +1468,6 @@ export default class Install extends Command {
1468
1468
  '-d',
1469
1469
  '--name',
1470
1470
  containerName,
1471
- '--restart',
1472
- 'always',
1473
1471
  '--network',
1474
1472
  networkName,
1475
1473
  '-e',
@@ -1512,8 +1510,6 @@ export default class Install extends Command {
1512
1510
  '-d',
1513
1511
  '--name',
1514
1512
  containerName,
1515
- '--restart',
1516
- 'always',
1517
1513
  '--network',
1518
1514
  networkName,
1519
1515
  '-e',
@@ -1558,8 +1554,6 @@ export default class Install extends Command {
1558
1554
  '-d',
1559
1555
  '--name',
1560
1556
  containerName,
1561
- '--restart',
1562
- 'always',
1563
1557
  '--network',
1564
1558
  networkName,
1565
1559
  '-e',
@@ -1604,8 +1598,6 @@ export default class Install extends Command {
1604
1598
  '-d',
1605
1599
  '--name',
1606
1600
  containerName,
1607
- '--restart',
1608
- 'always',
1609
1601
  '--network',
1610
1602
  networkName,
1611
1603
  '--platform',
@@ -1805,8 +1797,6 @@ export default class Install extends Command {
1805
1797
  '-d',
1806
1798
  '--name',
1807
1799
  containerName,
1808
- '--restart',
1809
- 'always',
1810
1800
  '--network',
1811
1801
  params.networkName,
1812
1802
  '-p',
@@ -225,8 +225,6 @@ export async function buildSavedDockerRunArgs(runtime, options) {
225
225
  '-d',
226
226
  '--name',
227
227
  runtime.containerName,
228
- '--restart',
229
- 'always',
230
228
  '--network',
231
229
  runtime.workspaceName,
232
230
  ];
@@ -99,6 +99,17 @@ function normalizeAuthConfig(config) {
99
99
  ? settings.log.retentionDays
100
100
  : undefined;
101
101
  const logEnabled = typeof settings.log?.enabled === 'boolean' ? settings.log.enabled : undefined;
102
+ const hasBinSettings = settings.bin?.docker ||
103
+ settings.bin?.caddy ||
104
+ settings.bin?.git ||
105
+ settings.bin?.nginx ||
106
+ settings.bin?.pnpm ||
107
+ settings.bin?.yarn;
108
+ const hasProxySettings = settings.proxy?.nbCliRoot ||
109
+ settings.proxy?.caddyDriver ||
110
+ settings.proxy?.nginxDriver ||
111
+ settings.proxy?.upstreamHost ||
112
+ settings.proxy?.host;
102
113
  return {
103
114
  name: config.name || config.dockerResourcePrefix,
104
115
  settings: {
@@ -123,12 +134,7 @@ function normalizeAuthConfig(config) {
123
134
  },
124
135
  }
125
136
  : {}),
126
- ...(settings.bin?.docker ||
127
- settings.bin?.caddy ||
128
- settings.bin?.git ||
129
- settings.bin?.nginx ||
130
- settings.bin?.pnpm ||
131
- settings.bin?.yarn
137
+ ...(hasBinSettings
132
138
  ? {
133
139
  bin: {
134
140
  ...(settings.bin?.docker ? { docker: normalizeOptionalString(settings.bin.docker) } : {}),
@@ -140,11 +146,7 @@ function normalizeAuthConfig(config) {
140
146
  },
141
147
  }
142
148
  : {}),
143
- ...(settings.proxy?.nbCliRoot ||
144
- settings.proxy?.caddyDriver ||
145
- settings.proxy?.nginxDriver ||
146
- settings.proxy?.upstreamHost ||
147
- settings.proxy?.host
149
+ ...(hasProxySettings
148
150
  ? {
149
151
  proxy: {
150
152
  ...(settings.proxy?.nbCliRoot ? { nbCliRoot: normalizeOptionalString(settings.proxy.nbCliRoot) } : {}),
@@ -22,9 +22,45 @@ const OAUTH_FETCH_TIMEOUT_MS = 15_000;
22
22
  const OAUTH_FETCH_RETRY_DELAYS_MS = [500, 1_000, 2_000];
23
23
  const DEFAULT_OAUTH_SCOPE = 'openid api offline_access';
24
24
  const DEFAULT_CLIENT_NAME = 'NocoBase CLI';
25
+ const DEVICE_CODE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
26
+ const DEFAULT_DEVICE_POLL_INTERVAL_SECONDS = 5;
25
27
  function normalizeBaseUrl(baseUrl) {
26
28
  return baseUrl.replace(/\/+$/, '');
27
29
  }
30
+ function buildDeviceVerificationPathFromApiBaseUrl(apiBaseUrl) {
31
+ const url = new URL(apiBaseUrl);
32
+ const subappMatch = url.pathname.match(/^(.*)\/api\/__app\/([^/]+)\/?$/);
33
+ if (subappMatch) {
34
+ const publicPath = (subappMatch[1] || '').replace(/\/+$/, '');
35
+ return `${publicPath}/apps/${subappMatch[2]}/idpOAuth/device`;
36
+ }
37
+ const appMatch = url.pathname.match(/^(.*)\/api\/?$/);
38
+ if (appMatch) {
39
+ const publicPath = (appMatch[1] || '').replace(/\/+$/, '');
40
+ return `${publicPath}/idpOAuth/device`;
41
+ }
42
+ return undefined;
43
+ }
44
+ export function resolveDeviceVerificationUrlForApiBaseUrl(verificationUrl, apiBaseUrl) {
45
+ try {
46
+ const devicePath = buildDeviceVerificationPathFromApiBaseUrl(apiBaseUrl);
47
+ if (!devicePath) {
48
+ return verificationUrl;
49
+ }
50
+ const originalUrl = new URL(verificationUrl);
51
+ if (!originalUrl.pathname.endsWith('/idpOAuth/device')) {
52
+ return verificationUrl;
53
+ }
54
+ const publicUrl = new URL(apiBaseUrl);
55
+ publicUrl.pathname = devicePath;
56
+ publicUrl.search = originalUrl.search;
57
+ publicUrl.hash = originalUrl.hash;
58
+ return publicUrl.toString();
59
+ }
60
+ catch {
61
+ return verificationUrl;
62
+ }
63
+ }
28
64
  export function getOauthMetadataUrl(baseUrl) {
29
65
  return `${normalizeBaseUrl(baseUrl)}/.well-known/oauth-authorization-server`;
30
66
  }
@@ -129,6 +165,16 @@ function getOauthFetchRetryDelays() {
129
165
  }
130
166
  return OAUTH_FETCH_RETRY_DELAYS_MS;
131
167
  }
168
+ function getDevicePollIntervalMs(intervalSeconds) {
169
+ const override = process.env.NB_CLI_OAUTH_DEVICE_POLL_INTERVAL_MS;
170
+ if (override !== undefined) {
171
+ const delay = Number(override);
172
+ if (Number.isFinite(delay) && delay >= 0) {
173
+ return Math.max(1, delay);
174
+ }
175
+ }
176
+ return Math.max(1, intervalSeconds ?? DEFAULT_DEVICE_POLL_INTERVAL_SECONDS) * 1000;
177
+ }
132
178
  function formatOauthRetryMessage(options) {
133
179
  return `${options.operation} failed (${options.reason}). Retrying ${options.attempt}/${options.maxAttempts}...`;
134
180
  }
@@ -223,6 +269,31 @@ async function registerOauthClient(metadata, redirectUri, options = {}) {
223
269
  if (!metadata.registration_endpoint) {
224
270
  throw new Error('OAuth server does not expose a dynamic client registration endpoint.');
225
271
  }
272
+ let body;
273
+ if (options.deviceFlow) {
274
+ body = {
275
+ client_name: DEFAULT_CLIENT_NAME,
276
+ application_type: 'native',
277
+ token_endpoint_auth_method: 'none',
278
+ grant_types: [DEVICE_CODE_GRANT_TYPE, 'refresh_token'],
279
+ response_types: [],
280
+ scope: DEFAULT_OAUTH_SCOPE,
281
+ };
282
+ }
283
+ else {
284
+ if (!redirectUri) {
285
+ throw new Error('OAuth redirect URI is required for browser sign-in.');
286
+ }
287
+ body = {
288
+ client_name: DEFAULT_CLIENT_NAME,
289
+ application_type: 'native',
290
+ token_endpoint_auth_method: 'none',
291
+ grant_types: ['authorization_code', 'refresh_token'],
292
+ response_types: ['code'],
293
+ scope: DEFAULT_OAUTH_SCOPE,
294
+ redirect_uris: [redirectUri],
295
+ };
296
+ }
226
297
  let response;
227
298
  try {
228
299
  response = await fetchWithOauthRetry(metadata.registration_endpoint, {
@@ -231,15 +302,7 @@ async function registerOauthClient(metadata, redirectUri, options = {}) {
231
302
  accept: 'application/json',
232
303
  'content-type': 'application/json',
233
304
  },
234
- body: JSON.stringify({
235
- client_name: DEFAULT_CLIENT_NAME,
236
- application_type: 'native',
237
- token_endpoint_auth_method: 'none',
238
- grant_types: ['authorization_code', 'refresh_token'],
239
- response_types: ['code'],
240
- scope: DEFAULT_OAUTH_SCOPE,
241
- redirect_uris: [redirectUri],
242
- }),
305
+ body: JSON.stringify(body),
243
306
  }, {
244
307
  operation: 'Registering OAuth client',
245
308
  onRetry: (message) => updateTask(message),
@@ -594,6 +657,10 @@ async function maybeOpenBrowser(url) {
594
657
  cleanup,
595
658
  };
596
659
  }
660
+ let browserOpener = maybeOpenBrowser;
661
+ export function setOauthBrowserOpenerForTests(opener) {
662
+ browserOpener = opener || maybeOpenBrowser;
663
+ }
597
664
  async function createLoopbackServer(state) {
598
665
  const result = await new Promise((resolve, reject) => {
599
666
  const server = createServer((req, res) => {
@@ -706,6 +773,109 @@ async function exchangeAuthorizationCode(options) {
706
773
  }
707
774
  return data;
708
775
  }
776
+ async function requestDeviceAuthorization(options) {
777
+ if (!options.metadata.device_authorization_endpoint) {
778
+ throw new Error('OAuth server does not expose a device authorization endpoint.');
779
+ }
780
+ const body = new URLSearchParams({
781
+ client_id: options.clientId,
782
+ scope: options.scope,
783
+ resource: options.resource,
784
+ });
785
+ let response;
786
+ try {
787
+ response = await fetchWithOauthRetry(options.metadata.device_authorization_endpoint, {
788
+ method: 'POST',
789
+ headers: {
790
+ accept: 'application/json',
791
+ 'content-type': 'application/x-www-form-urlencoded',
792
+ },
793
+ body,
794
+ }, {
795
+ operation: 'Starting OAuth device authorization',
796
+ onRetry: (message) => updateTask(message),
797
+ });
798
+ }
799
+ catch (error) {
800
+ throw new Error(formatOauthFetchFailure('Failed to start OAuth device authorization.', {
801
+ envName: options.envName,
802
+ baseUrl: options.baseUrl,
803
+ url: options.metadata.device_authorization_endpoint,
804
+ rawMessage: error instanceof Error ? error.message : String(error),
805
+ }));
806
+ }
807
+ const data = await parseJsonResponse(response);
808
+ if (!response.ok) {
809
+ throw new Error(formatOauthError('Failed to start OAuth device authorization', data, response.status));
810
+ }
811
+ if (!data ||
812
+ typeof data !== 'object' ||
813
+ typeof data.device_code !== 'string' ||
814
+ typeof data.user_code !== 'string' ||
815
+ typeof data.verification_uri !== 'string') {
816
+ throw new Error('OAuth device authorization response is missing device_code, user_code, or verification_uri.');
817
+ }
818
+ return data;
819
+ }
820
+ async function pollDeviceToken(options) {
821
+ let intervalMs = getDevicePollIntervalMs(options.interval);
822
+ const deadline = Date.now() + Math.max(1, options.expiresIn ?? OAUTH_LOGIN_TIMEOUT_MS / 1000) * 1000;
823
+ while (Date.now() < deadline) {
824
+ await sleep(intervalMs);
825
+ const body = new URLSearchParams({
826
+ grant_type: DEVICE_CODE_GRANT_TYPE,
827
+ client_id: options.clientId,
828
+ device_code: options.deviceCode,
829
+ });
830
+ let response;
831
+ try {
832
+ response = await fetchWithOauthRetry(options.metadata.token_endpoint, {
833
+ method: 'POST',
834
+ headers: {
835
+ accept: 'application/json',
836
+ 'content-type': 'application/x-www-form-urlencoded',
837
+ },
838
+ body,
839
+ }, {
840
+ operation: 'Polling OAuth device authorization',
841
+ onRetry: (message) => updateTask(message),
842
+ });
843
+ }
844
+ catch (error) {
845
+ throw new Error(formatOauthFetchFailure('Failed to poll OAuth device authorization.', {
846
+ envName: options.envName,
847
+ baseUrl: options.baseUrl,
848
+ url: options.metadata.token_endpoint,
849
+ rawMessage: error instanceof Error ? error.message : String(error),
850
+ }));
851
+ }
852
+ const data = await parseJsonResponse(response);
853
+ if (response.ok) {
854
+ if (!data || typeof data !== 'object' || typeof data.access_token !== 'string') {
855
+ throw new Error('OAuth token response is missing access_token.');
856
+ }
857
+ return data;
858
+ }
859
+ const oauthError = typeof data === 'object' && data ? String(data.error || '') : '';
860
+ if (oauthError === 'authorization_pending') {
861
+ updateTask(`Waiting for you to approve device sign-in for "${options.envName}"...`);
862
+ continue;
863
+ }
864
+ if (oauthError === 'slow_down') {
865
+ intervalMs += 5_000;
866
+ updateTask(`OAuth server asked us to slow down. Polling again in ${Math.ceil(intervalMs / 1000)}s...`);
867
+ continue;
868
+ }
869
+ if (oauthError === 'expired_token') {
870
+ throw new Error(`OAuth device sign-in expired. Run \`nb env auth ${options.envName}\` to try again.`);
871
+ }
872
+ if (oauthError === 'access_denied') {
873
+ throw new Error('OAuth device sign-in was denied.');
874
+ }
875
+ throw new Error(formatOauthError('Failed to poll OAuth device authorization', data, response.status));
876
+ }
877
+ throw new Error(`OAuth device sign-in timed out. Run \`nb env auth ${options.envName}\` to try again.`);
878
+ }
709
879
  async function refreshOauthAccessToken(options) {
710
880
  if (!options.auth.refreshToken || !options.auth.clientId) {
711
881
  throw new Error(`OAuth session for env "${options.envName}" cannot be refreshed. Run \`nb env auth ${options.envName}\`.`);
@@ -862,41 +1032,79 @@ export async function authenticateEnvWithBasic(options) {
862
1032
  }
863
1033
  return token;
864
1034
  }
865
- export async function authenticateEnvWithOauth(options) {
866
- const envName = options.envName ?? (await getCurrentEnvName({ scope: options.scope }));
867
- const env = await getEnv(envName, { scope: options.scope });
868
- const baseUrl = env?.baseUrl;
869
- if (!baseUrl) {
870
- throw new Error([
871
- env
872
- ? `Environment "${envName}" does not have an API base URL yet.`
873
- : `Environment "${envName}" has not been set up yet.`,
874
- env
875
- ? `Run \`nb env update ${envName} --api-base-url <url>\` to finish setting it up.`
876
- : `Run \`nb init --ui --env ${envName}\` first.`,
877
- ]
878
- .filter(Boolean)
879
- .join('\n'));
1035
+ async function authenticateEnvWithOauthDevice(options) {
1036
+ const resource = getOauthResource(options.metadata.issuer);
1037
+ let cleanupBrowserOpenTarget;
1038
+ try {
1039
+ updateTask(`Preparing device sign-in for "${options.envName}"...`);
1040
+ const registration = await registerOauthClient(options.metadata, undefined, {
1041
+ envName: options.envName,
1042
+ baseUrl: options.baseUrl,
1043
+ deviceFlow: true,
1044
+ });
1045
+ const deviceAuthorization = await requestDeviceAuthorization({
1046
+ metadata: options.metadata,
1047
+ clientId: registration.clientId,
1048
+ scope: DEFAULT_OAUTH_SCOPE,
1049
+ resource,
1050
+ envName: options.envName,
1051
+ baseUrl: options.baseUrl,
1052
+ });
1053
+ stopTask();
1054
+ const verificationUrl = resolveDeviceVerificationUrlForApiBaseUrl(deviceAuthorization.verification_uri_complete || deviceAuthorization.verification_uri, options.baseUrl);
1055
+ const browser = await browserOpener(verificationUrl);
1056
+ cleanupBrowserOpenTarget = browser.cleanup;
1057
+ if (!browser.opened) {
1058
+ printWarningBlock('We could not open your browser automatically. Open the URL below to approve device sign-in:');
1059
+ }
1060
+ else {
1061
+ printInfo(`Opening device sign-in for "${options.envName}" in your browser.`);
1062
+ printInfo('If the browser does not open automatically, open this URL manually:');
1063
+ }
1064
+ printInfo(verificationUrl);
1065
+ printInfo(`Enter code: ${deviceAuthorization.user_code}`);
1066
+ updateTask(`Waiting for you to approve device sign-in for "${options.envName}"...`);
1067
+ const tokenResponse = await pollDeviceToken({
1068
+ metadata: options.metadata,
1069
+ clientId: registration.clientId,
1070
+ deviceCode: deviceAuthorization.device_code,
1071
+ expiresIn: deviceAuthorization.expires_in,
1072
+ interval: deviceAuthorization.interval,
1073
+ envName: options.envName,
1074
+ baseUrl: options.baseUrl,
1075
+ });
1076
+ if (!tokenResponse.refresh_token) {
1077
+ printWarning('Sign-in succeeded, but no refresh token was returned. You may need to sign in again when this session expires.');
1078
+ }
1079
+ await setEnvOauthSession(options.envName, {
1080
+ type: 'oauth',
1081
+ accessToken: tokenResponse.access_token,
1082
+ refreshToken: tokenResponse.refresh_token,
1083
+ expiresAt: calculateExpiresAt(tokenResponse.expires_in),
1084
+ scope: tokenResponse.scope || DEFAULT_OAUTH_SCOPE,
1085
+ issuer: options.metadata.issuer,
1086
+ clientId: registration.clientId,
1087
+ resource,
1088
+ }, { scope: options.scope });
880
1089
  }
881
- printVerbose(`Starting OAuth sign-in for env "${envName}" using ${baseUrl}`);
882
- updateTask(`Checking sign-in settings for "${envName}"...`);
883
- const metadata = await fetchOauthServerMetadata(baseUrl, {
884
- envName,
885
- onRetry: (message) => updateTask(message),
886
- });
1090
+ finally {
1091
+ await cleanupBrowserOpenTarget?.().catch(() => undefined);
1092
+ }
1093
+ }
1094
+ async function authenticateEnvWithOauthBrowser(options) {
887
1095
  const state = encodeBase64Url(crypto.randomBytes(16));
888
1096
  const { codeVerifier, codeChallenge } = buildPkcePair();
889
1097
  const callback = await createLoopbackServer(state);
890
- const resource = getOauthResource(metadata.issuer);
1098
+ const resource = getOauthResource(options.metadata.issuer);
891
1099
  let cleanupBrowserOpenTarget;
892
1100
  try {
893
1101
  printVerbose(`OAuth callback listener ready at ${callback.redirectUri}`);
894
- updateTask(`Preparing secure browser sign-in for "${envName}"...`);
895
- const registration = await registerOauthClient(metadata, callback.redirectUri, {
896
- envName,
897
- baseUrl,
1102
+ updateTask(`Preparing secure browser sign-in for "${options.envName}"...`);
1103
+ const registration = await registerOauthClient(options.metadata, callback.redirectUri, {
1104
+ envName: options.envName,
1105
+ baseUrl: options.baseUrl,
898
1106
  });
899
- const authorizationUrl = new URL(metadata.authorization_endpoint);
1107
+ const authorizationUrl = new URL(options.metadata.authorization_endpoint);
900
1108
  authorizationUrl.searchParams.set('response_type', 'code');
901
1109
  authorizationUrl.searchParams.set('client_id', registration.clientId);
902
1110
  authorizationUrl.searchParams.set('redirect_uri', callback.redirectUri);
@@ -906,8 +1114,8 @@ export async function authenticateEnvWithOauth(options) {
906
1114
  authorizationUrl.searchParams.set('code_challenge', codeChallenge);
907
1115
  authorizationUrl.searchParams.set('code_challenge_method', 'S256');
908
1116
  authorizationUrl.searchParams.set('resource', resource);
909
- updateTask(`Waiting for you to finish signing in for "${envName}"...`);
910
- const browser = await maybeOpenBrowser(authorizationUrl.toString());
1117
+ updateTask(`Waiting for you to finish signing in for "${options.envName}"...`);
1118
+ const browser = await browserOpener(authorizationUrl.toString());
911
1119
  cleanupBrowserOpenTarget = browser.cleanup;
912
1120
  if (!browser.opened) {
913
1121
  printWarningBlock('We could not open your browser automatically. Open the URL below to continue signing in:');
@@ -918,7 +1126,7 @@ export async function authenticateEnvWithOauth(options) {
918
1126
  }
919
1127
  printInfo(authorizationUrl.toString());
920
1128
  const code = await new Promise((resolve, reject) => {
921
- const timeout = setTimeout(() => reject(new Error(`OAuth sign-in timed out after 5 minutes. Run \`nb env auth ${envName}\` to try again.`)), OAUTH_LOGIN_TIMEOUT_MS);
1129
+ const timeout = setTimeout(() => reject(new Error(`OAuth sign-in timed out after 5 minutes. Run \`nb env auth ${options.envName}\` to try again.`)), OAUTH_LOGIN_TIMEOUT_MS);
922
1130
  timeout.unref?.();
923
1131
  callback
924
1132
  .waitForCode()
@@ -931,27 +1139,27 @@ export async function authenticateEnvWithOauth(options) {
931
1139
  reject(error);
932
1140
  });
933
1141
  });
934
- updateTask(`Finishing sign-in for "${envName}"...`);
1142
+ updateTask(`Finishing sign-in for "${options.envName}"...`);
935
1143
  const tokenResponse = await exchangeAuthorizationCode({
936
- metadata,
1144
+ metadata: options.metadata,
937
1145
  clientId: registration.clientId,
938
1146
  redirectUri: callback.redirectUri,
939
1147
  code,
940
1148
  codeVerifier,
941
1149
  resource,
942
- envName,
943
- baseUrl,
1150
+ envName: options.envName,
1151
+ baseUrl: options.baseUrl,
944
1152
  });
945
1153
  if (!tokenResponse.refresh_token) {
946
1154
  printWarning('Sign-in succeeded, but no refresh token was returned. You may need to sign in again when this session expires.');
947
1155
  }
948
- await setEnvOauthSession(envName, {
1156
+ await setEnvOauthSession(options.envName, {
949
1157
  type: 'oauth',
950
1158
  accessToken: tokenResponse.access_token,
951
1159
  refreshToken: tokenResponse.refresh_token,
952
1160
  expiresAt: calculateExpiresAt(tokenResponse.expires_in),
953
1161
  scope: tokenResponse.scope || DEFAULT_OAUTH_SCOPE,
954
- issuer: metadata.issuer,
1162
+ issuer: options.metadata.issuer,
955
1163
  clientId: registration.clientId,
956
1164
  resource,
957
1165
  }, { scope: options.scope });
@@ -961,3 +1169,41 @@ export async function authenticateEnvWithOauth(options) {
961
1169
  await callback.close().catch(() => undefined);
962
1170
  }
963
1171
  }
1172
+ export async function authenticateEnvWithOauth(options) {
1173
+ const envName = options.envName ?? (await getCurrentEnvName({ scope: options.scope }));
1174
+ const env = await getEnv(envName, { scope: options.scope });
1175
+ const baseUrl = env?.baseUrl;
1176
+ if (!baseUrl) {
1177
+ throw new Error([
1178
+ env
1179
+ ? `Environment "${envName}" does not have an API base URL yet.`
1180
+ : `Environment "${envName}" has not been set up yet.`,
1181
+ env
1182
+ ? `Run \`nb env update ${envName} --api-base-url <url>\` to finish setting it up.`
1183
+ : `Run \`nb init --ui --env ${envName}\` first.`,
1184
+ ]
1185
+ .filter(Boolean)
1186
+ .join('\n'));
1187
+ }
1188
+ printVerbose(`Starting OAuth sign-in for env "${envName}" using ${baseUrl}`);
1189
+ updateTask(`Checking sign-in settings for "${envName}"...`);
1190
+ const metadata = await fetchOauthServerMetadata(baseUrl, {
1191
+ envName,
1192
+ onRetry: (message) => updateTask(message),
1193
+ });
1194
+ if (metadata.device_authorization_endpoint) {
1195
+ await authenticateEnvWithOauthDevice({
1196
+ envName,
1197
+ baseUrl,
1198
+ metadata,
1199
+ scope: options.scope,
1200
+ });
1201
+ return;
1202
+ }
1203
+ await authenticateEnvWithOauthBrowser({
1204
+ envName,
1205
+ baseUrl,
1206
+ metadata,
1207
+ scope: options.scope,
1208
+ });
1209
+ }
@@ -10,7 +10,7 @@ import { spawn } from 'node:child_process';
10
10
  import net from 'node:net';
11
11
  import { translateCli } from "./cli-locale.js";
12
12
  const API_BASE_URL_EXAMPLE = 'http://localhost:13000/api';
13
- const ENV_KEY_PATTERN = /^[A-Za-z0-9]+$/;
13
+ const ENV_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
14
14
  const APP_PUBLIC_PATH_PATTERN = /^\/(?:[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*)?\/?$/;
15
15
  const TCP_PORT_EXAMPLE = '13000';
16
16
  const API_BASE_URL_REQUEST_TIMEOUT_MS = 5_000;
@@ -7,6 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import fs from 'node:fs';
10
+ import os from 'node:os';
10
11
  import path from 'node:path';
11
12
  import { fileURLToPath } from 'node:url';
12
13
  import { commandOutput, run } from './run-npm.js';
@@ -124,6 +125,7 @@ function detectInstallMethod(packageRoot, options = {}) {
124
125
  })) {
125
126
  return 'pnpm-global';
126
127
  }
128
+ // Best-effort fallback for environments where pnpm probes are unavailable.
127
129
  if (isPnpmGlobalPath(packageRoot)) {
128
130
  return 'pnpm-global';
129
131
  }
@@ -141,6 +143,39 @@ function isPnpmGlobalPath(packageRoot) {
141
143
  }
142
144
  return segments.slice(globalIndex + 2).includes('node_modules');
143
145
  }
146
+ function normalizePnpmGlobalNodeModulesRoot(pnpmGlobalRoot) {
147
+ const normalizedGlobalRoot = normalizePath(pnpmGlobalRoot);
148
+ return path.basename(normalizedGlobalRoot) === 'node_modules'
149
+ ? normalizedGlobalRoot
150
+ : path.join(normalizedGlobalRoot, 'node_modules');
151
+ }
152
+ function readSiblingPnpmGlobalNodeModulesRoots(pnpmGlobalRoot) {
153
+ const globalNodeModulesDir = normalizePnpmGlobalNodeModulesRoot(pnpmGlobalRoot);
154
+ const globalProjectDir = path.dirname(globalNodeModulesDir);
155
+ let entries;
156
+ try {
157
+ entries = fs.readdirSync(globalProjectDir);
158
+ }
159
+ catch {
160
+ return [];
161
+ }
162
+ return entries
163
+ .filter((entry) => {
164
+ if (entry === path.basename(globalNodeModulesDir)) {
165
+ return false;
166
+ }
167
+ try {
168
+ return fs.statSync(path.join(globalProjectDir, entry)).isDirectory();
169
+ }
170
+ catch {
171
+ return false;
172
+ }
173
+ })
174
+ .map((entry) => path.join(globalProjectDir, entry, 'node_modules'));
175
+ }
176
+ function readPnpmGlobalNodeModulesRoots(pnpmGlobalRoot) {
177
+ return [normalizePnpmGlobalNodeModulesRoot(pnpmGlobalRoot), ...readSiblingPnpmGlobalNodeModulesRoots(pnpmGlobalRoot)];
178
+ }
144
179
  function packageNameToPath(packageName) {
145
180
  return packageName.split('/').filter(Boolean);
146
181
  }
@@ -156,10 +191,7 @@ function isPnpmGlobalRootPath(pnpmGlobalRoot, packageRoot) {
156
191
  if (isSubPath(pnpmGlobalRoot, packageRoot)) {
157
192
  return true;
158
193
  }
159
- const normalizedGlobalRoot = normalizePath(pnpmGlobalRoot);
160
- const globalNodeModulesDir = path.basename(normalizedGlobalRoot) === 'node_modules'
161
- ? normalizedGlobalRoot
162
- : path.join(normalizedGlobalRoot, 'node_modules');
194
+ const globalNodeModulesDir = normalizePnpmGlobalNodeModulesRoot(pnpmGlobalRoot);
163
195
  const globalProjectDir = path.dirname(globalNodeModulesDir);
164
196
  if (!isSubPath(globalProjectDir, packageRoot)) {
165
197
  return false;
@@ -172,7 +204,7 @@ function isPnpmGlobalRootPath(pnpmGlobalRoot, packageRoot) {
172
204
  return segments.slice(pnpmStoreIndex + 1).includes('node_modules');
173
205
  }
174
206
  function isPnpmGlobalPackagePath(pnpmGlobalRoot, packageRoot, packageName) {
175
- return isSameRealPath(path.join(pnpmGlobalRoot, ...packageNameToPath(packageName)), packageRoot);
207
+ return readPnpmGlobalNodeModulesRoots(pnpmGlobalRoot).some((pnpmGlobalNodeModulesRoot) => isSameRealPath(path.join(pnpmGlobalNodeModulesRoot, ...packageNameToPath(packageName)), packageRoot));
176
208
  }
177
209
  function isPnpmGlobalBinProjectPath(pnpmGlobalBin, packageRoot, packageName) {
178
210
  const pnpmHome = path.dirname(normalizePath(pnpmGlobalBin));
@@ -188,9 +220,9 @@ function isPnpmGlobalBinProjectPath(pnpmGlobalBin, packageRoot, packageName) {
188
220
  return false;
189
221
  }
190
222
  return entries.some((entry) => {
191
- const pnpmGlobalRoot = path.join(globalDir, entry, 'node_modules');
192
- return isPnpmGlobalRootPath(pnpmGlobalRoot, packageRoot)
193
- || isPnpmGlobalPackagePath(pnpmGlobalRoot, packageRoot, packageName);
223
+ const pnpmGlobalRoot = path.join(globalDir, entry);
224
+ return (isPnpmGlobalRootPath(pnpmGlobalRoot, packageRoot)
225
+ || isPnpmGlobalPackagePath(pnpmGlobalRoot, packageRoot, packageName));
194
226
  });
195
227
  }
196
228
  function isPnpmGlobalInstallPath(options) {
@@ -217,9 +249,27 @@ function cleanCommandPath(value) {
217
249
  const trimmed = value.trim();
218
250
  return trimmed ? trimmed : undefined;
219
251
  }
252
+ function resolvePackageManagerProbeCwd() {
253
+ const candidates = [os.homedir(), os.tmpdir(), path.parse(process.cwd()).root];
254
+ for (const candidate of candidates) {
255
+ if (!candidate) {
256
+ continue;
257
+ }
258
+ try {
259
+ if (fs.statSync(candidate).isDirectory()) {
260
+ return candidate;
261
+ }
262
+ }
263
+ catch {
264
+ // Ignore invalid probe cwd candidates and continue to the next one.
265
+ }
266
+ }
267
+ return undefined;
268
+ }
220
269
  async function readGlobalPrefix(commandOutputFn) {
221
270
  try {
222
271
  return cleanCommandPath(await commandOutputFn('npm', ['prefix', '-g'], {
272
+ cwd: resolvePackageManagerProbeCwd(),
223
273
  errorName: 'npm prefix',
224
274
  }));
225
275
  }
@@ -230,6 +280,7 @@ async function readGlobalPrefix(commandOutputFn) {
230
280
  async function readPnpmGlobalBin(commandOutputFn) {
231
281
  try {
232
282
  return cleanCommandPath(await commandOutputFn('pnpm', ['bin', '-g'], {
283
+ cwd: resolvePackageManagerProbeCwd(),
233
284
  errorName: 'pnpm bin',
234
285
  }));
235
286
  }
@@ -240,6 +291,7 @@ async function readPnpmGlobalBin(commandOutputFn) {
240
291
  async function readPnpmGlobalRoot(commandOutputFn) {
241
292
  try {
242
293
  return cleanCommandPath(await commandOutputFn('pnpm', ['root', '-g'], {
294
+ cwd: resolvePackageManagerProbeCwd(),
243
295
  errorName: 'pnpm root',
244
296
  }));
245
297
  }
@@ -250,6 +302,7 @@ async function readPnpmGlobalRoot(commandOutputFn) {
250
302
  async function readYarnGlobalDir(commandOutputFn) {
251
303
  try {
252
304
  return cleanCommandPath(await commandOutputFn('yarn', ['global', 'dir'], {
305
+ cwd: resolvePackageManagerProbeCwd(),
253
306
  errorName: 'yarn global dir',
254
307
  }));
255
308
  }
@@ -260,6 +313,7 @@ async function readYarnGlobalDir(commandOutputFn) {
260
313
  async function readYarnGlobalBin(commandOutputFn) {
261
314
  try {
262
315
  return cleanCommandPath(await commandOutputFn('yarn', ['global', 'bin'], {
316
+ cwd: resolvePackageManagerProbeCwd(),
263
317
  errorName: 'yarn global bin',
264
318
  }));
265
319
  }
@@ -367,6 +421,7 @@ export async function inspectSelfStatus(options = {}) {
367
421
  const channel = options.channel && options.channel !== 'auto' ? options.channel : detectChannel(currentVersion);
368
422
  const commandOutputFn = options.commandOutputFn ?? commandOutput;
369
423
  const { installMethod, globalPrefix } = await inspectSelfInstall({
424
+ currentBinPath: options.currentBinPath,
370
425
  packageName,
371
426
  packageRoot,
372
427
  commandOutputFn,
@@ -68,7 +68,7 @@
68
68
  "unreachable": "Unable to connect to the API base URL. Check the address and make sure the server is reachable. Details: {{details}}"
69
69
  },
70
70
  "envKey": {
71
- "invalid": "Use letters and numbers only."
71
+ "invalid": "Use letters, numbers, hyphens, and underscores only."
72
72
  },
73
73
  "appPublicPath": {
74
74
  "invalid": "Use / or a slash-separated path like /nocobase/ or /foo-bar/baz_2/. Each path segment may contain letters, numbers, hyphens, and underscores only."
@@ -68,7 +68,7 @@
68
68
  "unreachable": "无法连接到该 API 地址,请检查地址是否正确,并确认服务可访问。详情:{{details}}"
69
69
  },
70
70
  "envKey": {
71
- "invalid": "仅支持字母和数字。"
71
+ "invalid": "仅支持字母、数字、中划线和下划线。"
72
72
  },
73
73
  "appPublicPath": {
74
74
  "invalid": "请输入 /,或类似 /nocobase/、/foo-bar/baz_2/ 这样的路径。每个路径段仅支持字母、数字、中划线和下划线。"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/cli",
3
- "version": "2.1.11-test.9",
3
+ "version": "2.1.12-test.1",
4
4
  "description": "NocoBase Command Line Tool",
5
5
  "type": "module",
6
6
  "main": "dist/generated/command-registry.js",