@nocobase/cli 3.0.0-alpha.4 → 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 +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/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-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 +17 -49
- package/dist/lib/portal-destroy.js +34 -20
- package/dist/lib/portal-dev.js +4 -5
- 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 +186 -54
- package/dist/lib/run-npm.js +46 -0
- package/dist/locale/en-US.json +45 -14
- package/dist/locale/zh-CN.json +45 -14
- package/package.json +2 -2
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,
|
|
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,12 +14,15 @@ 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,
|
|
25
|
+
import { resolvePnpmInstallCommand, run, runPnpmInstallCommand } from './run-npm.js';
|
|
23
26
|
const execFileAsync = promisify(execFile);
|
|
24
27
|
const portalSourceText = (key, values, fallback) => translateCli(`commands.portalSource.${key}`, values, { fallback });
|
|
25
28
|
const PULL_SOURCE_OPERATION = {
|
|
@@ -128,11 +131,25 @@ async function packPortalSource(portalDir) {
|
|
|
128
131
|
},
|
|
129
132
|
};
|
|
130
133
|
}
|
|
131
|
-
async function
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
async function replaceExistingPortalDirectory(params) {
|
|
135
|
+
if (!(await pathExists(path.join(params.portalDir, '.git')))) {
|
|
136
|
+
await rm(params.portalDir, { recursive: true, force: true });
|
|
137
|
+
await rename(params.sourceDir, params.portalDir);
|
|
138
|
+
return;
|
|
135
139
|
}
|
|
140
|
+
const existingEntries = await readdir(params.portalDir);
|
|
141
|
+
await Promise.all(existingEntries
|
|
142
|
+
.filter((entry) => entry !== '.git')
|
|
143
|
+
.map((entry) => rm(path.join(params.portalDir, entry), { recursive: true, force: true })));
|
|
144
|
+
const sourceEntries = await readdir(params.sourceDir);
|
|
145
|
+
await Promise.all(sourceEntries.map((entry) => rename(path.join(params.sourceDir, entry), path.join(params.portalDir, entry))));
|
|
146
|
+
await rm(params.sourceDir, { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
async function replacePortalSourceFromArchive(params) {
|
|
149
|
+
const targetExists = await assertPortalDirectoryCanBeReplaced({
|
|
150
|
+
portalDir: params.portalDir,
|
|
151
|
+
force: params.force,
|
|
152
|
+
});
|
|
136
153
|
const parentDir = path.dirname(params.portalDir);
|
|
137
154
|
const tempDir = await mkdtemp(path.join(parentDir, `.${path.basename(params.portalDir)}-pull-`));
|
|
138
155
|
try {
|
|
@@ -143,7 +160,11 @@ async function replacePortalSourceFromArchive(params) {
|
|
|
143
160
|
filter: validatePortalSourceTarEntry,
|
|
144
161
|
});
|
|
145
162
|
if (targetExists) {
|
|
146
|
-
await
|
|
163
|
+
await replaceExistingPortalDirectory({
|
|
164
|
+
sourceDir: tempDir,
|
|
165
|
+
portalDir: params.portalDir,
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
147
168
|
}
|
|
148
169
|
await rename(tempDir, params.portalDir);
|
|
149
170
|
}
|
|
@@ -153,10 +174,10 @@ async function replacePortalSourceFromArchive(params) {
|
|
|
153
174
|
}
|
|
154
175
|
}
|
|
155
176
|
async function replacePortalSourceFromDirectory(params) {
|
|
156
|
-
const targetExists = await
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
177
|
+
const targetExists = await assertPortalDirectoryCanBeReplaced({
|
|
178
|
+
portalDir: params.portalDir,
|
|
179
|
+
force: params.force,
|
|
180
|
+
});
|
|
160
181
|
const parentDir = path.dirname(params.portalDir);
|
|
161
182
|
const tempDir = await mkdtemp(path.join(parentDir, `.${path.basename(params.portalDir)}-pull-`));
|
|
162
183
|
try {
|
|
@@ -165,7 +186,11 @@ async function replacePortalSourceFromDirectory(params) {
|
|
|
165
186
|
filter: (source) => shouldPackPortalSourceEntry(path.relative(params.sourceDir, source)),
|
|
166
187
|
});
|
|
167
188
|
if (targetExists) {
|
|
168
|
-
await
|
|
189
|
+
await replaceExistingPortalDirectory({
|
|
190
|
+
sourceDir: tempDir,
|
|
191
|
+
portalDir: params.portalDir,
|
|
192
|
+
});
|
|
193
|
+
return;
|
|
169
194
|
}
|
|
170
195
|
await rename(tempDir, params.portalDir);
|
|
171
196
|
}
|
|
@@ -174,6 +199,16 @@ async function replacePortalSourceFromDirectory(params) {
|
|
|
174
199
|
throw error;
|
|
175
200
|
}
|
|
176
201
|
}
|
|
202
|
+
async function assertPortalDirectoryCanBeReplaced(params) {
|
|
203
|
+
const targetExists = await pathExists(params.portalDir);
|
|
204
|
+
if (targetExists && !params.force) {
|
|
205
|
+
throw new Error(portalSourceText('errors.workspaceExists', { portalDir: params.portalDir }, `Portal already exists: ${params.portalDir}\nPass --force to delete it and pull again.`));
|
|
206
|
+
}
|
|
207
|
+
if (targetExists && params.force && !(await canReplacePortalDirectory(params.portalDir))) {
|
|
208
|
+
throw new Error(portalSourceText('errors.workspaceNotReplaceable', { portalDir: params.portalDir }, `Refusing to replace a non-portal directory: ${params.portalDir}`));
|
|
209
|
+
}
|
|
210
|
+
return targetExists;
|
|
211
|
+
}
|
|
177
212
|
async function runGit(args, cwd) {
|
|
178
213
|
return await execFileAsync('git', args, {
|
|
179
214
|
cwd,
|
|
@@ -185,24 +220,37 @@ async function installPortalDependencies(params) {
|
|
|
185
220
|
return {
|
|
186
221
|
dependenciesInstalled: false,
|
|
187
222
|
installSkipped: true,
|
|
223
|
+
installFailed: false,
|
|
188
224
|
};
|
|
189
225
|
}
|
|
190
226
|
if (!(await isFile(path.join(params.portalDir, 'package.json')))) {
|
|
191
227
|
return {
|
|
192
228
|
dependenciesInstalled: false,
|
|
193
229
|
installSkipped: true,
|
|
230
|
+
installFailed: false,
|
|
194
231
|
};
|
|
195
232
|
}
|
|
196
233
|
const runCommand = params.runCommand ?? run;
|
|
197
|
-
await
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
234
|
+
const installCommand = await resolvePnpmInstallCommand(params.portalDir);
|
|
235
|
+
try {
|
|
236
|
+
await runPnpmInstallCommand(runCommand, installCommand.args, {
|
|
237
|
+
cwd: params.portalDir,
|
|
238
|
+
env: buildPortalCommandEnv(),
|
|
239
|
+
envMode: 'replace',
|
|
240
|
+
errorName: installCommand.errorName,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return {
|
|
245
|
+
dependenciesInstalled: false,
|
|
246
|
+
installSkipped: false,
|
|
247
|
+
installFailed: true,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
203
250
|
return {
|
|
204
251
|
dependenciesInstalled: true,
|
|
205
252
|
installSkipped: false,
|
|
253
|
+
installFailed: false,
|
|
206
254
|
};
|
|
207
255
|
}
|
|
208
256
|
function readSourceRevision(data) {
|
|
@@ -219,10 +267,15 @@ async function resolvePortalSourceContext(options) {
|
|
|
219
267
|
const portal = validatePortalSlug(options.portal);
|
|
220
268
|
const apiBaseUrl = trimValue(options.env.apiBaseUrl);
|
|
221
269
|
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
270
|
const mode = options.env.kind;
|
|
271
|
+
const appContext = await resolvePortalAppContext(options);
|
|
272
|
+
const { app, appPublicPath, portalBaseApp } = appContext;
|
|
273
|
+
const portalDeployDir = resolvePortalDeployPath({ storagePath, app, portal });
|
|
274
|
+
const portalDir = options.sourcePath
|
|
275
|
+
? resolvePortalSourcePath(portal, options.sourcePath)
|
|
276
|
+
: resolveSavedPortalSourcePath(options.env, portal) ??
|
|
277
|
+
(options.defaultSourcePath ? resolvePortalSourcePath(portal) : portalDeployDir);
|
|
278
|
+
const portalBase = buildPortalBasePath({ app: portalBaseApp ?? app, appPublicPath, portal });
|
|
226
279
|
if (mode !== 'local' && mode !== 'docker' && mode !== 'http') {
|
|
227
280
|
throw new Error(portalSourceText('errors.unsupportedEnvKind', { kind: mode }, `Cannot sync portal source for ${mode} envs in the first version.`));
|
|
228
281
|
}
|
|
@@ -231,6 +284,7 @@ async function resolvePortalSourceContext(options) {
|
|
|
231
284
|
envName: options.envName,
|
|
232
285
|
cliVersion: options.cliVersion,
|
|
233
286
|
apiRequest: options.apiRequest,
|
|
287
|
+
appContext,
|
|
234
288
|
});
|
|
235
289
|
const item = findPortalListItem(list.items, portal);
|
|
236
290
|
if (!item) {
|
|
@@ -241,6 +295,7 @@ async function resolvePortalSourceContext(options) {
|
|
|
241
295
|
portal,
|
|
242
296
|
portalDir,
|
|
243
297
|
portalBase,
|
|
298
|
+
apiBaseUrl,
|
|
244
299
|
mode,
|
|
245
300
|
sourceStorage: item.sourceStorage || 'nocobase',
|
|
246
301
|
gitRepo: item.gitRepo,
|
|
@@ -271,6 +326,25 @@ function applyPortalConfigToContext(context, config) {
|
|
|
271
326
|
gitPath: config.git?.path ?? DEFAULT_PORTAL_GIT_PATH,
|
|
272
327
|
};
|
|
273
328
|
}
|
|
329
|
+
function getTemporaryGitPortalConfig(options) {
|
|
330
|
+
const gitRepo = trimValue(options.gitRepo);
|
|
331
|
+
if (!gitRepo) {
|
|
332
|
+
if (trimValue(options.gitBranch) || trimValue(options.gitPath)) {
|
|
333
|
+
throw new Error(portalSourceText('errors.gitRepoRequiredForTemporaryPull', undefined, [
|
|
334
|
+
'--git-branch and --git-path require --git-repo for a temporary Git pull.',
|
|
335
|
+
'To update the portal configuration, use `nb portal config`.',
|
|
336
|
+
].join(' ')));
|
|
337
|
+
}
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
return buildPortalConfig({
|
|
341
|
+
portal: options.portal,
|
|
342
|
+
sourceStorage: 'git',
|
|
343
|
+
gitRepo,
|
|
344
|
+
gitBranch: options.gitBranch,
|
|
345
|
+
gitPath: options.gitPath,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
274
348
|
function assertGitSourceConfig(context) {
|
|
275
349
|
if (!context.gitRepo) {
|
|
276
350
|
throw new Error(portalSourceText('errors.gitRepoMissing', { portal: context.portal }, `Portal "${context.portal}" uses Git source storage, but gitRepo is missing.`));
|
|
@@ -319,8 +393,76 @@ async function cloneGitSource(params) {
|
|
|
319
393
|
}
|
|
320
394
|
return repoDir;
|
|
321
395
|
}
|
|
396
|
+
async function setGitOriginRepository(params) {
|
|
397
|
+
try {
|
|
398
|
+
await runGit(['remote', 'get-url', 'origin'], params.repoDir);
|
|
399
|
+
await runGit(['remote', 'set-url', 'origin', params.repo], params.repoDir);
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
await runGit(['remote', 'add', 'origin', params.repo], params.repoDir);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
async function checkoutExistingGitRepository(params) {
|
|
406
|
+
await setGitOriginRepository({
|
|
407
|
+
repoDir: params.repoDir,
|
|
408
|
+
repo: params.repo,
|
|
409
|
+
});
|
|
410
|
+
try {
|
|
411
|
+
await runGit(['fetch', 'origin', params.branch], params.repoDir);
|
|
412
|
+
await runGit(['checkout', '-f', '-B', params.branch, 'FETCH_HEAD'], params.repoDir);
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
await runGit(['fetch', 'origin'], params.repoDir);
|
|
416
|
+
await runGit(['checkout', '-f', '-B', params.branch], params.repoDir);
|
|
417
|
+
}
|
|
418
|
+
await runGit(['clean', '-fdx'], params.repoDir);
|
|
419
|
+
}
|
|
420
|
+
async function pullGitRepositoryRootPortalSource(params) {
|
|
421
|
+
const targetExists = await assertPortalDirectoryCanBeReplaced({
|
|
422
|
+
portalDir: params.context.portalDir,
|
|
423
|
+
force: params.force,
|
|
424
|
+
});
|
|
425
|
+
await mkdir(path.dirname(params.context.portalDir), { recursive: true });
|
|
426
|
+
if (targetExists && (await pathExists(path.join(params.context.portalDir, '.git')))) {
|
|
427
|
+
await checkoutExistingGitRepository({
|
|
428
|
+
repoDir: params.context.portalDir,
|
|
429
|
+
repo: params.repo,
|
|
430
|
+
branch: params.branch,
|
|
431
|
+
});
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (targetExists) {
|
|
435
|
+
await rm(params.context.portalDir, { recursive: true, force: true });
|
|
436
|
+
}
|
|
437
|
+
const tempDir = await mkdtemp(path.join(path.dirname(params.context.portalDir), `.${path.basename(params.context.portalDir)}-git-pull-`));
|
|
438
|
+
try {
|
|
439
|
+
const repoDir = await cloneGitSource({
|
|
440
|
+
repo: params.repo,
|
|
441
|
+
branch: params.branch,
|
|
442
|
+
cwd: tempDir,
|
|
443
|
+
createBranch: true,
|
|
444
|
+
});
|
|
445
|
+
await rename(repoDir, params.context.portalDir);
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
await rm(params.context.portalDir, { recursive: true, force: true });
|
|
449
|
+
throw error;
|
|
450
|
+
}
|
|
451
|
+
finally {
|
|
452
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
453
|
+
}
|
|
454
|
+
}
|
|
322
455
|
async function pullGitPortalSource(params) {
|
|
323
456
|
const git = assertGitSourceConfig(params.context);
|
|
457
|
+
if (isGitRepositoryRootPath(git.gitPath)) {
|
|
458
|
+
await pullGitRepositoryRootPortalSource({
|
|
459
|
+
context: params.context,
|
|
460
|
+
repo: git.repo,
|
|
461
|
+
branch: git.branch,
|
|
462
|
+
force: params.force,
|
|
463
|
+
});
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
324
466
|
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'nocobase-cli-portal-git-pull-'));
|
|
325
467
|
try {
|
|
326
468
|
const repoDir = await cloneGitSource({
|
|
@@ -382,15 +524,23 @@ async function pushGitPortalSource(params) {
|
|
|
382
524
|
}
|
|
383
525
|
}
|
|
384
526
|
export async function pullPortalSource(options) {
|
|
385
|
-
const context = await resolvePortalSourceContext(
|
|
386
|
-
|
|
527
|
+
const context = await resolvePortalSourceContext({
|
|
528
|
+
...options,
|
|
529
|
+
defaultSourcePath: true,
|
|
530
|
+
});
|
|
531
|
+
const portalConfig = getTemporaryGitPortalConfig(options) ?? buildPortalConfigFromContext(context);
|
|
387
532
|
const sourceContext = applyPortalConfigToContext(context, portalConfig);
|
|
388
533
|
if (sourceContext.sourceStorage === 'git') {
|
|
389
534
|
await pullGitPortalSource({
|
|
390
535
|
context: sourceContext,
|
|
391
536
|
force: options.force,
|
|
392
537
|
});
|
|
393
|
-
await
|
|
538
|
+
await ensurePortalBuildHtmlReadsEnvOnly(sourceContext.portalDir);
|
|
539
|
+
await updatePortalEnvFiles({
|
|
540
|
+
portalDir: sourceContext.portalDir,
|
|
541
|
+
apiBaseUrl: sourceContext.apiBaseUrl,
|
|
542
|
+
portalBase: sourceContext.portalBase,
|
|
543
|
+
});
|
|
394
544
|
const installResult = await installPortalDependencies({
|
|
395
545
|
portalDir: sourceContext.portalDir,
|
|
396
546
|
installDependencies: options.installDependencies,
|
|
@@ -405,15 +555,6 @@ export async function pullPortalSource(options) {
|
|
|
405
555
|
if (sourceContext.sourceStorage !== 'nocobase') {
|
|
406
556
|
throw new Error(portalSourceText('errors.unsupportedSourceStorage', { sourceStorage: sourceContext.sourceStorage }, `Unsupported portal source storage: ${sourceContext.sourceStorage}`));
|
|
407
557
|
}
|
|
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
558
|
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'nocobase-cli-portal-pull-'));
|
|
418
559
|
const archivePath = path.join(tempDir, 'source.tar.gz');
|
|
419
560
|
const apiRequest = options.apiRequest ?? executeApiRequest;
|
|
@@ -437,7 +578,12 @@ export async function pullPortalSource(options) {
|
|
|
437
578
|
portalDir: sourceContext.portalDir,
|
|
438
579
|
force: options.force,
|
|
439
580
|
});
|
|
440
|
-
await
|
|
581
|
+
await ensurePortalBuildHtmlReadsEnvOnly(sourceContext.portalDir);
|
|
582
|
+
await updatePortalEnvFiles({
|
|
583
|
+
portalDir: sourceContext.portalDir,
|
|
584
|
+
apiBaseUrl: sourceContext.apiBaseUrl,
|
|
585
|
+
portalBase: sourceContext.portalBase,
|
|
586
|
+
});
|
|
441
587
|
const installResult = await installPortalDependencies({
|
|
442
588
|
portalDir: sourceContext.portalDir,
|
|
443
589
|
installDependencies: options.installDependencies,
|
|
@@ -454,19 +600,14 @@ export async function pullPortalSource(options) {
|
|
|
454
600
|
}
|
|
455
601
|
}
|
|
456
602
|
export async function pushPortalSource(options) {
|
|
457
|
-
const context = await resolvePortalSourceContext(
|
|
603
|
+
const context = await resolvePortalSourceContext({
|
|
604
|
+
...options,
|
|
605
|
+
defaultSourcePath: true,
|
|
606
|
+
});
|
|
458
607
|
if (!(await pathExists(context.portalDir))) {
|
|
459
608
|
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
609
|
}
|
|
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
|
-
});
|
|
610
|
+
const portalConfig = buildPortalConfigFromContext(context);
|
|
470
611
|
const sourceContext = applyPortalConfigToContext(context, portalConfig);
|
|
471
612
|
if (sourceContext.sourceStorage === 'git') {
|
|
472
613
|
const revision = await pushGitPortalSource({
|
|
@@ -485,15 +626,6 @@ export async function pushPortalSource(options) {
|
|
|
485
626
|
if (sourceContext.sourceStorage !== 'nocobase') {
|
|
486
627
|
throw new Error(portalSourceText('errors.unsupportedSourceStorage', { sourceStorage: sourceContext.sourceStorage }, `Unsupported portal source storage: ${sourceContext.sourceStorage}`));
|
|
487
628
|
}
|
|
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
629
|
const archive = await packPortalSource(sourceContext.portalDir);
|
|
498
630
|
const apiRequest = options.apiRequest ?? executeApiRequest;
|
|
499
631
|
try {
|
package/dist/lib/run-npm.js
CHANGED
|
@@ -105,6 +105,52 @@ export async function runPnpmCommand(runCommand, args, options) {
|
|
|
105
105
|
throw createMissingCommandError('pnpm', options.errorName ?? `pnpm ${args.join(' ')}`.trim(), error) ?? error;
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
|
+
function createInstallArgsWithoutTrustLockfile(args) {
|
|
109
|
+
if (!args.includes('install') || !args.includes('--trust-lockfile')) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
return args.filter((arg) => arg !== '--trust-lockfile');
|
|
113
|
+
}
|
|
114
|
+
function isFriendlyMissingPnpmError(error) {
|
|
115
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
116
|
+
return message.includes('because the pnpm executable could not be found');
|
|
117
|
+
}
|
|
118
|
+
function formatPnpmLabel(args) {
|
|
119
|
+
return `pnpm ${args.join(' ')}`.trim();
|
|
120
|
+
}
|
|
121
|
+
export async function resolvePnpmInstallCommand(cwd) {
|
|
122
|
+
const hasLockfile = await fsp
|
|
123
|
+
.stat(path.join(cwd, 'pnpm-lock.yaml'))
|
|
124
|
+
.then((stats) => stats.isFile())
|
|
125
|
+
.catch(() => false);
|
|
126
|
+
if (hasLockfile) {
|
|
127
|
+
const args = ['install', '--frozen-lockfile', '--trust-lockfile'];
|
|
128
|
+
return {
|
|
129
|
+
args,
|
|
130
|
+
errorName: formatPnpmLabel(args),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const args = ['install'];
|
|
134
|
+
return {
|
|
135
|
+
args,
|
|
136
|
+
errorName: formatPnpmLabel(args),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
export async function runPnpmInstallCommand(runCommand, args, options) {
|
|
140
|
+
try {
|
|
141
|
+
await runPnpmCommand(runCommand, args, options);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
const fallbackArgs = createInstallArgsWithoutTrustLockfile(args);
|
|
145
|
+
if (!fallbackArgs || isFriendlyMissingPnpmError(error)) {
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
await runPnpmCommand(runCommand, fallbackArgs, {
|
|
149
|
+
...options,
|
|
150
|
+
errorName: formatPnpmLabel(fallbackArgs),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
108
154
|
function isDockerDaemonUnavailableError(error) {
|
|
109
155
|
const message = error instanceof Error ? error.message : String(error);
|
|
110
156
|
return DOCKER_DAEMON_UNAVAILABLE_PATTERNS.some((pattern) => pattern.test(message));
|