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

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
@@ -5,7 +5,7 @@ import fs from 'node:fs';
5
5
  import { createRequire } from 'node:module';
6
6
  import path from 'node:path';
7
7
  import { fileURLToPath, pathToFileURL } from 'node:url';
8
- import { normalizeSessionEnv } from './session-env.js';
8
+ import { normalizeNodeOptions, normalizeSessionEnv } from './session-env.js';
9
9
 
10
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
11
  const requireFromCli = createRequire(import.meta.url);
@@ -18,6 +18,7 @@ if (process.env.NB_CLI_USE_DIST === '1') {
18
18
  }
19
19
 
20
20
  normalizeSessionEnv();
21
+ normalizeNodeOptions();
21
22
 
22
23
  /**
23
24
  * In the monorepo, plain `node` cannot load `.ts`. Re-exec once with `--import <tsx>`
@@ -4,6 +4,7 @@ const SESSION_ENV_SOURCES = [
4
4
  'COPILOT_AGENT_SESSION_ID',
5
5
  'CLAUDE_CODE_SESSION_ID',
6
6
  ];
7
+ const PRESERVE_SYMLINKS_FLAG = '--preserve-symlinks';
7
8
 
8
9
  export function resolveNormalizedSessionId(env = process.env) {
9
10
  for (const key of SESSION_ENV_SOURCES) {
@@ -25,3 +26,14 @@ export function normalizeSessionEnv(env = process.env) {
25
26
  env.NB_SESSION_ID = sessionId;
26
27
  return sessionId;
27
28
  }
29
+
30
+ export function normalizeNodeOptions(env = process.env) {
31
+ const currentNodeOptions = String(env.NODE_OPTIONS ?? '').trim();
32
+ const flags = currentNodeOptions ? currentNodeOptions.split(/\s+/) : [];
33
+
34
+ if (!flags.includes(PRESERVE_SYMLINKS_FLAG)) {
35
+ env.NODE_OPTIONS = [...flags, PRESERVE_SYMLINKS_FLAG].join(' ');
36
+ }
37
+
38
+ return env.NODE_OPTIONS;
39
+ }
@@ -10,7 +10,7 @@ import { Command, Flags } from '@oclif/core';
10
10
  import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
11
11
  import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runLocalNocoBaseCommand, startDockerContainer, } from '../../lib/app-runtime.js';
12
12
  import { AppHealthCheckError, formatAppUrl, isAppReady, resolveManagedAppApiBaseUrl, waitForAppReady, } from '../../lib/app-health.js';
13
- import { ensureBuiltinDbReady, ensureSavedLocalSource, recreateSavedDockerApp, } from '../../lib/app-managed-resources.js';
13
+ import { ensureBuiltinDbReady, ensureLocalPostinstall, ensureSavedLocalSource, recreateSavedDockerApp, } from '../../lib/app-managed-resources.js';
14
14
  import { run } from '../../lib/run-npm.js';
15
15
  import { announceTargetEnv, failTask, printInfo, startTask, succeedTask } from '../../lib/ui.js';
16
16
  function argvHasToken(argv, tokens) {
@@ -258,6 +258,17 @@ export default class AppStart extends Command {
258
258
  }
259
259
  return;
260
260
  }
261
+ try {
262
+ await ensureLocalPostinstall(runtime, {
263
+ verbose: flags.verbose,
264
+ onStartTask: startTask,
265
+ onSucceedTask: succeedTask,
266
+ onFailTask: failTask,
267
+ });
268
+ }
269
+ catch (error) {
270
+ this.error(error instanceof Error ? error.message : String(error));
271
+ }
261
272
  if (flags.daemon === false) {
262
273
  printInfo(`Starting NocoBase for "${runtime.envName}" in the foreground${appUrl ? ` at ${appUrl}` : ''}. Press Ctrl+C to stop.`);
263
274
  }
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { Command, Flags } from '@oclif/core';
10
10
  import { upsertEnv } from '../../lib/auth-store.js';
11
+ import { ensureLocalPostinstall } from '../../lib/app-managed-resources.js';
11
12
  import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runLocalNocoBaseCommand, startDockerContainer, stopDockerContainer, } from '../../lib/app-runtime.js';
12
13
  import { resolveConfiguredEnvPath } from '../../lib/cli-home.js';
13
14
  import { deriveBuiltinDbConnection } from '../../lib/builtin-db.js';
@@ -436,6 +437,12 @@ export default class AppUpgrade extends Command {
436
437
  else {
437
438
  printInfo(`Skipping code download for "${runtime.envName}" because this env is managed from an existing local app path.`);
438
439
  }
440
+ await ensureLocalPostinstall(runtime, {
441
+ verbose: flags.verbose,
442
+ onStartTask: startTask,
443
+ onSucceedTask: succeedTask,
444
+ onFailTask: failTask,
445
+ });
439
446
  startTask(`Starting upgraded NocoBase for "${runtime.envName}"...`);
440
447
  try {
441
448
  await runLocalNocoBaseCommand(runtime, AppUpgrade.buildLocalStartArgv(runtime), {
@@ -8,8 +8,9 @@
8
8
  */
9
9
  import { Command, Flags } from '@oclif/core';
10
10
  import { ensureCrossEnvConfirmed, hasExplicitEnvSelection } from '../../lib/env-guard.js';
11
+ import { ensureLocalPostinstall } from '../../lib/app-managed-resources.js';
11
12
  import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, runLocalNocoBaseCommand, } from '../../lib/app-runtime.js';
12
- import { announceTargetEnv, printInfo } from '../../lib/ui.js';
13
+ import { announceTargetEnv, failTask, printInfo, startTask, succeedTask } from '../../lib/ui.js';
13
14
  function formatUnsupportedRuntimeMessage(kind, envName) {
14
15
  if (kind === 'docker') {
15
16
  return [
@@ -159,6 +160,12 @@ export default class SourceDev extends Command {
159
160
  }
160
161
  printInfo(`Starting NocoBase dev mode for "${runtime.envName}" from ${runtime.projectRoot}. Press Ctrl+C to stop.`);
161
162
  try {
163
+ await ensureLocalPostinstall(runtime, {
164
+ onStartTask: startTask,
165
+ onSucceedTask: succeedTask,
166
+ onFailTask: failTask,
167
+ verbose: true,
168
+ });
162
169
  await runLocalNocoBaseCommand(runtime, npmArgs, {
163
170
  stdio: 'inherit',
164
171
  });
@@ -0,0 +1,92 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { Command, Flags } from '@oclif/core';
10
+ import { buildSuggestedInitCommand, publishSourceSnapshot } from '../../lib/source-publish.js';
11
+ import { failTask, printInfo, startTask, succeedTask } from '../../lib/ui.js';
12
+ function formatPublishFailure(message) {
13
+ return [
14
+ 'Couldn\'t publish a source snapshot.',
15
+ 'Check that Docker is running, the target npm registry is reachable, and the current directory is a NocoBase source repo.',
16
+ `Details: ${message}`,
17
+ ].join('\n');
18
+ }
19
+ export default class SourcePublish extends Command {
20
+ static description = 'Publish the current NocoBase source repo as a snapshot version to an npm registry for install testing.';
21
+ static examples = [
22
+ '<%= config.bin %> <%= command.id %> --snapshot',
23
+ '<%= config.bin %> <%= command.id %> --snapshot --cwd /path/to/nocobase/source',
24
+ '<%= config.bin %> <%= command.id %> --snapshot --npm-registry=http://127.0.0.1:4873',
25
+ '<%= config.bin %> <%= command.id %> --snapshot --json',
26
+ ];
27
+ static flags = {
28
+ snapshot: Flags.boolean({
29
+ description: 'Publish the current source repo as a unique snapshot version',
30
+ required: true,
31
+ default: false,
32
+ }),
33
+ 'npm-registry': Flags.string({
34
+ description: 'npm registry URL to publish to. Defaults to the running local source registry when available',
35
+ required: false,
36
+ }),
37
+ cwd: Flags.string({
38
+ description: 'Source repository path. Defaults to the nearest detected NocoBase source root from the current working directory',
39
+ required: false,
40
+ }),
41
+ json: Flags.boolean({
42
+ description: 'Print the publish result as JSON',
43
+ default: false,
44
+ }),
45
+ verbose: Flags.boolean({
46
+ description: 'Show detailed command output while versioning and publishing the snapshot',
47
+ default: false,
48
+ }),
49
+ };
50
+ async run() {
51
+ const { flags } = await this.parse(SourcePublish);
52
+ if (!flags.snapshot) {
53
+ this.error('`nb source publish` currently requires `--snapshot`.');
54
+ }
55
+ if (!flags.json) {
56
+ startTask('Publishing a source snapshot...');
57
+ }
58
+ try {
59
+ const result = await publishSourceSnapshot({
60
+ cwd: flags.cwd,
61
+ npmRegistry: flags['npm-registry'],
62
+ verbose: flags.verbose,
63
+ });
64
+ if (flags.json) {
65
+ this.log(JSON.stringify({
66
+ version: result.version,
67
+ npmRegistry: result.npmRegistry,
68
+ gitSha: result.gitSha,
69
+ projectRoot: result.projectRoot,
70
+ suggestedInitCommand: buildSuggestedInitCommand(result),
71
+ }, null, 2));
72
+ return;
73
+ }
74
+ succeedTask(`Published source snapshot ${result.version} to ${result.npmRegistry}.`);
75
+ printInfo(`Source root: ${result.projectRoot}`);
76
+ printInfo(`Snapshot version: ${result.version}`);
77
+ printInfo(`npm registry: ${result.npmRegistry}`);
78
+ printInfo(`Next: ${buildSuggestedInitCommand(result)}`);
79
+ }
80
+ catch (error) {
81
+ const message = error instanceof Error ? error.message : String(error);
82
+ if (flags.json) {
83
+ this.logToStderr(JSON.stringify({
84
+ error: formatPublishFailure(message),
85
+ }, null, 2));
86
+ this.exit(1);
87
+ }
88
+ failTask('Failed to publish the source snapshot.');
89
+ this.error(formatPublishFailure(message));
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { Command, Flags } from '@oclif/core';
10
+ import { run } from '../../../lib/run-npm.js';
11
+ import { printInfo } from '../../../lib/ui.js';
12
+ import { getSourceRegistryInfo } from '../../../lib/source-registry.js';
13
+ function formatLogsFailure(message) {
14
+ if (/does not exist/i.test(message)) {
15
+ return [
16
+ 'Can\'t show source registry logs yet.',
17
+ 'The saved source registry container could not be found on this machine.',
18
+ 'Start the registry first with `nb source registry start`.',
19
+ `Details: ${message}`,
20
+ ].join('\n');
21
+ }
22
+ return [
23
+ 'Couldn\'t show source registry logs.',
24
+ 'Check that Docker is installed and the source registry container still exists, then try again.',
25
+ `Details: ${message}`,
26
+ ].join('\n');
27
+ }
28
+ export default class SourceRegistryLogs extends Command {
29
+ static description = 'Show logs for the local Docker-based npm registry used for source tests.';
30
+ static examples = [
31
+ '<%= config.bin %> <%= command.id %>',
32
+ '<%= config.bin %> <%= command.id %> --tail 200',
33
+ '<%= config.bin %> <%= command.id %> --follow',
34
+ ];
35
+ static flags = {
36
+ tail: Flags.integer({
37
+ description: 'Number of recent log lines to show before following',
38
+ default: 100,
39
+ min: 0,
40
+ }),
41
+ follow: Flags.boolean({
42
+ char: 'f',
43
+ description: 'Keep streaming new log lines',
44
+ default: false,
45
+ allowNo: true,
46
+ }),
47
+ };
48
+ async run() {
49
+ const { flags } = await this.parse(SourceRegistryLogs);
50
+ const info = getSourceRegistryInfo();
51
+ printInfo(flags.follow
52
+ ? `Showing source registry logs from "${info.containerName}" (press Ctrl+C to stop).`
53
+ : `Showing recent source registry logs from "${info.containerName}".`);
54
+ const dockerArgs = ['logs', '--tail', String(flags.tail ?? 100)];
55
+ if (flags.follow) {
56
+ dockerArgs.push('--follow');
57
+ }
58
+ dockerArgs.push(info.containerName);
59
+ try {
60
+ await run('docker', dockerArgs, {
61
+ errorName: 'docker logs',
62
+ stdio: 'inherit',
63
+ });
64
+ }
65
+ catch (error) {
66
+ const message = error instanceof Error ? error.message : String(error);
67
+ this.error(formatLogsFailure(message));
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { Command, Flags } from '@oclif/core';
10
+ import { failTask, startTask, succeedTask } from '../../../lib/ui.js';
11
+ import { getSourceRegistryInfo, startSourceRegistry } from '../../../lib/source-registry.js';
12
+ function formatStartFailure(message) {
13
+ if (/port is already allocated|address already in use/i.test(message)) {
14
+ return [
15
+ 'Can\'t start the source registry.',
16
+ 'Port 4873 is already in use on this machine.',
17
+ 'Stop the conflicting process, or free the port before trying again.',
18
+ `Details: ${message}`,
19
+ ].join('\n');
20
+ }
21
+ return [
22
+ 'Couldn\'t start the source registry.',
23
+ 'Check that Docker is installed and running, then try again.',
24
+ `Details: ${message}`,
25
+ ].join('\n');
26
+ }
27
+ export default class SourceRegistryStart extends Command {
28
+ static description = 'Start the local Docker-based npm registry used for source snapshot publish and install tests.';
29
+ static examples = [
30
+ '<%= config.bin %> <%= command.id %>',
31
+ '<%= config.bin %> <%= command.id %> --verbose',
32
+ ];
33
+ static flags = {
34
+ verbose: Flags.boolean({
35
+ description: 'Show raw Docker output while starting the registry container',
36
+ default: false,
37
+ }),
38
+ };
39
+ async run() {
40
+ const { flags } = await this.parse(SourceRegistryStart);
41
+ const info = getSourceRegistryInfo();
42
+ startTask(`Starting the source registry at ${info.url}...`);
43
+ try {
44
+ const state = await startSourceRegistry({
45
+ stdio: flags.verbose ? 'inherit' : 'ignore',
46
+ });
47
+ succeedTask(state === 'already-running'
48
+ ? `The source registry is already running at ${info.url}.`
49
+ : `The source registry is running at ${info.url}.`);
50
+ }
51
+ catch (error) {
52
+ const message = error instanceof Error ? error.message : String(error);
53
+ failTask('Failed to start the source registry.');
54
+ this.error(formatStartFailure(message));
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { Command, Flags } from '@oclif/core';
10
+ import { renderTable } from '../../../lib/ui.js';
11
+ import { resolveSourceRegistryInfo } from '../../../lib/source-registry.js';
12
+ export default class SourceRegistryStatus extends Command {
13
+ static description = 'Show the status of the local Docker-based npm registry used for source tests.';
14
+ static examples = [
15
+ '<%= config.bin %> <%= command.id %>',
16
+ '<%= config.bin %> <%= command.id %> --json',
17
+ ];
18
+ static flags = {
19
+ json: Flags.boolean({
20
+ description: 'Print the source registry status as JSON',
21
+ default: false,
22
+ }),
23
+ };
24
+ async run() {
25
+ const { flags } = await this.parse(SourceRegistryStatus);
26
+ const info = await resolveSourceRegistryInfo();
27
+ if (flags.json) {
28
+ this.log(JSON.stringify(info, null, 2));
29
+ return;
30
+ }
31
+ this.log(renderTable(['Container', 'Status', 'URL', 'Storage'], [[info.containerName, info.status, info.url, info.storageDir]]));
32
+ }
33
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { Command, Flags } from '@oclif/core';
10
+ import { failTask, startTask, succeedTask } from '../../../lib/ui.js';
11
+ import { stopSourceRegistry } from '../../../lib/source-registry.js';
12
+ function formatStopFailure(message) {
13
+ return [
14
+ 'Couldn\'t stop the source registry.',
15
+ 'Check that Docker is installed and the saved registry container still exists, then try again.',
16
+ `Details: ${message}`,
17
+ ].join('\n');
18
+ }
19
+ export default class SourceRegistryStop extends Command {
20
+ static description = 'Stop the local Docker-based npm registry used for source snapshot tests.';
21
+ static examples = [
22
+ '<%= config.bin %> <%= command.id %>',
23
+ '<%= config.bin %> <%= command.id %> --verbose',
24
+ ];
25
+ static flags = {
26
+ verbose: Flags.boolean({
27
+ description: 'Show raw Docker output while stopping the registry container',
28
+ default: false,
29
+ }),
30
+ };
31
+ async run() {
32
+ const { flags } = await this.parse(SourceRegistryStop);
33
+ startTask('Stopping the source registry...');
34
+ try {
35
+ const state = await stopSourceRegistry({
36
+ stdio: flags.verbose ? 'inherit' : 'ignore',
37
+ });
38
+ succeedTask(state === 'already-stopped'
39
+ ? 'The source registry is already stopped.'
40
+ : 'The source registry has stopped.');
41
+ }
42
+ catch (error) {
43
+ const message = error instanceof Error ? error.message : String(error);
44
+ failTask('Failed to stop the source registry.');
45
+ this.error(formatStopFailure(message));
46
+ }
47
+ }
48
+ }
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
  import { mkdir, readdir } from 'node:fs/promises';
10
- import { dockerContainerExists, startDockerContainer } from './app-runtime.js';
10
+ import { dockerContainerExists, runLocalNocoBaseCommand, startDockerContainer } from './app-runtime.js';
11
11
  import { deriveBuiltinDbConnection, resolveBuiltinDbConnection } from './builtin-db.js';
12
12
  import { resolveConfiguredEnvPath } from './cli-home.js';
13
13
  import { resolveDockerEnvFileArg } from "./docker-env-file.js";
@@ -58,6 +58,14 @@ function formatLocalSourceRestoreFailure(envName, source, message) {
58
58
  `Details: ${message}`,
59
59
  ].join('\n');
60
60
  }
61
+ function formatLocalPostinstallFailure(envName, message) {
62
+ return [
63
+ `Couldn't prepare NocoBase for "${envName}".`,
64
+ 'The CLI was not able to run `nocobase-v1 postinstall` before starting the local app.',
65
+ 'Check the local dependencies, storage path, and saved env settings, then try again.',
66
+ `Details: ${message}`,
67
+ ].join('\n');
68
+ }
61
69
  function formatSavedDockerSettingsIncomplete(envName, missing) {
62
70
  return [
63
71
  `Can't start NocoBase for "${envName}" yet.`,
@@ -274,3 +282,16 @@ export async function ensureSavedLocalSource(runtime, runCommand, options) {
274
282
  throw new Error(formatLocalSourceRestoreFailure(runtime.envName, runtime.source, error instanceof Error ? error.message : String(error)));
275
283
  }
276
284
  }
285
+ export async function ensureLocalPostinstall(runtime, options) {
286
+ options?.onStartTask?.(`Running local postinstall for "${runtime.envName}"...`);
287
+ try {
288
+ await runLocalNocoBaseCommand(runtime, ['postinstall'], {
289
+ stdio: commandStdio(options?.verbose),
290
+ });
291
+ options?.onSucceedTask?.(`Local postinstall finished for "${runtime.envName}".`);
292
+ }
293
+ catch (error) {
294
+ options?.onFailTask?.(`Failed to run local postinstall for "${runtime.envName}".`);
295
+ throw new Error(formatLocalPostinstallFailure(runtime.envName, error instanceof Error ? error.message : String(error)));
296
+ }
297
+ }
@@ -44,16 +44,15 @@ export function resolveCwd(cwd) {
44
44
  }
45
45
  export function resolveProjectCwd(cwd) {
46
46
  const normalizedCwd = typeof cwd === 'string' && cwd.trim() === '' ? undefined : cwd;
47
- const next = normalizedCwd ?? process.cwd();
48
- const resolvedNext = resolveCwd(normalizedCwd);
49
- if (!normalizedCwd || path.isAbsolute(next)) {
50
- return resolvedNext;
51
- }
52
- const baseCwd = process.cwd();
53
- let current = baseCwd;
54
- const fallback = resolvedNext;
47
+ const fallback = resolveCwd(normalizedCwd);
48
+ const isAbsoluteInput = typeof normalizedCwd === 'string' && path.isAbsolute(normalizedCwd);
49
+ let current = isAbsoluteInput ? fallback : process.cwd();
55
50
  while (true) {
56
- const candidate = path.resolve(current, next);
51
+ const candidate = isAbsoluteInput
52
+ ? current
53
+ : normalizedCwd
54
+ ? path.resolve(current, normalizedCwd)
55
+ : current;
57
56
  if (hasLocalNocoBaseBinary(candidate)) {
58
57
  return candidate;
59
58
  }
@@ -0,0 +1,287 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import path from 'node:path';
10
+ import { commandOutput, resolveProjectCwd, run } from './run-npm.js';
11
+ import { DEFAULT_SOURCE_REGISTRY_PORT, parseSourceRegistryUrl, resolveSourceRegistryInfo } from './source-registry.js';
12
+ function trimValue(value) {
13
+ return String(value ?? '').trim();
14
+ }
15
+ function sanitizeEnvSegment(value) {
16
+ return trimValue(value).replace(/[^A-Za-z0-9]+/g, '').slice(0, 16);
17
+ }
18
+ export async function resolveSourcePublishRegistry(explicitRegistry) {
19
+ const normalized = trimValue(explicitRegistry);
20
+ if (normalized) {
21
+ return normalized;
22
+ }
23
+ const info = await resolveSourceRegistryInfo();
24
+ if (info.status === 'running') {
25
+ return info.url;
26
+ }
27
+ throw new Error([
28
+ 'No npm registry was provided for source publish.',
29
+ 'Start the local source registry with `nb source registry start`, or pass `--npm-registry <url>` explicitly.',
30
+ ].join('\n'));
31
+ }
32
+ export async function resolveGitSha(cwd) {
33
+ return await commandOutput('git', ['rev-parse', '--short', 'HEAD'], {
34
+ cwd,
35
+ errorName: 'git rev-parse',
36
+ });
37
+ }
38
+ export async function readRootVersion(cwd) {
39
+ const root = resolveProjectCwd(cwd);
40
+ const { readFile } = await import('node:fs/promises');
41
+ const content = await readFile(path.join(root, 'lerna.json'), 'utf8');
42
+ const parsed = JSON.parse(content);
43
+ const version = trimValue(parsed.version);
44
+ if (!version) {
45
+ throw new Error(`Couldn't read a version from ${path.join(root, 'lerna.json')}.`);
46
+ }
47
+ return version;
48
+ }
49
+ export function buildSnapshotVersion(baseVersion, gitSha, now = new Date()) {
50
+ const yyyy = String(now.getFullYear()).padStart(4, '0');
51
+ const mm = String(now.getMonth() + 1).padStart(2, '0');
52
+ const dd = String(now.getDate()).padStart(2, '0');
53
+ return `${baseVersion}-snapshot.${yyyy}${mm}${dd}.${gitSha}`;
54
+ }
55
+ export async function resolveGitBranch(cwd) {
56
+ const branch = trimValue(await commandOutput('git', ['branch', '--show-current'], {
57
+ cwd,
58
+ errorName: 'git branch --show-current',
59
+ }));
60
+ if (!branch) {
61
+ throw new Error('`nb source publish --snapshot` requires a named Git branch. Detached HEAD is not supported.');
62
+ }
63
+ return branch;
64
+ }
65
+ export async function hasLocalGitChanges(cwd) {
66
+ const output = await commandOutput('git', ['status', '--short', '--untracked-files=all'], {
67
+ cwd,
68
+ errorName: 'git status',
69
+ });
70
+ return trimValue(output).length > 0;
71
+ }
72
+ export function buildSourcePublishBranchName(gitSha, now = new Date()) {
73
+ const yyyy = String(now.getFullYear()).padStart(4, '0');
74
+ const mm = String(now.getMonth() + 1).padStart(2, '0');
75
+ const dd = String(now.getDate()).padStart(2, '0');
76
+ const hh = String(now.getHours()).padStart(2, '0');
77
+ const mi = String(now.getMinutes()).padStart(2, '0');
78
+ const ss = String(now.getSeconds()).padStart(2, '0');
79
+ return `nb/source-publish-${yyyy}${mm}${dd}${hh}${mi}${ss}-${gitSha}`;
80
+ }
81
+ async function runGit(args, options) {
82
+ await run('git', args, {
83
+ cwd: options?.cwd,
84
+ stdio: options?.stdio,
85
+ env: options?.env,
86
+ errorName: options?.errorName ?? `git ${args.join(' ')}`,
87
+ });
88
+ }
89
+ async function commitSourceSnapshotVersion(params) {
90
+ await runGit(['add', '-A'], {
91
+ cwd: params.cwd,
92
+ stdio: params.stdio,
93
+ errorName: 'git add',
94
+ });
95
+ await runGit(['commit', '--no-verify', '-m', `chore(source-publish): ${params.version}`], {
96
+ cwd: params.cwd,
97
+ stdio: params.stdio,
98
+ errorName: 'git commit',
99
+ });
100
+ }
101
+ async function createSourcePublishStash(params) {
102
+ if (!(await hasLocalGitChanges(params.cwd))) {
103
+ return undefined;
104
+ }
105
+ await runGit(['stash', 'push', '-u', '-m', params.label], {
106
+ cwd: params.cwd,
107
+ stdio: params.stdio,
108
+ errorName: 'git stash push',
109
+ });
110
+ return {
111
+ commit: trimValue(await commandOutput('git', ['rev-parse', '--verify', 'refs/stash'], {
112
+ cwd: params.cwd,
113
+ errorName: 'git rev-parse refs/stash',
114
+ })),
115
+ };
116
+ }
117
+ async function resolveSourcePublishStashReference(params) {
118
+ const output = trimValue(await commandOutput('git', ['stash', 'list', '--format=%gd%x00%H'], {
119
+ cwd: params.cwd,
120
+ errorName: 'git stash list',
121
+ }));
122
+ for (const line of output.split('\n')) {
123
+ const [reference, commit] = line.split('\x00');
124
+ if (trimValue(commit) === params.stash.commit) {
125
+ return trimValue(reference);
126
+ }
127
+ }
128
+ throw new Error(`Couldn't locate the saved stash for source publish: ${params.stash.commit}`);
129
+ }
130
+ function buildSourcePublishRecoveryError(params) {
131
+ const originalMessage = params.originalError instanceof Error
132
+ ? params.originalError.message
133
+ : String(params.originalError);
134
+ const cleanupMessage = params.cleanupError instanceof Error
135
+ ? params.cleanupError.message
136
+ : String(params.cleanupError);
137
+ const recoveryHints = [
138
+ `Project root: ${params.projectRoot}`,
139
+ `Temporary branch: ${params.temporaryBranch}`,
140
+ ];
141
+ if (params.stash) {
142
+ recoveryHints.push(`Saved stash commit: ${params.stash.commit}`);
143
+ }
144
+ return new Error([
145
+ originalMessage,
146
+ '',
147
+ 'Cleanup also failed after the publish attempt.',
148
+ `Cleanup error: ${cleanupMessage}`,
149
+ ...recoveryHints,
150
+ ].join('\n'));
151
+ }
152
+ export async function publishSourceSnapshot(params) {
153
+ const projectRoot = resolveProjectCwd(params.cwd);
154
+ const npmRegistry = await resolveSourcePublishRegistry(params.npmRegistry);
155
+ const originalBranch = await resolveGitBranch(projectRoot);
156
+ const gitSha = trimValue(await resolveGitSha(projectRoot));
157
+ const baseVersion = await readRootVersion(projectRoot);
158
+ const version = buildSnapshotVersion(baseVersion, gitSha, params.now);
159
+ const temporaryBranch = buildSourcePublishBranchName(gitSha, params.now);
160
+ const stdio = params.verbose ? 'inherit' : 'ignore';
161
+ let stash;
162
+ let onTemporaryBranch = false;
163
+ let branchCreated = false;
164
+ let publishError;
165
+ let result;
166
+ try {
167
+ stash = await createSourcePublishStash({
168
+ cwd: projectRoot,
169
+ label: temporaryBranch,
170
+ stdio,
171
+ });
172
+ await runGit(['switch', '-c', temporaryBranch], {
173
+ cwd: projectRoot,
174
+ stdio,
175
+ errorName: 'git switch',
176
+ });
177
+ branchCreated = true;
178
+ onTemporaryBranch = true;
179
+ if (stash) {
180
+ await runGit(['stash', 'apply', '--index', await resolveSourcePublishStashReference({
181
+ cwd: projectRoot,
182
+ stash,
183
+ })], {
184
+ cwd: projectRoot,
185
+ stdio,
186
+ errorName: 'git stash apply',
187
+ });
188
+ }
189
+ await run('yarn', ['lerna', 'version', version, '--force-publish=*', '--no-git-tag-version', '-y'], {
190
+ cwd: projectRoot,
191
+ errorName: 'lerna version',
192
+ stdio,
193
+ });
194
+ await commitSourceSnapshotVersion({
195
+ cwd: projectRoot,
196
+ version,
197
+ stdio,
198
+ });
199
+ await run('yarn', ['lerna', 'publish', 'from-package', '--registry', npmRegistry, '--dist-tag', 'local', '--yes', '--no-verify-access', '--git-head', gitSha], {
200
+ cwd: projectRoot,
201
+ errorName: 'lerna publish',
202
+ stdio,
203
+ env: {
204
+ npm_config_registry: npmRegistry,
205
+ },
206
+ });
207
+ result = {
208
+ version,
209
+ npmRegistry,
210
+ gitSha,
211
+ projectRoot,
212
+ };
213
+ }
214
+ catch (error) {
215
+ publishError = error;
216
+ }
217
+ try {
218
+ if (onTemporaryBranch) {
219
+ await runGit(['reset', '--hard', 'HEAD'], {
220
+ cwd: projectRoot,
221
+ stdio,
222
+ errorName: 'git reset --hard',
223
+ });
224
+ await runGit(['clean', '-fd'], {
225
+ cwd: projectRoot,
226
+ stdio,
227
+ errorName: 'git clean -fd',
228
+ });
229
+ await runGit(['switch', originalBranch], {
230
+ cwd: projectRoot,
231
+ stdio,
232
+ errorName: 'git switch',
233
+ });
234
+ onTemporaryBranch = false;
235
+ }
236
+ if (stash) {
237
+ await runGit(['stash', 'pop', '--index', await resolveSourcePublishStashReference({
238
+ cwd: projectRoot,
239
+ stash,
240
+ })], {
241
+ cwd: projectRoot,
242
+ stdio,
243
+ errorName: 'git stash pop',
244
+ });
245
+ stash = undefined;
246
+ }
247
+ if (branchCreated) {
248
+ await runGit(['branch', '-D', temporaryBranch], {
249
+ cwd: projectRoot,
250
+ stdio,
251
+ errorName: 'git branch -D',
252
+ });
253
+ }
254
+ }
255
+ catch (cleanupError) {
256
+ if (publishError) {
257
+ throw buildSourcePublishRecoveryError({
258
+ originalError: publishError,
259
+ cleanupError,
260
+ stash,
261
+ temporaryBranch,
262
+ projectRoot,
263
+ });
264
+ }
265
+ throw new Error([
266
+ 'The source snapshot was published, but local Git cleanup failed afterwards.',
267
+ `Cleanup error: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`,
268
+ `Project root: ${projectRoot}`,
269
+ `Temporary branch: ${temporaryBranch}`,
270
+ ...(stash ? [`Saved stash commit: ${stash.commit}`] : []),
271
+ ].join('\n'));
272
+ }
273
+ if (publishError) {
274
+ throw publishError;
275
+ }
276
+ return result;
277
+ }
278
+ export function buildSuggestedInitCommand(result) {
279
+ const { host, port } = parseSourceRegistryUrl(result.npmRegistry);
280
+ const normalizedRegistry = result.npmRegistry || `http://${host}:${port || DEFAULT_SOURCE_REGISTRY_PORT}`;
281
+ const suggestedEnv = ['snapshot', sanitizeEnvSegment(result.gitSha)].filter(Boolean).join('');
282
+ return [
283
+ `nb init --env ${suggestedEnv} --yes --source npm`,
284
+ `--version ${result.version}`,
285
+ `--npm-registry=${normalizedRegistry}`,
286
+ ].join(' ');
287
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import fsp from 'node:fs/promises';
10
+ import path from 'node:path';
11
+ import { commandOutput, commandSucceeds, resolveCwd, resolveProjectCwd, run } from './run-npm.js';
12
+ import { resolveCliHomeRoot } from './cli-home.js';
13
+ export const DEFAULT_SOURCE_REGISTRY_HOST = '127.0.0.1';
14
+ export const DEFAULT_SOURCE_REGISTRY_PORT = 4873;
15
+ export const DEFAULT_SOURCE_REGISTRY_CONTAINER_NAME = 'nb-source-registry';
16
+ export const DEFAULT_SOURCE_REGISTRY_IMAGE = 'verdaccio/verdaccio';
17
+ export function parseSourceRegistryUrl(url) {
18
+ const parsed = new URL(url);
19
+ const host = trimValue(parsed.hostname) || DEFAULT_SOURCE_REGISTRY_HOST;
20
+ const portText = trimValue(parsed.port);
21
+ const port = portText ? Number(portText) : DEFAULT_SOURCE_REGISTRY_PORT;
22
+ return {
23
+ host,
24
+ port: Number.isFinite(port) && port > 0 ? port : DEFAULT_SOURCE_REGISTRY_PORT,
25
+ };
26
+ }
27
+ function trimValue(value) {
28
+ return String(value ?? '').trim();
29
+ }
30
+ function asPosixPathForDockerMount(value) {
31
+ return resolveCwd(value).replace(/\\/g, '/');
32
+ }
33
+ export function resolveSourceRegistryRootDir() {
34
+ return path.join(resolveCliHomeRoot(), 'verdaccio');
35
+ }
36
+ export function resolveSourceRegistryConfigPath() {
37
+ return path.join(resolveSourceRegistryRootDir(), 'config.yaml');
38
+ }
39
+ export function resolveSourceRegistryStorageDir() {
40
+ return path.join(resolveSourceRegistryRootDir(), 'storage');
41
+ }
42
+ export function resolveSourceRegistryUrl(host = DEFAULT_SOURCE_REGISTRY_HOST, port = DEFAULT_SOURCE_REGISTRY_PORT) {
43
+ return `http://${host}:${port}`;
44
+ }
45
+ export function getSourceRegistryInfo() {
46
+ const host = DEFAULT_SOURCE_REGISTRY_HOST;
47
+ const port = DEFAULT_SOURCE_REGISTRY_PORT;
48
+ const rootDir = resolveSourceRegistryRootDir();
49
+ return {
50
+ containerName: DEFAULT_SOURCE_REGISTRY_CONTAINER_NAME,
51
+ image: DEFAULT_SOURCE_REGISTRY_IMAGE,
52
+ host,
53
+ port,
54
+ url: resolveSourceRegistryUrl(host, port),
55
+ rootDir,
56
+ configPath: resolveSourceRegistryConfigPath(),
57
+ storageDir: resolveSourceRegistryStorageDir(),
58
+ status: 'missing',
59
+ };
60
+ }
61
+ export function resolveSourceRegistryTemplatePath(cwd) {
62
+ return path.join(resolveProjectCwd(cwd), 'config.yaml');
63
+ }
64
+ function applySourceRegistryTemplateOverrides(template) {
65
+ return template
66
+ .replace(/\r\n/g, '\n')
67
+ .replace(/^storage:\s*.+$/m, 'storage: /verdaccio/storage')
68
+ .replace(/^(\s*)file:\s*\.\/*htpasswd\s*$/m, '$1file: /verdaccio/storage/htpasswd')
69
+ .replace(/^(\s*)publish:\s+\$authenticated\s*$/gm, '$1publish: $all')
70
+ .replace(/^(\s*)unpublish:\s+\$authenticated\s*$/gm, '$1unpublish: $all');
71
+ }
72
+ async function buildFallbackSourceRegistryConfigTemplate() {
73
+ return [
74
+ 'storage: ./storage',
75
+ 'auth:',
76
+ ' htpasswd:',
77
+ ' file: ./htpasswd',
78
+ 'uplinks:',
79
+ ' npmjs:',
80
+ ' url: https://registry.npmmirror.com/',
81
+ 'packages:',
82
+ " '@*/*':",
83
+ ' access: $all',
84
+ ' publish: $authenticated',
85
+ ' unpublish: $authenticated',
86
+ ' proxy: npmjs',
87
+ " '**':",
88
+ ' access: $all',
89
+ ' publish: $authenticated',
90
+ ' unpublish: $authenticated',
91
+ ' proxy: npmjs',
92
+ 'server:',
93
+ ' keepAliveTimeout: 60',
94
+ ' dotfiles: ignore',
95
+ 'max_body_size: 100mb',
96
+ 'middlewares:',
97
+ ' audit:',
98
+ ' enabled: true',
99
+ '',
100
+ ].join('\n');
101
+ }
102
+ export async function buildSourceRegistryConfig(cwd) {
103
+ const templatePath = resolveSourceRegistryTemplatePath(cwd);
104
+ try {
105
+ const template = await fsp.readFile(templatePath, 'utf8');
106
+ return applySourceRegistryTemplateOverrides(template);
107
+ }
108
+ catch {
109
+ const fallback = await buildFallbackSourceRegistryConfigTemplate();
110
+ return applySourceRegistryTemplateOverrides(fallback);
111
+ }
112
+ }
113
+ export async function ensureSourceRegistryFiles(cwd) {
114
+ const info = getSourceRegistryInfo();
115
+ await fsp.mkdir(info.storageDir, { recursive: true });
116
+ await fsp.mkdir(info.rootDir, { recursive: true });
117
+ await fsp.writeFile(info.configPath, await buildSourceRegistryConfig(cwd), 'utf8');
118
+ return info;
119
+ }
120
+ export async function sourceRegistryContainerExists(containerName = DEFAULT_SOURCE_REGISTRY_CONTAINER_NAME) {
121
+ return await commandSucceeds('docker', ['container', 'inspect', containerName]);
122
+ }
123
+ export async function sourceRegistryContainerIsRunning(containerName = DEFAULT_SOURCE_REGISTRY_CONTAINER_NAME) {
124
+ try {
125
+ const output = await commandOutput('docker', ['inspect', '--format', '{{.State.Running}}', containerName], { errorName: 'docker inspect' });
126
+ return trimValue(output) === 'true';
127
+ }
128
+ catch {
129
+ return false;
130
+ }
131
+ }
132
+ export async function resolveSourceRegistryInfo() {
133
+ const base = getSourceRegistryInfo();
134
+ if (!(await sourceRegistryContainerExists(base.containerName))) {
135
+ return base;
136
+ }
137
+ return {
138
+ ...base,
139
+ status: (await sourceRegistryContainerIsRunning(base.containerName)) ? 'running' : 'stopped',
140
+ };
141
+ }
142
+ export async function startSourceRegistry(options) {
143
+ const info = await ensureSourceRegistryFiles(options?.cwd);
144
+ const exists = await sourceRegistryContainerExists(info.containerName);
145
+ if (exists) {
146
+ if (await sourceRegistryContainerIsRunning(info.containerName)) {
147
+ return 'already-running';
148
+ }
149
+ await run('docker', ['start', info.containerName], {
150
+ errorName: 'docker start',
151
+ stdio: options?.stdio,
152
+ });
153
+ return 'started';
154
+ }
155
+ const configMount = `${asPosixPathForDockerMount(info.configPath)}:/verdaccio/conf/config.yaml`;
156
+ const storageMount = `${asPosixPathForDockerMount(info.storageDir)}:/verdaccio/storage`;
157
+ await run('docker', [
158
+ 'run',
159
+ '-d',
160
+ '--name',
161
+ info.containerName,
162
+ '-p',
163
+ `${info.port}:4873`,
164
+ '-v',
165
+ configMount,
166
+ '-v',
167
+ storageMount,
168
+ info.image,
169
+ ], {
170
+ errorName: 'docker run',
171
+ stdio: options?.stdio,
172
+ });
173
+ return 'started';
174
+ }
175
+ export async function stopSourceRegistry(options) {
176
+ const info = getSourceRegistryInfo();
177
+ if (!(await sourceRegistryContainerExists(info.containerName))) {
178
+ return 'already-stopped';
179
+ }
180
+ if (!(await sourceRegistryContainerIsRunning(info.containerName))) {
181
+ return 'already-stopped';
182
+ }
183
+ await run('docker', ['stop', info.containerName], {
184
+ errorName: 'docker stop',
185
+ stdio: options?.stdio,
186
+ });
187
+ return 'stopped';
188
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/cli",
3
- "version": "2.1.0-beta.34",
3
+ "version": "2.1.0-beta.35",
4
4
  "description": "NocoBase Command Line Tool",
5
5
  "type": "module",
6
6
  "main": "dist/generated/command-registry.js",
@@ -105,5 +105,5 @@
105
105
  "type": "git",
106
106
  "url": "git+https://github.com/nocobase/nocobase.git"
107
107
  },
108
- "gitHead": "ca804833299c547f8d49f8d58f73273a4bfcd03c"
108
+ "gitHead": "74310d8b9e9581fcde14b5a93d12b41ddb5bb325"
109
109
  }