@nocobase/cli 2.3.0-beta.5 → 2.4.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.
Files changed (56) hide show
  1. package/dist/commands/api/swagger/get.js +96 -0
  2. package/dist/commands/api/swagger/index.js +20 -0
  3. package/dist/commands/api/swagger/list.js +92 -0
  4. package/dist/commands/config/set.js +1 -0
  5. package/dist/commands/env/add.js +6 -0
  6. package/dist/commands/env/update.js +13 -0
  7. package/dist/commands/init.js +51 -5
  8. package/dist/commands/install.js +60 -3
  9. package/dist/commands/portal/config.js +99 -0
  10. package/dist/commands/portal/create.js +96 -0
  11. package/dist/commands/portal/deploy.js +81 -0
  12. package/dist/commands/portal/destroy.js +126 -0
  13. package/dist/commands/portal/dev.js +73 -0
  14. package/dist/commands/portal/index.js +20 -0
  15. package/dist/commands/portal/info.js +82 -0
  16. package/dist/commands/portal/list.js +98 -0
  17. package/dist/commands/portal/pull.js +113 -0
  18. package/dist/commands/portal/push.js +79 -0
  19. package/dist/commands/source/dev.js +1 -1
  20. package/dist/lib/api-client.js +35 -9
  21. package/dist/lib/app-client-entry-mode.js +28 -0
  22. package/dist/lib/app-managed-resources.js +2 -0
  23. package/dist/lib/auth-store.js +55 -2
  24. package/dist/lib/bootstrap.js +3 -1
  25. package/dist/lib/cli-config.js +20 -1
  26. package/dist/lib/env-auth.js +2 -36
  27. package/dist/lib/env-command-config.js +1 -0
  28. package/dist/lib/env-config.js +11 -0
  29. package/dist/lib/env-portal-config.js +30 -0
  30. package/dist/lib/env-proxy.js +154 -7
  31. package/dist/lib/managed-env-file.js +119 -9
  32. package/dist/lib/managed-init-env.js +4 -1
  33. package/dist/lib/portal-build-html.js +27 -0
  34. package/dist/lib/portal-command-env.js +31 -0
  35. package/dist/lib/portal-config.js +119 -0
  36. package/dist/lib/portal-configure.js +110 -0
  37. package/dist/lib/portal-create.js +515 -0
  38. package/dist/lib/portal-deploy.js +266 -0
  39. package/dist/lib/portal-destroy.js +114 -0
  40. package/dist/lib/portal-dev.js +78 -0
  41. package/dist/lib/portal-env-files.js +54 -0
  42. package/dist/lib/portal-info.js +28 -0
  43. package/dist/lib/portal-list.js +205 -0
  44. package/dist/lib/portal-path-safety.js +76 -0
  45. package/dist/lib/portal-source.js +692 -0
  46. package/dist/lib/prompt-catalog-core.js +2 -2
  47. package/dist/lib/prompt-catalog-terminal.js +4 -5
  48. package/dist/lib/prompt-web-ui.js +12 -6
  49. package/dist/lib/proxy-caddy.js +2 -0
  50. package/dist/lib/proxy-nginx.js +1 -0
  51. package/dist/lib/run-npm.js +85 -20
  52. package/dist/lib/swagger-command.js +52 -0
  53. package/dist/lib/ui.js +28 -1
  54. package/dist/locale/en-US.json +245 -1
  55. package/dist/locale/zh-CN.json +245 -1
  56. package/package.json +5 -2
@@ -0,0 +1,692 @@
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 { ensurePortalBuildHtmlReadsEnvOnly } from './portal-build-html.js';
18
+ import { buildPortalConfig, buildPortalConfigFromOptions, DEFAULT_PORTAL_GIT_PATH, } from './portal-config.js';
19
+ import { buildPortalCommandEnv } from './portal-command-env.js';
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';
23
+ import { listPortalWorkspaces } from './portal-list.js';
24
+ import { findPortalListItem } from './portal-info.js';
25
+ import { resolvePnpmInstallCommand, run, runPnpmInstallCommand } from './run-npm.js';
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
+ };
31
+ const portalSourceText = (key, values, fallback) => translateCli(`commands.portalSource.${key}`, values, { fallback });
32
+ const PULL_SOURCE_OPERATION = {
33
+ method: 'POST',
34
+ pathTemplate: '/multiPortals:pullSource',
35
+ hasBody: true,
36
+ bodyRequired: true,
37
+ responseType: 'binary',
38
+ parameters: [
39
+ {
40
+ name: 'app',
41
+ flagName: 'app',
42
+ in: 'body',
43
+ required: true,
44
+ },
45
+ {
46
+ name: 'portal',
47
+ flagName: 'portal',
48
+ in: 'body',
49
+ required: true,
50
+ },
51
+ ],
52
+ };
53
+ const PUSH_SOURCE_OPERATION = {
54
+ method: 'POST',
55
+ pathTemplate: '/multiPortals:pushSource',
56
+ requestContentType: 'multipart/form-data',
57
+ hasBody: true,
58
+ bodyRequired: true,
59
+ parameters: [
60
+ {
61
+ name: 'file',
62
+ flagName: 'file',
63
+ in: 'body',
64
+ required: true,
65
+ isFile: true,
66
+ },
67
+ {
68
+ name: 'app',
69
+ flagName: 'app',
70
+ in: 'body',
71
+ required: true,
72
+ },
73
+ {
74
+ name: 'portal',
75
+ flagName: 'portal',
76
+ in: 'body',
77
+ required: true,
78
+ },
79
+ {
80
+ name: 'message',
81
+ flagName: 'message',
82
+ in: 'body',
83
+ },
84
+ ],
85
+ };
86
+ function trimValue(value) {
87
+ return String(value ?? '').trim();
88
+ }
89
+ async function pathExists(target) {
90
+ try {
91
+ await stat(target);
92
+ return true;
93
+ }
94
+ catch {
95
+ return false;
96
+ }
97
+ }
98
+ async function isFile(target) {
99
+ try {
100
+ return (await stat(target)).isFile();
101
+ }
102
+ catch {
103
+ return false;
104
+ }
105
+ }
106
+ function shouldPackPortalSourceEntry(entryName) {
107
+ return !entryName
108
+ .split('/')
109
+ .some((segment) => segment.startsWith('._') || ['.git', 'node_modules', 'dist', '.DS_Store'].includes(segment));
110
+ }
111
+ function validatePortalSourceTarEntry(entryPath, entry) {
112
+ if (path.isAbsolute(entryPath) || entryPath.split(/[\\/]+/).includes('..')) {
113
+ return false;
114
+ }
115
+ const tarEntry = entry;
116
+ if (tarEntry.type === 'SymbolicLink' || tarEntry.type === 'Link' || typeof tarEntry.linkpath === 'string') {
117
+ return false;
118
+ }
119
+ return shouldPackPortalSourceEntry(entryPath);
120
+ }
121
+ async function packPortalSource(portalDir) {
122
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), 'nocobase-cli-portal-source-'));
123
+ const archivePath = path.join(tempDir, 'source.tar.gz');
124
+ const entries = (await readdir(portalDir)).filter(shouldPackPortalSourceEntry);
125
+ await tar.create({
126
+ cwd: portalDir,
127
+ file: archivePath,
128
+ gzip: true,
129
+ filter: (entryPath, entry) => validatePortalSourceTarEntry(entryPath, entry),
130
+ }, entries);
131
+ return {
132
+ archivePath,
133
+ cleanup: async () => {
134
+ await rm(tempDir, { recursive: true, force: true });
135
+ },
136
+ };
137
+ }
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;
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
+ });
157
+ const parentDir = path.dirname(params.portalDir);
158
+ const tempDir = await mkdtemp(path.join(parentDir, `.${path.basename(params.portalDir)}-pull-`));
159
+ try {
160
+ await tar.extract({
161
+ file: params.archivePath,
162
+ cwd: tempDir,
163
+ strict: true,
164
+ filter: validatePortalSourceTarEntry,
165
+ });
166
+ if (targetExists) {
167
+ await replaceExistingPortalDirectory({
168
+ sourceDir: tempDir,
169
+ portalDir: params.portalDir,
170
+ });
171
+ return;
172
+ }
173
+ await rename(tempDir, params.portalDir);
174
+ }
175
+ catch (error) {
176
+ await rm(tempDir, { recursive: true, force: true });
177
+ throw error;
178
+ }
179
+ }
180
+ async function replacePortalSourceFromDirectory(params) {
181
+ const targetExists = await assertPortalDirectoryCanBeReplaced({
182
+ portalDir: params.portalDir,
183
+ force: params.force,
184
+ });
185
+ const parentDir = path.dirname(params.portalDir);
186
+ const tempDir = await mkdtemp(path.join(parentDir, `.${path.basename(params.portalDir)}-pull-`));
187
+ try {
188
+ await cp(params.sourceDir, tempDir, {
189
+ recursive: true,
190
+ filter: (source) => shouldPackPortalSourceEntry(path.relative(params.sourceDir, source)),
191
+ });
192
+ if (targetExists) {
193
+ await replaceExistingPortalDirectory({
194
+ sourceDir: tempDir,
195
+ portalDir: params.portalDir,
196
+ });
197
+ return;
198
+ }
199
+ await rename(tempDir, params.portalDir);
200
+ }
201
+ catch (error) {
202
+ await rm(tempDir, { recursive: true, force: true });
203
+ throw error;
204
+ }
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
+ }
216
+ async function runGit(args, cwd) {
217
+ return await execFileAsync('git', args, {
218
+ cwd,
219
+ maxBuffer: 10 * 1024 * 1024,
220
+ });
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
+ }
249
+ async function installPortalDependencies(params) {
250
+ if (params.installDependencies === false) {
251
+ return {
252
+ dependenciesInstalled: false,
253
+ installSkipped: true,
254
+ installFailed: false,
255
+ };
256
+ }
257
+ if (!(await isFile(path.join(params.portalDir, 'package.json')))) {
258
+ return {
259
+ dependenciesInstalled: false,
260
+ installSkipped: true,
261
+ installFailed: false,
262
+ };
263
+ }
264
+ const runCommand = params.runCommand ?? run;
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
+ }
281
+ return {
282
+ dependenciesInstalled: true,
283
+ installSkipped: false,
284
+ installFailed: false,
285
+ };
286
+ }
287
+ function readSourceRevision(data) {
288
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
289
+ return undefined;
290
+ }
291
+ const direct = data.sourceRevision;
292
+ if (typeof direct === 'string' && direct.trim()) {
293
+ return direct;
294
+ }
295
+ return readSourceRevision(data.data);
296
+ }
297
+ async function resolvePortalSourceContext(options) {
298
+ const portal = validatePortalSlug(options.portal);
299
+ const apiBaseUrl = trimValue(options.env.apiBaseUrl);
300
+ const storagePath = resolvePortalStoragePath(options.env);
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 });
310
+ if (mode !== 'local' && mode !== 'docker' && mode !== 'http') {
311
+ throw new Error(portalSourceText('errors.unsupportedEnvKind', { kind: mode }, `Cannot sync portal source for ${mode} envs in the first version.`));
312
+ }
313
+ const list = await listPortalWorkspaces({
314
+ env: options.env,
315
+ envName: options.envName,
316
+ cliVersion: options.cliVersion,
317
+ apiRequest: options.apiRequest,
318
+ appContext,
319
+ });
320
+ const item = findPortalListItem(list.items, portal);
321
+ if (!item) {
322
+ throw new Error(portalSourceText('errors.notFound', { portal }, `Portal "${portal}" was not found. Run \`nb portal list\` to see available portals.`));
323
+ }
324
+ return {
325
+ app,
326
+ portal,
327
+ portalDir,
328
+ portalBase,
329
+ apiBaseUrl,
330
+ mode,
331
+ sourceStorage: item.sourceStorage || 'nocobase',
332
+ gitRepo: item.gitRepo,
333
+ gitBranch: item.gitBranch || 'main',
334
+ gitPath: item.gitPath || DEFAULT_PORTAL_GIT_PATH,
335
+ options: item.options,
336
+ };
337
+ }
338
+ function buildPortalConfigFromContext(context) {
339
+ if (Object.keys(context.options).length > 0) {
340
+ return buildPortalConfigFromOptions(context.options, context.portal);
341
+ }
342
+ const sourceStorage = context.sourceStorage || 'nocobase';
343
+ return buildPortalConfig({
344
+ portal: context.portal,
345
+ sourceStorage,
346
+ gitRepo: sourceStorage === 'git' ? context.gitRepo : undefined,
347
+ gitBranch: sourceStorage === 'git' ? context.gitBranch : undefined,
348
+ gitPath: sourceStorage === 'git' ? context.gitPath : undefined,
349
+ });
350
+ }
351
+ function applyPortalConfigToContext(context, config) {
352
+ return {
353
+ ...context,
354
+ sourceStorage: config.sourceStorage,
355
+ gitRepo: config.git?.repo ?? '',
356
+ gitBranch: config.git?.branch ?? 'main',
357
+ gitPath: config.git?.path ?? DEFAULT_PORTAL_GIT_PATH,
358
+ };
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
+ }
379
+ function assertGitSourceConfig(context) {
380
+ if (!context.gitRepo) {
381
+ throw new Error(portalSourceText('errors.gitRepoMissing', { portal: context.portal }, `Portal "${context.portal}" uses Git source storage, but gitRepo is missing.`));
382
+ }
383
+ return {
384
+ repo: context.gitRepo,
385
+ branch: context.gitBranch || 'main',
386
+ gitPath: context.gitPath || DEFAULT_PORTAL_GIT_PATH,
387
+ };
388
+ }
389
+ function isGitRepositoryRootPath(gitPath) {
390
+ return gitPath === DEFAULT_PORTAL_GIT_PATH;
391
+ }
392
+ async function copyPortalSourceToGitPath(params) {
393
+ if (isGitRepositoryRootPath(params.gitPath)) {
394
+ const existingEntries = await readdir(params.repoDir);
395
+ await Promise.all(existingEntries
396
+ .filter((entry) => entry !== '.git')
397
+ .map((entry) => rm(path.join(params.repoDir, entry), { recursive: true, force: true })));
398
+ const sourceEntries = (await readdir(params.portalDir)).filter(shouldPackPortalSourceEntry);
399
+ await Promise.all(sourceEntries.map((entry) => cp(path.join(params.portalDir, entry), path.join(params.repoDir, entry), {
400
+ recursive: true,
401
+ filter: (source) => shouldPackPortalSourceEntry(path.relative(params.portalDir, source)),
402
+ })));
403
+ return;
404
+ }
405
+ const targetDir = path.join(params.repoDir, params.gitPath);
406
+ await rm(targetDir, { recursive: true, force: true });
407
+ await mkdir(path.dirname(targetDir), { recursive: true });
408
+ await cp(params.portalDir, targetDir, {
409
+ recursive: true,
410
+ filter: (source) => shouldPackPortalSourceEntry(path.relative(params.portalDir, source)),
411
+ });
412
+ }
413
+ async function cloneGitSource(params) {
414
+ const repoDir = path.join(params.cwd, 'repo');
415
+ try {
416
+ await runGit(['clone', '--branch', params.branch, params.repo, repoDir]);
417
+ }
418
+ catch (error) {
419
+ if (!params.createBranch) {
420
+ throw error;
421
+ }
422
+ await runGit(['clone', params.repo, repoDir]);
423
+ await runGit(['checkout', '-B', params.branch], repoDir);
424
+ }
425
+ return repoDir;
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
+ }
486
+ async function pullGitPortalSource(params) {
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
+ }
497
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), 'nocobase-cli-portal-git-pull-'));
498
+ try {
499
+ const repoDir = await cloneGitSource({
500
+ repo: git.repo,
501
+ branch: git.branch,
502
+ cwd: tempDir,
503
+ createBranch: true,
504
+ });
505
+ const sourceDir = path.join(repoDir, git.gitPath);
506
+ if (!(await pathExists(sourceDir))) {
507
+ throw new Error(portalSourceText('errors.gitPathMissing', { gitPath: git.gitPath }, `Git path does not exist in the configured repository: ${git.gitPath}`));
508
+ }
509
+ await mkdir(path.dirname(params.context.portalDir), { recursive: true });
510
+ await replacePortalSourceFromDirectory({
511
+ sourceDir,
512
+ portalDir: params.context.portalDir,
513
+ force: params.force,
514
+ });
515
+ }
516
+ finally {
517
+ await rm(tempDir, { recursive: true, force: true });
518
+ }
519
+ }
520
+ async function pushGitPortalSource(params) {
521
+ const git = assertGitSourceConfig(params.context);
522
+ const localIdentity = await resolveLocalGitIdentity(params.context.portalDir);
523
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), 'nocobase-cli-portal-git-push-'));
524
+ try {
525
+ const repoDir = await cloneGitSource({
526
+ repo: git.repo,
527
+ branch: git.branch,
528
+ cwd: tempDir,
529
+ createBranch: true,
530
+ });
531
+ await copyPortalSourceToGitPath({
532
+ portalDir: params.context.portalDir,
533
+ repoDir,
534
+ gitPath: git.gitPath,
535
+ });
536
+ await runGit(['add', git.gitPath], repoDir);
537
+ const status = await runGit(['status', '--porcelain', '--', git.gitPath], repoDir);
538
+ if (!status.stdout.trim()) {
539
+ return undefined;
540
+ }
541
+ const commitIdentity = localIdentity ?? NOCOBASE_CLI_GIT_IDENTITY;
542
+ const commitArgs = [
543
+ '-c',
544
+ `user.name=${commitIdentity.name}`,
545
+ '-c',
546
+ `user.email=${commitIdentity.email}`,
547
+ 'commit',
548
+ '-m',
549
+ trimValue(params.message) || `chore(portal): update ${params.context.portal}`,
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);
555
+ await runGit(['push', 'origin', git.branch], repoDir);
556
+ const revision = await runGit(['rev-parse', 'HEAD'], repoDir);
557
+ return revision.stdout.trim();
558
+ }
559
+ finally {
560
+ await rm(tempDir, { recursive: true, force: true });
561
+ }
562
+ }
563
+ export async function pullPortalSource(options) {
564
+ const context = await resolvePortalSourceContext({
565
+ ...options,
566
+ defaultSourcePath: true,
567
+ });
568
+ const portalConfig = getTemporaryGitPortalConfig(options) ?? buildPortalConfigFromContext(context);
569
+ const sourceContext = applyPortalConfigToContext(context, portalConfig);
570
+ if (sourceContext.sourceStorage === 'git') {
571
+ await pullGitPortalSource({
572
+ context: sourceContext,
573
+ force: options.force,
574
+ });
575
+ await ensurePortalBuildHtmlReadsEnvOnly(sourceContext.portalDir);
576
+ await updatePortalEnvFiles({
577
+ portalDir: sourceContext.portalDir,
578
+ apiBaseUrl: sourceContext.apiBaseUrl,
579
+ portalBase: sourceContext.portalBase,
580
+ });
581
+ const installResult = await installPortalDependencies({
582
+ portalDir: sourceContext.portalDir,
583
+ installDependencies: options.installDependencies,
584
+ runCommand: options.runCommand,
585
+ });
586
+ return {
587
+ ...sourceContext,
588
+ changed: true,
589
+ ...installResult,
590
+ };
591
+ }
592
+ if (sourceContext.sourceStorage !== 'nocobase') {
593
+ throw new Error(portalSourceText('errors.unsupportedSourceStorage', { sourceStorage: sourceContext.sourceStorage }, `Unsupported portal source storage: ${sourceContext.sourceStorage}`));
594
+ }
595
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), 'nocobase-cli-portal-pull-'));
596
+ const archivePath = path.join(tempDir, 'source.tar.gz');
597
+ const apiRequest = options.apiRequest ?? executeApiRequest;
598
+ try {
599
+ const response = await apiRequest({
600
+ cliVersion: options.cliVersion ?? '',
601
+ envName: options.envName,
602
+ flags: {
603
+ app: sourceContext.app,
604
+ portal: sourceContext.portal,
605
+ output: archivePath,
606
+ },
607
+ operation: PULL_SOURCE_OPERATION,
608
+ });
609
+ if (!response.ok) {
610
+ 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)}`));
611
+ }
612
+ await mkdir(path.dirname(sourceContext.portalDir), { recursive: true });
613
+ await replacePortalSourceFromArchive({
614
+ archivePath,
615
+ portalDir: sourceContext.portalDir,
616
+ force: options.force,
617
+ });
618
+ await ensurePortalBuildHtmlReadsEnvOnly(sourceContext.portalDir);
619
+ await updatePortalEnvFiles({
620
+ portalDir: sourceContext.portalDir,
621
+ apiBaseUrl: sourceContext.apiBaseUrl,
622
+ portalBase: sourceContext.portalBase,
623
+ });
624
+ const installResult = await installPortalDependencies({
625
+ portalDir: sourceContext.portalDir,
626
+ installDependencies: options.installDependencies,
627
+ runCommand: options.runCommand,
628
+ });
629
+ return {
630
+ ...sourceContext,
631
+ changed: true,
632
+ ...installResult,
633
+ };
634
+ }
635
+ finally {
636
+ await rm(tempDir, { recursive: true, force: true });
637
+ }
638
+ }
639
+ export async function pushPortalSource(options) {
640
+ const context = await resolvePortalSourceContext({
641
+ ...options,
642
+ defaultSourcePath: true,
643
+ });
644
+ if (!(await pathExists(context.portalDir))) {
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.`));
646
+ }
647
+ const portalConfig = buildPortalConfigFromContext(context);
648
+ const sourceContext = applyPortalConfigToContext(context, portalConfig);
649
+ if (sourceContext.sourceStorage === 'git') {
650
+ const revision = await pushGitPortalSource({
651
+ context: sourceContext,
652
+ message: options.message,
653
+ });
654
+ return {
655
+ ...sourceContext,
656
+ changed: Boolean(revision),
657
+ sourceRevision: revision,
658
+ noopReason: revision
659
+ ? undefined
660
+ : portalSourceText('messages.gitPushNoop', undefined, 'No local source changes to push.'),
661
+ };
662
+ }
663
+ if (sourceContext.sourceStorage !== 'nocobase') {
664
+ throw new Error(portalSourceText('errors.unsupportedSourceStorage', { sourceStorage: sourceContext.sourceStorage }, `Unsupported portal source storage: ${sourceContext.sourceStorage}`));
665
+ }
666
+ const archive = await packPortalSource(sourceContext.portalDir);
667
+ const apiRequest = options.apiRequest ?? executeApiRequest;
668
+ try {
669
+ const response = await apiRequest({
670
+ cliVersion: options.cliVersion ?? '',
671
+ envName: options.envName,
672
+ flags: {
673
+ file: archive.archivePath,
674
+ app: sourceContext.app,
675
+ portal: sourceContext.portal,
676
+ message: options.message,
677
+ },
678
+ operation: PUSH_SOURCE_OPERATION,
679
+ });
680
+ if (!response.ok) {
681
+ 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)}`));
682
+ }
683
+ return {
684
+ ...sourceContext,
685
+ changed: true,
686
+ sourceRevision: readSourceRevision(response.data),
687
+ };
688
+ }
689
+ finally {
690
+ await archive.cleanup();
691
+ }
692
+ }