@nocobase/cli 3.0.0-alpha.1 → 3.0.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.
- package/dist/commands/api/resource/create.js +11 -2
- package/dist/commands/api/swagger/get.js +96 -0
- package/dist/commands/api/swagger/index.js +20 -0
- package/dist/commands/api/swagger/list.js +92 -0
- package/dist/commands/env/add.js +6 -0
- package/dist/commands/env/update.js +13 -0
- package/dist/commands/init.js +38 -0
- package/dist/commands/install.js +18 -62
- package/dist/commands/portal/config.js +16 -5
- package/dist/commands/portal/create.js +17 -26
- package/dist/commands/portal/destroy.js +28 -6
- package/dist/commands/portal/dev.js +2 -0
- package/dist/commands/portal/list.js +5 -5
- package/dist/commands/portal/pull.js +32 -3
- package/dist/lib/api-client.js +35 -9
- package/dist/lib/app-client-entry-mode.js +28 -0
- package/dist/lib/app-managed-resources.js +2 -0
- package/dist/lib/auth-store.js +52 -1
- package/dist/lib/bootstrap.js +3 -1
- package/dist/lib/browser.js +29 -0
- package/dist/lib/env-auth.js +2 -36
- package/dist/lib/env-command-config.js +1 -0
- package/dist/lib/env-config.js +10 -2
- package/dist/lib/env-portal-config.js +30 -0
- package/dist/lib/env-proxy.js +25 -4
- package/dist/lib/generated-command.js +81 -0
- package/dist/lib/managed-env-file.js +67 -13
- package/dist/lib/managed-init-env.js +0 -2
- package/dist/lib/plugin-import.js +30 -8
- package/dist/lib/portal-build-html.js +27 -0
- package/dist/lib/portal-config.js +6 -20
- package/dist/lib/portal-configure.js +49 -56
- package/dist/lib/portal-create.js +96 -14
- package/dist/lib/portal-deploy.js +30 -47
- package/dist/lib/portal-destroy.js +34 -20
- package/dist/lib/portal-dev.js +6 -7
- package/dist/lib/portal-env-files.js +2 -1
- package/dist/lib/portal-info.js +2 -5
- package/dist/lib/portal-list.js +20 -26
- package/dist/lib/portal-path-safety.js +76 -0
- package/dist/lib/portal-source.js +227 -58
- package/dist/lib/prompt-catalog-core.js +2 -2
- package/dist/lib/prompt-catalog-terminal.js +4 -5
- package/dist/lib/prompt-web-ui.js +12 -6
- package/dist/lib/resource-command.js +18 -2
- package/dist/lib/resource-request.js +8 -0
- package/dist/lib/run-npm.js +68 -4
- package/dist/lib/runtime-generator.js +28 -1
- package/dist/lib/swagger-command.js +52 -0
- package/dist/locale/en-US.json +81 -15
- package/dist/locale/zh-CN.json +81 -15
- package/package.json +2 -2
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,
|
|
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) {
|
|
@@ -159,8 +149,9 @@ async function listMultiPortalRecords(params) {
|
|
|
159
149
|
export async function listPortalWorkspaces(options) {
|
|
160
150
|
const apiBaseUrl = trimValue(options.env.apiBaseUrl);
|
|
161
151
|
const storagePath = resolvePortalStoragePath(options.env);
|
|
162
|
-
const { app, appPublicPath } =
|
|
152
|
+
const { app, appPublicPath, portalBaseApp } = options.appContext ?? (await resolvePortalAppContext(options));
|
|
163
153
|
const mode = options.env.kind;
|
|
154
|
+
const baseApp = portalBaseApp ?? app;
|
|
164
155
|
if (mode !== 'local' && mode !== 'docker' && mode !== 'http') {
|
|
165
156
|
throw new Error(portalListText('errors.unsupportedEnvKind', { kind: mode }, `Cannot list portals for ${mode} envs in the first version.`));
|
|
166
157
|
}
|
|
@@ -170,38 +161,41 @@ export async function listPortalWorkspaces(options) {
|
|
|
170
161
|
cliVersion: options.cliVersion,
|
|
171
162
|
apiRequest: options.apiRequest,
|
|
172
163
|
});
|
|
173
|
-
const items =
|
|
164
|
+
const items = records.map((record) => {
|
|
174
165
|
const uid = readRecordString(record, 'uid');
|
|
175
166
|
const portalName = readRecordString(record, 'portalName') || uid;
|
|
176
167
|
const routePath = readRecordString(record, 'routePath') || `/${portalName}`;
|
|
177
168
|
const portalType = readRecordString(record, 'portalType');
|
|
178
169
|
const enabled = readRecordBoolean(record, 'enabled');
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
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';
|
|
182
174
|
const isAi = portalType === 'ai';
|
|
183
|
-
const portalDir = isAi ?
|
|
175
|
+
const portalDir = isAi ? resolveSavedPortalSourcePath(options.env, portalName) ?? '' : '';
|
|
176
|
+
const deployDir = isAi ? resolvePortalDeployPath({ storagePath, app, portal: portalName }) : '';
|
|
184
177
|
return {
|
|
185
178
|
uid,
|
|
186
179
|
portalName,
|
|
187
180
|
routePath,
|
|
188
181
|
portalType,
|
|
189
182
|
enabled,
|
|
183
|
+
isDefault,
|
|
190
184
|
sourceStorage,
|
|
191
185
|
gitRepo: trimValue(git.repo) || readRecordString(record, 'gitRepo'),
|
|
192
186
|
gitBranch: trimValue(git.branch) || readRecordString(record, 'gitBranch'),
|
|
193
187
|
gitPath: trimValue(git.path) || readRecordString(record, 'gitPath'),
|
|
194
|
-
sourceRevision: trimValue(
|
|
195
|
-
options,
|
|
188
|
+
sourceRevision: trimValue(recordOptions.sourceRevision) || readRecordString(record, 'sourceRevision'),
|
|
189
|
+
options: recordOptions,
|
|
196
190
|
portalUrl: enabled
|
|
197
191
|
? buildPortalAccessUrl(apiBaseUrl, isAi
|
|
198
|
-
? buildPortalBasePath({ app, appPublicPath, portal: portalName })
|
|
199
|
-
: buildNoCodePortalBasePath({ app, appPublicPath, routePath }))
|
|
192
|
+
? buildPortalBasePath({ app: baseApp, appPublicPath, portal: portalName })
|
|
193
|
+
: buildNoCodePortalBasePath({ app: baseApp, appPublicPath, routePath }))
|
|
200
194
|
: '',
|
|
201
195
|
portalDir,
|
|
202
|
-
|
|
196
|
+
deployDir,
|
|
203
197
|
};
|
|
204
|
-
})
|
|
198
|
+
});
|
|
205
199
|
return {
|
|
206
200
|
app,
|
|
207
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
|
+
}
|
|
@@ -14,13 +14,20 @@ import path from 'node:path';
|
|
|
14
14
|
import * as tar from 'tar';
|
|
15
15
|
import { executeApiRequest } from './api-client.js';
|
|
16
16
|
import { translateCli } from './cli-locale.js';
|
|
17
|
-
import {
|
|
17
|
+
import { ensurePortalBuildHtmlReadsEnvOnly } from './portal-build-html.js';
|
|
18
|
+
import { buildPortalConfig, buildPortalConfigFromOptions, DEFAULT_PORTAL_GIT_PATH, } from './portal-config.js';
|
|
18
19
|
import { buildPortalCommandEnv } from './portal-command-env.js';
|
|
19
|
-
import {
|
|
20
|
+
import { canReplacePortalDirectory } from './portal-path-safety.js';
|
|
21
|
+
import { updatePortalEnvFiles } from './portal-env-files.js';
|
|
22
|
+
import { buildPortalBasePath, resolvePortalAppContext, resolvePortalDeployPath, resolvePortalSourcePath, resolveSavedPortalSourcePath, resolvePortalStoragePath, validatePortalSlug, } from './portal-create.js';
|
|
20
23
|
import { listPortalWorkspaces } from './portal-list.js';
|
|
21
24
|
import { findPortalListItem } from './portal-info.js';
|
|
22
|
-
import { run } from './run-npm.js';
|
|
25
|
+
import { resolvePnpmInstallCommand, run, runPnpmInstallCommand } from './run-npm.js';
|
|
23
26
|
const execFileAsync = promisify(execFile);
|
|
27
|
+
const NOCOBASE_CLI_GIT_IDENTITY = {
|
|
28
|
+
name: 'NocoBase CLI',
|
|
29
|
+
email: '314549027+nocobase-cli@users.noreply.github.com',
|
|
30
|
+
};
|
|
24
31
|
const portalSourceText = (key, values, fallback) => translateCli(`commands.portalSource.${key}`, values, { fallback });
|
|
25
32
|
const PULL_SOURCE_OPERATION = {
|
|
26
33
|
method: 'POST',
|
|
@@ -128,11 +135,25 @@ async function packPortalSource(portalDir) {
|
|
|
128
135
|
},
|
|
129
136
|
};
|
|
130
137
|
}
|
|
131
|
-
async function
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
138
|
+
async function replaceExistingPortalDirectory(params) {
|
|
139
|
+
if (!(await pathExists(path.join(params.portalDir, '.git')))) {
|
|
140
|
+
await rm(params.portalDir, { recursive: true, force: true });
|
|
141
|
+
await rename(params.sourceDir, params.portalDir);
|
|
142
|
+
return;
|
|
135
143
|
}
|
|
144
|
+
const existingEntries = await readdir(params.portalDir);
|
|
145
|
+
await Promise.all(existingEntries
|
|
146
|
+
.filter((entry) => entry !== '.git')
|
|
147
|
+
.map((entry) => rm(path.join(params.portalDir, entry), { recursive: true, force: true })));
|
|
148
|
+
const sourceEntries = await readdir(params.sourceDir);
|
|
149
|
+
await Promise.all(sourceEntries.map((entry) => rename(path.join(params.sourceDir, entry), path.join(params.portalDir, entry))));
|
|
150
|
+
await rm(params.sourceDir, { recursive: true, force: true });
|
|
151
|
+
}
|
|
152
|
+
async function replacePortalSourceFromArchive(params) {
|
|
153
|
+
const targetExists = await assertPortalDirectoryCanBeReplaced({
|
|
154
|
+
portalDir: params.portalDir,
|
|
155
|
+
force: params.force,
|
|
156
|
+
});
|
|
136
157
|
const parentDir = path.dirname(params.portalDir);
|
|
137
158
|
const tempDir = await mkdtemp(path.join(parentDir, `.${path.basename(params.portalDir)}-pull-`));
|
|
138
159
|
try {
|
|
@@ -143,7 +164,11 @@ async function replacePortalSourceFromArchive(params) {
|
|
|
143
164
|
filter: validatePortalSourceTarEntry,
|
|
144
165
|
});
|
|
145
166
|
if (targetExists) {
|
|
146
|
-
await
|
|
167
|
+
await replaceExistingPortalDirectory({
|
|
168
|
+
sourceDir: tempDir,
|
|
169
|
+
portalDir: params.portalDir,
|
|
170
|
+
});
|
|
171
|
+
return;
|
|
147
172
|
}
|
|
148
173
|
await rename(tempDir, params.portalDir);
|
|
149
174
|
}
|
|
@@ -153,10 +178,10 @@ async function replacePortalSourceFromArchive(params) {
|
|
|
153
178
|
}
|
|
154
179
|
}
|
|
155
180
|
async function replacePortalSourceFromDirectory(params) {
|
|
156
|
-
const targetExists = await
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
181
|
+
const targetExists = await assertPortalDirectoryCanBeReplaced({
|
|
182
|
+
portalDir: params.portalDir,
|
|
183
|
+
force: params.force,
|
|
184
|
+
});
|
|
160
185
|
const parentDir = path.dirname(params.portalDir);
|
|
161
186
|
const tempDir = await mkdtemp(path.join(parentDir, `.${path.basename(params.portalDir)}-pull-`));
|
|
162
187
|
try {
|
|
@@ -165,7 +190,11 @@ async function replacePortalSourceFromDirectory(params) {
|
|
|
165
190
|
filter: (source) => shouldPackPortalSourceEntry(path.relative(params.sourceDir, source)),
|
|
166
191
|
});
|
|
167
192
|
if (targetExists) {
|
|
168
|
-
await
|
|
193
|
+
await replaceExistingPortalDirectory({
|
|
194
|
+
sourceDir: tempDir,
|
|
195
|
+
portalDir: params.portalDir,
|
|
196
|
+
});
|
|
197
|
+
return;
|
|
169
198
|
}
|
|
170
199
|
await rename(tempDir, params.portalDir);
|
|
171
200
|
}
|
|
@@ -174,35 +203,85 @@ async function replacePortalSourceFromDirectory(params) {
|
|
|
174
203
|
throw error;
|
|
175
204
|
}
|
|
176
205
|
}
|
|
206
|
+
async function assertPortalDirectoryCanBeReplaced(params) {
|
|
207
|
+
const targetExists = await pathExists(params.portalDir);
|
|
208
|
+
if (targetExists && !params.force) {
|
|
209
|
+
throw new Error(portalSourceText('errors.workspaceExists', { portalDir: params.portalDir }, `Portal already exists: ${params.portalDir}\nPass --force to delete it and pull again.`));
|
|
210
|
+
}
|
|
211
|
+
if (targetExists && params.force && !(await canReplacePortalDirectory(params.portalDir))) {
|
|
212
|
+
throw new Error(portalSourceText('errors.workspaceNotReplaceable', { portalDir: params.portalDir }, `Refusing to replace a non-portal directory: ${params.portalDir}`));
|
|
213
|
+
}
|
|
214
|
+
return targetExists;
|
|
215
|
+
}
|
|
177
216
|
async function runGit(args, cwd) {
|
|
178
217
|
return await execFileAsync('git', args, {
|
|
179
218
|
cwd,
|
|
180
219
|
maxBuffer: 10 * 1024 * 1024,
|
|
181
220
|
});
|
|
182
221
|
}
|
|
222
|
+
function isValidGitIdentity(identity) {
|
|
223
|
+
return (!/[\r\n<>]/.test(identity.name) &&
|
|
224
|
+
!/[\r\n<>\s]/.test(identity.email) &&
|
|
225
|
+
identity.email.includes('@'));
|
|
226
|
+
}
|
|
227
|
+
async function resolveLocalGitIdentity(cwd) {
|
|
228
|
+
let output;
|
|
229
|
+
try {
|
|
230
|
+
output = (await runGit(['var', 'GIT_AUTHOR_IDENT'], cwd)).stdout;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
const match = /^(.*) <([^<>]+)> -?\d+ [+-]\d{4}$/.exec(trimValue(output));
|
|
236
|
+
if (!match) {
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
const identity = { name: match[1], email: match[2] };
|
|
240
|
+
if (!isValidGitIdentity(identity)) {
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
if (identity.name === NOCOBASE_CLI_GIT_IDENTITY.name &&
|
|
244
|
+
identity.email === NOCOBASE_CLI_GIT_IDENTITY.email) {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
return identity;
|
|
248
|
+
}
|
|
183
249
|
async function installPortalDependencies(params) {
|
|
184
250
|
if (params.installDependencies === false) {
|
|
185
251
|
return {
|
|
186
252
|
dependenciesInstalled: false,
|
|
187
253
|
installSkipped: true,
|
|
254
|
+
installFailed: false,
|
|
188
255
|
};
|
|
189
256
|
}
|
|
190
257
|
if (!(await isFile(path.join(params.portalDir, 'package.json')))) {
|
|
191
258
|
return {
|
|
192
259
|
dependenciesInstalled: false,
|
|
193
260
|
installSkipped: true,
|
|
261
|
+
installFailed: false,
|
|
194
262
|
};
|
|
195
263
|
}
|
|
196
264
|
const runCommand = params.runCommand ?? run;
|
|
197
|
-
await
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
265
|
+
const installCommand = await resolvePnpmInstallCommand(params.portalDir);
|
|
266
|
+
try {
|
|
267
|
+
await runPnpmInstallCommand(runCommand, installCommand.args, {
|
|
268
|
+
cwd: params.portalDir,
|
|
269
|
+
env: buildPortalCommandEnv(),
|
|
270
|
+
envMode: 'replace',
|
|
271
|
+
errorName: installCommand.errorName,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
return {
|
|
276
|
+
dependenciesInstalled: false,
|
|
277
|
+
installSkipped: false,
|
|
278
|
+
installFailed: true,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
203
281
|
return {
|
|
204
282
|
dependenciesInstalled: true,
|
|
205
283
|
installSkipped: false,
|
|
284
|
+
installFailed: false,
|
|
206
285
|
};
|
|
207
286
|
}
|
|
208
287
|
function readSourceRevision(data) {
|
|
@@ -219,10 +298,15 @@ async function resolvePortalSourceContext(options) {
|
|
|
219
298
|
const portal = validatePortalSlug(options.portal);
|
|
220
299
|
const apiBaseUrl = trimValue(options.env.apiBaseUrl);
|
|
221
300
|
const storagePath = resolvePortalStoragePath(options.env);
|
|
222
|
-
const { app, appPublicPath } = resolvePortalAppFromApiBaseUrl(apiBaseUrl, options.env.config.appPublicPath);
|
|
223
|
-
const portalDir = path.join(storagePath, 'portals', app, portal);
|
|
224
|
-
const portalBase = buildPortalBasePath({ app, appPublicPath, portal });
|
|
225
301
|
const mode = options.env.kind;
|
|
302
|
+
const appContext = await resolvePortalAppContext(options);
|
|
303
|
+
const { app, appPublicPath, portalBaseApp } = appContext;
|
|
304
|
+
const portalDeployDir = resolvePortalDeployPath({ storagePath, app, portal });
|
|
305
|
+
const portalDir = options.sourcePath
|
|
306
|
+
? resolvePortalSourcePath(portal, options.sourcePath)
|
|
307
|
+
: resolveSavedPortalSourcePath(options.env, portal) ??
|
|
308
|
+
(options.defaultSourcePath ? resolvePortalSourcePath(portal) : portalDeployDir);
|
|
309
|
+
const portalBase = buildPortalBasePath({ app: portalBaseApp ?? app, appPublicPath, portal });
|
|
226
310
|
if (mode !== 'local' && mode !== 'docker' && mode !== 'http') {
|
|
227
311
|
throw new Error(portalSourceText('errors.unsupportedEnvKind', { kind: mode }, `Cannot sync portal source for ${mode} envs in the first version.`));
|
|
228
312
|
}
|
|
@@ -231,6 +315,7 @@ async function resolvePortalSourceContext(options) {
|
|
|
231
315
|
envName: options.envName,
|
|
232
316
|
cliVersion: options.cliVersion,
|
|
233
317
|
apiRequest: options.apiRequest,
|
|
318
|
+
appContext,
|
|
234
319
|
});
|
|
235
320
|
const item = findPortalListItem(list.items, portal);
|
|
236
321
|
if (!item) {
|
|
@@ -241,6 +326,7 @@ async function resolvePortalSourceContext(options) {
|
|
|
241
326
|
portal,
|
|
242
327
|
portalDir,
|
|
243
328
|
portalBase,
|
|
329
|
+
apiBaseUrl,
|
|
244
330
|
mode,
|
|
245
331
|
sourceStorage: item.sourceStorage || 'nocobase',
|
|
246
332
|
gitRepo: item.gitRepo,
|
|
@@ -271,6 +357,25 @@ function applyPortalConfigToContext(context, config) {
|
|
|
271
357
|
gitPath: config.git?.path ?? DEFAULT_PORTAL_GIT_PATH,
|
|
272
358
|
};
|
|
273
359
|
}
|
|
360
|
+
function getTemporaryGitPortalConfig(options) {
|
|
361
|
+
const gitRepo = trimValue(options.gitRepo);
|
|
362
|
+
if (!gitRepo) {
|
|
363
|
+
if (trimValue(options.gitBranch) || trimValue(options.gitPath)) {
|
|
364
|
+
throw new Error(portalSourceText('errors.gitRepoRequiredForTemporaryPull', undefined, [
|
|
365
|
+
'--git-branch and --git-path require --git-repo for a temporary Git pull.',
|
|
366
|
+
'To update the portal configuration, use `nb portal config`.',
|
|
367
|
+
].join(' ')));
|
|
368
|
+
}
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
return buildPortalConfig({
|
|
372
|
+
portal: options.portal,
|
|
373
|
+
sourceStorage: 'git',
|
|
374
|
+
gitRepo,
|
|
375
|
+
gitBranch: options.gitBranch,
|
|
376
|
+
gitPath: options.gitPath,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
274
379
|
function assertGitSourceConfig(context) {
|
|
275
380
|
if (!context.gitRepo) {
|
|
276
381
|
throw new Error(portalSourceText('errors.gitRepoMissing', { portal: context.portal }, `Portal "${context.portal}" uses Git source storage, but gitRepo is missing.`));
|
|
@@ -319,8 +424,76 @@ async function cloneGitSource(params) {
|
|
|
319
424
|
}
|
|
320
425
|
return repoDir;
|
|
321
426
|
}
|
|
427
|
+
async function setGitOriginRepository(params) {
|
|
428
|
+
try {
|
|
429
|
+
await runGit(['remote', 'get-url', 'origin'], params.repoDir);
|
|
430
|
+
await runGit(['remote', 'set-url', 'origin', params.repo], params.repoDir);
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
await runGit(['remote', 'add', 'origin', params.repo], params.repoDir);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async function checkoutExistingGitRepository(params) {
|
|
437
|
+
await setGitOriginRepository({
|
|
438
|
+
repoDir: params.repoDir,
|
|
439
|
+
repo: params.repo,
|
|
440
|
+
});
|
|
441
|
+
try {
|
|
442
|
+
await runGit(['fetch', 'origin', params.branch], params.repoDir);
|
|
443
|
+
await runGit(['checkout', '-f', '-B', params.branch, 'FETCH_HEAD'], params.repoDir);
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
await runGit(['fetch', 'origin'], params.repoDir);
|
|
447
|
+
await runGit(['checkout', '-f', '-B', params.branch], params.repoDir);
|
|
448
|
+
}
|
|
449
|
+
await runGit(['clean', '-fdx'], params.repoDir);
|
|
450
|
+
}
|
|
451
|
+
async function pullGitRepositoryRootPortalSource(params) {
|
|
452
|
+
const targetExists = await assertPortalDirectoryCanBeReplaced({
|
|
453
|
+
portalDir: params.context.portalDir,
|
|
454
|
+
force: params.force,
|
|
455
|
+
});
|
|
456
|
+
await mkdir(path.dirname(params.context.portalDir), { recursive: true });
|
|
457
|
+
if (targetExists && (await pathExists(path.join(params.context.portalDir, '.git')))) {
|
|
458
|
+
await checkoutExistingGitRepository({
|
|
459
|
+
repoDir: params.context.portalDir,
|
|
460
|
+
repo: params.repo,
|
|
461
|
+
branch: params.branch,
|
|
462
|
+
});
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (targetExists) {
|
|
466
|
+
await rm(params.context.portalDir, { recursive: true, force: true });
|
|
467
|
+
}
|
|
468
|
+
const tempDir = await mkdtemp(path.join(path.dirname(params.context.portalDir), `.${path.basename(params.context.portalDir)}-git-pull-`));
|
|
469
|
+
try {
|
|
470
|
+
const repoDir = await cloneGitSource({
|
|
471
|
+
repo: params.repo,
|
|
472
|
+
branch: params.branch,
|
|
473
|
+
cwd: tempDir,
|
|
474
|
+
createBranch: true,
|
|
475
|
+
});
|
|
476
|
+
await rename(repoDir, params.context.portalDir);
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
await rm(params.context.portalDir, { recursive: true, force: true });
|
|
480
|
+
throw error;
|
|
481
|
+
}
|
|
482
|
+
finally {
|
|
483
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
484
|
+
}
|
|
485
|
+
}
|
|
322
486
|
async function pullGitPortalSource(params) {
|
|
323
487
|
const git = assertGitSourceConfig(params.context);
|
|
488
|
+
if (isGitRepositoryRootPath(git.gitPath)) {
|
|
489
|
+
await pullGitRepositoryRootPortalSource({
|
|
490
|
+
context: params.context,
|
|
491
|
+
repo: git.repo,
|
|
492
|
+
branch: git.branch,
|
|
493
|
+
force: params.force,
|
|
494
|
+
});
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
324
497
|
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'nocobase-cli-portal-git-pull-'));
|
|
325
498
|
try {
|
|
326
499
|
const repoDir = await cloneGitSource({
|
|
@@ -346,6 +519,7 @@ async function pullGitPortalSource(params) {
|
|
|
346
519
|
}
|
|
347
520
|
async function pushGitPortalSource(params) {
|
|
348
521
|
const git = assertGitSourceConfig(params.context);
|
|
522
|
+
const localIdentity = await resolveLocalGitIdentity(params.context.portalDir);
|
|
349
523
|
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'nocobase-cli-portal-git-push-'));
|
|
350
524
|
try {
|
|
351
525
|
const repoDir = await cloneGitSource({
|
|
@@ -364,15 +538,20 @@ async function pushGitPortalSource(params) {
|
|
|
364
538
|
if (!status.stdout.trim()) {
|
|
365
539
|
return undefined;
|
|
366
540
|
}
|
|
367
|
-
|
|
541
|
+
const commitIdentity = localIdentity ?? NOCOBASE_CLI_GIT_IDENTITY;
|
|
542
|
+
const commitArgs = [
|
|
368
543
|
'-c',
|
|
369
|
-
|
|
544
|
+
`user.name=${commitIdentity.name}`,
|
|
370
545
|
'-c',
|
|
371
|
-
|
|
546
|
+
`user.email=${commitIdentity.email}`,
|
|
372
547
|
'commit',
|
|
373
548
|
'-m',
|
|
374
549
|
trimValue(params.message) || `chore(portal): update ${params.context.portal}`,
|
|
375
|
-
]
|
|
550
|
+
];
|
|
551
|
+
if (localIdentity) {
|
|
552
|
+
commitArgs.push('-m', `Co-authored-by: ${NOCOBASE_CLI_GIT_IDENTITY.name} <${NOCOBASE_CLI_GIT_IDENTITY.email}>`);
|
|
553
|
+
}
|
|
554
|
+
await runGit(commitArgs, repoDir);
|
|
376
555
|
await runGit(['push', 'origin', git.branch], repoDir);
|
|
377
556
|
const revision = await runGit(['rev-parse', 'HEAD'], repoDir);
|
|
378
557
|
return revision.stdout.trim();
|
|
@@ -382,15 +561,23 @@ async function pushGitPortalSource(params) {
|
|
|
382
561
|
}
|
|
383
562
|
}
|
|
384
563
|
export async function pullPortalSource(options) {
|
|
385
|
-
const context = await resolvePortalSourceContext(
|
|
386
|
-
|
|
564
|
+
const context = await resolvePortalSourceContext({
|
|
565
|
+
...options,
|
|
566
|
+
defaultSourcePath: true,
|
|
567
|
+
});
|
|
568
|
+
const portalConfig = getTemporaryGitPortalConfig(options) ?? buildPortalConfigFromContext(context);
|
|
387
569
|
const sourceContext = applyPortalConfigToContext(context, portalConfig);
|
|
388
570
|
if (sourceContext.sourceStorage === 'git') {
|
|
389
571
|
await pullGitPortalSource({
|
|
390
572
|
context: sourceContext,
|
|
391
573
|
force: options.force,
|
|
392
574
|
});
|
|
393
|
-
await
|
|
575
|
+
await ensurePortalBuildHtmlReadsEnvOnly(sourceContext.portalDir);
|
|
576
|
+
await updatePortalEnvFiles({
|
|
577
|
+
portalDir: sourceContext.portalDir,
|
|
578
|
+
apiBaseUrl: sourceContext.apiBaseUrl,
|
|
579
|
+
portalBase: sourceContext.portalBase,
|
|
580
|
+
});
|
|
394
581
|
const installResult = await installPortalDependencies({
|
|
395
582
|
portalDir: sourceContext.portalDir,
|
|
396
583
|
installDependencies: options.installDependencies,
|
|
@@ -405,15 +592,6 @@ export async function pullPortalSource(options) {
|
|
|
405
592
|
if (sourceContext.sourceStorage !== 'nocobase') {
|
|
406
593
|
throw new Error(portalSourceText('errors.unsupportedSourceStorage', { sourceStorage: sourceContext.sourceStorage }, `Unsupported portal source storage: ${sourceContext.sourceStorage}`));
|
|
407
594
|
}
|
|
408
|
-
if (sourceContext.mode === 'local' || sourceContext.mode === 'docker') {
|
|
409
|
-
return {
|
|
410
|
-
...sourceContext,
|
|
411
|
-
changed: false,
|
|
412
|
-
noopReason: sourceContext.mode === 'local'
|
|
413
|
-
? portalSourceText('messages.localPullNoop', undefined, 'Portal source is already local.')
|
|
414
|
-
: portalSourceText('messages.dockerPullNoop', undefined, 'Portal source is already available through the Docker volume.'),
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
595
|
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'nocobase-cli-portal-pull-'));
|
|
418
596
|
const archivePath = path.join(tempDir, 'source.tar.gz');
|
|
419
597
|
const apiRequest = options.apiRequest ?? executeApiRequest;
|
|
@@ -437,7 +615,12 @@ export async function pullPortalSource(options) {
|
|
|
437
615
|
portalDir: sourceContext.portalDir,
|
|
438
616
|
force: options.force,
|
|
439
617
|
});
|
|
440
|
-
await
|
|
618
|
+
await ensurePortalBuildHtmlReadsEnvOnly(sourceContext.portalDir);
|
|
619
|
+
await updatePortalEnvFiles({
|
|
620
|
+
portalDir: sourceContext.portalDir,
|
|
621
|
+
apiBaseUrl: sourceContext.apiBaseUrl,
|
|
622
|
+
portalBase: sourceContext.portalBase,
|
|
623
|
+
});
|
|
441
624
|
const installResult = await installPortalDependencies({
|
|
442
625
|
portalDir: sourceContext.portalDir,
|
|
443
626
|
installDependencies: options.installDependencies,
|
|
@@ -454,19 +637,14 @@ export async function pullPortalSource(options) {
|
|
|
454
637
|
}
|
|
455
638
|
}
|
|
456
639
|
export async function pushPortalSource(options) {
|
|
457
|
-
const context = await resolvePortalSourceContext(
|
|
640
|
+
const context = await resolvePortalSourceContext({
|
|
641
|
+
...options,
|
|
642
|
+
defaultSourcePath: true,
|
|
643
|
+
});
|
|
458
644
|
if (!(await pathExists(context.portalDir))) {
|
|
459
645
|
throw new Error(portalSourceText('errors.workspaceMissing', { portalDir: context.portalDir, portal: context.portal }, `Portal does not exist: ${context.portalDir}\nRun \`nb portal create ${context.portal}\` first.`));
|
|
460
646
|
}
|
|
461
|
-
const portalConfig =
|
|
462
|
-
await syncPortalConfigToRemote({
|
|
463
|
-
portal: context.portal,
|
|
464
|
-
config: portalConfig,
|
|
465
|
-
currentOptions: context.options,
|
|
466
|
-
envName: options.envName,
|
|
467
|
-
cliVersion: options.cliVersion,
|
|
468
|
-
apiRequest: options.apiRequest,
|
|
469
|
-
});
|
|
647
|
+
const portalConfig = buildPortalConfigFromContext(context);
|
|
470
648
|
const sourceContext = applyPortalConfigToContext(context, portalConfig);
|
|
471
649
|
if (sourceContext.sourceStorage === 'git') {
|
|
472
650
|
const revision = await pushGitPortalSource({
|
|
@@ -485,15 +663,6 @@ export async function pushPortalSource(options) {
|
|
|
485
663
|
if (sourceContext.sourceStorage !== 'nocobase') {
|
|
486
664
|
throw new Error(portalSourceText('errors.unsupportedSourceStorage', { sourceStorage: sourceContext.sourceStorage }, `Unsupported portal source storage: ${sourceContext.sourceStorage}`));
|
|
487
665
|
}
|
|
488
|
-
if (sourceContext.mode === 'local' || sourceContext.mode === 'docker') {
|
|
489
|
-
return {
|
|
490
|
-
...sourceContext,
|
|
491
|
-
changed: false,
|
|
492
|
-
noopReason: sourceContext.mode === 'local'
|
|
493
|
-
? portalSourceText('messages.localPushNoop', undefined, 'Portal source is already local.')
|
|
494
|
-
: portalSourceText('messages.dockerPushNoop', undefined, 'Portal source is already available through the Docker volume.'),
|
|
495
|
-
};
|
|
496
|
-
}
|
|
497
666
|
const archive = await packPortalSource(sourceContext.portalDir);
|
|
498
667
|
const apiRequest = options.apiRequest ?? executeApiRequest;
|
|
499
668
|
try {
|
|
@@ -60,7 +60,7 @@ export function mergedBoolean(key, def, iv, useYesInitial) {
|
|
|
60
60
|
}
|
|
61
61
|
return def.initialValue ?? true;
|
|
62
62
|
}
|
|
63
|
-
export function mergedSelect(key, def, iv, useYesInitial) {
|
|
63
|
+
export function mergedSelect(key, def, iv, useYesInitial, valuesSoFar = {}) {
|
|
64
64
|
const enabledValueList = enabledSelectOptionValues(def.options);
|
|
65
65
|
if (hasIvKey(iv, key)) {
|
|
66
66
|
const s = String(iv[key]);
|
|
@@ -72,7 +72,7 @@ export function mergedSelect(key, def, iv, useYesInitial) {
|
|
|
72
72
|
if (useYesInitial && def.yesInitialValue !== undefined && enabledValueList.includes(def.yesInitialValue)) {
|
|
73
73
|
return def.yesInitialValue;
|
|
74
74
|
}
|
|
75
|
-
const d = def.initialValue;
|
|
75
|
+
const d = typeof def.initialValue === 'function' ? def.initialValue(valuesSoFar) : def.initialValue;
|
|
76
76
|
if (d !== undefined && enabledValueList.includes(d)) {
|
|
77
77
|
return d;
|
|
78
78
|
}
|