@nocobase/cli 2.3.0-alpha.1 → 3.0.0-alpha.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.
Files changed (43) hide show
  1. package/assets/env-proxy/nginx/snippets/uploads-location.conf +4 -1
  2. package/dist/commands/config/set.js +1 -0
  3. package/dist/commands/init.js +13 -5
  4. package/dist/commands/install.js +104 -3
  5. package/dist/commands/portal/config.js +88 -0
  6. package/dist/commands/portal/create.js +105 -0
  7. package/dist/commands/portal/deploy.js +81 -0
  8. package/dist/commands/portal/destroy.js +104 -0
  9. package/dist/commands/portal/dev.js +71 -0
  10. package/dist/commands/portal/index.js +20 -0
  11. package/dist/commands/portal/info.js +82 -0
  12. package/dist/commands/portal/list.js +98 -0
  13. package/dist/commands/portal/pull.js +84 -0
  14. package/dist/commands/portal/push.js +79 -0
  15. package/dist/commands/source/dev.js +1 -1
  16. package/dist/lib/api-client.js +7 -0
  17. package/dist/lib/auth-store.js +3 -1
  18. package/dist/lib/cli-config.js +20 -1
  19. package/dist/lib/env-auth.js +2 -2
  20. package/dist/lib/env-config.js +3 -0
  21. package/dist/lib/env-proxy.js +141 -8
  22. package/dist/lib/managed-env-file.js +58 -2
  23. package/dist/lib/managed-init-env.js +6 -1
  24. package/dist/lib/naming.js +9 -0
  25. package/dist/lib/portal-command-env.js +31 -0
  26. package/dist/lib/portal-config.js +133 -0
  27. package/dist/lib/portal-configure.js +117 -0
  28. package/dist/lib/portal-create.js +433 -0
  29. package/dist/lib/portal-deploy.js +298 -0
  30. package/dist/lib/portal-destroy.js +100 -0
  31. package/dist/lib/portal-dev.js +79 -0
  32. package/dist/lib/portal-env-files.js +53 -0
  33. package/dist/lib/portal-info.js +31 -0
  34. package/dist/lib/portal-list.js +211 -0
  35. package/dist/lib/portal-source.js +523 -0
  36. package/dist/lib/proxy-caddy.js +2 -0
  37. package/dist/lib/proxy-nginx.js +1 -0
  38. package/dist/lib/run-npm.js +17 -16
  39. package/dist/lib/ui.js +28 -1
  40. package/dist/locale/en-US.json +178 -0
  41. package/dist/locale/zh-CN.json +178 -0
  42. package/nocobase-ctl.config.json +111 -0
  43. package/package.json +5 -2
@@ -0,0 +1,298 @@
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 { chmod, mkdtemp, readdir, rm, stat } from 'node:fs/promises';
10
+ import os from 'node:os';
11
+ import path from 'node:path';
12
+ import * as tar from 'tar';
13
+ import { executeApiRequest } from './api-client.js';
14
+ import { translateCli } from './cli-locale.js';
15
+ import { buildPortalBasePath, resolvePortalAppFromApiBaseUrl, resolvePortalStoragePath, titleFromPortalSlug, validatePortalSlug, } from './portal-create.js';
16
+ import { buildPortalCommandEnv } from './portal-command-env.js';
17
+ import { updatePortalEnvFiles } from './portal-env-files.js';
18
+ import { mergePortalConfigIntoOptions, readPortalConfig } from './portal-config.js';
19
+ import { run } from './run-npm.js';
20
+ const portalDeployText = (key, values, fallback) => translateCli(`commands.portalDeploy.${key}`, values, { fallback });
21
+ const DEPLOY_OPERATION = {
22
+ method: 'POST',
23
+ pathTemplate: '/multiPortals:deploy',
24
+ requestContentType: 'multipart/form-data',
25
+ hasBody: true,
26
+ bodyRequired: true,
27
+ parameters: [
28
+ {
29
+ name: 'file',
30
+ flagName: 'file',
31
+ in: 'body',
32
+ required: true,
33
+ isFile: true,
34
+ },
35
+ {
36
+ name: 'app',
37
+ flagName: 'app',
38
+ in: 'body',
39
+ required: true,
40
+ },
41
+ {
42
+ name: 'portal',
43
+ flagName: 'portal',
44
+ in: 'body',
45
+ required: true,
46
+ },
47
+ {
48
+ name: 'basePath',
49
+ flagName: 'basePath',
50
+ in: 'body',
51
+ required: true,
52
+ },
53
+ ],
54
+ };
55
+ const FIRST_OR_CREATE_PORTAL_OPERATION = {
56
+ method: 'POST',
57
+ pathTemplate: '/multiPortals:firstOrCreate',
58
+ hasBody: true,
59
+ bodyRequired: true,
60
+ parameters: [
61
+ {
62
+ name: 'filterKeys[]',
63
+ flagName: 'filterKeys',
64
+ in: 'query',
65
+ required: true,
66
+ isArray: true,
67
+ },
68
+ ],
69
+ };
70
+ const DEFAULT_PORTAL_UI_LAYOUT_UID = 'admin-layout-model';
71
+ const PORTAL_PUBLIC_DIR_MODE = 0o755;
72
+ const PORTAL_PUBLIC_FILE_MODE = 0o644;
73
+ function trimValue(value) {
74
+ return String(value ?? '').trim();
75
+ }
76
+ function readDistPathFromUploadResponse(data) {
77
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
78
+ return undefined;
79
+ }
80
+ const directDistPath = data.distPath;
81
+ if (typeof directDistPath === 'string' && directDistPath.trim()) {
82
+ return directDistPath;
83
+ }
84
+ return readDistPathFromUploadResponse(data.data);
85
+ }
86
+ async function pathExists(target) {
87
+ try {
88
+ await stat(target);
89
+ return true;
90
+ }
91
+ catch {
92
+ return false;
93
+ }
94
+ }
95
+ async function chmodPortalDistTree(targetDir) {
96
+ await chmod(targetDir, PORTAL_PUBLIC_DIR_MODE);
97
+ const entries = await readdir(targetDir, { withFileTypes: true });
98
+ await Promise.all(entries.map(async (entry) => {
99
+ const entryPath = path.join(targetDir, entry.name);
100
+ if (entry.isDirectory()) {
101
+ await chmodPortalDistTree(entryPath);
102
+ return;
103
+ }
104
+ if (entry.isFile()) {
105
+ await chmod(entryPath, PORTAL_PUBLIC_FILE_MODE);
106
+ }
107
+ }));
108
+ }
109
+ async function ensurePortalDistPublicReadable(params) {
110
+ await chmod(path.join(params.storagePath, 'portals'), PORTAL_PUBLIC_DIR_MODE);
111
+ await chmod(path.join(params.storagePath, 'portals', params.app), PORTAL_PUBLIC_DIR_MODE);
112
+ await chmod(params.portalDir, PORTAL_PUBLIC_DIR_MODE);
113
+ await chmodPortalDistTree(params.distDir);
114
+ }
115
+ async function assertFileExists(filePath, message) {
116
+ try {
117
+ const fileStat = await stat(filePath);
118
+ if (fileStat.isFile()) {
119
+ return;
120
+ }
121
+ }
122
+ catch {
123
+ // Throw the normalized message below.
124
+ }
125
+ throw new Error(message);
126
+ }
127
+ async function packPortalDist(distDir) {
128
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), 'nocobase-cli-portal-dist-'));
129
+ const archivePath = path.join(tempDir, 'dist.tar.gz');
130
+ const entries = await readdir(distDir);
131
+ await tar.create({
132
+ cwd: distDir,
133
+ file: archivePath,
134
+ gzip: true,
135
+ }, entries);
136
+ return {
137
+ archivePath,
138
+ cleanup: async () => {
139
+ await rm(tempDir, { recursive: true, force: true });
140
+ },
141
+ };
142
+ }
143
+ async function uploadPortalDist(params) {
144
+ const apiRequest = params.apiRequest ?? executeApiRequest;
145
+ const response = await apiRequest({
146
+ cliVersion: params.cliVersion ?? '',
147
+ envName: params.envName,
148
+ flags: {
149
+ file: params.archivePath,
150
+ app: params.app,
151
+ portal: params.portal,
152
+ basePath: params.portalBase,
153
+ },
154
+ operation: DEPLOY_OPERATION,
155
+ });
156
+ if (!response.ok) {
157
+ throw new Error(portalDeployText('errors.uploadFailed', { status: response.status, details: JSON.stringify(response.data, null, 2) }, `Portal dist upload failed with status ${response.status}\n${JSON.stringify(response.data, null, 2)}`));
158
+ }
159
+ return {
160
+ distPath: readDistPathFromUploadResponse(response.data),
161
+ };
162
+ }
163
+ async function syncMultiPortalRecord(params) {
164
+ const apiRequest = params.apiRequest ?? executeApiRequest;
165
+ const body = {
166
+ uid: params.portal,
167
+ title: titleFromPortalSlug(params.portal),
168
+ portalType: 'ai',
169
+ portalName: params.portal,
170
+ routePath: `/${params.portal}`,
171
+ authCheck: true,
172
+ enabled: true,
173
+ uiLayoutUid: DEFAULT_PORTAL_UI_LAYOUT_UID,
174
+ skipCreatePortalDirectory: true,
175
+ };
176
+ if (params.config) {
177
+ body.options = mergePortalConfigIntoOptions(params.config);
178
+ }
179
+ const response = await apiRequest({
180
+ cliVersion: params.cliVersion ?? '',
181
+ envName: params.envName,
182
+ flags: {
183
+ filterKeys: ['portalName'],
184
+ body: JSON.stringify(body),
185
+ },
186
+ operation: FIRST_OR_CREATE_PORTAL_OPERATION,
187
+ });
188
+ if (!response.ok) {
189
+ throw new Error(portalDeployText('errors.recordSyncFailed', { status: response.status, details: JSON.stringify(response.data, null, 2) }, `Portal record sync failed with status ${response.status}\n${JSON.stringify(response.data, null, 2)}`));
190
+ }
191
+ }
192
+ export async function deployPortalWorkspace(options) {
193
+ const portal = validatePortalSlug(options.portal);
194
+ const apiBaseUrl = trimValue(options.env.apiBaseUrl);
195
+ const storagePath = resolvePortalStoragePath(options.env);
196
+ const { app, appPublicPath } = resolvePortalAppFromApiBaseUrl(apiBaseUrl, options.env.config.appPublicPath);
197
+ const portalBase = buildPortalBasePath({ app, appPublicPath, portal });
198
+ const portalDir = path.join(storagePath, 'portals', app, portal);
199
+ const distDir = path.join(portalDir, 'dist');
200
+ if (!(await pathExists(portalDir))) {
201
+ throw new Error(portalDeployText('errors.workspaceMissing', { portalDir, portal }, `Portal does not exist: ${portalDir}\nRun \`nb portal create ${portal}\` first.`));
202
+ }
203
+ await assertFileExists(path.join(portalDir, 'package.json'), portalDeployText('errors.packageJsonMissing', { portalDir }, `Portal is invalid: package.json is missing in ${portalDir}.`));
204
+ const portalConfig = await readPortalConfig(portalDir);
205
+ await updatePortalEnvFiles({
206
+ portalDir,
207
+ apiBaseUrl,
208
+ portalBase,
209
+ });
210
+ const runCommand = options.runCommand ?? run;
211
+ await runCommand('pnpm', ['install'], {
212
+ cwd: portalDir,
213
+ env: buildPortalCommandEnv(),
214
+ envMode: 'replace',
215
+ errorName: 'pnpm install',
216
+ });
217
+ await runCommand('pnpm', ['build'], {
218
+ cwd: portalDir,
219
+ env: buildPortalCommandEnv({
220
+ NOCOBASE_API_URL: apiBaseUrl,
221
+ NOCOBASE_PORTAL_BASE: portalBase,
222
+ }),
223
+ envMode: 'replace',
224
+ errorName: 'pnpm build',
225
+ });
226
+ await runCommand('pnpm', ['build:html'], {
227
+ cwd: portalDir,
228
+ env: buildPortalCommandEnv({
229
+ NOCOBASE_API_URL: apiBaseUrl,
230
+ NOCOBASE_PORTAL_BASE: portalBase,
231
+ }),
232
+ envMode: 'replace',
233
+ errorName: 'pnpm build:html',
234
+ });
235
+ await assertFileExists(path.join(distDir, 'index.html'), portalDeployText('errors.distMissing', { distDir }, `Portal build did not produce ${path.join(distDir, 'index.html')}.`));
236
+ await ensurePortalDistPublicReadable({
237
+ storagePath,
238
+ app,
239
+ portalDir,
240
+ distDir,
241
+ });
242
+ if (options.env.kind === 'local' || options.env.kind === 'docker') {
243
+ await syncMultiPortalRecord({
244
+ portal,
245
+ config: portalConfig,
246
+ envName: options.envName,
247
+ cliVersion: options.cliVersion,
248
+ apiRequest: options.apiRequest,
249
+ });
250
+ return {
251
+ app,
252
+ portal,
253
+ portalDir,
254
+ portalBase,
255
+ distDir,
256
+ mode: options.env.kind,
257
+ uploaded: false,
258
+ recordSynced: true,
259
+ };
260
+ }
261
+ if (options.env.kind !== 'http') {
262
+ throw new Error(portalDeployText('errors.unsupportedEnvKind', { kind: options.env.kind }, `Cannot deploy a portal for ${options.env.kind} envs in the first version.`));
263
+ }
264
+ const archive = await packPortalDist(distDir);
265
+ let uploadResult;
266
+ try {
267
+ uploadResult = await uploadPortalDist({
268
+ archivePath: archive.archivePath,
269
+ app,
270
+ portal,
271
+ portalBase,
272
+ envName: options.envName,
273
+ cliVersion: options.cliVersion,
274
+ apiRequest: options.apiRequest,
275
+ });
276
+ }
277
+ finally {
278
+ await archive.cleanup();
279
+ }
280
+ await syncMultiPortalRecord({
281
+ portal,
282
+ config: portalConfig,
283
+ envName: options.envName,
284
+ cliVersion: options.cliVersion,
285
+ apiRequest: options.apiRequest,
286
+ });
287
+ return {
288
+ app,
289
+ portal,
290
+ portalDir,
291
+ portalBase,
292
+ distDir,
293
+ serverDistPath: uploadResult.distPath,
294
+ mode: 'http',
295
+ uploaded: true,
296
+ recordSynced: true,
297
+ };
298
+ }
@@ -0,0 +1,100 @@
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 { rm, stat } 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
+ import { buildPortalBasePath, resolvePortalAppFromApiBaseUrl, resolvePortalStoragePath, validatePortalSlug, } from './portal-create.js';
14
+ const portalDestroyText = (key, values, fallback) => translateCli(`commands.portalDestroy.${key}`, values, { fallback });
15
+ const DESTROY_PORTAL_OPERATION = {
16
+ method: 'POST',
17
+ pathTemplate: '/multiPortals:destroy',
18
+ parameters: [
19
+ {
20
+ name: 'filterByTk',
21
+ flagName: 'filterByTk',
22
+ in: 'query',
23
+ required: true,
24
+ },
25
+ ],
26
+ };
27
+ function trimValue(value) {
28
+ return String(value ?? '').trim();
29
+ }
30
+ async function pathExists(target) {
31
+ try {
32
+ await stat(target);
33
+ return true;
34
+ }
35
+ catch {
36
+ return false;
37
+ }
38
+ }
39
+ function assertPortalDirIsInsideParent(parentDir, portalDir) {
40
+ const relative = path.relative(parentDir, portalDir);
41
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
42
+ throw new Error(portalDestroyText('errors.outsideParent', { parentDir, portalDir }, `Refusing to delete a portal outside ${parentDir}: ${portalDir}`));
43
+ }
44
+ }
45
+ async function destroyMultiPortalRecord(params) {
46
+ const apiRequest = params.apiRequest ?? executeApiRequest;
47
+ const response = await apiRequest({
48
+ cliVersion: params.cliVersion ?? '',
49
+ envName: params.envName,
50
+ flags: {
51
+ filterByTk: params.portal,
52
+ },
53
+ operation: DESTROY_PORTAL_OPERATION,
54
+ });
55
+ if (response.ok) {
56
+ return true;
57
+ }
58
+ if (params.force && response.status === 404) {
59
+ return false;
60
+ }
61
+ throw new Error(portalDestroyText('errors.recordDestroyFailed', { status: response.status, details: JSON.stringify(response.data, null, 2) }, `Portal record destroy failed with status ${response.status}\n${JSON.stringify(response.data, null, 2)}`));
62
+ }
63
+ export async function destroyPortalWorkspace(options) {
64
+ const portal = validatePortalSlug(options.portal);
65
+ const apiBaseUrl = trimValue(options.env.apiBaseUrl);
66
+ const storagePath = resolvePortalStoragePath(options.env);
67
+ const { app, appPublicPath } = resolvePortalAppFromApiBaseUrl(apiBaseUrl, options.env.config.appPublicPath);
68
+ const portalBase = buildPortalBasePath({ app, appPublicPath, portal });
69
+ const portalParentDir = path.join(storagePath, 'portals', app);
70
+ const portalDir = path.join(portalParentDir, portal);
71
+ const mode = options.env.kind;
72
+ if (mode !== 'local' && mode !== 'docker' && mode !== 'http') {
73
+ throw new Error(portalDestroyText('errors.unsupportedEnvKind', { kind: mode }, `Cannot destroy a portal for ${mode} envs in the first version.`));
74
+ }
75
+ const destroyMode = mode;
76
+ assertPortalDirIsInsideParent(portalParentDir, portalDir);
77
+ const workspaceExists = await pathExists(portalDir);
78
+ if (!workspaceExists && !options.force) {
79
+ throw new Error(portalDestroyText('errors.workspaceMissing', { portalDir, portal }, `Portal does not exist: ${portalDir}\nPass --force to ignore missing local files.`));
80
+ }
81
+ const recordDeleted = await destroyMultiPortalRecord({
82
+ portal,
83
+ envName: options.envName,
84
+ cliVersion: options.cliVersion,
85
+ force: options.force,
86
+ apiRequest: options.apiRequest,
87
+ });
88
+ if (workspaceExists) {
89
+ await rm(portalDir, { recursive: true, force: true });
90
+ }
91
+ return {
92
+ app,
93
+ portal,
94
+ portalDir,
95
+ portalBase,
96
+ mode: destroyMode,
97
+ recordDeleted,
98
+ workspaceDeleted: workspaceExists,
99
+ };
100
+ }
@@ -0,0 +1,79 @@
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 { stat } from 'node:fs/promises';
10
+ import path from 'node:path';
11
+ import { translateCli } from './cli-locale.js';
12
+ import { buildPortalBasePath, resolvePortalAppFromApiBaseUrl, resolvePortalStoragePath, validatePortalSlug, } from './portal-create.js';
13
+ import { buildPortalCommandEnv } from './portal-command-env.js';
14
+ import { updatePortalEnvFiles } from './portal-env-files.js';
15
+ import { run } from './run-npm.js';
16
+ const portalDevText = (key, values, fallback) => translateCli(`commands.portalDev.${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
+ async function assertFileExists(filePath, message) {
30
+ try {
31
+ const fileStat = await stat(filePath);
32
+ if (fileStat.isFile()) {
33
+ return;
34
+ }
35
+ }
36
+ catch {
37
+ // Throw the normalized message below.
38
+ }
39
+ throw new Error(message);
40
+ }
41
+ export async function devPortalWorkspace(options) {
42
+ const portal = validatePortalSlug(options.portal);
43
+ const apiBaseUrl = trimValue(options.env.apiBaseUrl);
44
+ if (options.env.kind === 'ssh') {
45
+ throw new Error(portalDevText('errors.sshUnsupported', undefined, 'Cannot start a portal in dev mode for ssh envs in the first version.'));
46
+ }
47
+ const storagePath = resolvePortalStoragePath(options.env);
48
+ const { app, appPublicPath } = resolvePortalAppFromApiBaseUrl(apiBaseUrl, options.env.config.appPublicPath);
49
+ const portalBase = buildPortalBasePath({ app, appPublicPath, portal });
50
+ const portalDir = path.join(storagePath, 'portals', app, portal);
51
+ if (!(await pathExists(portalDir))) {
52
+ throw new Error(portalDevText('errors.workspaceMissing', { portalDir, portal }, `Portal does not exist: ${portalDir}\nRun \`nb portal create ${portal}\` first.`));
53
+ }
54
+ await assertFileExists(path.join(portalDir, 'package.json'), portalDevText('errors.packageJsonMissing', { portalDir }, `Portal is invalid: package.json is missing in ${portalDir}.`));
55
+ await updatePortalEnvFiles({
56
+ portalDir,
57
+ apiBaseUrl,
58
+ portalBase,
59
+ });
60
+ const result = {
61
+ app,
62
+ portal,
63
+ portalDir,
64
+ portalBase,
65
+ mode: options.env.kind,
66
+ };
67
+ options.onStart?.(result);
68
+ const runCommand = options.runCommand ?? run;
69
+ await runCommand('pnpm', ['dev'], {
70
+ cwd: portalDir,
71
+ env: buildPortalCommandEnv({
72
+ NOCOBASE_API_URL: apiBaseUrl,
73
+ NOCOBASE_PORTAL_BASE: portalBase,
74
+ }),
75
+ envMode: 'replace',
76
+ errorName: 'pnpm dev',
77
+ });
78
+ return result;
79
+ }
@@ -0,0 +1,53 @@
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 { resolvePortalEnvApiUrl } from './portal-create.js';
12
+ function upsertEnvContent(content, values) {
13
+ const nextValues = { ...values };
14
+ const lines = content ? content.replace(/\r\n/g, '\n').split('\n') : [];
15
+ const result = [];
16
+ for (const line of lines) {
17
+ if (!line && result.length === lines.length - 1) {
18
+ continue;
19
+ }
20
+ const match = line.match(/^(\s*)([A-Za-z_][A-Za-z0-9_]*)\s*=/);
21
+ const key = match?.[2];
22
+ if (key && Object.prototype.hasOwnProperty.call(nextValues, key)) {
23
+ result.push(`${key}=${nextValues[key]}`);
24
+ delete nextValues[key];
25
+ continue;
26
+ }
27
+ result.push(line);
28
+ }
29
+ for (const [key, value] of Object.entries(nextValues)) {
30
+ result.push(`${key}=${value}`);
31
+ }
32
+ return `${result.join('\n').replace(/\n*$/, '')}\n`;
33
+ }
34
+ export async function upsertPortalEnvFile(filePath, values) {
35
+ let content = '';
36
+ try {
37
+ content = await readFile(filePath, 'utf-8');
38
+ }
39
+ catch {
40
+ content = '';
41
+ }
42
+ await writeFile(filePath, upsertEnvContent(content, values), 'utf-8');
43
+ }
44
+ export async function updatePortalEnvFiles(params) {
45
+ await upsertPortalEnvFile(path.join(params.portalDir, '.env'), {
46
+ NOCOBASE_API_URL: resolvePortalEnvApiUrl(params.apiBaseUrl),
47
+ NOCOBASE_PORTAL_BASE: params.portalBase,
48
+ });
49
+ await upsertPortalEnvFile(path.join(params.portalDir, '.env.local'), {
50
+ NOCOBASE_API_URL: params.apiBaseUrl,
51
+ NOCOBASE_PORTAL_BASE: params.portalBase,
52
+ });
53
+ }
@@ -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
+ import { translateCli } from './cli-locale.js';
10
+ import { toPortalOutputItem } from './portal-list.js';
11
+ const portalInfoText = (key, values, fallback) => translateCli(`commands.portalInfo.${key}`, values, { fallback });
12
+ function formatBoolean(value) {
13
+ if (value === null) {
14
+ return '';
15
+ }
16
+ return value ? 'yes' : 'no';
17
+ }
18
+ export function findPortalListItem(items, portal) {
19
+ return items.find((item) => item.portalName === portal || item.uid === portal);
20
+ }
21
+ export function formatPortalInfo(item) {
22
+ const outputItem = toPortalOutputItem(item);
23
+ return [
24
+ `${portalInfoText('fields.name', undefined, 'Name')}: ${outputItem.name}`,
25
+ `${portalInfoText('fields.url', undefined, 'URL')}: ${outputItem.url}`,
26
+ `${portalInfoText('fields.portalType', undefined, 'Portal type')}: ${outputItem.portalType}`,
27
+ `${portalInfoText('fields.path', undefined, 'Local path')}: ${outputItem.localPath}`,
28
+ `${portalInfoText('fields.enabled', undefined, 'Enabled')}: ${formatBoolean(outputItem.enabled)}`,
29
+ `${portalInfoText('fields.localSynced', undefined, 'Local synced')}: ${formatBoolean(outputItem.localSynced)}`,
30
+ ].join('\n');
31
+ }