@nocobase/cli 3.0.0-alpha.1 → 3.0.0-alpha.12

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 (52) hide show
  1. package/dist/commands/api/resource/create.js +11 -2
  2. package/dist/commands/api/swagger/get.js +96 -0
  3. package/dist/commands/api/swagger/index.js +20 -0
  4. package/dist/commands/api/swagger/list.js +92 -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 +38 -0
  8. package/dist/commands/install.js +18 -62
  9. package/dist/commands/portal/config.js +16 -5
  10. package/dist/commands/portal/create.js +17 -26
  11. package/dist/commands/portal/destroy.js +28 -6
  12. package/dist/commands/portal/dev.js +2 -0
  13. package/dist/commands/portal/list.js +5 -5
  14. package/dist/commands/portal/pull.js +32 -3
  15. package/dist/lib/api-client.js +35 -9
  16. package/dist/lib/app-client-entry-mode.js +28 -0
  17. package/dist/lib/app-managed-resources.js +2 -0
  18. package/dist/lib/auth-store.js +52 -1
  19. package/dist/lib/bootstrap.js +3 -1
  20. package/dist/lib/browser.js +29 -0
  21. package/dist/lib/env-auth.js +2 -36
  22. package/dist/lib/env-command-config.js +1 -0
  23. package/dist/lib/env-config.js +10 -2
  24. package/dist/lib/env-portal-config.js +30 -0
  25. package/dist/lib/env-proxy.js +25 -4
  26. package/dist/lib/generated-command.js +81 -0
  27. package/dist/lib/managed-env-file.js +67 -13
  28. package/dist/lib/managed-init-env.js +0 -2
  29. package/dist/lib/plugin-import.js +30 -8
  30. package/dist/lib/portal-build-html.js +27 -0
  31. package/dist/lib/portal-config.js +6 -20
  32. package/dist/lib/portal-configure.js +49 -56
  33. package/dist/lib/portal-create.js +96 -14
  34. package/dist/lib/portal-deploy.js +30 -47
  35. package/dist/lib/portal-destroy.js +34 -20
  36. package/dist/lib/portal-dev.js +6 -7
  37. package/dist/lib/portal-env-files.js +2 -1
  38. package/dist/lib/portal-info.js +2 -5
  39. package/dist/lib/portal-list.js +20 -26
  40. package/dist/lib/portal-path-safety.js +76 -0
  41. package/dist/lib/portal-source.js +227 -58
  42. package/dist/lib/prompt-catalog-core.js +2 -2
  43. package/dist/lib/prompt-catalog-terminal.js +4 -5
  44. package/dist/lib/prompt-web-ui.js +12 -6
  45. package/dist/lib/resource-command.js +18 -2
  46. package/dist/lib/resource-request.js +8 -0
  47. package/dist/lib/run-npm.js +68 -4
  48. package/dist/lib/runtime-generator.js +28 -1
  49. package/dist/lib/swagger-command.js +52 -0
  50. package/dist/locale/en-US.json +81 -15
  51. package/dist/locale/zh-CN.json +81 -15
  52. package/package.json +2 -2
@@ -6,43 +6,23 @@
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 { mkdir, stat } from 'node:fs/promises';
10
- import path from 'node:path';
11
9
  import { translateCli } from './cli-locale.js';
12
- import { resolvePortalAppFromApiBaseUrl, resolvePortalStoragePath, validatePortalSlug, } from './portal-create.js';
13
- import { buildPortalConfig, buildPortalConfigFromOptions, readPortalConfig, syncPortalConfigToRemote, writePortalConfig, } from './portal-config.js';
10
+ import { resolvePortalAppContext, resolvePortalSourcePath, resolveSavedPortalSourcePath, validatePortalSlug, } from './portal-create.js';
11
+ import { buildPortalConfig, buildPortalConfigFromOptions, syncPortalConfigToRemote, } from './portal-config.js';
14
12
  import { findPortalListItem } from './portal-info.js';
15
13
  import { listPortalWorkspaces } from './portal-list.js';
16
14
  const portalConfigureText = (key, values, fallback) => translateCli(`commands.portalConfigure.${key}`, values, { fallback });
17
15
  function trimValue(value) {
18
16
  return String(value ?? '').trim();
19
17
  }
20
- async function pathExists(target) {
21
- try {
22
- await stat(target);
23
- return true;
24
- }
25
- catch {
26
- return false;
27
- }
28
- }
29
- function hasConfigurationChange(options) {
18
+ function hasSourceConfigurationChange(options) {
30
19
  return Boolean(options.sourceStorage !== undefined ||
31
20
  trimValue(options.gitRepo) ||
32
21
  trimValue(options.gitBranch) ||
33
22
  trimValue(options.gitPath));
34
23
  }
35
- async function readExistingConfig(portalDir) {
36
- try {
37
- return await readPortalConfig(portalDir);
38
- }
39
- catch (error) {
40
- const code = error.code;
41
- if (code === 'ENOENT') {
42
- return undefined;
43
- }
44
- throw error;
45
- }
24
+ function hasPathConfigurationChange(options) {
25
+ return Boolean(trimValue(options.sourcePath));
46
26
  }
47
27
  function buildConfigFromRemoteOptions(params) {
48
28
  if (params.options && Object.keys(params.options).length > 0) {
@@ -60,34 +40,49 @@ function buildConfigFromRemoteOptions(params) {
60
40
  });
61
41
  }
62
42
  export async function configurePortalWorkspace(options) {
63
- if (!hasConfigurationChange(options)) {
64
- throw new Error(portalConfigureText('errors.noChanges', undefined, 'No portal configuration changes were provided. Pass --source-storage or a --git-* flag.'));
43
+ const hasSourceChange = hasSourceConfigurationChange(options);
44
+ const hasPathChange = hasPathConfigurationChange(options);
45
+ if (!hasSourceChange && !hasPathChange) {
46
+ throw new Error(portalConfigureText('errors.noChanges', undefined, 'No portal configuration changes were provided. Pass --path, --source-storage, or a --git-* flag.'));
65
47
  }
66
48
  const portal = validatePortalSlug(options.portal);
67
- const apiBaseUrl = trimValue(options.env.apiBaseUrl);
68
- const storagePath = resolvePortalStoragePath(options.env);
69
- const { app } = resolvePortalAppFromApiBaseUrl(apiBaseUrl, options.env.config.appPublicPath);
70
- const portalDir = path.join(storagePath, 'portals', app, portal);
71
- if (!(await pathExists(portalDir))) {
72
- throw new Error(portalConfigureText('errors.workspaceMissing', { portalDir, portal }, `Portal does not exist: ${portalDir}\nRun \`nb portal create ${portal}\` or \`nb portal pull ${portal}\` first.`));
73
- }
49
+ const portalDir = hasPathChange
50
+ ? resolvePortalSourcePath(portal, options.sourcePath)
51
+ : resolveSavedPortalSourcePath(options.env, portal) ?? '';
52
+ const appContext = await resolvePortalAppContext(options);
53
+ const { app } = appContext;
54
+ let config;
55
+ let remoteSynced = false;
74
56
  const list = await listPortalWorkspaces({
75
57
  env: options.env,
76
58
  envName: options.envName,
77
59
  cliVersion: options.cliVersion,
78
60
  apiRequest: options.apiRequest,
61
+ appContext,
79
62
  });
80
63
  const remoteItem = findPortalListItem(list.items, portal);
81
- const existingConfig = (await readExistingConfig(portalDir)) ??
82
- buildConfigFromRemoteOptions({
64
+ if (!remoteItem) {
65
+ throw new Error(portalConfigureText('errors.notFound', { portal }, `Portal "${portal}" was not found. Run \`nb portal list\` to see available portals.`));
66
+ }
67
+ if (!hasSourceChange) {
68
+ return {
69
+ app,
83
70
  portal,
84
- options: remoteItem?.options,
85
- sourceStorage: remoteItem?.sourceStorage,
86
- gitRepo: remoteItem?.gitRepo,
87
- gitBranch: remoteItem?.gitBranch,
88
- gitPath: remoteItem?.gitPath,
89
- });
90
- const config = buildPortalConfig({
71
+ portalDir,
72
+ config: undefined,
73
+ remoteSynced: false,
74
+ pathUpdated: hasPathChange,
75
+ };
76
+ }
77
+ const existingConfig = buildConfigFromRemoteOptions({
78
+ portal,
79
+ options: remoteItem.options,
80
+ sourceStorage: remoteItem.sourceStorage,
81
+ gitRepo: remoteItem.gitRepo,
82
+ gitBranch: remoteItem.gitBranch,
83
+ gitPath: remoteItem.gitPath,
84
+ });
85
+ config = buildPortalConfig({
91
86
  portal,
92
87
  sourceStorage: options.sourceStorage,
93
88
  gitRepo: options.gitRepo,
@@ -95,23 +90,21 @@ export async function configurePortalWorkspace(options) {
95
90
  gitPath: options.gitPath,
96
91
  existingConfig,
97
92
  });
98
- await mkdir(portalDir, { recursive: true });
99
- await writePortalConfig(portalDir, config);
100
- if (remoteItem) {
101
- await syncPortalConfigToRemote({
102
- portal,
103
- config,
104
- currentOptions: remoteItem.options,
105
- envName: options.envName,
106
- cliVersion: options.cliVersion,
107
- apiRequest: options.apiRequest,
108
- });
109
- }
93
+ await syncPortalConfigToRemote({
94
+ portal,
95
+ config,
96
+ currentOptions: remoteItem.options,
97
+ envName: options.envName,
98
+ cliVersion: options.cliVersion,
99
+ apiRequest: options.apiRequest,
100
+ });
101
+ remoteSynced = true;
110
102
  return {
111
103
  app,
112
104
  portal,
113
105
  portalDir,
114
106
  config,
115
- remoteSynced: Boolean(remoteItem),
107
+ remoteSynced,
108
+ pathUpdated: hasPathChange,
116
109
  };
117
110
  }
@@ -17,11 +17,14 @@ import { createGunzip } from 'node:zlib';
17
17
  import * as tar from 'tar';
18
18
  import { appendAppPublicPath, resolveAppPublicPath } from './app-public-path.js';
19
19
  import { executeApiRequest } from './api-client.js';
20
+ import { resolveEnvPortalPath } from './auth-store.js';
20
21
  import { resolveEnvRelativePath } from './cli-home.js';
21
22
  import { translateCli } from './cli-locale.js';
23
+ import { ensurePortalBuildHtmlReadsEnvOnly } from './portal-build-html.js';
22
24
  import { buildPortalCommandEnv } from './portal-command-env.js';
23
- import { buildPortalConfig, mergePortalConfigIntoOptions, writePortalConfig, } from './portal-config.js';
24
- import { run } from './run-npm.js';
25
+ import { canReplacePortalDirectory } from './portal-path-safety.js';
26
+ import { buildPortalConfig, mergePortalConfigIntoOptions, } from './portal-config.js';
27
+ import { resolvePnpmInstallCommand, run, runPnpmInstallCommand } from './run-npm.js';
25
28
  const DEFAULT_PORTAL_TEMPLATE = '@nocobase/portal-template-default';
26
29
  const DEFAULT_PORTAL_APP_NAME = 'main';
27
30
  const TEMPLATE_COPY_EXCLUDED_NAMES = new Set(['.git', 'node_modules', '.DS_Store']);
@@ -42,6 +45,11 @@ const FIRST_OR_CREATE_PORTAL_OPERATION = {
42
45
  },
43
46
  ],
44
47
  };
48
+ const APP_INFO_OPERATION = {
49
+ method: 'GET',
50
+ pathTemplate: '/app:getInfo',
51
+ parameters: [],
52
+ };
45
53
  function trimValue(value) {
46
54
  return String(value ?? '').trim();
47
55
  }
@@ -319,6 +327,54 @@ export function resolvePortalAppFromApiBaseUrl(apiBaseUrl, appPublicPath) {
319
327
  appPublicPath: resolveAppPublicPath(configuredPublicPath || inferredPublicPath),
320
328
  };
321
329
  }
330
+ function readAppNameFromInfo(data) {
331
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
332
+ return undefined;
333
+ }
334
+ const direct = data.name;
335
+ if (typeof direct === 'string' && direct.trim()) {
336
+ return direct.trim();
337
+ }
338
+ return readAppNameFromInfo(data.data);
339
+ }
340
+ function isValidPortalAppName(value) {
341
+ return /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(value);
342
+ }
343
+ async function resolvePortalAppFromServer(options) {
344
+ const apiRequest = options.apiRequest ?? executeApiRequest;
345
+ try {
346
+ const response = await apiRequest({
347
+ cliVersion: options.cliVersion ?? '',
348
+ envName: options.envName,
349
+ flags: {},
350
+ operation: APP_INFO_OPERATION,
351
+ });
352
+ if (!response.ok) {
353
+ return undefined;
354
+ }
355
+ const appName = readAppNameFromInfo(response.data);
356
+ return appName && isValidPortalAppName(appName) ? appName : undefined;
357
+ }
358
+ catch {
359
+ return undefined;
360
+ }
361
+ }
362
+ export async function resolvePortalAppContext(options) {
363
+ const apiBaseUrl = trimValue(options.env.apiBaseUrl);
364
+ const resolvedApp = resolvePortalAppFromApiBaseUrl(apiBaseUrl, options.env.config.appPublicPath);
365
+ if (options.env.kind !== 'http') {
366
+ return {
367
+ ...resolvedApp,
368
+ portalBaseApp: resolvedApp.app,
369
+ };
370
+ }
371
+ const serverApp = await resolvePortalAppFromServer(options);
372
+ return {
373
+ app: serverApp ?? resolvedApp.app,
374
+ appPublicPath: resolvedApp.appPublicPath,
375
+ portalBaseApp: resolvedApp.app,
376
+ };
377
+ }
322
378
  export function buildPortalBasePath(params) {
323
379
  const segment = params.app === DEFAULT_PORTAL_APP_NAME
324
380
  ? `x/${params.portal}`
@@ -350,6 +406,19 @@ export function resolvePortalStoragePath(env) {
350
406
  }
351
407
  return path.resolve(process.cwd(), 'storage');
352
408
  }
409
+ function resolveAbsolutePath(value) {
410
+ return path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
411
+ }
412
+ export function resolvePortalSourcePath(portal, sourcePath) {
413
+ return resolveAbsolutePath(trimValue(sourcePath) || portal);
414
+ }
415
+ export function resolveSavedPortalSourcePath(env, portal) {
416
+ const sourcePath = resolveEnvPortalPath(env.config, portal);
417
+ return sourcePath ? resolveAbsolutePath(sourcePath) : undefined;
418
+ }
419
+ export function resolvePortalDeployPath(params) {
420
+ return path.join(params.storagePath, 'portals', params.app, params.portal);
421
+ }
353
422
  export async function createPortalWorkspace(options) {
354
423
  const portal = validatePortalSlug(options.portal);
355
424
  const title = trimValue(options.title) || titleFromPortalSlug(portal);
@@ -362,16 +431,18 @@ export async function createPortalWorkspace(options) {
362
431
  });
363
432
  const apiBaseUrl = trimValue(options.env.apiBaseUrl);
364
433
  const envApiUrl = resolvePortalEnvApiUrl(apiBaseUrl);
365
- const storagePath = resolvePortalStoragePath(options.env);
366
- const { app, appPublicPath } = resolvePortalAppFromApiBaseUrl(apiBaseUrl, options.env.config.appPublicPath);
367
- const portalBase = buildPortalBasePath({ app, appPublicPath, portal });
368
- const portalParentDir = path.join(storagePath, 'portals', app);
369
- const portalDir = path.join(portalParentDir, portal);
434
+ const { app, appPublicPath, portalBaseApp } = await resolvePortalAppContext(options);
435
+ const portalBase = buildPortalBasePath({ app: portalBaseApp ?? app, appPublicPath, portal });
436
+ const portalDir = resolvePortalSourcePath(portal, options.sourcePath);
437
+ const portalParentDir = path.dirname(portalDir);
370
438
  assertPortalDirIsInsideParent(portalParentDir, portalDir);
371
439
  const targetExists = await pathExists(portalDir);
372
440
  if (targetExists && !options.force) {
373
441
  throw new Error(portalCreateText('errors.workspaceExists', { portalDir }, `Portal already exists: ${portalDir}\nPass --force to delete it and create a new portal.`));
374
442
  }
443
+ if (targetExists && options.force && !(await canReplacePortalDirectory(portalDir))) {
444
+ throw new Error(portalCreateText('errors.workspaceNotReplaceable', { portalDir }, `Refusing to replace a non-portal directory: ${portalDir}`));
445
+ }
375
446
  const template = await resolvePortalTemplate(options.template, {
376
447
  npmRegistry: trimValue(options.env.config.npmRegistry),
377
448
  runCommand: options.runCommand,
@@ -381,23 +452,32 @@ export async function createPortalWorkspace(options) {
381
452
  let shouldCleanupTempDir = true;
382
453
  try {
383
454
  await copyTemplate(template.dir, tempDir);
455
+ await ensurePortalBuildHtmlReadsEnvOnly(tempDir);
384
456
  await writeFile(path.join(tempDir, '.env'), [`NOCOBASE_API_URL=${envApiUrl}`, `NOCOBASE_PORTAL_BASE=${portalBase}`].join('\n') + '\n', 'utf-8');
385
457
  await writeFile(path.join(tempDir, '.env.local'), [`NOCOBASE_API_URL=${apiBaseUrl}`, `NOCOBASE_PORTAL_BASE=${portalBase}`].join('\n') + '\n', 'utf-8');
386
- await writePortalConfig(tempDir, portalConfig);
387
458
  if (targetExists) {
388
459
  await rm(portalDir, { recursive: true, force: true });
389
460
  }
390
461
  await rename(tempDir, portalDir);
391
462
  shouldCleanupTempDir = false;
392
463
  const hasPackageJson = await pathExists(path.join(portalDir, 'package.json'));
464
+ let dependenciesInstalled = false;
465
+ let installFailed = false;
393
466
  if (hasPackageJson) {
394
467
  const runCommand = options.runCommand ?? run;
395
- await runCommand('pnpm', ['install'], {
396
- cwd: portalDir,
397
- env: buildPortalCommandEnv(),
398
- envMode: 'replace',
399
- errorName: 'pnpm install',
400
- });
468
+ const installCommand = await resolvePnpmInstallCommand(portalDir);
469
+ try {
470
+ await runPnpmInstallCommand(runCommand, installCommand.args, {
471
+ cwd: portalDir,
472
+ env: buildPortalCommandEnv(),
473
+ envMode: 'replace',
474
+ errorName: installCommand.errorName,
475
+ });
476
+ dependenciesInstalled = true;
477
+ }
478
+ catch {
479
+ installFailed = true;
480
+ }
401
481
  }
402
482
  else {
403
483
  options.onSkipInstall?.(portalCreateText('messages.skipInstall', { portalDir }, `Skipped pnpm install because package.json was not found in ${portalDir}.`));
@@ -421,6 +501,8 @@ export async function createPortalWorkspace(options) {
421
501
  portalBase,
422
502
  template,
423
503
  installSkipped: !hasPackageJson,
504
+ dependenciesInstalled,
505
+ installFailed,
424
506
  sourceStorage: portalConfig.sourceStorage,
425
507
  };
426
508
  }
@@ -12,11 +12,11 @@ import path from 'node:path';
12
12
  import * as tar from 'tar';
13
13
  import { executeApiRequest } from './api-client.js';
14
14
  import { translateCli } from './cli-locale.js';
15
- import { buildPortalBasePath, resolvePortalAppFromApiBaseUrl, resolvePortalStoragePath, titleFromPortalSlug, validatePortalSlug, } from './portal-create.js';
15
+ import { ensurePortalBuildHtmlReadsEnvOnly } from './portal-build-html.js';
16
+ import { buildPortalBasePath, resolvePortalAppContext, resolvePortalEnvApiUrl, resolveSavedPortalSourcePath, resolvePortalSourcePath, titleFromPortalSlug, validatePortalSlug, } from './portal-create.js';
16
17
  import { buildPortalCommandEnv } from './portal-command-env.js';
17
18
  import { updatePortalEnvFiles } from './portal-env-files.js';
18
- import { mergePortalConfigIntoOptions, readPortalConfig } from './portal-config.js';
19
- import { run } from './run-npm.js';
19
+ import { resolvePnpmInstallCommand, run, runPnpmCommand, runPnpmInstallCommand } from './run-npm.js';
20
20
  const portalDeployText = (key, values, fallback) => translateCli(`commands.portalDeploy.${key}`, values, { fallback });
21
21
  const DEPLOY_OPERATION = {
22
22
  method: 'POST',
@@ -106,12 +106,6 @@ async function chmodPortalDistTree(targetDir) {
106
106
  }
107
107
  }));
108
108
  }
109
- async function ensurePortalDistPublicReadable(params) {
110
- await chmod(path.join(params.storagePath, 'portals'), PORTAL_PUBLIC_DIR_MODE);
111
- await chmod(path.join(params.storagePath, 'portals', params.app), PORTAL_PUBLIC_DIR_MODE);
112
- await chmod(params.portalDir, PORTAL_PUBLIC_DIR_MODE);
113
- await chmodPortalDistTree(params.distDir);
114
- }
115
109
  async function assertFileExists(filePath, message) {
116
110
  try {
117
111
  const fileStat = await stat(filePath);
@@ -173,9 +167,6 @@ async function syncMultiPortalRecord(params) {
173
167
  uiLayoutUid: DEFAULT_PORTAL_UI_LAYOUT_UID,
174
168
  skipCreatePortalDirectory: true,
175
169
  };
176
- if (params.config) {
177
- body.options = mergePortalConfigIntoOptions(params.config);
178
- }
179
170
  const response = await apiRequest({
180
171
  cliVersion: params.cliVersion ?? '',
181
172
  envName: params.envName,
@@ -192,23 +183,31 @@ async function syncMultiPortalRecord(params) {
192
183
  export async function deployPortalWorkspace(options) {
193
184
  const portal = validatePortalSlug(options.portal);
194
185
  const apiBaseUrl = trimValue(options.env.apiBaseUrl);
195
- const storagePath = resolvePortalStoragePath(options.env);
196
- const { app, appPublicPath } = resolvePortalAppFromApiBaseUrl(apiBaseUrl, options.env.config.appPublicPath);
197
- const portalBase = buildPortalBasePath({ app, appPublicPath, portal });
198
- const portalDir = path.join(storagePath, 'portals', app, portal);
186
+ const envApiUrl = resolvePortalEnvApiUrl(apiBaseUrl);
187
+ const { app, appPublicPath, portalBaseApp } = await resolvePortalAppContext(options);
188
+ const portalBase = buildPortalBasePath({ app: portalBaseApp ?? app, appPublicPath, portal });
189
+ const deployBase = buildPortalBasePath({ app, appPublicPath, portal });
190
+ const portalDir = resolveSavedPortalSourcePath(options.env, portal) ?? resolvePortalSourcePath(portal);
199
191
  const distDir = path.join(portalDir, 'dist');
200
192
  if (!(await pathExists(portalDir))) {
201
193
  throw new Error(portalDeployText('errors.workspaceMissing', { portalDir, portal }, `Portal does not exist: ${portalDir}\nRun \`nb portal create ${portal}\` first.`));
202
194
  }
203
195
  await assertFileExists(path.join(portalDir, 'package.json'), portalDeployText('errors.packageJsonMissing', { portalDir }, `Portal is invalid: package.json is missing in ${portalDir}.`));
204
- const portalConfig = await readPortalConfig(portalDir);
205
196
  await updatePortalEnvFiles({
206
197
  portalDir,
207
198
  apiBaseUrl,
208
199
  portalBase,
209
200
  });
201
+ await ensurePortalBuildHtmlReadsEnvOnly(portalDir);
210
202
  const runCommand = options.runCommand ?? run;
211
- await runCommand('pnpm', ['build'], {
203
+ const installCommand = await resolvePnpmInstallCommand(portalDir);
204
+ await runPnpmInstallCommand(runCommand, installCommand.args, {
205
+ cwd: portalDir,
206
+ env: buildPortalCommandEnv(),
207
+ envMode: 'replace',
208
+ errorName: installCommand.errorName,
209
+ });
210
+ await runPnpmCommand(runCommand, ['build'], {
212
211
  cwd: portalDir,
213
212
  env: buildPortalCommandEnv({
214
213
  NOCOBASE_API_URL: apiBaseUrl,
@@ -217,33 +216,18 @@ export async function deployPortalWorkspace(options) {
217
216
  envMode: 'replace',
218
217
  errorName: 'pnpm build',
219
218
  });
220
- await assertFileExists(path.join(distDir, 'index.html'), portalDeployText('errors.distMissing', { distDir }, `Portal build did not produce ${path.join(distDir, 'index.html')}.`));
221
- await ensurePortalDistPublicReadable({
222
- storagePath,
223
- app,
224
- portalDir,
225
- distDir,
219
+ await runPnpmCommand(runCommand, ['build:html'], {
220
+ cwd: portalDir,
221
+ env: buildPortalCommandEnv({
222
+ NOCOBASE_API_URL: envApiUrl,
223
+ NOCOBASE_PORTAL_BASE: portalBase,
224
+ }),
225
+ envMode: 'replace',
226
+ errorName: 'pnpm build:html',
226
227
  });
227
- if (options.env.kind === 'local' || options.env.kind === 'docker') {
228
- await syncMultiPortalRecord({
229
- portal,
230
- config: portalConfig,
231
- envName: options.envName,
232
- cliVersion: options.cliVersion,
233
- apiRequest: options.apiRequest,
234
- });
235
- return {
236
- app,
237
- portal,
238
- portalDir,
239
- portalBase,
240
- distDir,
241
- mode: options.env.kind,
242
- uploaded: false,
243
- recordSynced: true,
244
- };
245
- }
246
- if (options.env.kind !== 'http') {
228
+ await assertFileExists(path.join(distDir, 'index.html'), portalDeployText('errors.distMissing', { distDir }, `Portal build did not produce ${path.join(distDir, 'index.html')}.`));
229
+ await chmodPortalDistTree(distDir);
230
+ if (options.env.kind !== 'local' && options.env.kind !== 'docker' && options.env.kind !== 'http') {
247
231
  throw new Error(portalDeployText('errors.unsupportedEnvKind', { kind: options.env.kind }, `Cannot deploy a portal for ${options.env.kind} envs in the first version.`));
248
232
  }
249
233
  const archive = await packPortalDist(distDir);
@@ -253,7 +237,7 @@ export async function deployPortalWorkspace(options) {
253
237
  archivePath: archive.archivePath,
254
238
  app,
255
239
  portal,
256
- portalBase,
240
+ portalBase: deployBase,
257
241
  envName: options.envName,
258
242
  cliVersion: options.cliVersion,
259
243
  apiRequest: options.apiRequest,
@@ -264,7 +248,6 @@ export async function deployPortalWorkspace(options) {
264
248
  }
265
249
  await syncMultiPortalRecord({
266
250
  portal,
267
- config: portalConfig,
268
251
  envName: options.envName,
269
252
  cliVersion: options.cliVersion,
270
253
  apiRequest: options.apiRequest,
@@ -276,7 +259,7 @@ export async function deployPortalWorkspace(options) {
276
259
  portalBase,
277
260
  distDir,
278
261
  serverDistPath: uploadResult.distPath,
279
- mode: 'http',
262
+ mode: options.env.kind,
280
263
  uploaded: true,
281
264
  recordSynced: true,
282
265
  };
@@ -10,23 +10,22 @@ import { rm, stat } from 'node:fs/promises';
10
10
  import path from 'node:path';
11
11
  import { executeApiRequest } from './api-client.js';
12
12
  import { translateCli } from './cli-locale.js';
13
- import { buildPortalBasePath, resolvePortalAppFromApiBaseUrl, resolvePortalStoragePath, validatePortalSlug, } from './portal-create.js';
13
+ import { isUnsafePortalDeletePath } from './portal-path-safety.js';
14
+ import { buildPortalBasePath, resolvePortalAppContext, resolvePortalDeployPath, resolveSavedPortalSourcePath, resolvePortalStoragePath, validatePortalSlug, } from './portal-create.js';
14
15
  const portalDestroyText = (key, values, fallback) => translateCli(`commands.portalDestroy.${key}`, values, { fallback });
15
16
  const DESTROY_PORTAL_OPERATION = {
16
17
  method: 'POST',
17
18
  pathTemplate: '/multiPortals:destroy',
18
19
  parameters: [
19
20
  {
20
- name: 'filterByTk',
21
- flagName: 'filterByTk',
21
+ name: 'filter',
22
+ flagName: 'filter',
22
23
  in: 'query',
24
+ type: 'object',
23
25
  required: true,
24
26
  },
25
27
  ],
26
28
  };
27
- function trimValue(value) {
28
- return String(value ?? '').trim();
29
- }
30
29
  async function pathExists(target) {
31
30
  try {
32
31
  await stat(target);
@@ -48,7 +47,9 @@ async function destroyMultiPortalRecord(params) {
48
47
  cliVersion: params.cliVersion ?? '',
49
48
  envName: params.envName,
50
49
  flags: {
51
- filterByTk: params.portal,
50
+ filter: {
51
+ portalName: params.portal,
52
+ },
52
53
  },
53
54
  operation: DESTROY_PORTAL_OPERATION,
54
55
  });
@@ -62,21 +63,29 @@ async function destroyMultiPortalRecord(params) {
62
63
  }
63
64
  export async function destroyPortalWorkspace(options) {
64
65
  const portal = validatePortalSlug(options.portal);
65
- const apiBaseUrl = trimValue(options.env.apiBaseUrl);
66
66
  const storagePath = resolvePortalStoragePath(options.env);
67
- const { app, appPublicPath } = resolvePortalAppFromApiBaseUrl(apiBaseUrl, options.env.config.appPublicPath);
68
- const portalBase = buildPortalBasePath({ app, appPublicPath, portal });
69
- const portalParentDir = path.join(storagePath, 'portals', app);
70
- const portalDir = path.join(portalParentDir, portal);
67
+ const { app, appPublicPath, portalBaseApp } = await resolvePortalAppContext(options);
68
+ const portalBase = buildPortalBasePath({ app: portalBaseApp ?? app, appPublicPath, portal });
69
+ const portalDeployDir = resolvePortalDeployPath({ storagePath, app, portal });
70
+ const portalDevDir = resolveSavedPortalSourcePath(options.env, portal) ?? '';
71
71
  const mode = options.env.kind;
72
72
  if (mode !== 'local' && mode !== 'docker' && mode !== 'http') {
73
73
  throw new Error(portalDestroyText('errors.unsupportedEnvKind', { kind: mode }, `Cannot destroy a portal for ${mode} envs in the first version.`));
74
74
  }
75
75
  const destroyMode = mode;
76
- assertPortalDirIsInsideParent(portalParentDir, portalDir);
77
- const workspaceExists = await pathExists(portalDir);
78
- if (!workspaceExists && !options.force) {
79
- throw new Error(portalDestroyText('errors.workspaceMissing', { portalDir, portal }, `Portal does not exist: ${portalDir}\nPass --force to ignore missing local files.`));
76
+ if (portalDevDir) {
77
+ assertPortalDirIsInsideParent(path.dirname(portalDevDir), portalDevDir);
78
+ }
79
+ assertPortalDirIsInsideParent(path.dirname(portalDeployDir), portalDeployDir);
80
+ const developmentPathIsDeploymentPath = portalDevDir
81
+ ? path.resolve(portalDevDir) === path.resolve(portalDeployDir)
82
+ : false;
83
+ const deploymentPathExists = await pathExists(portalDeployDir);
84
+ const developmentPathExists = options.deleteDevPath && portalDevDir && !developmentPathIsDeploymentPath
85
+ ? await pathExists(portalDevDir)
86
+ : false;
87
+ if (developmentPathExists && (await isUnsafePortalDeletePath(portalDevDir))) {
88
+ throw new Error(portalDestroyText('errors.unsafeDevelopmentPath', { portalDir: portalDevDir }, `Refusing to delete an unsafe portal development path: ${portalDevDir}`));
80
89
  }
81
90
  const recordDeleted = await destroyMultiPortalRecord({
82
91
  portal,
@@ -85,16 +94,21 @@ export async function destroyPortalWorkspace(options) {
85
94
  force: options.force,
86
95
  apiRequest: options.apiRequest,
87
96
  });
88
- if (workspaceExists) {
89
- await rm(portalDir, { recursive: true, force: true });
97
+ if (deploymentPathExists) {
98
+ await rm(portalDeployDir, { recursive: true, force: true });
99
+ }
100
+ if (developmentPathExists) {
101
+ await rm(portalDevDir, { recursive: true, force: true });
90
102
  }
91
103
  return {
92
104
  app,
93
105
  portal,
94
- portalDir,
106
+ developmentPath: portalDevDir,
107
+ deploymentPath: portalDeployDir,
95
108
  portalBase,
96
109
  mode: destroyMode,
97
110
  recordDeleted,
98
- workspaceDeleted: workspaceExists,
111
+ developmentPathDeleted: developmentPathExists || (developmentPathIsDeploymentPath && deploymentPathExists),
112
+ deploymentPathDeleted: deploymentPathExists,
99
113
  };
100
114
  }
@@ -9,10 +9,10 @@
9
9
  import { stat } from 'node:fs/promises';
10
10
  import path from 'node:path';
11
11
  import { translateCli } from './cli-locale.js';
12
- import { buildPortalBasePath, resolvePortalAppFromApiBaseUrl, resolvePortalStoragePath, validatePortalSlug, } from './portal-create.js';
12
+ import { buildPortalBasePath, resolvePortalAppContext, resolveSavedPortalSourcePath, resolvePortalSourcePath, validatePortalSlug, } from './portal-create.js';
13
13
  import { buildPortalCommandEnv } from './portal-command-env.js';
14
14
  import { updatePortalEnvFiles } from './portal-env-files.js';
15
- import { run } from './run-npm.js';
15
+ import { run, runPnpmCommand } from './run-npm.js';
16
16
  const portalDevText = (key, values, fallback) => translateCli(`commands.portalDev.${key}`, values, { fallback });
17
17
  function trimValue(value) {
18
18
  return String(value ?? '').trim();
@@ -44,10 +44,9 @@ export async function devPortalWorkspace(options) {
44
44
  if (options.env.kind === 'ssh') {
45
45
  throw new Error(portalDevText('errors.sshUnsupported', undefined, 'Cannot start a portal in dev mode for ssh envs in the first version.'));
46
46
  }
47
- const storagePath = resolvePortalStoragePath(options.env);
48
- const { app, appPublicPath } = resolvePortalAppFromApiBaseUrl(apiBaseUrl, options.env.config.appPublicPath);
49
- const portalBase = buildPortalBasePath({ app, appPublicPath, portal });
50
- const portalDir = path.join(storagePath, 'portals', app, portal);
47
+ const { app, appPublicPath, portalBaseApp } = await resolvePortalAppContext(options);
48
+ const portalBase = buildPortalBasePath({ app: portalBaseApp ?? app, appPublicPath, portal });
49
+ const portalDir = resolveSavedPortalSourcePath(options.env, portal) ?? resolvePortalSourcePath(portal);
51
50
  if (!(await pathExists(portalDir))) {
52
51
  throw new Error(portalDevText('errors.workspaceMissing', { portalDir, portal }, `Portal does not exist: ${portalDir}\nRun \`nb portal create ${portal}\` first.`));
53
52
  }
@@ -66,7 +65,7 @@ export async function devPortalWorkspace(options) {
66
65
  };
67
66
  options.onStart?.(result);
68
67
  const runCommand = options.runCommand ?? run;
69
- await runCommand('pnpm', ['dev'], {
68
+ await runPnpmCommand(runCommand, ['dev'], {
70
69
  cwd: portalDir,
71
70
  env: buildPortalCommandEnv({
72
71
  NOCOBASE_API_URL: apiBaseUrl,
@@ -42,8 +42,9 @@ export async function upsertPortalEnvFile(filePath, values) {
42
42
  await writeFile(filePath, upsertEnvContent(content, values), 'utf-8');
43
43
  }
44
44
  export async function updatePortalEnvFiles(params) {
45
+ const envApiUrl = resolvePortalEnvApiUrl(params.apiBaseUrl);
45
46
  await upsertPortalEnvFile(path.join(params.portalDir, '.env'), {
46
- NOCOBASE_API_URL: resolvePortalEnvApiUrl(params.apiBaseUrl),
47
+ NOCOBASE_API_URL: envApiUrl,
47
48
  NOCOBASE_PORTAL_BASE: params.portalBase,
48
49
  });
49
50
  await upsertPortalEnvFile(path.join(params.portalDir, '.env.local'), {
@@ -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.path', undefined, 'Local path')}: ${outputItem.localPath}`,
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
  }