@nocobase/cli 2.1.3 → 2.1.4-test.2

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/bin/run.js CHANGED
@@ -27,6 +27,56 @@ if (!isSupportedNodeVersion()) {
27
27
  normalizeSessionEnv();
28
28
  normalizeNodeOptions();
29
29
 
30
+ const windowsAdministratorCheckScript = [
31
+ '$identity = [Security.Principal.WindowsIdentity]::GetCurrent();',
32
+ '$principal = New-Object Security.Principal.WindowsPrincipal($identity);',
33
+ 'if ($principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { exit 0 }',
34
+ 'exit 1',
35
+ ].join(' ');
36
+
37
+ function isWindowsAdministrator() {
38
+ for (const command of ['pwsh.exe', 'powershell.exe']) {
39
+ const result = spawnSync(
40
+ command,
41
+ ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', windowsAdministratorCheckScript],
42
+ {
43
+ stdio: 'ignore',
44
+ windowsHide: true,
45
+ },
46
+ );
47
+
48
+ if (result.error?.code === 'ENOENT') {
49
+ continue;
50
+ }
51
+
52
+ return result.status === 0;
53
+ }
54
+
55
+ return false;
56
+ }
57
+
58
+ function ensureWindowsAdministrator() {
59
+ if (process.platform !== 'win32' || process.env.NB_CLI_WINDOWS_ADMIN_CHECKED === '1') {
60
+ return;
61
+ }
62
+
63
+ if (!isWindowsAdministrator()) {
64
+ console.error(
65
+ pc.red(
66
+ [
67
+ 'NocoBase CLI must be run as Administrator on Windows.',
68
+ 'Open PowerShell 5 or PowerShell 7 with "Run as administrator", then run the command again.',
69
+ ].join('\n'),
70
+ ),
71
+ );
72
+ process.exit(1);
73
+ }
74
+
75
+ process.env.NB_CLI_WINDOWS_ADMIN_CHECKED = '1';
76
+ }
77
+
78
+ ensureWindowsAdministrator();
79
+
30
80
  /**
31
81
  * In the monorepo, plain `node` cannot load `.ts`. Re-exec once with `--import <tsx>`
32
82
  * (same effect as a dedicated dev entry with `#!/usr/bin/env -S node --import tsx`).
@@ -143,6 +143,7 @@ export default class AppDestroy extends Command {
143
143
  }
144
144
  announceTargetEnv(runtime.envName);
145
145
  try {
146
+ const retryCommand = `nb env remove ${runtime.envName} --purge --force`;
146
147
  if (runtime.kind === 'docker') {
147
148
  startTask(`Removing Docker app container for "${runtime.envName}"...`);
148
149
  const state = await removeDockerContainerIfExists(runtime.containerName, {
@@ -192,7 +193,7 @@ export default class AppDestroy extends Command {
192
193
  const localAppPath = resolveManagedLocalAppPath(runtime);
193
194
  if (localAppPath && removesManagedLocalAppFiles) {
194
195
  startTask(`Removing managed local app files for "${runtime.envName}"...`);
195
- await removePathIfExists(localAppPath, `managed app files for "${runtime.envName}"`);
196
+ await removePathIfExists(localAppPath, `managed app files for "${runtime.envName}"`, { retryCommand });
196
197
  succeedTask(`Managed local app files removed for "${runtime.envName}".`);
197
198
  }
198
199
  else {
@@ -206,13 +207,13 @@ export default class AppDestroy extends Command {
206
207
  ];
207
208
  startTask(`Removing proxy entry files for "${runtime.envName}"...`);
208
209
  for (const proxyEntryDir of proxyEntryDirs) {
209
- await removePathIfExists(proxyEntryDir, `proxy entry files for "${runtime.envName}"`);
210
+ await removePathIfExists(proxyEntryDir, `proxy entry files for "${runtime.envName}"`, { retryCommand });
210
211
  }
211
212
  succeedTask(`Proxy entry files removed for "${runtime.envName}".`);
212
213
  const configuredStoragePath = resolveConfiguredStoragePath(runtime.env.config);
213
214
  if (configuredStoragePath) {
214
215
  startTask(`Removing storage data for "${runtime.envName}"...`);
215
- await removePathIfExists(configuredStoragePath, `storage data for "${runtime.envName}"`);
216
+ await removePathIfExists(configuredStoragePath, `storage data for "${runtime.envName}"`, { retryCommand });
216
217
  succeedTask(`Storage data removed for "${runtime.envName}".`);
217
218
  }
218
219
  else {
@@ -25,10 +25,54 @@ function assertSafeRemovalPath(target, label) {
25
25
  throw new Error(`Refusing to remove ${label} at "${resolved}" because it is too broad.`);
26
26
  }
27
27
  }
28
- export async function removePathIfExists(target, label) {
28
+ function getErrorCode(error) {
29
+ if (!(error instanceof Error)) {
30
+ return undefined;
31
+ }
32
+ const { code } = error;
33
+ return typeof code === 'string' ? code : undefined;
34
+ }
35
+ function isPermissionDeniedError(error) {
36
+ const code = getErrorCode(error);
37
+ return code === 'EACCES' || code === 'EPERM';
38
+ }
39
+ function formatOriginalError(error) {
40
+ const message = error instanceof Error ? error.message : String(error);
41
+ const code = getErrorCode(error);
42
+ return code && !message.includes(code) ? `${code}: ${message}` : message;
43
+ }
44
+ function quoteShellValue(value) {
45
+ return `"${value.replace(/(["\\$`])/g, '\\$1')}"`;
46
+ }
47
+ function formatPermissionDeniedRemovalError(target, label, error, options) {
48
+ const retryLines = os.platform() === 'win32' ? [] : [` sudo chown -R "$(id -u):$(id -g)" ${quoteShellValue(target)}`];
49
+ if (options.retryCommand) {
50
+ retryLines.push(` ${options.retryCommand}`);
51
+ }
52
+ return [
53
+ `Failed to remove ${label} at "${target}".`,
54
+ 'The current user cannot delete one or more files under this path. Files may have been created by a Docker container running as root.',
55
+ '',
56
+ retryLines.length > 0
57
+ ? 'Fix ownership or permissions, then retry:'
58
+ : 'Fix ownership or permissions, then retry the command.',
59
+ ...retryLines,
60
+ '',
61
+ `Original error: ${formatOriginalError(error)}`,
62
+ ].join('\n');
63
+ }
64
+ export async function removePathIfExists(target, label, options = {}) {
29
65
  const resolved = path.resolve(target);
30
66
  assertSafeRemovalPath(resolved, label);
31
- await fsp.rm(resolved, { recursive: true, force: true });
67
+ try {
68
+ await fsp.rm(resolved, { recursive: true, force: true });
69
+ }
70
+ catch (error) {
71
+ if (isPermissionDeniedError(error)) {
72
+ throw new Error(formatPermissionDeniedRemovalError(resolved, label, error, options));
73
+ }
74
+ throw error;
75
+ }
32
76
  }
33
77
  function isMissingDockerContainerError(error) {
34
78
  const message = error instanceof Error ? error.message : String(error);
@@ -116,7 +160,9 @@ export function managedDockerNetworkName(runtime) {
116
160
  return runtime.dockerNetworkName?.trim() || runtime.workspaceName?.trim() || undefined;
117
161
  }
118
162
  export function resolveManagedLocalAppPath(runtime) {
119
- return resolveConfiguredAppPath(runtime.env.config) || runtime.projectRoot || resolveConfiguredPath(runtime.env.config.appRootPath);
163
+ return (resolveConfiguredAppPath(runtime.env.config) ||
164
+ runtime.projectRoot ||
165
+ resolveConfiguredPath(runtime.env.config.appRootPath));
120
166
  }
121
167
  export function shouldRemoveManagedLocalAppFiles(runtime) {
122
168
  return runtime.source === 'npm' || runtime.source === 'git' || runtime.source === 'local';
@@ -143,11 +143,14 @@ export default class LicenseActivate extends Command {
143
143
  const validation = await validateLicenseKey(runtime, resolvedKey);
144
144
  const ok = !validation.keyStatus && validation.envMatch && validation.domainMatch && validation.licenseStatus === 'active';
145
145
  const licenseKeyPath = ok ? await saveLicenseKey(runtime, resolvedKey) : resolveLicenseKeyFile(runtime);
146
+ const shouldResolveInstanceId = Boolean(interactiveKeyFlowInstanceId || ok || (flags.json && !validation.keyStatus));
146
147
  const payload = {
147
148
  ok,
148
149
  env: runtime.envName,
149
150
  kind: runtime.kind,
150
- instanceId: interactiveKeyFlowInstanceId ?? (await ensureInstanceId(runtime)),
151
+ instanceId: shouldResolveInstanceId
152
+ ? interactiveKeyFlowInstanceId ?? (await ensureInstanceId(runtime))
153
+ : undefined,
151
154
  mode: 'key',
152
155
  key: redactLicenseKey(resolvedKey),
153
156
  keyFile: keyFile || undefined,
@@ -10,8 +10,8 @@ import { Flags } from '@oclif/core';
10
10
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
11
11
  import path from 'node:path';
12
12
  import { getEnvAsync, getInstanceIdAsync, keyDecrypt } from '@nocobase/license-kit';
13
- import { checkExternalDbConnection, readExternalDbConnectionConfig, } from "../../lib/db-connection-check.js";
14
- import { DEFAULT_DOCKER_REGISTRY, DEFAULT_DOCKER_VERSION, resolveDockerImageRef, } from "../../lib/docker-image.js";
13
+ import { checkExternalDbConnection, readExternalDbConnectionConfig } from "../../lib/db-connection-check.js";
14
+ import { DEFAULT_DOCKER_REGISTRY, DEFAULT_DOCKER_VERSION, resolveDockerImageRef } from "../../lib/docker-image.js";
15
15
  import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime } from '../../lib/app-runtime.js';
16
16
  import { buildRuntimeEnvVars } from '../../lib/runtime-env-vars.js';
17
17
  import { resolveLicensePkgUrlFromConfig } from '../../lib/cli-config.js';
@@ -102,12 +102,7 @@ function buildDockerLicenseDbFlagArgs(envVars) {
102
102
  ];
103
103
  }
104
104
  async function runDockerLicenseJsonCommand(runtime, commandArgs) {
105
- const args = [
106
- 'run',
107
- '--rm',
108
- '--network',
109
- runtime.dockerNetworkName || runtime.workspaceName,
110
- ];
105
+ const args = ['run', '--rm', '--network', runtime.dockerNetworkName || runtime.workspaceName];
111
106
  const dockerPlatform = normalizeDockerPlatform(runtime.env.config?.dockerPlatform);
112
107
  if (dockerPlatform) {
113
108
  args.push('--platform', dockerPlatform);
@@ -193,11 +188,11 @@ export async function generateValidatedInstanceIdFromEnvVars(envVars) {
193
188
  }
194
189
  async function generateInstanceIdForDockerRuntime(runtime) {
195
190
  const envVars = await buildRuntimeEnvVars(runtime);
196
- const payload = await runDockerLicenseJsonCommand(runtime, [
191
+ const payload = (await runDockerLicenseJsonCommand(runtime, [
197
192
  'license',
198
193
  'generate-id',
199
194
  ...buildDockerLicenseDbFlagArgs(envVars),
200
- ]);
195
+ ]));
201
196
  const instanceId = trimValue(payload.instanceId);
202
197
  if (!instanceId) {
203
198
  throw new Error('Docker instance ID generation did not return an instance ID.');
@@ -337,7 +332,7 @@ export async function getLicenseStatus(keyData) {
337
332
  }),
338
333
  signal: controller.signal,
339
334
  });
340
- const payload = await response.json();
335
+ const payload = (await response.json());
341
336
  return payload?.data?.status === 'active' ? 'active' : 'invalid';
342
337
  }
343
338
  catch {
@@ -356,6 +351,22 @@ export async function validateLicenseKey(runtime, key) {
356
351
  catch {
357
352
  keyStatus = 'invalid';
358
353
  }
354
+ if (keyStatus) {
355
+ const currentDomain = appUrl(runtime);
356
+ return {
357
+ current: {
358
+ env: undefined,
359
+ domain: currentDomain ? new URL(currentDomain).host : '',
360
+ },
361
+ keyData,
362
+ keyStatus,
363
+ dbMatch: false,
364
+ sysMatch: false,
365
+ envMatch: false,
366
+ domainMatch: false,
367
+ licenseStatus: 'invalid',
368
+ };
369
+ }
359
370
  const currentEnv = await getCurrentLicenseEnv(runtime);
360
371
  const currentDomain = appUrl(runtime);
361
372
  const dbMatch = isDbMatch(currentEnv, keyData);
@@ -391,7 +402,7 @@ export async function resolveLicenseServiceUrl(value) {
391
402
  return (await resolveLicensePkgUrl(value)).replace(/\/+$/, '');
392
403
  }
393
404
  export async function resolveLicensePkgUrl(value) {
394
- const normalized = String(value ?? '').trim() || await resolveLicensePkgUrlFromConfig();
405
+ const normalized = String(value ?? '').trim() || (await resolveLicensePkgUrlFromConfig());
395
406
  return normalized.replace(/\/+$/, '') + '/';
396
407
  }
397
408
  function shouldRedactOutputKey(key) {
@@ -414,9 +425,7 @@ export function sanitizeLicenseOutput(value) {
414
425
  if (value && typeof value === 'object') {
415
426
  return Object.fromEntries(Object.entries(value).map(([key, nestedValue]) => [
416
427
  key,
417
- shouldRedactOutputKey(key)
418
- ? redactOutputValue(String(nestedValue ?? ''))
419
- : sanitizeLicenseOutput(nestedValue),
428
+ shouldRedactOutputKey(key) ? redactOutputValue(String(nestedValue ?? '')) : sanitizeLicenseOutput(nestedValue),
420
429
  ]));
421
430
  }
422
431
  return value;
@@ -20,7 +20,7 @@ export default class SelfCheck extends Command {
20
20
  static flags = {
21
21
  channel: Flags.string({
22
22
  description: 'Release channel to compare against. Defaults to the current CLI channel.',
23
- options: ['auto', 'latest', 'beta', 'alpha'],
23
+ options: ['auto', 'latest', 'test', 'beta', 'alpha'],
24
24
  default: 'auto',
25
25
  }),
26
26
  json: Flags.boolean({
@@ -31,12 +31,12 @@ export default class SelfUpdate extends Command {
31
31
  '<%= config.bin %> <%= command.id %>',
32
32
  '<%= config.bin %> <%= command.id %> --yes',
33
33
  '<%= config.bin %> <%= command.id %> --skills',
34
- '<%= config.bin %> <%= command.id %> --channel alpha --json',
34
+ '<%= config.bin %> <%= command.id %> --channel test --json',
35
35
  ];
36
36
  static flags = {
37
37
  channel: Flags.string({
38
38
  description: 'Release channel to update to. Defaults to the current CLI channel.',
39
- options: ['auto', 'latest', 'beta', 'alpha'],
39
+ options: ['auto', 'latest', 'test', 'beta', 'alpha'],
40
40
  default: 'auto',
41
41
  }),
42
42
  yes: Flags.boolean({
@@ -97,6 +97,9 @@ function detectChannel(currentVersion) {
97
97
  if (/-beta(?:[.-]|$)/i.test(currentVersion)) {
98
98
  return 'beta';
99
99
  }
100
+ if (/-test(?:[.-]|$)/i.test(currentVersion)) {
101
+ return 'test';
102
+ }
100
103
  return 'latest';
101
104
  }
102
105
  function readCurrentVersion(packageRoot) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/cli",
3
- "version": "2.1.3",
3
+ "version": "2.1.4-test.2",
4
4
  "description": "NocoBase Command Line Tool",
5
5
  "type": "module",
6
6
  "main": "dist/generated/command-registry.js",
@@ -142,6 +142,5 @@
142
142
  "repository": {
143
143
  "type": "git",
144
144
  "url": "git+https://github.com/nocobase/nocobase.git"
145
- },
146
- "gitHead": "f61e75119a74bbac25879f4edb8cf9913c99098a"
145
+ }
147
146
  }