@nocobase/cli 2.2.0-alpha.1 → 2.2.0-alpha.11

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.
Files changed (78) hide show
  1. package/assets/env-proxy/nginx/snippets/proxy-location.conf +1 -0
  2. package/assets/env-proxy/nginx/snippets/uploads-location.conf +4 -1
  3. package/bin/early-locale.js +89 -0
  4. package/bin/node-version.js +35 -0
  5. package/bin/run.js +9 -0
  6. package/bin/windows-admin.js +60 -0
  7. package/dist/commands/app/destroy.js +4 -3
  8. package/dist/commands/app/restart.js +38 -0
  9. package/dist/commands/app/shared.js +49 -3
  10. package/dist/commands/app/start.js +95 -0
  11. package/dist/commands/app/upgrade.js +11 -0
  12. package/dist/commands/config/set.js +1 -0
  13. package/dist/commands/env/info.js +11 -1
  14. package/dist/commands/examples/prompts-stages.js +2 -2
  15. package/dist/commands/examples/prompts-test.js +2 -2
  16. package/dist/commands/init.js +152 -14
  17. package/dist/commands/install.js +256 -109
  18. package/dist/commands/license/activate.js +4 -1
  19. package/dist/commands/license/shared.js +24 -15
  20. package/dist/commands/portal/config.js +88 -0
  21. package/dist/commands/portal/create.js +104 -0
  22. package/dist/commands/portal/deploy.js +81 -0
  23. package/dist/commands/portal/destroy.js +104 -0
  24. package/dist/commands/portal/dev.js +71 -0
  25. package/dist/commands/portal/index.js +20 -0
  26. package/dist/commands/portal/info.js +82 -0
  27. package/dist/commands/portal/list.js +98 -0
  28. package/dist/commands/portal/pull.js +84 -0
  29. package/dist/commands/portal/push.js +79 -0
  30. package/dist/commands/proxy/caddy/generate.js +93 -7
  31. package/dist/commands/proxy/nginx/generate.js +98 -7
  32. package/dist/commands/revision/create.js +1 -1
  33. package/dist/commands/self/check.js +1 -1
  34. package/dist/commands/self/update.js +4 -4
  35. package/dist/commands/skills/check.js +4 -5
  36. package/dist/commands/skills/install.js +18 -1
  37. package/dist/commands/skills/update.js +19 -4
  38. package/dist/commands/source/dev.js +10 -6
  39. package/dist/commands/source/download.js +85 -16
  40. package/dist/lib/api-command-compat.js +51 -8
  41. package/dist/lib/app-managed-resources.js +104 -5
  42. package/dist/lib/auth-store.js +105 -13
  43. package/dist/lib/cli-config.js +93 -2
  44. package/dist/lib/docker-image.js +94 -6
  45. package/dist/lib/env-auth.js +291 -45
  46. package/dist/lib/env-config.js +14 -0
  47. package/dist/lib/env-proxy-config.js +48 -0
  48. package/dist/lib/env-proxy.js +276 -61
  49. package/dist/lib/hook-script.js +160 -0
  50. package/dist/lib/managed-init-env.js +6 -1
  51. package/dist/lib/portal-command-env.js +31 -0
  52. package/dist/lib/portal-config.js +133 -0
  53. package/dist/lib/portal-configure.js +117 -0
  54. package/dist/lib/portal-create.js +433 -0
  55. package/dist/lib/portal-deploy.js +283 -0
  56. package/dist/lib/portal-destroy.js +100 -0
  57. package/dist/lib/portal-dev.js +79 -0
  58. package/dist/lib/portal-env-files.js +53 -0
  59. package/dist/lib/portal-info.js +31 -0
  60. package/dist/lib/portal-list.js +211 -0
  61. package/dist/lib/portal-source.js +523 -0
  62. package/dist/lib/prompt-catalog-terminal.js +32 -19
  63. package/dist/lib/prompt-validators.js +1 -1
  64. package/dist/lib/prompt-web-ui.js +20 -13
  65. package/dist/lib/proxy-caddy.js +77 -9
  66. package/dist/lib/proxy-nginx.js +71 -11
  67. package/dist/lib/run-npm.js +21 -16
  68. package/dist/lib/self-manager.js +254 -46
  69. package/dist/lib/skills-manager.js +116 -23
  70. package/dist/lib/source-publish.js +2 -2
  71. package/dist/lib/startup-update.js +1 -1
  72. package/dist/lib/ui.js +28 -1
  73. package/dist/locale/en-US.json +227 -43
  74. package/dist/locale/zh-CN.json +227 -43
  75. package/package.json +11 -2
  76. package/scripts/build.mjs +0 -34
  77. package/scripts/clean.mjs +0 -9
  78. package/tsconfig.json +0 -19
@@ -0,0 +1,160 @@
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 { copyFile, mkdir } from 'node:fs/promises';
10
+ import { createRequire } from 'node:module';
11
+ import path from 'node:path';
12
+ export const ENV_HOOK_SCRIPT_CONFIG_PATH = '.nb/hooks.mjs';
13
+ const require = createRequire(import.meta.url);
14
+ const { spawn } = require('node:child_process');
15
+ function trimValue(value) {
16
+ return String(value ?? '').trim();
17
+ }
18
+ function normalizeHookPhase(value) {
19
+ const text = trimValue(value);
20
+ if (text === 'init' || text === 'upgrade' || text === 'restore' || text === 'source-download' || text === 'app-start') {
21
+ return text;
22
+ }
23
+ return 'init';
24
+ }
25
+ function normalizeHookCommand(value) {
26
+ const text = trimValue(value);
27
+ if (text === 'source:download' || text === 'app:start' || text === 'app:restart' || text === 'app:upgrade') {
28
+ return text;
29
+ }
30
+ return 'init';
31
+ }
32
+ function normalizeHookSource(value) {
33
+ const text = trimValue(value);
34
+ if (text === 'npm' || text === 'git' || text === 'docker') {
35
+ return text;
36
+ }
37
+ return undefined;
38
+ }
39
+ function isDependencyHookSource(source) {
40
+ return source === 'npm' || source === 'git';
41
+ }
42
+ function isRecord(value) {
43
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
44
+ }
45
+ export function resolveHookScriptPath(params) {
46
+ const hookScript = trimValue(params.hookScript);
47
+ if (!hookScript) {
48
+ return undefined;
49
+ }
50
+ if (path.isAbsolute(hookScript)) {
51
+ return hookScript;
52
+ }
53
+ const appPath = trimValue(params.appPath);
54
+ if (!appPath) {
55
+ return hookScript;
56
+ }
57
+ const usesWindowsSeparators = appPath.includes('\\') || /^[a-zA-Z]:([\\/]|$)/.test(appPath) || appPath.startsWith('\\\\');
58
+ return usesWindowsSeparators ? path.win32.join(appPath, hookScript) : path.posix.join(appPath, hookScript);
59
+ }
60
+ export async function persistHookScript(params) {
61
+ const sourcePath = path.resolve(params.sourcePath);
62
+ const targetPath = path.join(params.appPath, ENV_HOOK_SCRIPT_CONFIG_PATH);
63
+ await mkdir(path.dirname(targetPath), { recursive: true });
64
+ if (path.resolve(sourcePath) !== path.resolve(targetPath)) {
65
+ await copyFile(sourcePath, targetPath);
66
+ }
67
+ return ENV_HOOK_SCRIPT_CONFIG_PATH;
68
+ }
69
+ const hookRunnerScript = `
70
+ import { pathToFileURL } from 'node:url';
71
+
72
+ const knownHookNames = ['beforeDependencyInstall', 'beforeAppInstall', 'afterAppStart'];
73
+ const [, hookScriptPath, hookName, contextJson] = process.argv;
74
+ const url = pathToFileURL(hookScriptPath);
75
+ url.searchParams.set('t', String(Date.now()));
76
+
77
+ const imported = await import(url.href);
78
+ const hooks = imported.default ?? imported;
79
+
80
+ if (!hooks || typeof hooks !== 'object' || Array.isArray(hooks)) {
81
+ throw new Error('Hook script must export an object.');
82
+ }
83
+
84
+ for (const knownHookName of knownHookNames) {
85
+ if (Object.prototype.hasOwnProperty.call(hooks, knownHookName) && typeof hooks[knownHookName] !== 'function') {
86
+ throw new Error(\`Hook "\${knownHookName}" must be a function.\`);
87
+ }
88
+ }
89
+
90
+ const hook = hooks[hookName];
91
+ if (typeof hook === 'function') {
92
+ await hook(JSON.parse(contextJson));
93
+ }
94
+ `;
95
+ async function runHookInSubprocess(params) {
96
+ await new Promise((resolve, reject) => {
97
+ const child = spawn(process.execPath, ['--input-type=module', '--eval', hookRunnerScript, params.hookScriptPath, params.hookName, JSON.stringify(params.context)], {
98
+ stdio: ['ignore', 'pipe', 'pipe'],
99
+ });
100
+ let stdout = '';
101
+ let stderr = '';
102
+ child.stdout?.on?.('data', (chunk) => {
103
+ stdout += String(chunk);
104
+ });
105
+ child.stderr?.on?.('data', (chunk) => {
106
+ stderr += String(chunk);
107
+ });
108
+ child.once('error', reject);
109
+ child.once('close', (code) => {
110
+ if (code === 0) {
111
+ resolve();
112
+ return;
113
+ }
114
+ const output = stderr.trim() || stdout.trim();
115
+ reject(new Error(output || `Hook process exited with code ${code ?? 'unknown'}.`));
116
+ });
117
+ });
118
+ }
119
+ export function buildHookContext(params) {
120
+ const source = normalizeHookSource(params.source);
121
+ if (!source) {
122
+ return undefined;
123
+ }
124
+ const version = trimValue(params.version);
125
+ return {
126
+ phase: normalizeHookPhase(params.phase),
127
+ command: normalizeHookCommand(params.command),
128
+ envName: trimValue(params.envName),
129
+ source,
130
+ ...(version ? { version } : {}),
131
+ appPath: params.appPath,
132
+ sourcePath: params.sourcePath,
133
+ storagePath: params.storagePath,
134
+ hookScript: params.hookScript,
135
+ envConfig: { ...(params.envConfig ?? {}) },
136
+ };
137
+ }
138
+ export function buildBeforeDependencyInstallHookContext(params) {
139
+ const context = buildHookContext(params);
140
+ if (!context || !isDependencyHookSource(context.source)) {
141
+ return undefined;
142
+ }
143
+ return context;
144
+ }
145
+ export async function runHookScriptHook(params) {
146
+ try {
147
+ await runHookInSubprocess(params);
148
+ }
149
+ catch (error) {
150
+ const message = error instanceof Error ? error.message : String(error);
151
+ throw new Error([`Hook script failed: ${params.hookScriptPath}`, `Hook stage: ${params.hookName}`, `Details: ${message}`].join('\n'));
152
+ }
153
+ }
154
+ export async function runBeforeDependencyInstallHook(params) {
155
+ await runHookScriptHook({
156
+ hookScriptPath: params.hookScriptPath,
157
+ hookName: 'beforeDependencyInstall',
158
+ context: params.context,
159
+ });
160
+ }
@@ -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
+ }