@nocobase/cli 2.2.0-beta.8 → 2.2.0-test.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/assets/env-proxy/nginx/app.conf.tpl +23 -0
  2. package/assets/env-proxy/nginx/nocobase.conf.tpl +5 -0
  3. package/assets/env-proxy/nginx/snippets/dist-location.conf +5 -0
  4. package/assets/env-proxy/nginx/snippets/gzip.conf +17 -0
  5. package/assets/env-proxy/nginx/snippets/log-format-http.conf +13 -0
  6. package/assets/env-proxy/nginx/snippets/maps-http.conf +14 -0
  7. package/assets/env-proxy/nginx/snippets/mime-types.conf +98 -0
  8. package/assets/env-proxy/nginx/snippets/proxy-location.conf +18 -0
  9. package/assets/env-proxy/nginx/snippets/spa-location.conf +6 -0
  10. package/assets/env-proxy/nginx/snippets/uploads-location.conf +24 -0
  11. package/dist/commands/app/start.js +25 -2
  12. package/dist/commands/config/set.js +1 -0
  13. package/dist/commands/env/info.js +11 -1
  14. package/dist/commands/init.js +131 -4
  15. package/dist/commands/install.js +179 -131
  16. package/dist/commands/portal/create.js +105 -0
  17. package/dist/commands/portal/deploy.js +81 -0
  18. package/dist/commands/portal/destroy.js +104 -0
  19. package/dist/commands/portal/dev.js +71 -0
  20. package/dist/commands/portal/index.js +20 -0
  21. package/dist/commands/portal/info.js +82 -0
  22. package/dist/commands/portal/list.js +98 -0
  23. package/dist/commands/portal/pull.js +77 -0
  24. package/dist/commands/portal/push.js +79 -0
  25. package/dist/commands/proxy/caddy/generate.js +93 -7
  26. package/dist/commands/proxy/nginx/generate.js +98 -7
  27. package/dist/commands/revision/create.js +1 -1
  28. package/dist/commands/source/dev.js +1 -1
  29. package/dist/commands/source/download.js +18 -14
  30. package/dist/lib/app-managed-resources.js +3 -2
  31. package/dist/lib/auth-store.js +71 -1
  32. package/dist/lib/cli-config.js +74 -2
  33. package/dist/lib/docker-image.js +94 -6
  34. package/dist/lib/env-config.js +8 -0
  35. package/dist/lib/env-proxy-config.js +48 -0
  36. package/dist/lib/env-proxy.js +266 -61
  37. package/dist/lib/managed-init-env.js +6 -1
  38. package/dist/lib/portal-command-env.js +31 -0
  39. package/dist/lib/portal-create.js +488 -0
  40. package/dist/lib/portal-deploy.js +275 -0
  41. package/dist/lib/portal-destroy.js +100 -0
  42. package/dist/lib/portal-dev.js +79 -0
  43. package/dist/lib/portal-env-files.js +53 -0
  44. package/dist/lib/portal-info.js +31 -0
  45. package/dist/lib/portal-list.js +197 -0
  46. package/dist/lib/portal-source.js +416 -0
  47. package/dist/lib/portal-template.js +190 -0
  48. package/dist/lib/prompt-catalog-terminal.js +32 -19
  49. package/dist/lib/prompt-web-ui.js +13 -2
  50. package/dist/lib/proxy-caddy.js +77 -9
  51. package/dist/lib/proxy-nginx.js +71 -11
  52. package/dist/lib/run-npm.js +17 -16
  53. package/dist/lib/ui.js +28 -1
  54. package/dist/locale/en-US.json +192 -38
  55. package/dist/locale/zh-CN.json +192 -38
  56. package/package.json +6 -3
@@ -0,0 +1,197 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { stat } from 'node:fs/promises';
10
+ import path from 'node:path';
11
+ import { appendAppPublicPath } from './app-public-path.js';
12
+ import { executeApiRequest } from './api-client.js';
13
+ import { translateCli } from './cli-locale.js';
14
+ import { buildPortalBasePath, resolvePortalAppFromApiBaseUrl, resolvePortalStoragePath, } from './portal-create.js';
15
+ const portalListText = (key, values, fallback) => translateCli(`commands.portalList.${key}`, values, { fallback });
16
+ const LIST_PORTALS_OPERATION = {
17
+ method: 'GET',
18
+ pathTemplate: '/multiPortals:list',
19
+ parameters: [
20
+ {
21
+ name: 'pageSize',
22
+ flagName: 'pageSize',
23
+ in: 'query',
24
+ type: 'integer',
25
+ },
26
+ {
27
+ name: 'sort[]',
28
+ flagName: 'sort',
29
+ in: 'query',
30
+ isArray: true,
31
+ },
32
+ ],
33
+ };
34
+ function trimValue(value) {
35
+ return String(value ?? '').trim();
36
+ }
37
+ function readRecordString(record, key) {
38
+ return trimValue(record[key]);
39
+ }
40
+ function readRecordBoolean(record, key) {
41
+ return record[key] === true;
42
+ }
43
+ function readRecordObject(record, key) {
44
+ const value = record[key];
45
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
46
+ return {};
47
+ }
48
+ return value;
49
+ }
50
+ function readListData(data) {
51
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
52
+ return [];
53
+ }
54
+ const directData = data.data;
55
+ if (Array.isArray(directData)) {
56
+ return directData.filter((item) => (Boolean(item) && typeof item === 'object' && !Array.isArray(item)));
57
+ }
58
+ return readListData(directData);
59
+ }
60
+ async function pathExists(target) {
61
+ try {
62
+ await stat(target);
63
+ return true;
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ }
69
+ function buildPortalAccessUrl(apiBaseUrl, portalBase) {
70
+ try {
71
+ const baseUrl = new URL(apiBaseUrl);
72
+ return new URL(portalBase, baseUrl.origin).toString();
73
+ }
74
+ catch {
75
+ return portalBase;
76
+ }
77
+ }
78
+ function normalizeRootPath(pathname) {
79
+ const trimmed = pathname.trim();
80
+ if (!trimmed || trimmed === '/') {
81
+ return '/';
82
+ }
83
+ return `/${trimmed.replace(/^\/+/, '').replace(/\/+$/, '')}`;
84
+ }
85
+ function isAbsoluteUrl(value) {
86
+ return /^[a-z][a-z\d+\-.]*:\/\//i.test(value) || value.startsWith('//');
87
+ }
88
+ function stripBasePath(pathname, basePath) {
89
+ const pathValue = normalizeRootPath(pathname);
90
+ const baseValue = normalizeRootPath(basePath);
91
+ if (pathValue === baseValue) {
92
+ return '/';
93
+ }
94
+ if (pathValue.startsWith(`${baseValue}/`)) {
95
+ return pathValue.slice(baseValue.length) || '/';
96
+ }
97
+ return pathValue;
98
+ }
99
+ function normalizeNoCodeRoutePath(routePath, appPublicPath) {
100
+ let normalizedRoutePath = normalizeRootPath(routePath);
101
+ for (const basePath of [
102
+ appendAppPublicPath(appPublicPath, 'v', { trailingSlash: false }),
103
+ appendAppPublicPath(appPublicPath, 'x', { trailingSlash: false }),
104
+ '/v',
105
+ '/x',
106
+ ]) {
107
+ normalizedRoutePath = stripBasePath(normalizedRoutePath, basePath);
108
+ }
109
+ return normalizedRoutePath;
110
+ }
111
+ function buildNoCodePortalBasePath(params) {
112
+ if (isAbsoluteUrl(params.routePath)) {
113
+ return params.routePath;
114
+ }
115
+ const normalizedRoutePath = normalizeNoCodeRoutePath(params.routePath, params.appPublicPath);
116
+ const segment = normalizedRoutePath === '/' ? 'v' : `v/${normalizedRoutePath.replace(/^\/+/, '')}`;
117
+ return appendAppPublicPath(params.appPublicPath, segment, { trailingSlash: normalizedRoutePath === '/' });
118
+ }
119
+ export function toPortalOutputItem(item) {
120
+ return {
121
+ name: item.routeName,
122
+ url: item.portalUrl,
123
+ developmentMode: item.developmentMode,
124
+ localPath: item.localSynced === true ? item.portalDir : '',
125
+ enabled: item.enabled,
126
+ sourceStorage: item.sourceStorage,
127
+ localSynced: item.localSynced,
128
+ };
129
+ }
130
+ async function listMultiPortalRecords(params) {
131
+ const apiRequest = params.apiRequest ?? executeApiRequest;
132
+ const response = await apiRequest({
133
+ cliVersion: params.cliVersion ?? '',
134
+ envName: params.envName,
135
+ flags: {
136
+ pageSize: 200,
137
+ sort: ['routeName'],
138
+ },
139
+ operation: LIST_PORTALS_OPERATION,
140
+ });
141
+ if (!response.ok) {
142
+ throw new Error(portalListText('errors.listFailed', { status: response.status, details: JSON.stringify(response.data, null, 2) }, `Portal list failed with status ${response.status}\n${JSON.stringify(response.data, null, 2)}`));
143
+ }
144
+ return readListData(response.data);
145
+ }
146
+ export async function listPortalWorkspaces(options) {
147
+ const apiBaseUrl = trimValue(options.env.apiBaseUrl);
148
+ const storagePath = resolvePortalStoragePath(options.env);
149
+ const { app, appPublicPath } = resolvePortalAppFromApiBaseUrl(apiBaseUrl, options.env.config.appPublicPath);
150
+ const mode = options.env.kind;
151
+ if (mode !== 'local' && mode !== 'docker' && mode !== 'http') {
152
+ throw new Error(portalListText('errors.unsupportedEnvKind', { kind: mode }, `Cannot list Portal workspaces for ${mode} envs in the first version.`));
153
+ }
154
+ const listMode = mode;
155
+ const records = await listMultiPortalRecords({
156
+ envName: options.envName,
157
+ cliVersion: options.cliVersion,
158
+ apiRequest: options.apiRequest,
159
+ });
160
+ const items = await Promise.all(records.map(async (record) => {
161
+ const uid = readRecordString(record, 'uid');
162
+ const routeName = readRecordString(record, 'routeName') || uid;
163
+ const routePath = readRecordString(record, 'routePath') || `/${routeName}`;
164
+ const developmentMode = readRecordString(record, 'developmentMode');
165
+ const enabled = readRecordBoolean(record, 'enabled');
166
+ const options = readRecordObject(record, 'options');
167
+ const git = readRecordObject(options, 'git');
168
+ const sourceStorage = trimValue(options.sourceStorage) || readRecordString(record, 'sourceStorage') || 'nocobase';
169
+ const isVibeCoding = developmentMode === 'vibe-coding';
170
+ const portalDir = isVibeCoding ? path.join(storagePath, 'portals', app, routeName) : '';
171
+ return {
172
+ uid,
173
+ routeName,
174
+ routePath,
175
+ developmentMode,
176
+ enabled,
177
+ sourceStorage,
178
+ gitRepo: trimValue(git.repo) || readRecordString(record, 'gitRepo'),
179
+ gitBranch: trimValue(git.branch) || readRecordString(record, 'gitBranch'),
180
+ gitPath: trimValue(git.path) || readRecordString(record, 'gitPath'),
181
+ sourceRevision: trimValue(options.sourceRevision) || readRecordString(record, 'sourceRevision'),
182
+ portalUrl: enabled
183
+ ? buildPortalAccessUrl(apiBaseUrl, isVibeCoding
184
+ ? buildPortalBasePath({ app, appPublicPath, portal: routeName })
185
+ : buildNoCodePortalBasePath({ appPublicPath, routePath }))
186
+ : '',
187
+ portalDir,
188
+ localSynced: isVibeCoding ? await pathExists(portalDir) : null,
189
+ };
190
+ }));
191
+ return {
192
+ app,
193
+ mode: listMode,
194
+ storagePath,
195
+ items,
196
+ };
197
+ }
@@ -0,0 +1,416 @@
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 { buildPortalBasePath, resolvePortalAppFromApiBaseUrl, resolvePortalStoragePath, validatePortalSlug, } from './portal-create.js';
18
+ import { listPortalWorkspaces } from './portal-list.js';
19
+ import { findPortalListItem } from './portal-info.js';
20
+ const execFileAsync = promisify(execFile);
21
+ const portalSourceText = (key, values, fallback) => translateCli(`commands.portalSource.${key}`, values, { fallback });
22
+ const PULL_SOURCE_OPERATION = {
23
+ method: 'POST',
24
+ pathTemplate: '/multiPortals:pullSource',
25
+ hasBody: true,
26
+ bodyRequired: true,
27
+ responseType: 'binary',
28
+ parameters: [
29
+ {
30
+ name: 'app',
31
+ flagName: 'app',
32
+ in: 'body',
33
+ required: true,
34
+ },
35
+ {
36
+ name: 'portal',
37
+ flagName: 'portal',
38
+ in: 'body',
39
+ required: true,
40
+ },
41
+ ],
42
+ };
43
+ const PUSH_SOURCE_OPERATION = {
44
+ method: 'POST',
45
+ pathTemplate: '/multiPortals:pushSource',
46
+ requestContentType: 'multipart/form-data',
47
+ hasBody: true,
48
+ bodyRequired: true,
49
+ parameters: [
50
+ {
51
+ name: 'file',
52
+ flagName: 'file',
53
+ in: 'body',
54
+ required: true,
55
+ isFile: true,
56
+ },
57
+ {
58
+ name: 'app',
59
+ flagName: 'app',
60
+ in: 'body',
61
+ required: true,
62
+ },
63
+ {
64
+ name: 'portal',
65
+ flagName: 'portal',
66
+ in: 'body',
67
+ required: true,
68
+ },
69
+ {
70
+ name: 'message',
71
+ flagName: 'message',
72
+ in: 'body',
73
+ },
74
+ ],
75
+ };
76
+ function trimValue(value) {
77
+ return String(value ?? '').trim();
78
+ }
79
+ async function pathExists(target) {
80
+ try {
81
+ await stat(target);
82
+ return true;
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ }
88
+ function shouldPackPortalSourceEntry(entryName) {
89
+ return !entryName.split('/').some((segment) => ['.git', 'node_modules', 'dist', '.DS_Store'].includes(segment));
90
+ }
91
+ function validatePortalSourceTarEntry(entryPath, entry) {
92
+ if (path.isAbsolute(entryPath) || entryPath.split(/[\\/]+/).includes('..')) {
93
+ return false;
94
+ }
95
+ const tarEntry = entry;
96
+ if (tarEntry.type === 'SymbolicLink' || tarEntry.type === 'Link' || typeof tarEntry.linkpath === 'string') {
97
+ return false;
98
+ }
99
+ return shouldPackPortalSourceEntry(entryPath);
100
+ }
101
+ async function packPortalSource(portalDir) {
102
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), 'nocobase-cli-portal-source-'));
103
+ const archivePath = path.join(tempDir, 'source.tar.gz');
104
+ const entries = (await readdir(portalDir)).filter(shouldPackPortalSourceEntry);
105
+ await tar.create({
106
+ cwd: portalDir,
107
+ file: archivePath,
108
+ gzip: true,
109
+ filter: (entryPath, entry) => validatePortalSourceTarEntry(entryPath, entry),
110
+ }, entries);
111
+ return {
112
+ archivePath,
113
+ cleanup: async () => {
114
+ await rm(tempDir, { recursive: true, force: true });
115
+ },
116
+ };
117
+ }
118
+ async function replacePortalSourceFromArchive(params) {
119
+ const targetExists = await pathExists(params.portalDir);
120
+ if (targetExists && !params.force) {
121
+ throw new Error(portalSourceText('errors.workspaceExists', { portalDir: params.portalDir }, `Portal workspace already exists: ${params.portalDir}\nPass --force to delete it and pull again.`));
122
+ }
123
+ const parentDir = path.dirname(params.portalDir);
124
+ const tempDir = await mkdtemp(path.join(parentDir, `.${path.basename(params.portalDir)}-pull-`));
125
+ try {
126
+ await tar.extract({
127
+ file: params.archivePath,
128
+ cwd: tempDir,
129
+ strict: true,
130
+ filter: validatePortalSourceTarEntry,
131
+ });
132
+ if (targetExists) {
133
+ await rm(params.portalDir, { recursive: true, force: true });
134
+ }
135
+ await rename(tempDir, params.portalDir);
136
+ }
137
+ catch (error) {
138
+ await rm(tempDir, { recursive: true, force: true });
139
+ throw error;
140
+ }
141
+ }
142
+ async function replacePortalSourceFromDirectory(params) {
143
+ const targetExists = await pathExists(params.portalDir);
144
+ if (targetExists && !params.force) {
145
+ throw new Error(portalSourceText('errors.workspaceExists', { portalDir: params.portalDir }, `Portal workspace already exists: ${params.portalDir}\nPass --force to delete it and pull again.`));
146
+ }
147
+ const parentDir = path.dirname(params.portalDir);
148
+ const tempDir = await mkdtemp(path.join(parentDir, `.${path.basename(params.portalDir)}-pull-`));
149
+ try {
150
+ await cp(params.sourceDir, tempDir, {
151
+ recursive: true,
152
+ filter: (source) => shouldPackPortalSourceEntry(path.relative(params.sourceDir, source)),
153
+ });
154
+ if (targetExists) {
155
+ await rm(params.portalDir, { recursive: true, force: true });
156
+ }
157
+ await rename(tempDir, params.portalDir);
158
+ }
159
+ catch (error) {
160
+ await rm(tempDir, { recursive: true, force: true });
161
+ throw error;
162
+ }
163
+ }
164
+ async function runGit(args, cwd) {
165
+ return await execFileAsync('git', args, {
166
+ cwd,
167
+ maxBuffer: 10 * 1024 * 1024,
168
+ });
169
+ }
170
+ function readSourceRevision(data) {
171
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
172
+ return undefined;
173
+ }
174
+ const direct = data.sourceRevision;
175
+ if (typeof direct === 'string' && direct.trim()) {
176
+ return direct;
177
+ }
178
+ return readSourceRevision(data.data);
179
+ }
180
+ async function resolvePortalSourceContext(options) {
181
+ const portal = validatePortalSlug(options.portal);
182
+ const apiBaseUrl = trimValue(options.env.apiBaseUrl);
183
+ const storagePath = resolvePortalStoragePath(options.env);
184
+ const { app, appPublicPath } = resolvePortalAppFromApiBaseUrl(apiBaseUrl, options.env.config.appPublicPath);
185
+ const portalDir = path.join(storagePath, 'portals', app, portal);
186
+ const portalBase = buildPortalBasePath({ app, appPublicPath, portal });
187
+ const mode = options.env.kind;
188
+ if (mode !== 'local' && mode !== 'docker' && mode !== 'http') {
189
+ throw new Error(portalSourceText('errors.unsupportedEnvKind', { kind: mode }, `Cannot sync Portal source for ${mode} envs in the first version.`));
190
+ }
191
+ const list = await listPortalWorkspaces({
192
+ env: options.env,
193
+ envName: options.envName,
194
+ cliVersion: options.cliVersion,
195
+ apiRequest: options.apiRequest,
196
+ });
197
+ const item = findPortalListItem(list.items, portal);
198
+ if (!item) {
199
+ throw new Error(portalSourceText('errors.notFound', { portal }, `Portal "${portal}" was not found. Run \`nb portal list\` to see available portals.`));
200
+ }
201
+ return {
202
+ app,
203
+ portal,
204
+ portalDir,
205
+ portalBase,
206
+ mode,
207
+ sourceStorage: item.sourceStorage || 'nocobase',
208
+ gitRepo: item.gitRepo,
209
+ gitBranch: item.gitBranch || 'main',
210
+ gitPath: item.gitPath || portal,
211
+ };
212
+ }
213
+ function assertGitSourceConfig(context) {
214
+ if (!context.gitRepo) {
215
+ throw new Error(portalSourceText('errors.gitRepoMissing', { portal: context.portal }, `Portal "${context.portal}" uses Git source storage, but gitRepo is missing.`));
216
+ }
217
+ return {
218
+ repo: context.gitRepo,
219
+ branch: context.gitBranch || 'main',
220
+ gitPath: context.gitPath || context.portal,
221
+ };
222
+ }
223
+ async function cloneGitSource(params) {
224
+ const repoDir = path.join(params.cwd, 'repo');
225
+ try {
226
+ await runGit(['clone', '--branch', params.branch, params.repo, repoDir]);
227
+ }
228
+ catch (error) {
229
+ if (!params.createBranch) {
230
+ throw error;
231
+ }
232
+ await runGit(['clone', params.repo, repoDir]);
233
+ await runGit(['checkout', '-B', params.branch], repoDir);
234
+ }
235
+ return repoDir;
236
+ }
237
+ async function pullGitPortalSource(params) {
238
+ const git = assertGitSourceConfig(params.context);
239
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), 'nocobase-cli-portal-git-pull-'));
240
+ try {
241
+ const repoDir = await cloneGitSource({
242
+ repo: git.repo,
243
+ branch: git.branch,
244
+ cwd: tempDir,
245
+ createBranch: true,
246
+ });
247
+ const sourceDir = path.join(repoDir, git.gitPath);
248
+ if (!(await pathExists(sourceDir))) {
249
+ throw new Error(portalSourceText('errors.gitPathMissing', { gitPath: git.gitPath }, `Git path does not exist in the configured repository: ${git.gitPath}`));
250
+ }
251
+ await mkdir(path.dirname(params.context.portalDir), { recursive: true });
252
+ await replacePortalSourceFromDirectory({
253
+ sourceDir,
254
+ portalDir: params.context.portalDir,
255
+ force: params.force,
256
+ });
257
+ }
258
+ finally {
259
+ await rm(tempDir, { recursive: true, force: true });
260
+ }
261
+ }
262
+ async function pushGitPortalSource(params) {
263
+ const git = assertGitSourceConfig(params.context);
264
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), 'nocobase-cli-portal-git-push-'));
265
+ try {
266
+ const repoDir = await cloneGitSource({
267
+ repo: git.repo,
268
+ branch: git.branch,
269
+ cwd: tempDir,
270
+ });
271
+ const targetDir = path.join(repoDir, git.gitPath);
272
+ await rm(targetDir, { recursive: true, force: true });
273
+ await mkdir(path.dirname(targetDir), { recursive: true });
274
+ await cp(params.context.portalDir, targetDir, {
275
+ recursive: true,
276
+ filter: (source) => shouldPackPortalSourceEntry(path.relative(params.context.portalDir, source)),
277
+ });
278
+ await runGit(['add', git.gitPath], repoDir);
279
+ const status = await runGit(['status', '--porcelain', '--', git.gitPath], repoDir);
280
+ if (!status.stdout.trim()) {
281
+ return undefined;
282
+ }
283
+ await runGit([
284
+ '-c',
285
+ 'user.name=NocoBase CLI',
286
+ '-c',
287
+ 'user.email=nocobase-cli@localhost',
288
+ 'commit',
289
+ '-m',
290
+ trimValue(params.message) || `chore(portal): update ${params.context.portal}`,
291
+ ], repoDir);
292
+ await runGit(['push', 'origin', git.branch], repoDir);
293
+ const revision = await runGit(['rev-parse', 'HEAD'], repoDir);
294
+ return revision.stdout.trim();
295
+ }
296
+ finally {
297
+ await rm(tempDir, { recursive: true, force: true });
298
+ }
299
+ }
300
+ export async function pullPortalSource(options) {
301
+ const context = await resolvePortalSourceContext(options);
302
+ if (context.sourceStorage === 'git') {
303
+ await pullGitPortalSource({
304
+ context,
305
+ force: options.force,
306
+ });
307
+ return {
308
+ ...context,
309
+ changed: true,
310
+ };
311
+ }
312
+ if (context.sourceStorage !== 'nocobase') {
313
+ throw new Error(portalSourceText('errors.unsupportedSourceStorage', { sourceStorage: context.sourceStorage }, `Unsupported Portal source storage: ${context.sourceStorage}`));
314
+ }
315
+ if (context.mode === 'local' || context.mode === 'docker') {
316
+ return {
317
+ ...context,
318
+ changed: false,
319
+ noopReason: context.mode === 'local'
320
+ ? portalSourceText('messages.localPullNoop', undefined, 'Portal source is already local.')
321
+ : portalSourceText('messages.dockerPullNoop', undefined, 'Portal source is already available through the Docker volume.'),
322
+ };
323
+ }
324
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), 'nocobase-cli-portal-pull-'));
325
+ const archivePath = path.join(tempDir, 'source.tar.gz');
326
+ const apiRequest = options.apiRequest ?? executeApiRequest;
327
+ try {
328
+ const response = await apiRequest({
329
+ cliVersion: options.cliVersion ?? '',
330
+ envName: options.envName,
331
+ flags: {
332
+ app: context.app,
333
+ portal: context.portal,
334
+ output: archivePath,
335
+ },
336
+ operation: PULL_SOURCE_OPERATION,
337
+ });
338
+ if (!response.ok) {
339
+ 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)}`));
340
+ }
341
+ await mkdir(path.dirname(context.portalDir), { recursive: true });
342
+ await replacePortalSourceFromArchive({
343
+ archivePath,
344
+ portalDir: context.portalDir,
345
+ force: options.force,
346
+ });
347
+ return {
348
+ ...context,
349
+ changed: true,
350
+ };
351
+ }
352
+ finally {
353
+ await rm(tempDir, { recursive: true, force: true });
354
+ }
355
+ }
356
+ export async function pushPortalSource(options) {
357
+ const context = await resolvePortalSourceContext(options);
358
+ if (context.sourceStorage === 'git') {
359
+ if (!(await pathExists(context.portalDir))) {
360
+ throw new Error(portalSourceText('errors.workspaceMissing', { portalDir: context.portalDir, portal: context.portal }, `Portal workspace does not exist: ${context.portalDir}\nRun \`nb portal create ${context.portal}\` first.`));
361
+ }
362
+ const revision = await pushGitPortalSource({
363
+ context,
364
+ message: options.message,
365
+ });
366
+ return {
367
+ ...context,
368
+ changed: Boolean(revision),
369
+ sourceRevision: revision,
370
+ noopReason: revision
371
+ ? undefined
372
+ : portalSourceText('messages.gitPushNoop', undefined, 'No local source changes to push.'),
373
+ };
374
+ }
375
+ if (context.sourceStorage !== 'nocobase') {
376
+ throw new Error(portalSourceText('errors.unsupportedSourceStorage', { sourceStorage: context.sourceStorage }, `Unsupported Portal source storage: ${context.sourceStorage}`));
377
+ }
378
+ if (context.mode === 'local' || context.mode === 'docker') {
379
+ return {
380
+ ...context,
381
+ changed: false,
382
+ noopReason: context.mode === 'local'
383
+ ? portalSourceText('messages.localPushNoop', undefined, 'Portal source is already local.')
384
+ : portalSourceText('messages.dockerPushNoop', undefined, 'Portal source is already available through the Docker volume.'),
385
+ };
386
+ }
387
+ if (!(await pathExists(context.portalDir))) {
388
+ throw new Error(portalSourceText('errors.workspaceMissing', { portalDir: context.portalDir, portal: context.portal }, `Portal workspace does not exist: ${context.portalDir}\nRun \`nb portal create ${context.portal}\` first.`));
389
+ }
390
+ const archive = await packPortalSource(context.portalDir);
391
+ const apiRequest = options.apiRequest ?? executeApiRequest;
392
+ try {
393
+ const response = await apiRequest({
394
+ cliVersion: options.cliVersion ?? '',
395
+ envName: options.envName,
396
+ flags: {
397
+ file: archive.archivePath,
398
+ app: context.app,
399
+ portal: context.portal,
400
+ message: options.message,
401
+ },
402
+ operation: PUSH_SOURCE_OPERATION,
403
+ });
404
+ if (!response.ok) {
405
+ 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)}`));
406
+ }
407
+ return {
408
+ ...context,
409
+ changed: true,
410
+ sourceRevision: readSourceRevision(response.data),
411
+ };
412
+ }
413
+ finally {
414
+ await archive.cleanup();
415
+ }
416
+ }