@nocobase/cli 2.1.31 → 2.2.0-alpha.10

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.
@@ -34,6 +34,7 @@ export const SUPPORTED_CLI_CONFIG_KEYS = [
34
34
  'locale',
35
35
  'default-ui-host',
36
36
  'default-api-host',
37
+ 'default-portal-template',
37
38
  'update.policy',
38
39
  'license.pkg-url',
39
40
  'docker.network',
@@ -120,7 +121,10 @@ function pruneSettings(config) {
120
121
  delete config.settings.locale;
121
122
  }
122
123
  const init = config.settings?.init;
123
- if (init && !trimValue(init.defaultUiHost) && !trimValue(init.defaultApiHost)) {
124
+ if (init &&
125
+ !trimValue(init.defaultUiHost) &&
126
+ !trimValue(init.defaultApiHost) &&
127
+ !trimValue(init.defaultPortalTemplate)) {
124
128
  delete config.settings?.init;
125
129
  }
126
130
  const update = config.settings?.update;
@@ -181,6 +185,8 @@ export function getExplicitCliConfigValue(config, key) {
181
185
  return trimValue(config.settings?.init?.defaultUiHost);
182
186
  case 'default-api-host':
183
187
  return trimValue(config.settings?.init?.defaultApiHost);
188
+ case 'default-portal-template':
189
+ return trimValue(config.settings?.init?.defaultPortalTemplate);
184
190
  case 'update.policy':
185
191
  return normalizeCliUpdatePolicy(config.settings?.update?.policy);
186
192
  case 'license.pkg-url':
@@ -233,6 +239,8 @@ export function getEffectiveCliConfigValue(config, key) {
233
239
  return '127.0.0.1';
234
240
  case 'default-api-host':
235
241
  return '127.0.0.1';
242
+ case 'default-portal-template':
243
+ return explicit ?? '';
236
244
  case 'update.policy':
237
245
  return explicit ?? DEFAULT_UPDATE_POLICY;
238
246
  case 'license.pkg-url':
@@ -377,6 +385,12 @@ export async function setCliConfigValue(key, value, options = {}) {
377
385
  defaultApiHost: normalized,
378
386
  };
379
387
  break;
388
+ case 'default-portal-template':
389
+ config.settings.init = {
390
+ ...(config.settings.init ?? {}),
391
+ defaultPortalTemplate: normalized,
392
+ };
393
+ break;
380
394
  case 'update.policy':
381
395
  config.settings.update = {
382
396
  ...(config.settings.update ?? {}),
@@ -512,6 +526,11 @@ export async function deleteCliConfigValue(key, options = {}) {
512
526
  delete config.settings.init.defaultApiHost;
513
527
  }
514
528
  break;
529
+ case 'default-portal-template':
530
+ if (config.settings.init) {
531
+ delete config.settings.init.defaultPortalTemplate;
532
+ }
533
+ break;
515
534
  case 'update.policy':
516
535
  if (config.settings.update) {
517
536
  delete config.settings.update.policy;
@@ -36,6 +36,9 @@ const STRING_ENV_CONFIG_KEYS = [
36
36
  'dbSchema',
37
37
  'dbTablePrefix',
38
38
  'lang',
39
+ 'portalType',
40
+ 'portalName',
41
+ 'portalTemplate',
39
42
  'rootUsername',
40
43
  'rootEmail',
41
44
  'rootPassword',
@@ -23,6 +23,7 @@ const DEFAULT_MODERN_CLIENT_PREFIX = 'v';
23
23
  const DEFAULT_API_CLIENT_STORAGE_PREFIX = 'NOCOBASE_';
24
24
  const DEFAULT_API_CLIENT_STORAGE_TYPE = 'localStorage';
25
25
  const DEFAULT_ESM_CDN_BASE_URL = 'https://esm.sh';
26
+ const PORTAL_CLIENT_PREFIX = 'x';
26
27
  const LOCAL_APP_PACKAGE_JSON_PATH = 'node_modules/@nocobase/app/package.json';
27
28
  const MANAGED_PROXY_BLOCK_BEGIN = '# BEGIN NocoBase proxy';
28
29
  const MANAGED_PROXY_BLOCK_END = '# END NocoBase proxy';
@@ -489,6 +490,7 @@ function buildNginxManagedConfigBlock(context) {
489
490
  const v2PublicPathNoTrailingSlash = trimTrailingSlash(context.v2PublicPath);
490
491
  const apiBasePathNoTrailingSlash = trimTrailingSlash(context.apiBasePath);
491
492
  const appPublicPathNoTrailingSlash = trimTrailingSlash(context.appPublicPath);
493
+ const fileAccessPath = `${context.appPublicPath}files/`;
492
494
  const isRootMounted = context.appPublicPath === '/';
493
495
  const appPublicPathRedirectBlock = isRootMounted
494
496
  ? ''
@@ -526,6 +528,22 @@ function buildNginxManagedConfigBlock(context) {
526
528
  ` include ${context.snippetsDir}/proxy-location.conf;`,
527
529
  ' }',
528
530
  '',
531
+ ` location ^~ ${fileAccessPath} {`,
532
+ ` proxy_pass ${context.backendUrl};`,
533
+ ` include ${context.snippetsDir}/proxy-location.conf;`,
534
+ ' }',
535
+ ...(!isRootMounted
536
+ ? [
537
+ '',
538
+ ' location ^~ /files/ {',
539
+ ` proxy_pass ${context.backendUrl};`,
540
+ ` include ${context.snippetsDir}/proxy-location.conf;`,
541
+ ' }',
542
+ ]
543
+ : []),
544
+ '',
545
+ buildNginxPortalLocationBlock(context),
546
+ '',
529
547
  ` location = ${apiBasePathNoTrailingSlash} {`,
530
548
  ` return 308 ${context.apiBasePath}$is_args$args;`,
531
549
  ' }',
@@ -551,7 +569,7 @@ function buildNginxManagedConfigBlock(context) {
551
569
  ` include ${context.snippetsDir}/spa-location.conf;`,
552
570
  ' }',
553
571
  '',
554
- ` location ^~ ${context.appPublicPath} {`,
572
+ ` location ${context.appPublicPath} {`,
555
573
  ` alias ${context.publicDir}/;`,
556
574
  ` try_files $uri /index-v1.html =404;`,
557
575
  ` include ${context.snippetsDir}/spa-location.conf;`,
@@ -560,6 +578,65 @@ function buildNginxManagedConfigBlock(context) {
560
578
  ` ${MANAGED_NGINX_CONFIG_BLOCK_END}`,
561
579
  ].join('\n');
562
580
  }
581
+ function buildNginxPortalRootPublicPath(appPublicPath) {
582
+ return appPublicPath === DEFAULT_APP_PUBLIC_PATH
583
+ ? `/${PORTAL_CLIENT_PREFIX}/`
584
+ : `${trimTrailingSlash(appPublicPath)}/${PORTAL_CLIENT_PREFIX}/`;
585
+ }
586
+ function buildNginxPortalLocationBlock(context) {
587
+ const portalBasePath = trimTrailingSlash(buildNginxPortalRootPublicPath(context.appPublicPath));
588
+ const portalBasePathPattern = escapeRegExp(portalBasePath);
589
+ return [
590
+ ` location ^~ ${portalBasePath}/apps/ {`,
591
+ ' absolute_redirect off;',
592
+ '',
593
+ ` if ($uri ~ ^${portalBasePathPattern}/apps/(?<subapp>[A-Za-z0-9_-]+)/(?<portal>[A-Za-z0-9_-]+)$) {`,
594
+ ` return 308 ${portalBasePath}/apps/$subapp/$portal/$is_args$args;`,
595
+ ' }',
596
+ '',
597
+ ` if ($uri !~ ^${portalBasePathPattern}/apps/(?<subapp>[A-Za-z0-9_-]+)/(?<portal>[A-Za-z0-9_-]+)/(?<portal_path>.*)$) {`,
598
+ ' return 404;',
599
+ ' }',
600
+ '',
601
+ ` root ${context.storageDir};`,
602
+ '',
603
+ ' if ($portal_path = "") {',
604
+ ' rewrite ^ /portals/$subapp/$portal/dist/index.html break;',
605
+ ' }',
606
+ '',
607
+ ' try_files',
608
+ ' /portals/$subapp/$portal/dist/$portal_path',
609
+ ' /portals/$subapp/$portal/dist/$portal_path/',
610
+ ' /portals/$subapp/$portal/dist/index.html',
611
+ ' =404;',
612
+ ' }',
613
+ '',
614
+ ` location ^~ ${portalBasePath}/ {`,
615
+ ' absolute_redirect off;',
616
+ '',
617
+ ` if ($uri ~ ^${portalBasePathPattern}/(?<portal>[A-Za-z0-9_-]+)$) {`,
618
+ ` return 308 ${portalBasePath}/$portal/$is_args$args;`,
619
+ ' }',
620
+ '',
621
+ ` if ($uri !~ ^${portalBasePathPattern}/(?<portal>[A-Za-z0-9_-]+)/(?<portal_path>.*)$) {`,
622
+ ' return 404;',
623
+ ' }',
624
+ '',
625
+ ` root ${context.storageDir};`,
626
+ '',
627
+ ' if ($portal_path = "") {',
628
+ ' rewrite ^ /portals/main/$portal/dist/index.html break;',
629
+ ' }',
630
+ '',
631
+ ' try_files',
632
+ ' /portals/main/$portal/dist/$portal_path',
633
+ ' /portals/main/$portal/dist/$portal_path/',
634
+ ' /portals/main/$portal/dist/index.html',
635
+ ' =404;',
636
+ ' }',
637
+ '',
638
+ ].join('\n');
639
+ }
563
640
  function buildNginxRuntimeConfig(context, variant) {
564
641
  return {
565
642
  __webpack_public_path__: context.cdnBaseUrl,
@@ -593,6 +670,7 @@ async function buildEnvProxyNginxRenderContext(source, options) {
593
670
  const mappedPublicDir = await mapProxyPathFromCliRoot(publicDir, options);
594
671
  const mappedSnippetsDir = await mapProxyPathFromCliRoot(snippetsDir, options);
595
672
  const mappedDistRootDir = await mapProxyPathFromCliRoot(distRootDir, options);
673
+ const mappedStorageDir = await mapProxyPathFromCliRoot(source.storagePath, options);
596
674
  const mappedUploadsDir = await mapProxyPathFromCliRoot(uploadsDir, options);
597
675
  const v2PublicPath = `${source.settings.appPublicPath.replace(/\/$/, '')}/${source.settings.modernClientPrefix}/`;
598
676
  return {
@@ -617,6 +695,7 @@ async function buildEnvProxyNginxRenderContext(source, options) {
617
695
  modernClientPrefix: source.settings.modernClientPrefix,
618
696
  proxyHost,
619
697
  snippetsDir: mappedSnippetsDir,
698
+ storageDir: mappedStorageDir,
620
699
  uploadsDir: mappedUploadsDir,
621
700
  v2PublicPath,
622
701
  wsPath: source.settings.wsPath,
@@ -1098,6 +1177,7 @@ function buildCaddyContextCommentLines(siteAddress, context, publicDir) {
1098
1177
  }
1099
1178
  function renderCaddyAppTemplate(siteAddress, context, publicDir) {
1100
1179
  const uploadsPath = `${context.appPublicPath}storage/uploads/`;
1180
+ const fileAccessPathMatcher = toCaddyPathMatcher(`${context.appPublicPath}files/`);
1101
1181
  const distPathMatcher = toCaddyPathMatcher(context.distPath);
1102
1182
  const uploadsPathMatcher = toCaddyPathMatcher(uploadsPath);
1103
1183
  const apiPathMatcher = toCaddyPathMatcher(context.apiBasePath);
@@ -1168,7 +1248,19 @@ function renderCaddyAppTemplate(siteAddress, context, publicDir) {
1168
1248
  ` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
1169
1249
  ' }',
1170
1250
  '',
1171
- ' # Keep API and WS routes above the SPA fallbacks.',
1251
+ ' # Keep file, API and WS routes above the SPA fallbacks.',
1252
+ ` handle ${fileAccessPathMatcher} {`,
1253
+ ` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
1254
+ ' }',
1255
+ ...(context.appPublicPath === DEFAULT_APP_PUBLIC_PATH
1256
+ ? []
1257
+ : [
1258
+ '',
1259
+ ' handle /files/* {',
1260
+ ` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
1261
+ ' }',
1262
+ ]),
1263
+ '',
1172
1264
  ` handle ${apiPathMatcher} {`,
1173
1265
  ` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
1174
1266
  ' }',
@@ -15,7 +15,7 @@ export function resolveManagedSetupState(value) {
15
15
  export function isPreparedSetupState(value) {
16
16
  return resolveManagedSetupState(value) === 'prepared';
17
17
  }
18
- export function buildInitAppEnvVarsFromConfig(config) {
18
+ export function buildInitAppEnvVarsFromConfig(config, options = {}) {
19
19
  const out = {};
20
20
  const put = (key, value) => {
21
21
  const text = trimValue(value);
@@ -28,5 +28,10 @@ export function buildInitAppEnvVarsFromConfig(config) {
28
28
  put('INIT_ROOT_EMAIL', config?.rootEmail);
29
29
  put('INIT_ROOT_PASSWORD', config?.rootPassword);
30
30
  put('INIT_ROOT_NICKNAME', config?.rootNickname);
31
+ if (options.includePortal !== false) {
32
+ put('INIT_PORTAL_TYPE', config?.portalType);
33
+ put('INIT_PORTAL_NAME', config?.portalName);
34
+ put('INIT_PORTAL_TEMPLATE', config?.portalTemplate);
35
+ }
31
36
  return out;
32
37
  }
@@ -0,0 +1,31 @@
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
+ const PORTAL_COMMAND_BASE_ENV_KEYS = [
10
+ 'PATH',
11
+ 'Path',
12
+ 'PATHEXT',
13
+ 'SystemRoot',
14
+ 'WINDIR',
15
+ 'ComSpec',
16
+ 'HOME',
17
+ 'USERPROFILE',
18
+ 'TMPDIR',
19
+ 'TEMP',
20
+ 'TMP',
21
+ ];
22
+ export function buildPortalCommandEnv(env = {}) {
23
+ const out = {};
24
+ for (const key of PORTAL_COMMAND_BASE_ENV_KEYS) {
25
+ const value = process.env[key];
26
+ if (value) {
27
+ out[key] = value;
28
+ }
29
+ }
30
+ return { ...out, ...env };
31
+ }
@@ -0,0 +1,133 @@
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 { readFile, writeFile } from 'node:fs/promises';
10
+ import path from 'node:path';
11
+ import { executeApiRequest } from './api-client.js';
12
+ import { translateCli } from './cli-locale.js';
13
+ export const DEFAULT_PORTAL_GIT_PATH = '.';
14
+ const portalConfigText = (key, values, fallback) => translateCli(`commands.portalConfig.${key}`, values, { fallback });
15
+ const UPDATE_PORTAL_OPERATION = {
16
+ method: 'POST',
17
+ pathTemplate: '/multiPortals:update',
18
+ hasBody: true,
19
+ bodyRequired: true,
20
+ parameters: [
21
+ {
22
+ name: 'filterByTk',
23
+ flagName: 'filterByTk',
24
+ in: 'query',
25
+ required: true,
26
+ },
27
+ ],
28
+ };
29
+ function trimValue(value) {
30
+ return String(value ?? '').trim();
31
+ }
32
+ function readObject(value) {
33
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
34
+ }
35
+ function validatePortalSourceStorage(value) {
36
+ const sourceStorage = trimValue(value) || 'nocobase';
37
+ if (sourceStorage === 'nocobase' || sourceStorage === 'git') {
38
+ return sourceStorage;
39
+ }
40
+ throw new Error(portalConfigText('errors.invalidSourceStorage', { value: sourceStorage }, `Invalid source storage "${sourceStorage}". Use "nocobase" or "git".`));
41
+ }
42
+ function isFullGitRemoteUrl(value) {
43
+ return /^(?:https?:\/\/|ssh:\/\/|file:\/\/|git@[^:]+:).+/.test(value);
44
+ }
45
+ function validateGitPath(value) {
46
+ const gitPath = trimValue(value);
47
+ if (!gitPath || path.isAbsolute(gitPath) || gitPath.split(/[\\/]+/).includes('..')) {
48
+ throw new Error(portalConfigText('errors.invalidGitPath', { value }, '--git-path must be a relative path inside the Git repository.'));
49
+ }
50
+ return gitPath.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
51
+ }
52
+ export function buildPortalConfig(options) {
53
+ const sourceStorage = validatePortalSourceStorage(options.sourceStorage ?? options.existingConfig?.sourceStorage);
54
+ const hasGitOption = Boolean(trimValue(options.gitRepo) || trimValue(options.gitBranch) || trimValue(options.gitPath));
55
+ if (sourceStorage === 'nocobase') {
56
+ if (hasGitOption) {
57
+ throw new Error(portalConfigText('errors.gitOptionsForNocobaseStorage', undefined, '--git-repo, --git-branch, and --git-path can only be used with --source-storage git.'));
58
+ }
59
+ return { sourceStorage };
60
+ }
61
+ const repo = trimValue(options.gitRepo) || options.existingConfig?.git?.repo || '';
62
+ if (!repo) {
63
+ throw new Error(portalConfigText('errors.gitRepoRequired', undefined, '--git-repo is required when --source-storage is git.'));
64
+ }
65
+ if (!isFullGitRemoteUrl(repo)) {
66
+ throw new Error(portalConfigText('errors.gitRepoInvalid', undefined, '--git-repo must be a full Git remote URL.'));
67
+ }
68
+ return {
69
+ sourceStorage,
70
+ git: {
71
+ repo,
72
+ branch: trimValue(options.gitBranch) || options.existingConfig?.git?.branch || 'main',
73
+ path: validateGitPath(trimValue(options.gitPath) || options.existingConfig?.git?.path || DEFAULT_PORTAL_GIT_PATH),
74
+ },
75
+ };
76
+ }
77
+ export function buildPortalConfigFromOptions(options, portal) {
78
+ const sourceOptions = readObject(options);
79
+ const git = readObject(sourceOptions.git);
80
+ return buildPortalConfig({
81
+ portal,
82
+ sourceStorage: trimValue(sourceOptions.sourceStorage) || 'nocobase',
83
+ gitRepo: trimValue(git.repo),
84
+ gitBranch: trimValue(git.branch),
85
+ gitPath: trimValue(git.path),
86
+ });
87
+ }
88
+ export function mergePortalConfigIntoOptions(config, currentOptions) {
89
+ const nextOptions = {
90
+ ...(currentOptions ?? {}),
91
+ sourceStorage: config.sourceStorage,
92
+ };
93
+ if (config.sourceStorage === 'git') {
94
+ nextOptions.git = config.git;
95
+ }
96
+ else {
97
+ delete nextOptions.git;
98
+ }
99
+ return nextOptions;
100
+ }
101
+ export async function readPortalConfig(portalDir) {
102
+ const configPath = path.join(portalDir, 'portal.config.json');
103
+ const data = JSON.parse(await readFile(configPath, 'utf-8'));
104
+ const config = readObject(data);
105
+ const git = readObject(config.git);
106
+ return buildPortalConfig({
107
+ portal: path.basename(portalDir),
108
+ sourceStorage: trimValue(config.sourceStorage),
109
+ gitRepo: trimValue(git.repo),
110
+ gitBranch: trimValue(git.branch),
111
+ gitPath: trimValue(git.path),
112
+ });
113
+ }
114
+ export async function writePortalConfig(portalDir, config) {
115
+ await writeFile(path.join(portalDir, 'portal.config.json'), `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
116
+ }
117
+ export async function syncPortalConfigToRemote(options) {
118
+ const apiRequest = options.apiRequest ?? executeApiRequest;
119
+ const response = await apiRequest({
120
+ cliVersion: options.cliVersion ?? '',
121
+ envName: options.envName,
122
+ flags: {
123
+ filterByTk: options.portal,
124
+ body: JSON.stringify({
125
+ options: mergePortalConfigIntoOptions(options.config, options.currentOptions),
126
+ }),
127
+ },
128
+ operation: UPDATE_PORTAL_OPERATION,
129
+ });
130
+ if (!response.ok) {
131
+ throw new Error(portalConfigText('errors.updateFailed', { status: response.status, details: JSON.stringify(response.data, null, 2) }, `Portal config update failed with status ${response.status}\n${JSON.stringify(response.data, null, 2)}`));
132
+ }
133
+ }
@@ -0,0 +1,117 @@
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 { mkdir, stat } from 'node:fs/promises';
10
+ import path from 'node:path';
11
+ import { translateCli } from './cli-locale.js';
12
+ import { resolvePortalAppFromApiBaseUrl, resolvePortalStoragePath, validatePortalSlug, } from './portal-create.js';
13
+ import { buildPortalConfig, buildPortalConfigFromOptions, readPortalConfig, syncPortalConfigToRemote, writePortalConfig, } from './portal-config.js';
14
+ import { findPortalListItem } from './portal-info.js';
15
+ import { listPortalWorkspaces } from './portal-list.js';
16
+ const portalConfigureText = (key, values, fallback) => translateCli(`commands.portalConfigure.${key}`, values, { fallback });
17
+ function trimValue(value) {
18
+ return String(value ?? '').trim();
19
+ }
20
+ async function pathExists(target) {
21
+ try {
22
+ await stat(target);
23
+ return true;
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+ function hasConfigurationChange(options) {
30
+ return Boolean(options.sourceStorage !== undefined ||
31
+ trimValue(options.gitRepo) ||
32
+ trimValue(options.gitBranch) ||
33
+ trimValue(options.gitPath));
34
+ }
35
+ async function readExistingConfig(portalDir) {
36
+ try {
37
+ return await readPortalConfig(portalDir);
38
+ }
39
+ catch (error) {
40
+ const code = error.code;
41
+ if (code === 'ENOENT') {
42
+ return undefined;
43
+ }
44
+ throw error;
45
+ }
46
+ }
47
+ function buildConfigFromRemoteOptions(params) {
48
+ if (params.options && Object.keys(params.options).length > 0) {
49
+ return buildPortalConfigFromOptions(params.options, params.portal);
50
+ }
51
+ if (!params.sourceStorage && !params.gitRepo && !params.gitBranch && !params.gitPath) {
52
+ return undefined;
53
+ }
54
+ return buildPortalConfig({
55
+ portal: params.portal,
56
+ sourceStorage: params.sourceStorage,
57
+ gitRepo: params.gitRepo,
58
+ gitBranch: params.gitBranch,
59
+ gitPath: params.gitPath,
60
+ });
61
+ }
62
+ export async function configurePortalWorkspace(options) {
63
+ if (!hasConfigurationChange(options)) {
64
+ throw new Error(portalConfigureText('errors.noChanges', undefined, 'No portal configuration changes were provided. Pass --source-storage or a --git-* flag.'));
65
+ }
66
+ const portal = validatePortalSlug(options.portal);
67
+ const apiBaseUrl = trimValue(options.env.apiBaseUrl);
68
+ const storagePath = resolvePortalStoragePath(options.env);
69
+ const { app } = resolvePortalAppFromApiBaseUrl(apiBaseUrl, options.env.config.appPublicPath);
70
+ const portalDir = path.join(storagePath, 'portals', app, portal);
71
+ if (!(await pathExists(portalDir))) {
72
+ throw new Error(portalConfigureText('errors.workspaceMissing', { portalDir, portal }, `Portal does not exist: ${portalDir}\nRun \`nb portal create ${portal}\` or \`nb portal pull ${portal}\` first.`));
73
+ }
74
+ const list = await listPortalWorkspaces({
75
+ env: options.env,
76
+ envName: options.envName,
77
+ cliVersion: options.cliVersion,
78
+ apiRequest: options.apiRequest,
79
+ });
80
+ const remoteItem = findPortalListItem(list.items, portal);
81
+ const existingConfig = (await readExistingConfig(portalDir)) ??
82
+ buildConfigFromRemoteOptions({
83
+ portal,
84
+ options: remoteItem?.options,
85
+ sourceStorage: remoteItem?.sourceStorage,
86
+ gitRepo: remoteItem?.gitRepo,
87
+ gitBranch: remoteItem?.gitBranch,
88
+ gitPath: remoteItem?.gitPath,
89
+ });
90
+ const config = buildPortalConfig({
91
+ portal,
92
+ sourceStorage: options.sourceStorage,
93
+ gitRepo: options.gitRepo,
94
+ gitBranch: options.gitBranch,
95
+ gitPath: options.gitPath,
96
+ existingConfig,
97
+ });
98
+ await mkdir(portalDir, { recursive: true });
99
+ await writePortalConfig(portalDir, config);
100
+ if (remoteItem) {
101
+ await syncPortalConfigToRemote({
102
+ portal,
103
+ config,
104
+ currentOptions: remoteItem.options,
105
+ envName: options.envName,
106
+ cliVersion: options.cliVersion,
107
+ apiRequest: options.apiRequest,
108
+ });
109
+ }
110
+ return {
111
+ app,
112
+ portal,
113
+ portalDir,
114
+ config,
115
+ remoteSynced: Boolean(remoteItem),
116
+ };
117
+ }