@nocobase/cli 3.0.0-alpha.5 → 3.0.0-alpha.6
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/dist/commands/portal/config.js +16 -5
- package/dist/commands/portal/create.js +13 -25
- package/dist/commands/portal/destroy.js +28 -6
- package/dist/commands/portal/list.js +5 -5
- package/dist/commands/portal/pull.js +28 -2
- package/dist/lib/api-client.js +35 -9
- package/dist/lib/auth-store.js +51 -1
- package/dist/lib/env-auth.js +1 -1
- package/dist/lib/env-config.js +5 -0
- package/dist/lib/env-portal-config.js +30 -0
- package/dist/lib/portal-config.js +0 -17
- package/dist/lib/portal-configure.js +46 -54
- package/dist/lib/portal-create.js +21 -5
- package/dist/lib/portal-deploy.js +5 -42
- package/dist/lib/portal-destroy.js +27 -9
- package/dist/lib/portal-dev.js +2 -3
- package/dist/lib/portal-info.js +2 -5
- package/dist/lib/portal-list.js +16 -23
- package/dist/lib/portal-path-safety.js +76 -0
- package/dist/lib/portal-source.js +60 -43
- package/dist/locale/en-US.json +34 -15
- package/dist/locale/zh-CN.json +34 -15
- package/package.json +2 -2
|
@@ -6,43 +6,23 @@
|
|
|
6
6
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
|
-
import { mkdir, stat } from 'node:fs/promises';
|
|
10
|
-
import path from 'node:path';
|
|
11
9
|
import { translateCli } from './cli-locale.js';
|
|
12
|
-
import { resolvePortalAppContext,
|
|
13
|
-
import { buildPortalConfig, buildPortalConfigFromOptions,
|
|
10
|
+
import { resolvePortalAppContext, resolvePortalSourcePath, resolveSavedPortalSourcePath, validatePortalSlug, } from './portal-create.js';
|
|
11
|
+
import { buildPortalConfig, buildPortalConfigFromOptions, syncPortalConfigToRemote, } from './portal-config.js';
|
|
14
12
|
import { findPortalListItem } from './portal-info.js';
|
|
15
13
|
import { listPortalWorkspaces } from './portal-list.js';
|
|
16
14
|
const portalConfigureText = (key, values, fallback) => translateCli(`commands.portalConfigure.${key}`, values, { fallback });
|
|
17
15
|
function trimValue(value) {
|
|
18
16
|
return String(value ?? '').trim();
|
|
19
17
|
}
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
await stat(target);
|
|
23
|
-
return true;
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
function hasConfigurationChange(options) {
|
|
18
|
+
function hasSourceConfigurationChange(options) {
|
|
30
19
|
return Boolean(options.sourceStorage !== undefined ||
|
|
31
20
|
trimValue(options.gitRepo) ||
|
|
32
21
|
trimValue(options.gitBranch) ||
|
|
33
22
|
trimValue(options.gitPath));
|
|
34
23
|
}
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
}
|
|
24
|
+
function hasPathConfigurationChange(options) {
|
|
25
|
+
return Boolean(trimValue(options.sourcePath));
|
|
46
26
|
}
|
|
47
27
|
function buildConfigFromRemoteOptions(params) {
|
|
48
28
|
if (params.options && Object.keys(params.options).length > 0) {
|
|
@@ -60,17 +40,19 @@ function buildConfigFromRemoteOptions(params) {
|
|
|
60
40
|
});
|
|
61
41
|
}
|
|
62
42
|
export async function configurePortalWorkspace(options) {
|
|
63
|
-
|
|
64
|
-
|
|
43
|
+
const hasSourceChange = hasSourceConfigurationChange(options);
|
|
44
|
+
const hasPathChange = hasPathConfigurationChange(options);
|
|
45
|
+
if (!hasSourceChange && !hasPathChange) {
|
|
46
|
+
throw new Error(portalConfigureText('errors.noChanges', undefined, 'No portal configuration changes were provided. Pass --path, --source-storage, or a --git-* flag.'));
|
|
65
47
|
}
|
|
66
48
|
const portal = validatePortalSlug(options.portal);
|
|
67
|
-
const
|
|
49
|
+
const portalDir = hasPathChange
|
|
50
|
+
? resolvePortalSourcePath(portal, options.sourcePath)
|
|
51
|
+
: resolveSavedPortalSourcePath(options.env, portal) ?? '';
|
|
68
52
|
const appContext = await resolvePortalAppContext(options);
|
|
69
53
|
const { app } = appContext;
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
}
|
|
54
|
+
let config;
|
|
55
|
+
let remoteSynced = false;
|
|
74
56
|
const list = await listPortalWorkspaces({
|
|
75
57
|
env: options.env,
|
|
76
58
|
envName: options.envName,
|
|
@@ -79,16 +61,28 @@ export async function configurePortalWorkspace(options) {
|
|
|
79
61
|
appContext,
|
|
80
62
|
});
|
|
81
63
|
const remoteItem = findPortalListItem(list.items, portal);
|
|
82
|
-
|
|
83
|
-
|
|
64
|
+
if (!remoteItem) {
|
|
65
|
+
throw new Error(portalConfigureText('errors.notFound', { portal }, `Portal "${portal}" was not found. Run \`nb portal list\` to see available portals.`));
|
|
66
|
+
}
|
|
67
|
+
if (!hasSourceChange) {
|
|
68
|
+
return {
|
|
69
|
+
app,
|
|
84
70
|
portal,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
71
|
+
portalDir,
|
|
72
|
+
config: undefined,
|
|
73
|
+
remoteSynced: false,
|
|
74
|
+
pathUpdated: hasPathChange,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const existingConfig = buildConfigFromRemoteOptions({
|
|
78
|
+
portal,
|
|
79
|
+
options: remoteItem.options,
|
|
80
|
+
sourceStorage: remoteItem.sourceStorage,
|
|
81
|
+
gitRepo: remoteItem.gitRepo,
|
|
82
|
+
gitBranch: remoteItem.gitBranch,
|
|
83
|
+
gitPath: remoteItem.gitPath,
|
|
84
|
+
});
|
|
85
|
+
config = buildPortalConfig({
|
|
92
86
|
portal,
|
|
93
87
|
sourceStorage: options.sourceStorage,
|
|
94
88
|
gitRepo: options.gitRepo,
|
|
@@ -96,23 +90,21 @@ export async function configurePortalWorkspace(options) {
|
|
|
96
90
|
gitPath: options.gitPath,
|
|
97
91
|
existingConfig,
|
|
98
92
|
});
|
|
99
|
-
await
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
apiRequest: options.apiRequest,
|
|
109
|
-
});
|
|
110
|
-
}
|
|
93
|
+
await syncPortalConfigToRemote({
|
|
94
|
+
portal,
|
|
95
|
+
config,
|
|
96
|
+
currentOptions: remoteItem.options,
|
|
97
|
+
envName: options.envName,
|
|
98
|
+
cliVersion: options.cliVersion,
|
|
99
|
+
apiRequest: options.apiRequest,
|
|
100
|
+
});
|
|
101
|
+
remoteSynced = true;
|
|
111
102
|
return {
|
|
112
103
|
app,
|
|
113
104
|
portal,
|
|
114
105
|
portalDir,
|
|
115
106
|
config,
|
|
116
|
-
remoteSynced
|
|
107
|
+
remoteSynced,
|
|
108
|
+
pathUpdated: hasPathChange,
|
|
117
109
|
};
|
|
118
110
|
}
|
|
@@ -17,11 +17,13 @@ import { createGunzip } from 'node:zlib';
|
|
|
17
17
|
import * as tar from 'tar';
|
|
18
18
|
import { appendAppPublicPath, resolveAppPublicPath } from './app-public-path.js';
|
|
19
19
|
import { executeApiRequest } from './api-client.js';
|
|
20
|
+
import { resolveEnvPortalPath } from './auth-store.js';
|
|
20
21
|
import { resolveEnvRelativePath } from './cli-home.js';
|
|
21
22
|
import { translateCli } from './cli-locale.js';
|
|
22
23
|
import { ensurePortalBuildHtmlReadsEnvOnly } from './portal-build-html.js';
|
|
23
24
|
import { buildPortalCommandEnv } from './portal-command-env.js';
|
|
24
|
-
import {
|
|
25
|
+
import { canReplacePortalDirectory } from './portal-path-safety.js';
|
|
26
|
+
import { buildPortalConfig, mergePortalConfigIntoOptions, } from './portal-config.js';
|
|
25
27
|
import { resolvePnpmInstallCommand, run, runPnpmInstallCommand } from './run-npm.js';
|
|
26
28
|
const DEFAULT_PORTAL_TEMPLATE = '@nocobase/portal-template-default';
|
|
27
29
|
const DEFAULT_PORTAL_APP_NAME = 'main';
|
|
@@ -404,6 +406,19 @@ export function resolvePortalStoragePath(env) {
|
|
|
404
406
|
}
|
|
405
407
|
return path.resolve(process.cwd(), 'storage');
|
|
406
408
|
}
|
|
409
|
+
function resolveAbsolutePath(value) {
|
|
410
|
+
return path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
|
|
411
|
+
}
|
|
412
|
+
export function resolvePortalSourcePath(portal, sourcePath) {
|
|
413
|
+
return resolveAbsolutePath(trimValue(sourcePath) || portal);
|
|
414
|
+
}
|
|
415
|
+
export function resolveSavedPortalSourcePath(env, portal) {
|
|
416
|
+
const sourcePath = resolveEnvPortalPath(env.config, portal);
|
|
417
|
+
return sourcePath ? resolveAbsolutePath(sourcePath) : undefined;
|
|
418
|
+
}
|
|
419
|
+
export function resolvePortalDeployPath(params) {
|
|
420
|
+
return path.join(params.storagePath, 'portals', params.app, params.portal);
|
|
421
|
+
}
|
|
407
422
|
export async function createPortalWorkspace(options) {
|
|
408
423
|
const portal = validatePortalSlug(options.portal);
|
|
409
424
|
const title = trimValue(options.title) || titleFromPortalSlug(portal);
|
|
@@ -416,16 +431,18 @@ export async function createPortalWorkspace(options) {
|
|
|
416
431
|
});
|
|
417
432
|
const apiBaseUrl = trimValue(options.env.apiBaseUrl);
|
|
418
433
|
const envApiUrl = resolvePortalEnvApiUrl(apiBaseUrl);
|
|
419
|
-
const storagePath = resolvePortalStoragePath(options.env);
|
|
420
434
|
const { app, appPublicPath, portalBaseApp } = await resolvePortalAppContext(options);
|
|
421
435
|
const portalBase = buildPortalBasePath({ app: portalBaseApp ?? app, appPublicPath, portal });
|
|
422
|
-
const
|
|
423
|
-
const
|
|
436
|
+
const portalDir = resolvePortalSourcePath(portal, options.sourcePath);
|
|
437
|
+
const portalParentDir = path.dirname(portalDir);
|
|
424
438
|
assertPortalDirIsInsideParent(portalParentDir, portalDir);
|
|
425
439
|
const targetExists = await pathExists(portalDir);
|
|
426
440
|
if (targetExists && !options.force) {
|
|
427
441
|
throw new Error(portalCreateText('errors.workspaceExists', { portalDir }, `Portal already exists: ${portalDir}\nPass --force to delete it and create a new portal.`));
|
|
428
442
|
}
|
|
443
|
+
if (targetExists && options.force && !(await canReplacePortalDirectory(portalDir))) {
|
|
444
|
+
throw new Error(portalCreateText('errors.workspaceNotReplaceable', { portalDir }, `Refusing to replace a non-portal directory: ${portalDir}`));
|
|
445
|
+
}
|
|
429
446
|
const template = await resolvePortalTemplate(options.template, {
|
|
430
447
|
npmRegistry: trimValue(options.env.config.npmRegistry),
|
|
431
448
|
runCommand: options.runCommand,
|
|
@@ -438,7 +455,6 @@ export async function createPortalWorkspace(options) {
|
|
|
438
455
|
await ensurePortalBuildHtmlReadsEnvOnly(tempDir);
|
|
439
456
|
await writeFile(path.join(tempDir, '.env'), [`NOCOBASE_API_URL=${envApiUrl}`, `NOCOBASE_PORTAL_BASE=${portalBase}`].join('\n') + '\n', 'utf-8');
|
|
440
457
|
await writeFile(path.join(tempDir, '.env.local'), [`NOCOBASE_API_URL=${apiBaseUrl}`, `NOCOBASE_PORTAL_BASE=${portalBase}`].join('\n') + '\n', 'utf-8');
|
|
441
|
-
await writePortalConfig(tempDir, portalConfig);
|
|
442
458
|
if (targetExists) {
|
|
443
459
|
await rm(portalDir, { recursive: true, force: true });
|
|
444
460
|
}
|
|
@@ -13,10 +13,9 @@ import * as tar from 'tar';
|
|
|
13
13
|
import { executeApiRequest } from './api-client.js';
|
|
14
14
|
import { translateCli } from './cli-locale.js';
|
|
15
15
|
import { ensurePortalBuildHtmlReadsEnvOnly } from './portal-build-html.js';
|
|
16
|
-
import { buildPortalBasePath, resolvePortalAppContext, resolvePortalEnvApiUrl,
|
|
16
|
+
import { buildPortalBasePath, resolvePortalAppContext, resolvePortalEnvApiUrl, resolveSavedPortalSourcePath, resolvePortalSourcePath, titleFromPortalSlug, validatePortalSlug, } from './portal-create.js';
|
|
17
17
|
import { buildPortalCommandEnv } from './portal-command-env.js';
|
|
18
18
|
import { updatePortalEnvFiles } from './portal-env-files.js';
|
|
19
|
-
import { mergePortalConfigIntoOptions, readPortalConfig } from './portal-config.js';
|
|
20
19
|
import { resolvePnpmInstallCommand, run, runPnpmCommand, runPnpmInstallCommand } from './run-npm.js';
|
|
21
20
|
const portalDeployText = (key, values, fallback) => translateCli(`commands.portalDeploy.${key}`, values, { fallback });
|
|
22
21
|
const DEPLOY_OPERATION = {
|
|
@@ -107,12 +106,6 @@ async function chmodPortalDistTree(targetDir) {
|
|
|
107
106
|
}
|
|
108
107
|
}));
|
|
109
108
|
}
|
|
110
|
-
async function ensurePortalDistPublicReadable(params) {
|
|
111
|
-
await chmod(path.join(params.storagePath, 'portals'), PORTAL_PUBLIC_DIR_MODE);
|
|
112
|
-
await chmod(path.join(params.storagePath, 'portals', params.app), PORTAL_PUBLIC_DIR_MODE);
|
|
113
|
-
await chmod(params.portalDir, PORTAL_PUBLIC_DIR_MODE);
|
|
114
|
-
await chmodPortalDistTree(params.distDir);
|
|
115
|
-
}
|
|
116
109
|
async function assertFileExists(filePath, message) {
|
|
117
110
|
try {
|
|
118
111
|
const fileStat = await stat(filePath);
|
|
@@ -174,9 +167,6 @@ async function syncMultiPortalRecord(params) {
|
|
|
174
167
|
uiLayoutUid: DEFAULT_PORTAL_UI_LAYOUT_UID,
|
|
175
168
|
skipCreatePortalDirectory: true,
|
|
176
169
|
};
|
|
177
|
-
if (params.config) {
|
|
178
|
-
body.options = mergePortalConfigIntoOptions(params.config);
|
|
179
|
-
}
|
|
180
170
|
const response = await apiRequest({
|
|
181
171
|
cliVersion: params.cliVersion ?? '',
|
|
182
172
|
envName: params.envName,
|
|
@@ -194,17 +184,15 @@ export async function deployPortalWorkspace(options) {
|
|
|
194
184
|
const portal = validatePortalSlug(options.portal);
|
|
195
185
|
const apiBaseUrl = trimValue(options.env.apiBaseUrl);
|
|
196
186
|
const envApiUrl = resolvePortalEnvApiUrl(apiBaseUrl);
|
|
197
|
-
const storagePath = resolvePortalStoragePath(options.env);
|
|
198
187
|
const { app, appPublicPath, portalBaseApp } = await resolvePortalAppContext(options);
|
|
199
188
|
const portalBase = buildPortalBasePath({ app: portalBaseApp ?? app, appPublicPath, portal });
|
|
200
189
|
const deployBase = buildPortalBasePath({ app, appPublicPath, portal });
|
|
201
|
-
const portalDir =
|
|
190
|
+
const portalDir = resolveSavedPortalSourcePath(options.env, portal) ?? resolvePortalSourcePath(portal);
|
|
202
191
|
const distDir = path.join(portalDir, 'dist');
|
|
203
192
|
if (!(await pathExists(portalDir))) {
|
|
204
193
|
throw new Error(portalDeployText('errors.workspaceMissing', { portalDir, portal }, `Portal does not exist: ${portalDir}\nRun \`nb portal create ${portal}\` first.`));
|
|
205
194
|
}
|
|
206
195
|
await assertFileExists(path.join(portalDir, 'package.json'), portalDeployText('errors.packageJsonMissing', { portalDir }, `Portal is invalid: package.json is missing in ${portalDir}.`));
|
|
207
|
-
const portalConfig = await readPortalConfig(portalDir);
|
|
208
196
|
await updatePortalEnvFiles({
|
|
209
197
|
portalDir,
|
|
210
198
|
apiBaseUrl,
|
|
@@ -238,32 +226,8 @@ export async function deployPortalWorkspace(options) {
|
|
|
238
226
|
errorName: 'pnpm build:html',
|
|
239
227
|
});
|
|
240
228
|
await assertFileExists(path.join(distDir, 'index.html'), portalDeployText('errors.distMissing', { distDir }, `Portal build did not produce ${path.join(distDir, 'index.html')}.`));
|
|
241
|
-
await
|
|
242
|
-
|
|
243
|
-
app,
|
|
244
|
-
portalDir,
|
|
245
|
-
distDir,
|
|
246
|
-
});
|
|
247
|
-
if (options.env.kind === 'local' || options.env.kind === 'docker') {
|
|
248
|
-
await syncMultiPortalRecord({
|
|
249
|
-
portal,
|
|
250
|
-
config: portalConfig,
|
|
251
|
-
envName: options.envName,
|
|
252
|
-
cliVersion: options.cliVersion,
|
|
253
|
-
apiRequest: options.apiRequest,
|
|
254
|
-
});
|
|
255
|
-
return {
|
|
256
|
-
app,
|
|
257
|
-
portal,
|
|
258
|
-
portalDir,
|
|
259
|
-
portalBase,
|
|
260
|
-
distDir,
|
|
261
|
-
mode: options.env.kind,
|
|
262
|
-
uploaded: false,
|
|
263
|
-
recordSynced: true,
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
if (options.env.kind !== 'http') {
|
|
229
|
+
await chmodPortalDistTree(distDir);
|
|
230
|
+
if (options.env.kind !== 'local' && options.env.kind !== 'docker' && options.env.kind !== 'http') {
|
|
267
231
|
throw new Error(portalDeployText('errors.unsupportedEnvKind', { kind: options.env.kind }, `Cannot deploy a portal for ${options.env.kind} envs in the first version.`));
|
|
268
232
|
}
|
|
269
233
|
const archive = await packPortalDist(distDir);
|
|
@@ -284,7 +248,6 @@ export async function deployPortalWorkspace(options) {
|
|
|
284
248
|
}
|
|
285
249
|
await syncMultiPortalRecord({
|
|
286
250
|
portal,
|
|
287
|
-
config: portalConfig,
|
|
288
251
|
envName: options.envName,
|
|
289
252
|
cliVersion: options.cliVersion,
|
|
290
253
|
apiRequest: options.apiRequest,
|
|
@@ -296,7 +259,7 @@ export async function deployPortalWorkspace(options) {
|
|
|
296
259
|
portalBase,
|
|
297
260
|
distDir,
|
|
298
261
|
serverDistPath: uploadResult.distPath,
|
|
299
|
-
mode:
|
|
262
|
+
mode: options.env.kind,
|
|
300
263
|
uploaded: true,
|
|
301
264
|
recordSynced: true,
|
|
302
265
|
};
|
|
@@ -10,7 +10,8 @@ import { rm, stat } from 'node:fs/promises';
|
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { executeApiRequest } from './api-client.js';
|
|
12
12
|
import { translateCli } from './cli-locale.js';
|
|
13
|
-
import {
|
|
13
|
+
import { isUnsafePortalDeletePath } from './portal-path-safety.js';
|
|
14
|
+
import { buildPortalBasePath, resolvePortalAppContext, resolvePortalDeployPath, resolveSavedPortalSourcePath, resolvePortalStoragePath, validatePortalSlug, } from './portal-create.js';
|
|
14
15
|
const portalDestroyText = (key, values, fallback) => translateCli(`commands.portalDestroy.${key}`, values, { fallback });
|
|
15
16
|
const DESTROY_PORTAL_OPERATION = {
|
|
16
17
|
method: 'POST',
|
|
@@ -65,15 +66,27 @@ export async function destroyPortalWorkspace(options) {
|
|
|
65
66
|
const storagePath = resolvePortalStoragePath(options.env);
|
|
66
67
|
const { app, appPublicPath, portalBaseApp } = await resolvePortalAppContext(options);
|
|
67
68
|
const portalBase = buildPortalBasePath({ app: portalBaseApp ?? app, appPublicPath, portal });
|
|
68
|
-
const
|
|
69
|
-
const
|
|
69
|
+
const portalDeployDir = resolvePortalDeployPath({ storagePath, app, portal });
|
|
70
|
+
const portalDevDir = resolveSavedPortalSourcePath(options.env, portal) ?? '';
|
|
70
71
|
const mode = options.env.kind;
|
|
71
72
|
if (mode !== 'local' && mode !== 'docker' && mode !== 'http') {
|
|
72
73
|
throw new Error(portalDestroyText('errors.unsupportedEnvKind', { kind: mode }, `Cannot destroy a portal for ${mode} envs in the first version.`));
|
|
73
74
|
}
|
|
74
75
|
const destroyMode = mode;
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
if (portalDevDir) {
|
|
77
|
+
assertPortalDirIsInsideParent(path.dirname(portalDevDir), portalDevDir);
|
|
78
|
+
}
|
|
79
|
+
assertPortalDirIsInsideParent(path.dirname(portalDeployDir), portalDeployDir);
|
|
80
|
+
const developmentPathIsDeploymentPath = portalDevDir
|
|
81
|
+
? path.resolve(portalDevDir) === path.resolve(portalDeployDir)
|
|
82
|
+
: false;
|
|
83
|
+
const deploymentPathExists = await pathExists(portalDeployDir);
|
|
84
|
+
const developmentPathExists = options.deleteDevPath && portalDevDir && !developmentPathIsDeploymentPath
|
|
85
|
+
? await pathExists(portalDevDir)
|
|
86
|
+
: false;
|
|
87
|
+
if (developmentPathExists && (await isUnsafePortalDeletePath(portalDevDir))) {
|
|
88
|
+
throw new Error(portalDestroyText('errors.unsafeDevelopmentPath', { portalDir: portalDevDir }, `Refusing to delete an unsafe portal development path: ${portalDevDir}`));
|
|
89
|
+
}
|
|
77
90
|
const recordDeleted = await destroyMultiPortalRecord({
|
|
78
91
|
portal,
|
|
79
92
|
envName: options.envName,
|
|
@@ -81,16 +94,21 @@ export async function destroyPortalWorkspace(options) {
|
|
|
81
94
|
force: options.force,
|
|
82
95
|
apiRequest: options.apiRequest,
|
|
83
96
|
});
|
|
84
|
-
if (
|
|
85
|
-
await rm(
|
|
97
|
+
if (deploymentPathExists) {
|
|
98
|
+
await rm(portalDeployDir, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
if (developmentPathExists) {
|
|
101
|
+
await rm(portalDevDir, { recursive: true, force: true });
|
|
86
102
|
}
|
|
87
103
|
return {
|
|
88
104
|
app,
|
|
89
105
|
portal,
|
|
90
|
-
|
|
106
|
+
developmentPath: portalDevDir,
|
|
107
|
+
deploymentPath: portalDeployDir,
|
|
91
108
|
portalBase,
|
|
92
109
|
mode: destroyMode,
|
|
93
110
|
recordDeleted,
|
|
94
|
-
|
|
111
|
+
developmentPathDeleted: developmentPathExists || (developmentPathIsDeploymentPath && deploymentPathExists),
|
|
112
|
+
deploymentPathDeleted: deploymentPathExists,
|
|
95
113
|
};
|
|
96
114
|
}
|
package/dist/lib/portal-dev.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { stat } from 'node:fs/promises';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { translateCli } from './cli-locale.js';
|
|
12
|
-
import { buildPortalBasePath, resolvePortalAppContext,
|
|
12
|
+
import { buildPortalBasePath, resolvePortalAppContext, resolveSavedPortalSourcePath, resolvePortalSourcePath, validatePortalSlug, } from './portal-create.js';
|
|
13
13
|
import { buildPortalCommandEnv } from './portal-command-env.js';
|
|
14
14
|
import { updatePortalEnvFiles } from './portal-env-files.js';
|
|
15
15
|
import { run, runPnpmCommand } from './run-npm.js';
|
|
@@ -44,10 +44,9 @@ export async function devPortalWorkspace(options) {
|
|
|
44
44
|
if (options.env.kind === 'ssh') {
|
|
45
45
|
throw new Error(portalDevText('errors.sshUnsupported', undefined, 'Cannot start a portal in dev mode for ssh envs in the first version.'));
|
|
46
46
|
}
|
|
47
|
-
const storagePath = resolvePortalStoragePath(options.env);
|
|
48
47
|
const { app, appPublicPath, portalBaseApp } = await resolvePortalAppContext(options);
|
|
49
48
|
const portalBase = buildPortalBasePath({ app: portalBaseApp ?? app, appPublicPath, portal });
|
|
50
|
-
const portalDir =
|
|
49
|
+
const portalDir = resolveSavedPortalSourcePath(options.env, portal) ?? resolvePortalSourcePath(portal);
|
|
51
50
|
if (!(await pathExists(portalDir))) {
|
|
52
51
|
throw new Error(portalDevText('errors.workspaceMissing', { portalDir, portal }, `Portal does not exist: ${portalDir}\nRun \`nb portal create ${portal}\` first.`));
|
|
53
52
|
}
|
package/dist/lib/portal-info.js
CHANGED
|
@@ -10,9 +10,6 @@ import { translateCli } from './cli-locale.js';
|
|
|
10
10
|
import { toPortalOutputItem } from './portal-list.js';
|
|
11
11
|
const portalInfoText = (key, values, fallback) => translateCli(`commands.portalInfo.${key}`, values, { fallback });
|
|
12
12
|
function formatBoolean(value) {
|
|
13
|
-
if (value === null) {
|
|
14
|
-
return '';
|
|
15
|
-
}
|
|
16
13
|
return value ? 'yes' : 'no';
|
|
17
14
|
}
|
|
18
15
|
export function findPortalListItem(items, portal) {
|
|
@@ -24,8 +21,8 @@ export function formatPortalInfo(item) {
|
|
|
24
21
|
`${portalInfoText('fields.name', undefined, 'Name')}: ${outputItem.name}`,
|
|
25
22
|
`${portalInfoText('fields.url', undefined, 'URL')}: ${outputItem.url}`,
|
|
26
23
|
`${portalInfoText('fields.portalType', undefined, 'Portal type')}: ${outputItem.portalType}`,
|
|
27
|
-
`${portalInfoText('fields.
|
|
24
|
+
`${portalInfoText('fields.developmentPath', undefined, 'Development path')}: ${outputItem.developmentPath}`,
|
|
25
|
+
`${portalInfoText('fields.deploymentPath', undefined, 'Deployment path')}: ${outputItem.deploymentPath}`,
|
|
28
26
|
`${portalInfoText('fields.enabled', undefined, 'Enabled')}: ${formatBoolean(outputItem.enabled)}`,
|
|
29
|
-
`${portalInfoText('fields.localSynced', undefined, 'Local synced')}: ${formatBoolean(outputItem.localSynced)}`,
|
|
30
27
|
].join('\n');
|
|
31
28
|
}
|
package/dist/lib/portal-list.js
CHANGED
|
@@ -6,12 +6,10 @@
|
|
|
6
6
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
|
-
import { stat } from 'node:fs/promises';
|
|
10
|
-
import path from 'node:path';
|
|
11
9
|
import { appendAppPublicPath } from './app-public-path.js';
|
|
12
10
|
import { executeApiRequest } from './api-client.js';
|
|
13
11
|
import { translateCli } from './cli-locale.js';
|
|
14
|
-
import { buildPortalBasePath, resolvePortalAppContext, resolvePortalStoragePath, } from './portal-create.js';
|
|
12
|
+
import { buildPortalBasePath, resolvePortalDeployPath, resolvePortalAppContext, resolveSavedPortalSourcePath, resolvePortalStoragePath, } from './portal-create.js';
|
|
15
13
|
const portalListText = (key, values, fallback) => translateCli(`commands.portalList.${key}`, values, { fallback });
|
|
16
14
|
const LIST_PORTALS_OPERATION = {
|
|
17
15
|
method: 'GET',
|
|
@@ -57,15 +55,6 @@ function readListData(data) {
|
|
|
57
55
|
}
|
|
58
56
|
return readListData(directData);
|
|
59
57
|
}
|
|
60
|
-
async function pathExists(target) {
|
|
61
|
-
try {
|
|
62
|
-
await stat(target);
|
|
63
|
-
return true;
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
58
|
function buildPortalAccessUrl(apiBaseUrl, portalBase) {
|
|
70
59
|
try {
|
|
71
60
|
const baseUrl = new URL(apiBaseUrl);
|
|
@@ -134,10 +123,11 @@ export function toPortalOutputItem(item) {
|
|
|
134
123
|
name: item.portalName,
|
|
135
124
|
url: item.portalUrl,
|
|
136
125
|
portalType: item.portalType,
|
|
137
|
-
|
|
126
|
+
developmentPath: item.portalDir,
|
|
127
|
+
deploymentPath: item.deployDir,
|
|
138
128
|
enabled: item.enabled,
|
|
129
|
+
isDefault: item.isDefault,
|
|
139
130
|
sourceStorage: item.sourceStorage,
|
|
140
|
-
localSynced: item.localSynced,
|
|
141
131
|
};
|
|
142
132
|
}
|
|
143
133
|
async function listMultiPortalRecords(params) {
|
|
@@ -171,38 +161,41 @@ export async function listPortalWorkspaces(options) {
|
|
|
171
161
|
cliVersion: options.cliVersion,
|
|
172
162
|
apiRequest: options.apiRequest,
|
|
173
163
|
});
|
|
174
|
-
const items =
|
|
164
|
+
const items = records.map((record) => {
|
|
175
165
|
const uid = readRecordString(record, 'uid');
|
|
176
166
|
const portalName = readRecordString(record, 'portalName') || uid;
|
|
177
167
|
const routePath = readRecordString(record, 'routePath') || `/${portalName}`;
|
|
178
168
|
const portalType = readRecordString(record, 'portalType');
|
|
179
169
|
const enabled = readRecordBoolean(record, 'enabled');
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
const
|
|
170
|
+
const isDefault = readRecordBoolean(record, 'isDefault');
|
|
171
|
+
const recordOptions = readRecordObject(record, 'options');
|
|
172
|
+
const git = readRecordObject(recordOptions, 'git');
|
|
173
|
+
const sourceStorage = trimValue(recordOptions.sourceStorage) || readRecordString(record, 'sourceStorage') || 'nocobase';
|
|
183
174
|
const isAi = portalType === 'ai';
|
|
184
|
-
const portalDir = isAi ?
|
|
175
|
+
const portalDir = isAi ? resolveSavedPortalSourcePath(options.env, portalName) ?? '' : '';
|
|
176
|
+
const deployDir = isAi ? resolvePortalDeployPath({ storagePath, app, portal: portalName }) : '';
|
|
185
177
|
return {
|
|
186
178
|
uid,
|
|
187
179
|
portalName,
|
|
188
180
|
routePath,
|
|
189
181
|
portalType,
|
|
190
182
|
enabled,
|
|
183
|
+
isDefault,
|
|
191
184
|
sourceStorage,
|
|
192
185
|
gitRepo: trimValue(git.repo) || readRecordString(record, 'gitRepo'),
|
|
193
186
|
gitBranch: trimValue(git.branch) || readRecordString(record, 'gitBranch'),
|
|
194
187
|
gitPath: trimValue(git.path) || readRecordString(record, 'gitPath'),
|
|
195
|
-
sourceRevision: trimValue(
|
|
196
|
-
options,
|
|
188
|
+
sourceRevision: trimValue(recordOptions.sourceRevision) || readRecordString(record, 'sourceRevision'),
|
|
189
|
+
options: recordOptions,
|
|
197
190
|
portalUrl: enabled
|
|
198
191
|
? buildPortalAccessUrl(apiBaseUrl, isAi
|
|
199
192
|
? buildPortalBasePath({ app: baseApp, appPublicPath, portal: portalName })
|
|
200
193
|
: buildNoCodePortalBasePath({ app: baseApp, appPublicPath, routePath }))
|
|
201
194
|
: '',
|
|
202
195
|
portalDir,
|
|
203
|
-
|
|
196
|
+
deployDir,
|
|
204
197
|
};
|
|
205
|
-
})
|
|
198
|
+
});
|
|
206
199
|
return {
|
|
207
200
|
app,
|
|
208
201
|
mode: listMode,
|
|
@@ -0,0 +1,76 @@
|
|
|
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 { readdir, readFile, realpath, stat } from 'node:fs/promises';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
async function resolveExistingPath(target) {
|
|
13
|
+
const resolved = path.resolve(target);
|
|
14
|
+
try {
|
|
15
|
+
return await realpath(resolved);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return resolved;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function isSameOrAncestor(candidate, child) {
|
|
22
|
+
const relative = path.relative(candidate, child);
|
|
23
|
+
return !relative || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
24
|
+
}
|
|
25
|
+
export async function isUnsafePortalDeletePath(target) {
|
|
26
|
+
const resolvedTarget = await resolveExistingPath(target);
|
|
27
|
+
const root = path.parse(resolvedTarget).root;
|
|
28
|
+
if (resolvedTarget === root) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
const homeDir = await resolveExistingPath(os.homedir());
|
|
32
|
+
if (resolvedTarget === homeDir) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
const cwd = await resolveExistingPath(process.cwd());
|
|
36
|
+
return isSameOrAncestor(resolvedTarget, cwd);
|
|
37
|
+
}
|
|
38
|
+
async function isDirectory(target) {
|
|
39
|
+
try {
|
|
40
|
+
return (await stat(target)).isDirectory();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function isEmptyDirectory(target) {
|
|
47
|
+
try {
|
|
48
|
+
return (await readdir(target)).length === 0;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function hasNocoBasePackageField(target) {
|
|
55
|
+
try {
|
|
56
|
+
const data = JSON.parse(await readFile(path.join(target, 'package.json'), 'utf-8'));
|
|
57
|
+
return (!!data &&
|
|
58
|
+
typeof data === 'object' &&
|
|
59
|
+
!Array.isArray(data) &&
|
|
60
|
+
Object.prototype.hasOwnProperty.call(data, 'nocobase'));
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export async function canReplacePortalDirectory(target) {
|
|
67
|
+
const resolvedTarget = await resolveExistingPath(target);
|
|
68
|
+
const root = path.parse(resolvedTarget).root;
|
|
69
|
+
if (resolvedTarget === root || resolvedTarget === (await resolveExistingPath(os.homedir()))) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
if (!(await isDirectory(resolvedTarget))) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return (await isEmptyDirectory(resolvedTarget)) || (await hasNocoBasePackageField(resolvedTarget));
|
|
76
|
+
}
|