@nocobase/cli 2.3.0-alpha.1 → 3.0.0-alpha.1
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/assets/env-proxy/nginx/snippets/uploads-location.conf +4 -1
- package/dist/commands/config/set.js +1 -0
- package/dist/commands/init.js +13 -5
- package/dist/commands/install.js +104 -3
- package/dist/commands/portal/config.js +88 -0
- package/dist/commands/portal/create.js +105 -0
- package/dist/commands/portal/deploy.js +81 -0
- package/dist/commands/portal/destroy.js +104 -0
- package/dist/commands/portal/dev.js +71 -0
- package/dist/commands/portal/index.js +20 -0
- package/dist/commands/portal/info.js +82 -0
- package/dist/commands/portal/list.js +98 -0
- package/dist/commands/portal/pull.js +84 -0
- package/dist/commands/portal/push.js +79 -0
- package/dist/commands/source/dev.js +1 -1
- package/dist/lib/api-client.js +7 -0
- package/dist/lib/auth-store.js +3 -1
- package/dist/lib/cli-config.js +20 -1
- package/dist/lib/env-auth.js +2 -2
- package/dist/lib/env-config.js +3 -0
- package/dist/lib/env-proxy.js +141 -8
- package/dist/lib/managed-env-file.js +58 -2
- package/dist/lib/managed-init-env.js +6 -1
- package/dist/lib/naming.js +9 -0
- package/dist/lib/portal-command-env.js +31 -0
- package/dist/lib/portal-config.js +133 -0
- package/dist/lib/portal-configure.js +117 -0
- package/dist/lib/portal-create.js +433 -0
- package/dist/lib/portal-deploy.js +283 -0
- package/dist/lib/portal-destroy.js +100 -0
- package/dist/lib/portal-dev.js +79 -0
- package/dist/lib/portal-env-files.js +53 -0
- package/dist/lib/portal-info.js +31 -0
- package/dist/lib/portal-list.js +211 -0
- package/dist/lib/portal-source.js +523 -0
- package/dist/lib/proxy-caddy.js +2 -0
- package/dist/lib/proxy-nginx.js +1 -0
- package/dist/lib/run-npm.js +17 -16
- package/dist/lib/ui.js +28 -1
- package/dist/locale/en-US.json +178 -0
- package/dist/locale/zh-CN.json +178 -0
- package/nocobase-ctl.config.json +111 -0
- package/package.json +5 -2
|
@@ -0,0 +1,523 @@
|
|
|
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 { execFile } from 'node:child_process';
|
|
10
|
+
import { promisify } from 'node:util';
|
|
11
|
+
import { cp, mkdir, mkdtemp, readdir, rename, rm, stat } from 'node:fs/promises';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import * as tar from 'tar';
|
|
15
|
+
import { executeApiRequest } from './api-client.js';
|
|
16
|
+
import { translateCli } from './cli-locale.js';
|
|
17
|
+
import { buildPortalConfig, buildPortalConfigFromOptions, DEFAULT_PORTAL_GIT_PATH, readPortalConfig, syncPortalConfigToRemote, writePortalConfig, } from './portal-config.js';
|
|
18
|
+
import { buildPortalCommandEnv } from './portal-command-env.js';
|
|
19
|
+
import { buildPortalBasePath, resolvePortalAppFromApiBaseUrl, resolvePortalStoragePath, validatePortalSlug, } from './portal-create.js';
|
|
20
|
+
import { listPortalWorkspaces } from './portal-list.js';
|
|
21
|
+
import { findPortalListItem } from './portal-info.js';
|
|
22
|
+
import { run } from './run-npm.js';
|
|
23
|
+
const execFileAsync = promisify(execFile);
|
|
24
|
+
const portalSourceText = (key, values, fallback) => translateCli(`commands.portalSource.${key}`, values, { fallback });
|
|
25
|
+
const PULL_SOURCE_OPERATION = {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
pathTemplate: '/multiPortals:pullSource',
|
|
28
|
+
hasBody: true,
|
|
29
|
+
bodyRequired: true,
|
|
30
|
+
responseType: 'binary',
|
|
31
|
+
parameters: [
|
|
32
|
+
{
|
|
33
|
+
name: 'app',
|
|
34
|
+
flagName: 'app',
|
|
35
|
+
in: 'body',
|
|
36
|
+
required: true,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'portal',
|
|
40
|
+
flagName: 'portal',
|
|
41
|
+
in: 'body',
|
|
42
|
+
required: true,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
const PUSH_SOURCE_OPERATION = {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
pathTemplate: '/multiPortals:pushSource',
|
|
49
|
+
requestContentType: 'multipart/form-data',
|
|
50
|
+
hasBody: true,
|
|
51
|
+
bodyRequired: true,
|
|
52
|
+
parameters: [
|
|
53
|
+
{
|
|
54
|
+
name: 'file',
|
|
55
|
+
flagName: 'file',
|
|
56
|
+
in: 'body',
|
|
57
|
+
required: true,
|
|
58
|
+
isFile: true,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'app',
|
|
62
|
+
flagName: 'app',
|
|
63
|
+
in: 'body',
|
|
64
|
+
required: true,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'portal',
|
|
68
|
+
flagName: 'portal',
|
|
69
|
+
in: 'body',
|
|
70
|
+
required: true,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'message',
|
|
74
|
+
flagName: 'message',
|
|
75
|
+
in: 'body',
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
function trimValue(value) {
|
|
80
|
+
return String(value ?? '').trim();
|
|
81
|
+
}
|
|
82
|
+
async function pathExists(target) {
|
|
83
|
+
try {
|
|
84
|
+
await stat(target);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function isFile(target) {
|
|
92
|
+
try {
|
|
93
|
+
return (await stat(target)).isFile();
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function shouldPackPortalSourceEntry(entryName) {
|
|
100
|
+
return !entryName
|
|
101
|
+
.split('/')
|
|
102
|
+
.some((segment) => segment.startsWith('._') || ['.git', 'node_modules', 'dist', '.DS_Store'].includes(segment));
|
|
103
|
+
}
|
|
104
|
+
function validatePortalSourceTarEntry(entryPath, entry) {
|
|
105
|
+
if (path.isAbsolute(entryPath) || entryPath.split(/[\\/]+/).includes('..')) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
const tarEntry = entry;
|
|
109
|
+
if (tarEntry.type === 'SymbolicLink' || tarEntry.type === 'Link' || typeof tarEntry.linkpath === 'string') {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
return shouldPackPortalSourceEntry(entryPath);
|
|
113
|
+
}
|
|
114
|
+
async function packPortalSource(portalDir) {
|
|
115
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'nocobase-cli-portal-source-'));
|
|
116
|
+
const archivePath = path.join(tempDir, 'source.tar.gz');
|
|
117
|
+
const entries = (await readdir(portalDir)).filter(shouldPackPortalSourceEntry);
|
|
118
|
+
await tar.create({
|
|
119
|
+
cwd: portalDir,
|
|
120
|
+
file: archivePath,
|
|
121
|
+
gzip: true,
|
|
122
|
+
filter: (entryPath, entry) => validatePortalSourceTarEntry(entryPath, entry),
|
|
123
|
+
}, entries);
|
|
124
|
+
return {
|
|
125
|
+
archivePath,
|
|
126
|
+
cleanup: async () => {
|
|
127
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
async function replacePortalSourceFromArchive(params) {
|
|
132
|
+
const targetExists = await pathExists(params.portalDir);
|
|
133
|
+
if (targetExists && !params.force) {
|
|
134
|
+
throw new Error(portalSourceText('errors.workspaceExists', { portalDir: params.portalDir }, `Portal already exists: ${params.portalDir}\nPass --force to delete it and pull again.`));
|
|
135
|
+
}
|
|
136
|
+
const parentDir = path.dirname(params.portalDir);
|
|
137
|
+
const tempDir = await mkdtemp(path.join(parentDir, `.${path.basename(params.portalDir)}-pull-`));
|
|
138
|
+
try {
|
|
139
|
+
await tar.extract({
|
|
140
|
+
file: params.archivePath,
|
|
141
|
+
cwd: tempDir,
|
|
142
|
+
strict: true,
|
|
143
|
+
filter: validatePortalSourceTarEntry,
|
|
144
|
+
});
|
|
145
|
+
if (targetExists) {
|
|
146
|
+
await rm(params.portalDir, { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
await rename(tempDir, params.portalDir);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function replacePortalSourceFromDirectory(params) {
|
|
156
|
+
const targetExists = await pathExists(params.portalDir);
|
|
157
|
+
if (targetExists && !params.force) {
|
|
158
|
+
throw new Error(portalSourceText('errors.workspaceExists', { portalDir: params.portalDir }, `Portal already exists: ${params.portalDir}\nPass --force to delete it and pull again.`));
|
|
159
|
+
}
|
|
160
|
+
const parentDir = path.dirname(params.portalDir);
|
|
161
|
+
const tempDir = await mkdtemp(path.join(parentDir, `.${path.basename(params.portalDir)}-pull-`));
|
|
162
|
+
try {
|
|
163
|
+
await cp(params.sourceDir, tempDir, {
|
|
164
|
+
recursive: true,
|
|
165
|
+
filter: (source) => shouldPackPortalSourceEntry(path.relative(params.sourceDir, source)),
|
|
166
|
+
});
|
|
167
|
+
if (targetExists) {
|
|
168
|
+
await rm(params.portalDir, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
await rename(tempDir, params.portalDir);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function runGit(args, cwd) {
|
|
178
|
+
return await execFileAsync('git', args, {
|
|
179
|
+
cwd,
|
|
180
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
async function installPortalDependencies(params) {
|
|
184
|
+
if (params.installDependencies === false) {
|
|
185
|
+
return {
|
|
186
|
+
dependenciesInstalled: false,
|
|
187
|
+
installSkipped: true,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (!(await isFile(path.join(params.portalDir, 'package.json')))) {
|
|
191
|
+
return {
|
|
192
|
+
dependenciesInstalled: false,
|
|
193
|
+
installSkipped: true,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const runCommand = params.runCommand ?? run;
|
|
197
|
+
await runCommand('pnpm', ['install'], {
|
|
198
|
+
cwd: params.portalDir,
|
|
199
|
+
env: buildPortalCommandEnv(),
|
|
200
|
+
envMode: 'replace',
|
|
201
|
+
errorName: 'pnpm install',
|
|
202
|
+
});
|
|
203
|
+
return {
|
|
204
|
+
dependenciesInstalled: true,
|
|
205
|
+
installSkipped: false,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function readSourceRevision(data) {
|
|
209
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
const direct = data.sourceRevision;
|
|
213
|
+
if (typeof direct === 'string' && direct.trim()) {
|
|
214
|
+
return direct;
|
|
215
|
+
}
|
|
216
|
+
return readSourceRevision(data.data);
|
|
217
|
+
}
|
|
218
|
+
async function resolvePortalSourceContext(options) {
|
|
219
|
+
const portal = validatePortalSlug(options.portal);
|
|
220
|
+
const apiBaseUrl = trimValue(options.env.apiBaseUrl);
|
|
221
|
+
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
|
+
const mode = options.env.kind;
|
|
226
|
+
if (mode !== 'local' && mode !== 'docker' && mode !== 'http') {
|
|
227
|
+
throw new Error(portalSourceText('errors.unsupportedEnvKind', { kind: mode }, `Cannot sync portal source for ${mode} envs in the first version.`));
|
|
228
|
+
}
|
|
229
|
+
const list = await listPortalWorkspaces({
|
|
230
|
+
env: options.env,
|
|
231
|
+
envName: options.envName,
|
|
232
|
+
cliVersion: options.cliVersion,
|
|
233
|
+
apiRequest: options.apiRequest,
|
|
234
|
+
});
|
|
235
|
+
const item = findPortalListItem(list.items, portal);
|
|
236
|
+
if (!item) {
|
|
237
|
+
throw new Error(portalSourceText('errors.notFound', { portal }, `Portal "${portal}" was not found. Run \`nb portal list\` to see available portals.`));
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
app,
|
|
241
|
+
portal,
|
|
242
|
+
portalDir,
|
|
243
|
+
portalBase,
|
|
244
|
+
mode,
|
|
245
|
+
sourceStorage: item.sourceStorage || 'nocobase',
|
|
246
|
+
gitRepo: item.gitRepo,
|
|
247
|
+
gitBranch: item.gitBranch || 'main',
|
|
248
|
+
gitPath: item.gitPath || DEFAULT_PORTAL_GIT_PATH,
|
|
249
|
+
options: item.options,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function buildPortalConfigFromContext(context) {
|
|
253
|
+
if (Object.keys(context.options).length > 0) {
|
|
254
|
+
return buildPortalConfigFromOptions(context.options, context.portal);
|
|
255
|
+
}
|
|
256
|
+
const sourceStorage = context.sourceStorage || 'nocobase';
|
|
257
|
+
return buildPortalConfig({
|
|
258
|
+
portal: context.portal,
|
|
259
|
+
sourceStorage,
|
|
260
|
+
gitRepo: sourceStorage === 'git' ? context.gitRepo : undefined,
|
|
261
|
+
gitBranch: sourceStorage === 'git' ? context.gitBranch : undefined,
|
|
262
|
+
gitPath: sourceStorage === 'git' ? context.gitPath : undefined,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
function applyPortalConfigToContext(context, config) {
|
|
266
|
+
return {
|
|
267
|
+
...context,
|
|
268
|
+
sourceStorage: config.sourceStorage,
|
|
269
|
+
gitRepo: config.git?.repo ?? '',
|
|
270
|
+
gitBranch: config.git?.branch ?? 'main',
|
|
271
|
+
gitPath: config.git?.path ?? DEFAULT_PORTAL_GIT_PATH,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function assertGitSourceConfig(context) {
|
|
275
|
+
if (!context.gitRepo) {
|
|
276
|
+
throw new Error(portalSourceText('errors.gitRepoMissing', { portal: context.portal }, `Portal "${context.portal}" uses Git source storage, but gitRepo is missing.`));
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
repo: context.gitRepo,
|
|
280
|
+
branch: context.gitBranch || 'main',
|
|
281
|
+
gitPath: context.gitPath || DEFAULT_PORTAL_GIT_PATH,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function isGitRepositoryRootPath(gitPath) {
|
|
285
|
+
return gitPath === DEFAULT_PORTAL_GIT_PATH;
|
|
286
|
+
}
|
|
287
|
+
async function copyPortalSourceToGitPath(params) {
|
|
288
|
+
if (isGitRepositoryRootPath(params.gitPath)) {
|
|
289
|
+
const existingEntries = await readdir(params.repoDir);
|
|
290
|
+
await Promise.all(existingEntries
|
|
291
|
+
.filter((entry) => entry !== '.git')
|
|
292
|
+
.map((entry) => rm(path.join(params.repoDir, entry), { recursive: true, force: true })));
|
|
293
|
+
const sourceEntries = (await readdir(params.portalDir)).filter(shouldPackPortalSourceEntry);
|
|
294
|
+
await Promise.all(sourceEntries.map((entry) => cp(path.join(params.portalDir, entry), path.join(params.repoDir, entry), {
|
|
295
|
+
recursive: true,
|
|
296
|
+
filter: (source) => shouldPackPortalSourceEntry(path.relative(params.portalDir, source)),
|
|
297
|
+
})));
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const targetDir = path.join(params.repoDir, params.gitPath);
|
|
301
|
+
await rm(targetDir, { recursive: true, force: true });
|
|
302
|
+
await mkdir(path.dirname(targetDir), { recursive: true });
|
|
303
|
+
await cp(params.portalDir, targetDir, {
|
|
304
|
+
recursive: true,
|
|
305
|
+
filter: (source) => shouldPackPortalSourceEntry(path.relative(params.portalDir, source)),
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
async function cloneGitSource(params) {
|
|
309
|
+
const repoDir = path.join(params.cwd, 'repo');
|
|
310
|
+
try {
|
|
311
|
+
await runGit(['clone', '--branch', params.branch, params.repo, repoDir]);
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
if (!params.createBranch) {
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
await runGit(['clone', params.repo, repoDir]);
|
|
318
|
+
await runGit(['checkout', '-B', params.branch], repoDir);
|
|
319
|
+
}
|
|
320
|
+
return repoDir;
|
|
321
|
+
}
|
|
322
|
+
async function pullGitPortalSource(params) {
|
|
323
|
+
const git = assertGitSourceConfig(params.context);
|
|
324
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'nocobase-cli-portal-git-pull-'));
|
|
325
|
+
try {
|
|
326
|
+
const repoDir = await cloneGitSource({
|
|
327
|
+
repo: git.repo,
|
|
328
|
+
branch: git.branch,
|
|
329
|
+
cwd: tempDir,
|
|
330
|
+
createBranch: true,
|
|
331
|
+
});
|
|
332
|
+
const sourceDir = path.join(repoDir, git.gitPath);
|
|
333
|
+
if (!(await pathExists(sourceDir))) {
|
|
334
|
+
throw new Error(portalSourceText('errors.gitPathMissing', { gitPath: git.gitPath }, `Git path does not exist in the configured repository: ${git.gitPath}`));
|
|
335
|
+
}
|
|
336
|
+
await mkdir(path.dirname(params.context.portalDir), { recursive: true });
|
|
337
|
+
await replacePortalSourceFromDirectory({
|
|
338
|
+
sourceDir,
|
|
339
|
+
portalDir: params.context.portalDir,
|
|
340
|
+
force: params.force,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
finally {
|
|
344
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
async function pushGitPortalSource(params) {
|
|
348
|
+
const git = assertGitSourceConfig(params.context);
|
|
349
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'nocobase-cli-portal-git-push-'));
|
|
350
|
+
try {
|
|
351
|
+
const repoDir = await cloneGitSource({
|
|
352
|
+
repo: git.repo,
|
|
353
|
+
branch: git.branch,
|
|
354
|
+
cwd: tempDir,
|
|
355
|
+
createBranch: true,
|
|
356
|
+
});
|
|
357
|
+
await copyPortalSourceToGitPath({
|
|
358
|
+
portalDir: params.context.portalDir,
|
|
359
|
+
repoDir,
|
|
360
|
+
gitPath: git.gitPath,
|
|
361
|
+
});
|
|
362
|
+
await runGit(['add', git.gitPath], repoDir);
|
|
363
|
+
const status = await runGit(['status', '--porcelain', '--', git.gitPath], repoDir);
|
|
364
|
+
if (!status.stdout.trim()) {
|
|
365
|
+
return undefined;
|
|
366
|
+
}
|
|
367
|
+
await runGit([
|
|
368
|
+
'-c',
|
|
369
|
+
'user.name=NocoBase CLI',
|
|
370
|
+
'-c',
|
|
371
|
+
'user.email=nocobase-cli@localhost',
|
|
372
|
+
'commit',
|
|
373
|
+
'-m',
|
|
374
|
+
trimValue(params.message) || `chore(portal): update ${params.context.portal}`,
|
|
375
|
+
], repoDir);
|
|
376
|
+
await runGit(['push', 'origin', git.branch], repoDir);
|
|
377
|
+
const revision = await runGit(['rev-parse', 'HEAD'], repoDir);
|
|
378
|
+
return revision.stdout.trim();
|
|
379
|
+
}
|
|
380
|
+
finally {
|
|
381
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
export async function pullPortalSource(options) {
|
|
385
|
+
const context = await resolvePortalSourceContext(options);
|
|
386
|
+
const portalConfig = buildPortalConfigFromContext(context);
|
|
387
|
+
const sourceContext = applyPortalConfigToContext(context, portalConfig);
|
|
388
|
+
if (sourceContext.sourceStorage === 'git') {
|
|
389
|
+
await pullGitPortalSource({
|
|
390
|
+
context: sourceContext,
|
|
391
|
+
force: options.force,
|
|
392
|
+
});
|
|
393
|
+
await writePortalConfig(sourceContext.portalDir, portalConfig);
|
|
394
|
+
const installResult = await installPortalDependencies({
|
|
395
|
+
portalDir: sourceContext.portalDir,
|
|
396
|
+
installDependencies: options.installDependencies,
|
|
397
|
+
runCommand: options.runCommand,
|
|
398
|
+
});
|
|
399
|
+
return {
|
|
400
|
+
...sourceContext,
|
|
401
|
+
changed: true,
|
|
402
|
+
...installResult,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
if (sourceContext.sourceStorage !== 'nocobase') {
|
|
406
|
+
throw new Error(portalSourceText('errors.unsupportedSourceStorage', { sourceStorage: sourceContext.sourceStorage }, `Unsupported portal source storage: ${sourceContext.sourceStorage}`));
|
|
407
|
+
}
|
|
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
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'nocobase-cli-portal-pull-'));
|
|
418
|
+
const archivePath = path.join(tempDir, 'source.tar.gz');
|
|
419
|
+
const apiRequest = options.apiRequest ?? executeApiRequest;
|
|
420
|
+
try {
|
|
421
|
+
const response = await apiRequest({
|
|
422
|
+
cliVersion: options.cliVersion ?? '',
|
|
423
|
+
envName: options.envName,
|
|
424
|
+
flags: {
|
|
425
|
+
app: sourceContext.app,
|
|
426
|
+
portal: sourceContext.portal,
|
|
427
|
+
output: archivePath,
|
|
428
|
+
},
|
|
429
|
+
operation: PULL_SOURCE_OPERATION,
|
|
430
|
+
});
|
|
431
|
+
if (!response.ok) {
|
|
432
|
+
throw new Error(portalSourceText('errors.pullFailed', { status: response.status, details: JSON.stringify(response.data, null, 2) }, `Portal source pull failed with status ${response.status}\n${JSON.stringify(response.data, null, 2)}`));
|
|
433
|
+
}
|
|
434
|
+
await mkdir(path.dirname(sourceContext.portalDir), { recursive: true });
|
|
435
|
+
await replacePortalSourceFromArchive({
|
|
436
|
+
archivePath,
|
|
437
|
+
portalDir: sourceContext.portalDir,
|
|
438
|
+
force: options.force,
|
|
439
|
+
});
|
|
440
|
+
await writePortalConfig(sourceContext.portalDir, portalConfig);
|
|
441
|
+
const installResult = await installPortalDependencies({
|
|
442
|
+
portalDir: sourceContext.portalDir,
|
|
443
|
+
installDependencies: options.installDependencies,
|
|
444
|
+
runCommand: options.runCommand,
|
|
445
|
+
});
|
|
446
|
+
return {
|
|
447
|
+
...sourceContext,
|
|
448
|
+
changed: true,
|
|
449
|
+
...installResult,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
finally {
|
|
453
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
export async function pushPortalSource(options) {
|
|
457
|
+
const context = await resolvePortalSourceContext(options);
|
|
458
|
+
if (!(await pathExists(context.portalDir))) {
|
|
459
|
+
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
|
+
}
|
|
461
|
+
const portalConfig = await readPortalConfig(context.portalDir);
|
|
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
|
+
});
|
|
470
|
+
const sourceContext = applyPortalConfigToContext(context, portalConfig);
|
|
471
|
+
if (sourceContext.sourceStorage === 'git') {
|
|
472
|
+
const revision = await pushGitPortalSource({
|
|
473
|
+
context: sourceContext,
|
|
474
|
+
message: options.message,
|
|
475
|
+
});
|
|
476
|
+
return {
|
|
477
|
+
...sourceContext,
|
|
478
|
+
changed: Boolean(revision),
|
|
479
|
+
sourceRevision: revision,
|
|
480
|
+
noopReason: revision
|
|
481
|
+
? undefined
|
|
482
|
+
: portalSourceText('messages.gitPushNoop', undefined, 'No local source changes to push.'),
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
if (sourceContext.sourceStorage !== 'nocobase') {
|
|
486
|
+
throw new Error(portalSourceText('errors.unsupportedSourceStorage', { sourceStorage: sourceContext.sourceStorage }, `Unsupported portal source storage: ${sourceContext.sourceStorage}`));
|
|
487
|
+
}
|
|
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
|
+
const archive = await packPortalSource(sourceContext.portalDir);
|
|
498
|
+
const apiRequest = options.apiRequest ?? executeApiRequest;
|
|
499
|
+
try {
|
|
500
|
+
const response = await apiRequest({
|
|
501
|
+
cliVersion: options.cliVersion ?? '',
|
|
502
|
+
envName: options.envName,
|
|
503
|
+
flags: {
|
|
504
|
+
file: archive.archivePath,
|
|
505
|
+
app: sourceContext.app,
|
|
506
|
+
portal: sourceContext.portal,
|
|
507
|
+
message: options.message,
|
|
508
|
+
},
|
|
509
|
+
operation: PUSH_SOURCE_OPERATION,
|
|
510
|
+
});
|
|
511
|
+
if (!response.ok) {
|
|
512
|
+
throw new Error(portalSourceText('errors.pushFailed', { status: response.status, details: JSON.stringify(response.data, null, 2) }, `Portal source push failed with status ${response.status}\n${JSON.stringify(response.data, null, 2)}`));
|
|
513
|
+
}
|
|
514
|
+
return {
|
|
515
|
+
...sourceContext,
|
|
516
|
+
changed: true,
|
|
517
|
+
sourceRevision: readSourceRevision(response.data),
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
finally {
|
|
521
|
+
await archive.cleanup();
|
|
522
|
+
}
|
|
523
|
+
}
|
package/dist/lib/proxy-caddy.js
CHANGED
|
@@ -79,6 +79,7 @@ export async function writeCaddyProxyBundle(runtime, appEntryOptions, runtimeCon
|
|
|
79
79
|
writeFile(bundle.appConfigPath, nextAppConfigContent, 'utf8'),
|
|
80
80
|
writeFile(bundle.indexV1Path, bundle.indexV1Content, 'utf8'),
|
|
81
81
|
writeFile(bundle.indexV2Path, bundle.indexV2Content, 'utf8'),
|
|
82
|
+
writeFile(bundle.indexSettingsPath, bundle.indexSettingsContent, 'utf8'),
|
|
82
83
|
writeFile(bundle.mainConfigPath, bundle.mainConfigContent, 'utf8'),
|
|
83
84
|
]);
|
|
84
85
|
return {
|
|
@@ -100,6 +101,7 @@ export async function writeManualCaddyProxyBundle(input, appEntryOptions, runtim
|
|
|
100
101
|
writeFile(bundle.appConfigPath, nextAppConfigContent, 'utf8'),
|
|
101
102
|
writeFile(bundle.indexV1Path, bundle.indexV1Content, 'utf8'),
|
|
102
103
|
writeFile(bundle.indexV2Path, bundle.indexV2Content, 'utf8'),
|
|
104
|
+
writeFile(bundle.indexSettingsPath, bundle.indexSettingsContent, 'utf8'),
|
|
103
105
|
writeFile(bundle.mainConfigPath, bundle.mainConfigContent, 'utf8'),
|
|
104
106
|
]);
|
|
105
107
|
return {
|
package/dist/lib/proxy-nginx.js
CHANGED
|
@@ -107,6 +107,7 @@ async function writeResolvedNginxProxyBundle(bundle, appEntryOptions, options) {
|
|
|
107
107
|
writeFile(bundle.appConfigPath, nextAppConfigContent, 'utf8'),
|
|
108
108
|
writeFile(bundle.indexV1Path, bundle.indexV1Content, 'utf8'),
|
|
109
109
|
writeFile(bundle.indexV2Path, bundle.indexV2Content, 'utf8'),
|
|
110
|
+
writeFile(bundle.indexSettingsPath, bundle.indexSettingsContent, 'utf8'),
|
|
110
111
|
writeFile(bundle.mainConfigPath, bundle.mainConfigContent, 'utf8'),
|
|
111
112
|
syncEnvProxyNginxSnippets(),
|
|
112
113
|
]);
|
package/dist/lib/run-npm.js
CHANGED
|
@@ -48,6 +48,10 @@ const MISSING_COMMAND_SPECS = {
|
|
|
48
48
|
displayName: 'pnpm',
|
|
49
49
|
configKey: 'bin.pnpm',
|
|
50
50
|
},
|
|
51
|
+
npm: {
|
|
52
|
+
displayName: 'npm',
|
|
53
|
+
configKey: 'bin.npm',
|
|
54
|
+
},
|
|
51
55
|
};
|
|
52
56
|
const DOCKER_DAEMON_UNAVAILABLE_PATTERNS = [
|
|
53
57
|
/cannot connect to the docker daemon/i,
|
|
@@ -61,6 +65,15 @@ async function resolveCommandName(name) {
|
|
|
61
65
|
function shouldTeeInheritedOutput(options) {
|
|
62
66
|
return options?.stdio === 'inherit' && Boolean(String(process.env.NB_CLI_ACTIVE_LOG_FILE ?? '').trim());
|
|
63
67
|
}
|
|
68
|
+
function buildProcessEnv(options) {
|
|
69
|
+
if (options?.envMode === 'replace') {
|
|
70
|
+
return options.env ?? {};
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
...process.env,
|
|
74
|
+
...options?.env,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
64
77
|
function createMissingCommandError(name, label, error) {
|
|
65
78
|
const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : undefined;
|
|
66
79
|
if (code !== 'ENOENT') {
|
|
@@ -146,10 +159,7 @@ export async function run(name, args, options) {
|
|
|
146
159
|
const child = spawn(command, [...args], {
|
|
147
160
|
stdio,
|
|
148
161
|
cwd,
|
|
149
|
-
env:
|
|
150
|
-
...process.env,
|
|
151
|
-
...options?.env,
|
|
152
|
-
},
|
|
162
|
+
env: buildProcessEnv(options),
|
|
153
163
|
windowsHide: process.platform === 'win32',
|
|
154
164
|
});
|
|
155
165
|
if (options?.stdio === 'pipe' || shouldTeeInheritedOutput(options)) {
|
|
@@ -268,10 +278,7 @@ export async function commandSucceeds(name, args, options) {
|
|
|
268
278
|
return await new Promise((resolve, reject) => {
|
|
269
279
|
const child = spawn(command, [...args], {
|
|
270
280
|
cwd,
|
|
271
|
-
env:
|
|
272
|
-
...process.env,
|
|
273
|
-
...options?.env,
|
|
274
|
-
},
|
|
281
|
+
env: buildProcessEnv(options),
|
|
275
282
|
stdio: 'ignore',
|
|
276
283
|
windowsHide: process.platform === 'win32',
|
|
277
284
|
});
|
|
@@ -302,10 +309,7 @@ export async function commandOutput(name, args, options) {
|
|
|
302
309
|
return await new Promise((resolve, reject) => {
|
|
303
310
|
const child = spawn(command, [...args], {
|
|
304
311
|
cwd,
|
|
305
|
-
env:
|
|
306
|
-
...process.env,
|
|
307
|
-
...options?.env,
|
|
308
|
-
},
|
|
312
|
+
env: buildProcessEnv(options),
|
|
309
313
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
310
314
|
windowsHide: process.platform === 'win32',
|
|
311
315
|
});
|
|
@@ -364,10 +368,7 @@ export async function commandOutputViaFile(name, args, options) {
|
|
|
364
368
|
const result = await new Promise((resolve, reject) => {
|
|
365
369
|
const child = spawn(command, [...args], {
|
|
366
370
|
cwd,
|
|
367
|
-
env:
|
|
368
|
-
...process.env,
|
|
369
|
-
...options?.env,
|
|
370
|
-
},
|
|
371
|
+
env: buildProcessEnv(options),
|
|
371
372
|
stdio: ['ignore', stdoutHandle.fd, stderrHandle.fd],
|
|
372
373
|
windowsHide: process.platform === 'win32',
|
|
373
374
|
});
|
package/dist/lib/ui.js
CHANGED
|
@@ -15,8 +15,35 @@ let verboseMode = false;
|
|
|
15
15
|
let lastStaticTaskMessage;
|
|
16
16
|
let lastStaticTaskAt = 0;
|
|
17
17
|
const STATIC_TASK_UPDATE_THROTTLE_MS = 3_000;
|
|
18
|
+
function isCombiningCodePoint(codePoint) {
|
|
19
|
+
return ((codePoint >= 0x0300 && codePoint <= 0x036f) ||
|
|
20
|
+
(codePoint >= 0x1ab0 && codePoint <= 0x1aff) ||
|
|
21
|
+
(codePoint >= 0x1dc0 && codePoint <= 0x1dff) ||
|
|
22
|
+
(codePoint >= 0x20d0 && codePoint <= 0x20ff) ||
|
|
23
|
+
(codePoint >= 0xfe20 && codePoint <= 0xfe2f));
|
|
24
|
+
}
|
|
25
|
+
function isFullWidthCodePoint(codePoint) {
|
|
26
|
+
return (codePoint >= 0x1100 &&
|
|
27
|
+
(codePoint <= 0x115f ||
|
|
28
|
+
codePoint === 0x2329 ||
|
|
29
|
+
codePoint === 0x232a ||
|
|
30
|
+
(codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
|
|
31
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
|
|
32
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
|
33
|
+
(codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
|
|
34
|
+
(codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
|
|
35
|
+
(codePoint >= 0xff00 && codePoint <= 0xff60) ||
|
|
36
|
+
(codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
|
|
37
|
+
(codePoint >= 0x20000 && codePoint <= 0x3fffd)));
|
|
38
|
+
}
|
|
18
39
|
function stringWidth(value) {
|
|
19
|
-
return Array.from(value).
|
|
40
|
+
return Array.from(value).reduce((width, character) => {
|
|
41
|
+
const codePoint = character.codePointAt(0);
|
|
42
|
+
if (!codePoint || codePoint === 0 || codePoint < 32 || isCombiningCodePoint(codePoint)) {
|
|
43
|
+
return width;
|
|
44
|
+
}
|
|
45
|
+
return width + (isFullWidthCodePoint(codePoint) ? 2 : 1);
|
|
46
|
+
}, 0);
|
|
20
47
|
}
|
|
21
48
|
function pad(value, width) {
|
|
22
49
|
const padding = Math.max(0, width - stringWidth(value));
|