@nocobase/cli 2.2.0-beta.7 → 2.2.0-beta.8
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.
- package/dist/commands/install.js +0 -10
- package/dist/commands/self/update.js +2 -2
- package/dist/lib/app-managed-resources.js +0 -2
- package/dist/lib/auth-store.js +14 -6
- package/dist/lib/cli-config.js +19 -0
- package/dist/lib/env-auth.js +291 -45
- package/dist/lib/prompt-validators.js +1 -1
- package/dist/lib/run-npm.js +4 -0
- package/dist/lib/self-manager.js +251 -46
- package/dist/lib/startup-update.js +1 -1
- package/dist/locale/en-US.json +1 -1
- package/dist/locale/zh-CN.json +1 -1
- package/package.json +2 -2
package/dist/commands/install.js
CHANGED
|
@@ -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',
|
|
@@ -26,7 +26,7 @@ function formatSkillsUpdateMessage(result, verbose) {
|
|
|
26
26
|
}
|
|
27
27
|
export default class SelfUpdate extends Command {
|
|
28
28
|
static summary = 'Update the globally installed NocoBase CLI';
|
|
29
|
-
static description = 'Update the current NocoBase CLI install when it is managed by a standard global npm install.';
|
|
29
|
+
static description = 'Update the current NocoBase CLI install when it is managed by a standard global npm, pnpm, or yarn install.';
|
|
30
30
|
static examples = [
|
|
31
31
|
'<%= config.bin %> <%= command.id %>',
|
|
32
32
|
'<%= config.bin %> <%= command.id %> --yes',
|
|
@@ -77,7 +77,7 @@ export default class SelfUpdate extends Command {
|
|
|
77
77
|
message: flags.skills
|
|
78
78
|
? `Update ${status.packageName} from ${status.currentVersion} to ${status.latestVersion} and refresh the globally installed NocoBase AI coding skills?`
|
|
79
79
|
: `Update ${status.packageName} from ${status.currentVersion} to ${status.latestVersion}?`,
|
|
80
|
-
default:
|
|
80
|
+
default: true,
|
|
81
81
|
});
|
|
82
82
|
}
|
|
83
83
|
catch {
|
package/dist/lib/auth-store.js
CHANGED
|
@@ -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,22 +134,19 @@ function normalizeAuthConfig(config) {
|
|
|
123
134
|
},
|
|
124
135
|
}
|
|
125
136
|
: {}),
|
|
126
|
-
...(
|
|
137
|
+
...(hasBinSettings
|
|
127
138
|
? {
|
|
128
139
|
bin: {
|
|
129
140
|
...(settings.bin?.docker ? { docker: normalizeOptionalString(settings.bin.docker) } : {}),
|
|
130
141
|
...(settings.bin?.caddy ? { caddy: normalizeOptionalString(settings.bin.caddy) } : {}),
|
|
131
142
|
...(settings.bin?.git ? { git: normalizeOptionalString(settings.bin.git) } : {}),
|
|
132
143
|
...(settings.bin?.nginx ? { nginx: normalizeOptionalString(settings.bin.nginx) } : {}),
|
|
144
|
+
...(settings.bin?.pnpm ? { pnpm: normalizeOptionalString(settings.bin.pnpm) } : {}),
|
|
133
145
|
...(settings.bin?.yarn ? { yarn: normalizeOptionalString(settings.bin.yarn) } : {}),
|
|
134
146
|
},
|
|
135
147
|
}
|
|
136
148
|
: {}),
|
|
137
|
-
...(
|
|
138
|
-
settings.proxy?.caddyDriver ||
|
|
139
|
-
settings.proxy?.nginxDriver ||
|
|
140
|
-
settings.proxy?.upstreamHost ||
|
|
141
|
-
settings.proxy?.host
|
|
149
|
+
...(hasProxySettings
|
|
142
150
|
? {
|
|
143
151
|
proxy: {
|
|
144
152
|
...(settings.proxy?.nbCliRoot ? { nbCliRoot: normalizeOptionalString(settings.proxy.nbCliRoot) } : {}),
|
package/dist/lib/cli-config.js
CHANGED
|
@@ -16,6 +16,7 @@ export const DEFAULT_DOCKER_BIN = 'docker';
|
|
|
16
16
|
export const DEFAULT_CADDY_BIN = 'caddy';
|
|
17
17
|
export const DEFAULT_GIT_BIN = 'git';
|
|
18
18
|
export const DEFAULT_NGINX_BIN = 'nginx';
|
|
19
|
+
export const DEFAULT_PNPM_BIN = 'pnpm';
|
|
19
20
|
export const PROXY_PROVIDER_OPTIONS = ['nginx', 'caddy'];
|
|
20
21
|
export const DEFAULT_PROXY_PROVIDER = 'nginx';
|
|
21
22
|
export const NGINX_PROXY_DRIVER_OPTIONS = ['local', 'docker'];
|
|
@@ -40,6 +41,7 @@ export const SUPPORTED_CLI_CONFIG_KEYS = [
|
|
|
40
41
|
'bin.caddy',
|
|
41
42
|
'bin.git',
|
|
42
43
|
'bin.nginx',
|
|
44
|
+
'bin.pnpm',
|
|
43
45
|
'proxy.nb-cli-root',
|
|
44
46
|
'proxy.caddy-driver',
|
|
45
47
|
'proxy.nginx-driver',
|
|
@@ -136,6 +138,7 @@ function pruneSettings(config) {
|
|
|
136
138
|
!trimValue(bin.caddy) &&
|
|
137
139
|
!trimValue(bin.git) &&
|
|
138
140
|
!trimValue(bin.nginx) &&
|
|
141
|
+
!trimValue(bin.pnpm) &&
|
|
139
142
|
!trimValue(bin.yarn)) {
|
|
140
143
|
delete config.settings?.bin;
|
|
141
144
|
}
|
|
@@ -187,6 +190,8 @@ export function getExplicitCliConfigValue(config, key) {
|
|
|
187
190
|
return trimValue(config.settings?.bin?.git);
|
|
188
191
|
case 'bin.nginx':
|
|
189
192
|
return trimValue(config.settings?.bin?.nginx);
|
|
193
|
+
case 'bin.pnpm':
|
|
194
|
+
return trimValue(config.settings?.bin?.pnpm);
|
|
190
195
|
case 'proxy.nb-cli-root':
|
|
191
196
|
return trimValue(config.settings?.proxy?.nbCliRoot);
|
|
192
197
|
case 'proxy.caddy-driver':
|
|
@@ -233,6 +238,8 @@ export function getEffectiveCliConfigValue(config, key) {
|
|
|
233
238
|
return DEFAULT_GIT_BIN;
|
|
234
239
|
case 'bin.nginx':
|
|
235
240
|
return DEFAULT_NGINX_BIN;
|
|
241
|
+
case 'bin.pnpm':
|
|
242
|
+
return DEFAULT_PNPM_BIN;
|
|
236
243
|
case 'proxy.nb-cli-root':
|
|
237
244
|
return explicit ?? resolveCliHomeRoot();
|
|
238
245
|
case 'proxy.caddy-driver':
|
|
@@ -387,6 +394,12 @@ export async function setCliConfigValue(key, value, options = {}) {
|
|
|
387
394
|
nginx: normalized,
|
|
388
395
|
};
|
|
389
396
|
break;
|
|
397
|
+
case 'bin.pnpm':
|
|
398
|
+
config.settings.bin = {
|
|
399
|
+
...(config.settings.bin ?? {}),
|
|
400
|
+
pnpm: normalized,
|
|
401
|
+
};
|
|
402
|
+
break;
|
|
390
403
|
case 'proxy.nb-cli-root':
|
|
391
404
|
config.settings.proxy = {
|
|
392
405
|
...(config.settings.proxy ?? {}),
|
|
@@ -496,6 +509,11 @@ export async function deleteCliConfigValue(key, options = {}) {
|
|
|
496
509
|
delete config.settings.bin.nginx;
|
|
497
510
|
}
|
|
498
511
|
break;
|
|
512
|
+
case 'bin.pnpm':
|
|
513
|
+
if (config.settings.bin) {
|
|
514
|
+
delete config.settings.bin.pnpm;
|
|
515
|
+
}
|
|
516
|
+
break;
|
|
499
517
|
case 'proxy.nb-cli-root':
|
|
500
518
|
if (config.settings.proxy) {
|
|
501
519
|
delete config.settings.proxy.nbCliRoot;
|
|
@@ -556,6 +574,7 @@ const CONFIGURABLE_COMMAND_KEYS = {
|
|
|
556
574
|
caddy: 'bin.caddy',
|
|
557
575
|
git: 'bin.git',
|
|
558
576
|
nginx: 'bin.nginx',
|
|
577
|
+
pnpm: 'bin.pnpm',
|
|
559
578
|
yarn: 'bin.yarn',
|
|
560
579
|
};
|
|
561
580
|
export function isConfigurableCommandName(value) {
|
package/dist/lib/env-auth.js
CHANGED
|
@@ -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
|
-
|
|
866
|
-
const
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
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
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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
|
|
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-
|
|
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;
|
package/dist/lib/run-npm.js
CHANGED
package/dist/lib/self-manager.js
CHANGED
|
@@ -7,14 +7,12 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
import fs from 'node:fs';
|
|
10
|
-
import
|
|
10
|
+
import os from 'node:os';
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
|
-
import { resolveCliHomeDir } from './cli-home.js';
|
|
14
13
|
import { commandOutput, run } from './run-npm.js';
|
|
15
14
|
const DEFAULT_PACKAGE_NAME = '@nocobase/cli';
|
|
16
15
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
17
|
-
const INSTALL_METHOD_CACHE_FILE = 'self-install-methods.json';
|
|
18
16
|
function normalizePath(value) {
|
|
19
17
|
return path.resolve(value);
|
|
20
18
|
}
|
|
@@ -108,56 +106,216 @@ function readCurrentVersion(packageRoot) {
|
|
|
108
106
|
const pkg = JSON.parse(content);
|
|
109
107
|
return String(pkg.version ?? '').trim();
|
|
110
108
|
}
|
|
111
|
-
function detectInstallMethod(packageRoot,
|
|
109
|
+
function detectInstallMethod(packageRoot, options = {}) {
|
|
112
110
|
if (fs.existsSync(path.join(packageRoot, 'src'))
|
|
113
111
|
&& fs.existsSync(path.join(packageRoot, 'tsconfig.json'))) {
|
|
114
112
|
return 'source';
|
|
115
113
|
}
|
|
116
|
-
if (globalPrefix && isSubPath(globalPrefix, packageRoot)) {
|
|
114
|
+
if (options.globalPrefix && isSubPath(options.globalPrefix, packageRoot)) {
|
|
117
115
|
return 'npm-global';
|
|
118
116
|
}
|
|
117
|
+
if (options.yarnGlobalDir && isSubPath(options.yarnGlobalDir, packageRoot)) {
|
|
118
|
+
return 'yarn-global';
|
|
119
|
+
}
|
|
120
|
+
if (isPnpmGlobalInstallPath({
|
|
121
|
+
packageName: options.packageName ?? DEFAULT_PACKAGE_NAME,
|
|
122
|
+
packageRoot,
|
|
123
|
+
pnpmGlobalBin: options.pnpmGlobalBin,
|
|
124
|
+
pnpmGlobalRoot: options.pnpmGlobalRoot,
|
|
125
|
+
})) {
|
|
126
|
+
return 'pnpm-global';
|
|
127
|
+
}
|
|
128
|
+
// Best-effort fallback for environments where pnpm probes are unavailable.
|
|
129
|
+
if (isPnpmGlobalPath(packageRoot)) {
|
|
130
|
+
return 'pnpm-global';
|
|
131
|
+
}
|
|
119
132
|
if (packageRoot.includes(`${path.sep}node_modules${path.sep}`)) {
|
|
120
133
|
return 'package-local';
|
|
121
134
|
}
|
|
122
135
|
return 'unknown';
|
|
123
136
|
}
|
|
124
|
-
function
|
|
125
|
-
|
|
137
|
+
function isPnpmGlobalPath(packageRoot) {
|
|
138
|
+
const normalized = normalizePath(packageRoot);
|
|
139
|
+
const segments = normalized.split(path.sep).filter(Boolean);
|
|
140
|
+
const globalIndex = segments.findIndex((segment, index) => segment === 'global' && segments[index - 1] === 'pnpm');
|
|
141
|
+
if (globalIndex === -1) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
return segments.slice(globalIndex + 2).includes('node_modules');
|
|
126
145
|
}
|
|
127
|
-
function
|
|
128
|
-
|
|
146
|
+
function normalizePnpmGlobalNodeModulesRoot(pnpmGlobalRoot) {
|
|
147
|
+
const normalizedGlobalRoot = normalizePath(pnpmGlobalRoot);
|
|
148
|
+
return path.basename(normalizedGlobalRoot) === 'node_modules'
|
|
149
|
+
? normalizedGlobalRoot
|
|
150
|
+
: path.join(normalizedGlobalRoot, 'node_modules');
|
|
129
151
|
}
|
|
130
|
-
|
|
152
|
+
function readSiblingPnpmGlobalNodeModulesRoots(pnpmGlobalRoot) {
|
|
153
|
+
const globalNodeModulesDir = normalizePnpmGlobalNodeModulesRoot(pnpmGlobalRoot);
|
|
154
|
+
const globalProjectDir = path.dirname(globalNodeModulesDir);
|
|
155
|
+
let entries;
|
|
131
156
|
try {
|
|
132
|
-
|
|
133
|
-
return JSON.parse(raw);
|
|
157
|
+
entries = fs.readdirSync(globalProjectDir);
|
|
134
158
|
}
|
|
135
159
|
catch {
|
|
136
|
-
return
|
|
160
|
+
return [];
|
|
137
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'));
|
|
138
175
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
142
|
-
await fsp.writeFile(filePath, JSON.stringify(state, null, 2));
|
|
176
|
+
function readPnpmGlobalNodeModulesRoots(pnpmGlobalRoot) {
|
|
177
|
+
return [normalizePnpmGlobalNodeModulesRoot(pnpmGlobalRoot), ...readSiblingPnpmGlobalNodeModulesRoots(pnpmGlobalRoot)];
|
|
143
178
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return state.entries?.[getInstallMethodCacheKey(packageRoot)];
|
|
179
|
+
function packageNameToPath(packageName) {
|
|
180
|
+
return packageName.split('/').filter(Boolean);
|
|
147
181
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
182
|
+
function isSameRealPath(left, right) {
|
|
183
|
+
try {
|
|
184
|
+
return normalizePath(fs.realpathSync(left)) === normalizePath(fs.realpathSync(right));
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function isPnpmGlobalRootPath(pnpmGlobalRoot, packageRoot) {
|
|
191
|
+
if (isSubPath(pnpmGlobalRoot, packageRoot)) {
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
const globalNodeModulesDir = normalizePnpmGlobalNodeModulesRoot(pnpmGlobalRoot);
|
|
195
|
+
const globalProjectDir = path.dirname(globalNodeModulesDir);
|
|
196
|
+
if (!isSubPath(globalProjectDir, packageRoot)) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
const segments = normalizePath(packageRoot).split(path.sep).filter(Boolean);
|
|
200
|
+
const pnpmStoreIndex = segments.indexOf('.pnpm');
|
|
201
|
+
if (pnpmStoreIndex === -1) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
return segments.slice(pnpmStoreIndex + 1).includes('node_modules');
|
|
205
|
+
}
|
|
206
|
+
function isPnpmGlobalPackagePath(pnpmGlobalRoot, packageRoot, packageName) {
|
|
207
|
+
return readPnpmGlobalNodeModulesRoots(pnpmGlobalRoot).some((pnpmGlobalNodeModulesRoot) => isSameRealPath(path.join(pnpmGlobalNodeModulesRoot, ...packageNameToPath(packageName)), packageRoot));
|
|
208
|
+
}
|
|
209
|
+
function isPnpmGlobalBinProjectPath(pnpmGlobalBin, packageRoot, packageName) {
|
|
210
|
+
const pnpmHome = path.dirname(normalizePath(pnpmGlobalBin));
|
|
211
|
+
const globalDir = path.join(pnpmHome, 'global');
|
|
212
|
+
if (isSubPath(globalDir, packageRoot)) {
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
let entries;
|
|
216
|
+
try {
|
|
217
|
+
entries = fs.readdirSync(globalDir);
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
return entries.some((entry) => {
|
|
223
|
+
const pnpmGlobalRoot = path.join(globalDir, entry);
|
|
224
|
+
return (isPnpmGlobalRootPath(pnpmGlobalRoot, packageRoot)
|
|
225
|
+
|| isPnpmGlobalPackagePath(pnpmGlobalRoot, packageRoot, packageName));
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
function isPnpmGlobalInstallPath(options) {
|
|
229
|
+
if (options.pnpmGlobalRoot) {
|
|
230
|
+
const matchedRoot = isPnpmGlobalRootPath(options.pnpmGlobalRoot, options.packageRoot)
|
|
231
|
+
|| isPnpmGlobalPackagePath(options.pnpmGlobalRoot, options.packageRoot, options.packageName);
|
|
232
|
+
if (matchedRoot) {
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (options.pnpmGlobalBin) {
|
|
237
|
+
return isPnpmGlobalBinProjectPath(options.pnpmGlobalBin, options.packageRoot, options.packageName);
|
|
238
|
+
}
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
function readCurrentBinPath(currentBinPath) {
|
|
242
|
+
const candidate = String(currentBinPath ?? process.argv[1] ?? '').trim();
|
|
243
|
+
if (!candidate) {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
return normalizePath(candidate);
|
|
247
|
+
}
|
|
248
|
+
function cleanCommandPath(value) {
|
|
249
|
+
const trimmed = value.trim();
|
|
250
|
+
return trimmed ? trimmed : undefined;
|
|
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;
|
|
155
268
|
}
|
|
156
269
|
async function readGlobalPrefix(commandOutputFn) {
|
|
157
270
|
try {
|
|
158
|
-
return (await commandOutputFn('npm', ['prefix', '-g'], {
|
|
271
|
+
return cleanCommandPath(await commandOutputFn('npm', ['prefix', '-g'], {
|
|
272
|
+
cwd: resolvePackageManagerProbeCwd(),
|
|
159
273
|
errorName: 'npm prefix',
|
|
160
|
-
}))
|
|
274
|
+
}));
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async function readPnpmGlobalBin(commandOutputFn) {
|
|
281
|
+
try {
|
|
282
|
+
return cleanCommandPath(await commandOutputFn('pnpm', ['bin', '-g'], {
|
|
283
|
+
cwd: resolvePackageManagerProbeCwd(),
|
|
284
|
+
errorName: 'pnpm bin',
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async function readPnpmGlobalRoot(commandOutputFn) {
|
|
292
|
+
try {
|
|
293
|
+
return cleanCommandPath(await commandOutputFn('pnpm', ['root', '-g'], {
|
|
294
|
+
cwd: resolvePackageManagerProbeCwd(),
|
|
295
|
+
errorName: 'pnpm root',
|
|
296
|
+
}));
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
async function readYarnGlobalDir(commandOutputFn) {
|
|
303
|
+
try {
|
|
304
|
+
return cleanCommandPath(await commandOutputFn('yarn', ['global', 'dir'], {
|
|
305
|
+
cwd: resolvePackageManagerProbeCwd(),
|
|
306
|
+
errorName: 'yarn global dir',
|
|
307
|
+
}));
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async function readYarnGlobalBin(commandOutputFn) {
|
|
314
|
+
try {
|
|
315
|
+
return cleanCommandPath(await commandOutputFn('yarn', ['global', 'bin'], {
|
|
316
|
+
cwd: resolvePackageManagerProbeCwd(),
|
|
317
|
+
errorName: 'yarn global bin',
|
|
318
|
+
}));
|
|
161
319
|
}
|
|
162
320
|
catch {
|
|
163
321
|
return undefined;
|
|
@@ -174,21 +332,21 @@ function getUnsupportedSelfUpdateReason(installMethod) {
|
|
|
174
332
|
if (installMethod === 'source') {
|
|
175
333
|
return [
|
|
176
334
|
'This CLI is running from source in a repository checkout.',
|
|
177
|
-
'Automatic self-update is only supported for standard global npm installs.',
|
|
335
|
+
'Automatic self-update is only supported for standard global npm, pnpm, or yarn installs.',
|
|
178
336
|
'Upgrade this checkout through your repo workflow instead.',
|
|
179
337
|
].join(' ');
|
|
180
338
|
}
|
|
181
339
|
if (installMethod === 'package-local') {
|
|
182
340
|
return [
|
|
183
341
|
'This CLI is installed from a local project dependency tree.',
|
|
184
|
-
'Automatic self-update is only supported for standard global npm installs.',
|
|
342
|
+
'Automatic self-update is only supported for standard global npm, pnpm, or yarn installs.',
|
|
185
343
|
'Upgrade the parent project dependency that provides this CLI instead.',
|
|
186
344
|
].join(' ');
|
|
187
345
|
}
|
|
188
346
|
if (installMethod === 'unknown') {
|
|
189
347
|
return [
|
|
190
|
-
'This CLI install could not be recognized as a standard global npm install.',
|
|
191
|
-
'Automatic self-update is only supported for standard global npm installs.',
|
|
348
|
+
'This CLI install could not be recognized as a standard global npm, pnpm, or yarn install.',
|
|
349
|
+
'Automatic self-update is only supported for standard global npm, pnpm, or yarn installs.',
|
|
192
350
|
].join(' ');
|
|
193
351
|
}
|
|
194
352
|
return undefined;
|
|
@@ -217,22 +375,45 @@ export function getSelfUpdatePackageSpec(status) {
|
|
|
217
375
|
}
|
|
218
376
|
export async function inspectSelfInstall(options = {}) {
|
|
219
377
|
const packageRoot = options.packageRoot ? normalizePath(options.packageRoot) : PACKAGE_ROOT;
|
|
378
|
+
const packageName = options.packageName ?? DEFAULT_PACKAGE_NAME;
|
|
220
379
|
const commandOutputFn = options.commandOutputFn ?? commandOutput;
|
|
221
|
-
const
|
|
222
|
-
const globalPrefix =
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
380
|
+
const currentBinPath = readCurrentBinPath(options.currentBinPath);
|
|
381
|
+
const globalPrefix = await readGlobalPrefix(commandOutputFn);
|
|
382
|
+
const pnpmGlobalBin = await readPnpmGlobalBin(commandOutputFn);
|
|
383
|
+
const pnpmGlobalRoot = await readPnpmGlobalRoot(commandOutputFn);
|
|
384
|
+
const yarnGlobalDir = await readYarnGlobalDir(commandOutputFn);
|
|
385
|
+
const yarnGlobalBin = await readYarnGlobalBin(commandOutputFn);
|
|
386
|
+
const installMethod = detectInstallMethodFromSignals({
|
|
387
|
+
packageRoot,
|
|
388
|
+
currentBinPath,
|
|
389
|
+
globalPrefix,
|
|
390
|
+
packageName,
|
|
391
|
+
pnpmGlobalBin,
|
|
392
|
+
pnpmGlobalRoot,
|
|
393
|
+
yarnGlobalBin,
|
|
394
|
+
yarnGlobalDir,
|
|
395
|
+
});
|
|
230
396
|
return {
|
|
231
397
|
packageRoot,
|
|
232
398
|
installMethod,
|
|
233
399
|
globalPrefix,
|
|
234
400
|
};
|
|
235
401
|
}
|
|
402
|
+
function detectInstallMethodFromSignals(options) {
|
|
403
|
+
if (options.currentBinPath && options.yarnGlobalBin && isSubPath(options.yarnGlobalBin, options.currentBinPath)) {
|
|
404
|
+
return 'yarn-global';
|
|
405
|
+
}
|
|
406
|
+
if (options.currentBinPath && options.pnpmGlobalBin && isSubPath(options.pnpmGlobalBin, options.currentBinPath)) {
|
|
407
|
+
return 'pnpm-global';
|
|
408
|
+
}
|
|
409
|
+
return detectInstallMethod(options.packageRoot, {
|
|
410
|
+
globalPrefix: options.globalPrefix,
|
|
411
|
+
packageName: options.packageName,
|
|
412
|
+
pnpmGlobalBin: options.pnpmGlobalBin,
|
|
413
|
+
pnpmGlobalRoot: options.pnpmGlobalRoot,
|
|
414
|
+
yarnGlobalDir: options.yarnGlobalDir,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
236
417
|
export async function inspectSelfStatus(options = {}) {
|
|
237
418
|
const packageRoot = options.packageRoot ? normalizePath(options.packageRoot) : PACKAGE_ROOT;
|
|
238
419
|
const packageName = options.packageName ?? DEFAULT_PACKAGE_NAME;
|
|
@@ -240,6 +421,8 @@ export async function inspectSelfStatus(options = {}) {
|
|
|
240
421
|
const channel = options.channel && options.channel !== 'auto' ? options.channel : detectChannel(currentVersion);
|
|
241
422
|
const commandOutputFn = options.commandOutputFn ?? commandOutput;
|
|
242
423
|
const { installMethod, globalPrefix } = await inspectSelfInstall({
|
|
424
|
+
currentBinPath: options.currentBinPath,
|
|
425
|
+
packageName,
|
|
243
426
|
packageRoot,
|
|
244
427
|
commandOutputFn,
|
|
245
428
|
});
|
|
@@ -261,7 +444,7 @@ export async function inspectSelfStatus(options = {}) {
|
|
|
261
444
|
latestVersion,
|
|
262
445
|
updateAvailable,
|
|
263
446
|
installMethod,
|
|
264
|
-
updatable: installMethod === 'npm-global',
|
|
447
|
+
updatable: installMethod === 'npm-global' || installMethod === 'pnpm-global' || installMethod === 'yarn-global',
|
|
265
448
|
updateBlockedReason: getUnsupportedSelfUpdateReason(installMethod),
|
|
266
449
|
globalPrefix,
|
|
267
450
|
registryError,
|
|
@@ -270,9 +453,30 @@ export async function inspectSelfStatus(options = {}) {
|
|
|
270
453
|
export function formatUnsupportedSelfUpdateMessage(status) {
|
|
271
454
|
return status.updateBlockedReason
|
|
272
455
|
?? [
|
|
273
|
-
'Automatic self-update is only supported for standard global npm installs.',
|
|
456
|
+
'Automatic self-update is only supported for standard global npm, pnpm, or yarn installs.',
|
|
274
457
|
].join('\n');
|
|
275
458
|
}
|
|
459
|
+
function resolveSelfUpdateInstallCommand(installMethod, packageSpec) {
|
|
460
|
+
if (installMethod === 'pnpm-global') {
|
|
461
|
+
return {
|
|
462
|
+
command: 'pnpm',
|
|
463
|
+
args: ['add', '-g', packageSpec],
|
|
464
|
+
errorName: 'pnpm add',
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
if (installMethod === 'yarn-global') {
|
|
468
|
+
return {
|
|
469
|
+
command: 'yarn',
|
|
470
|
+
args: ['global', 'add', packageSpec],
|
|
471
|
+
errorName: 'yarn global add',
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
command: 'npm',
|
|
476
|
+
args: ['install', '-g', packageSpec],
|
|
477
|
+
errorName: 'npm install',
|
|
478
|
+
};
|
|
479
|
+
}
|
|
276
480
|
export async function updateSelf(options = {}) {
|
|
277
481
|
const status = await inspectSelfStatus(options);
|
|
278
482
|
if (!status.updatable) {
|
|
@@ -291,9 +495,10 @@ export async function updateSelf(options = {}) {
|
|
|
291
495
|
};
|
|
292
496
|
}
|
|
293
497
|
const packageSpec = getSelfUpdatePackageSpec(status);
|
|
294
|
-
|
|
498
|
+
const installCommand = resolveSelfUpdateInstallCommand(status.installMethod, packageSpec);
|
|
499
|
+
await (options.runFn ?? run)(installCommand.command, installCommand.args, {
|
|
295
500
|
stdio: options.verbose ? 'inherit' : 'ignore',
|
|
296
|
-
errorName:
|
|
501
|
+
errorName: installCommand.errorName,
|
|
297
502
|
});
|
|
298
503
|
return {
|
|
299
504
|
action: 'updated',
|
|
@@ -151,7 +151,7 @@ export async function shouldRunStartupUpdateCheck(argv, now = new Date()) {
|
|
|
151
151
|
return readCurrentInstallLastCheckedDate(state) !== todayStamp(now);
|
|
152
152
|
}
|
|
153
153
|
export function shouldEnableStartupUpdateForInstallMethod(installMethod) {
|
|
154
|
-
return installMethod === 'npm-global';
|
|
154
|
+
return installMethod === 'npm-global' || installMethod === 'pnpm-global' || installMethod === 'yarn-global';
|
|
155
155
|
}
|
|
156
156
|
function hasPendingUpdates(selfStatus, skillsStatus) {
|
|
157
157
|
return Boolean(selfStatus.updateAvailable || skillsStatus.updateAvailable === true);
|
package/dist/locale/en-US.json
CHANGED
|
@@ -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
|
|
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."
|
package/dist/locale/zh-CN.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/cli",
|
|
3
|
-
"version": "2.2.0-beta.
|
|
3
|
+
"version": "2.2.0-beta.8",
|
|
4
4
|
"description": "NocoBase Command Line Tool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/generated/command-registry.js",
|
|
@@ -143,5 +143,5 @@
|
|
|
143
143
|
"type": "git",
|
|
144
144
|
"url": "git+https://github.com/nocobase/nocobase.git"
|
|
145
145
|
},
|
|
146
|
-
"gitHead": "
|
|
146
|
+
"gitHead": "fa2502c1e9faf6d74b3f51b42dbc6546638d46af"
|
|
147
147
|
}
|