@nocobase/cli 2.2.0-test.16 → 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.
Files changed (37) hide show
  1. package/dist/commands/app/start.js +1 -21
  2. package/dist/commands/config/set.js +1 -1
  3. package/dist/commands/init.js +12 -121
  4. package/dist/commands/install.js +39 -55
  5. package/dist/commands/portal/config.js +88 -0
  6. package/dist/commands/portal/create.js +7 -6
  7. package/dist/commands/portal/deploy.js +2 -2
  8. package/dist/commands/portal/destroy.js +6 -6
  9. package/dist/commands/portal/dev.js +3 -3
  10. package/dist/commands/portal/index.js +1 -1
  11. package/dist/commands/portal/info.js +3 -3
  12. package/dist/commands/portal/list.js +5 -5
  13. package/dist/commands/portal/pull.js +11 -4
  14. package/dist/commands/portal/push.js +4 -4
  15. package/dist/lib/api-client.js +7 -0
  16. package/dist/lib/env-auth.js +2 -2
  17. package/dist/lib/env-config.js +1 -1
  18. package/dist/lib/env-proxy.js +68 -6
  19. package/dist/lib/managed-env-file.js +58 -2
  20. package/dist/lib/managed-init-env.js +1 -1
  21. package/dist/lib/naming.js +9 -0
  22. package/dist/lib/portal-config.js +133 -0
  23. package/dist/lib/portal-configure.js +117 -0
  24. package/dist/lib/portal-create.js +24 -79
  25. package/dist/lib/portal-deploy.js +23 -15
  26. package/dist/lib/portal-destroy.js +3 -3
  27. package/dist/lib/portal-dev.js +3 -3
  28. package/dist/lib/portal-info.js +2 -2
  29. package/dist/lib/portal-list.js +32 -18
  30. package/dist/lib/portal-source.js +150 -43
  31. package/dist/lib/proxy-caddy.js +2 -0
  32. package/dist/lib/proxy-nginx.js +1 -0
  33. package/dist/locale/en-US.json +67 -43
  34. package/dist/locale/zh-CN.json +59 -35
  35. package/nocobase-ctl.config.json +111 -0
  36. package/package.json +4 -3
  37. package/dist/lib/portal-template.js +0 -190
@@ -20,10 +20,11 @@ import { executeApiRequest } from './api-client.js';
20
20
  import { resolveEnvRelativePath } from './cli-home.js';
21
21
  import { translateCli } from './cli-locale.js';
22
22
  import { buildPortalCommandEnv } from './portal-command-env.js';
23
+ import { buildPortalConfig, mergePortalConfigIntoOptions, writePortalConfig, } from './portal-config.js';
23
24
  import { run } from './run-npm.js';
24
25
  const DEFAULT_PORTAL_TEMPLATE = '@nocobase/portal-template-default';
25
26
  const DEFAULT_PORTAL_APP_NAME = 'main';
26
- const TEMPLATE_COPY_EXCLUDED_NAMES = new Set(['.git', 'node_modules', '.DS_Store', 'dist']);
27
+ const TEMPLATE_COPY_EXCLUDED_NAMES = new Set(['.git', 'node_modules', '.DS_Store']);
27
28
  const NPM_PACK_TIMEOUT_MS = 30_000;
28
29
  const portalCreateText = (key, values, fallback) => translateCli(`commands.portalCreate.${key}`, values, { fallback });
29
30
  const FIRST_OR_CREATE_PORTAL_OPERATION = {
@@ -96,7 +97,7 @@ function safeTempPrefix(parentDir, portal) {
96
97
  function assertPortalDirIsInsideParent(parentDir, portalDir) {
97
98
  const relative = path.relative(parentDir, portalDir);
98
99
  if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
99
- throw new Error(portalCreateText('errors.outsideParent', { parentDir, portalDir }, `Refusing to modify a Portal workspace outside ${parentDir}: ${portalDir}`));
100
+ throw new Error(portalCreateText('errors.outsideParent', { parentDir, portalDir }, `Refusing to modify a portal outside ${parentDir}: ${portalDir}`));
100
101
  }
101
102
  }
102
103
  function shouldCopyTemplateEntry(templateDir, source) {
@@ -165,7 +166,7 @@ async function downloadNpmTemplatePackage(params) {
165
166
  }
166
167
  catch (error) {
167
168
  const message = error instanceof Error ? error.message : String(error);
168
- throw new Error(portalCreateText('errors.templateExtractFailed', { source: params.source, details: message }, `Failed to extract Portal template "${params.source}": ${message}`));
169
+ throw new Error(portalCreateText('errors.templateExtractFailed', { source: params.source, details: message }, `Failed to extract portal template "${params.source}": ${message}`));
169
170
  }
170
171
  if (!(await pathExists(path.join(extractRoot, 'package.json')))) {
171
172
  throw new Error(portalCreateText('errors.templateMissingPackageJson', { source: params.source }, `Portal template "${params.source}" is invalid: package.json is missing.`));
@@ -184,7 +185,7 @@ async function downloadNpmTemplatePackage(params) {
184
185
  }
185
186
  catch (error) {
186
187
  const details = trimValue(stderr) || trimValue(stdout) || (error instanceof Error ? error.message : String(error));
187
- throw new Error(portalCreateText('errors.templateDownloadFailed', { source: params.source, details }, `Failed to download Portal template "${params.source}" with npm pack. ${details}`));
188
+ throw new Error(portalCreateText('errors.templateDownloadFailed', { source: params.source, details }, `Failed to download portal template "${params.source}" with npm pack. ${details}`));
188
189
  }
189
190
  finally {
190
191
  if (shouldCleanupPackRoot) {
@@ -247,74 +248,29 @@ export async function resolvePortalTemplate(source = DEFAULT_PORTAL_TEMPLATE, op
247
248
  export function validatePortalSlug(value) {
248
249
  const portal = trimValue(value);
249
250
  if (!/^[a-z0-9][a-z0-9_-]*$/.test(portal)) {
250
- throw new Error(portalCreateText('errors.invalidPortalName', { value }, `Invalid Portal name "${value}". Use lowercase letters, numbers, underscores, or hyphens, ` +
251
+ throw new Error(portalCreateText('errors.invalidPortalName', { value }, `Invalid portal name "${value}". Use lowercase letters, numbers, underscores, or hyphens, ` +
251
252
  'and start with a lowercase letter or number.'));
252
253
  }
253
254
  return portal;
254
255
  }
255
- function validatePortalSourceStorage(value) {
256
- const sourceStorage = trimValue(value) || 'nocobase';
257
- if (sourceStorage === 'nocobase' || sourceStorage === 'git') {
258
- return sourceStorage;
259
- }
260
- throw new Error(portalCreateText('errors.invalidSourceStorage', { value: sourceStorage }, `Invalid source storage "${sourceStorage}". Use "nocobase" or "git".`));
261
- }
262
- function isFullGitRemoteUrl(value) {
263
- return /^(?:https?:\/\/|ssh:\/\/|git@[^:]+:).+/.test(value);
264
- }
265
- function validateGitPath(value) {
266
- const gitPath = trimValue(value);
267
- if (!gitPath || path.isAbsolute(gitPath) || gitPath.split(/[\\/]+/).includes('..')) {
268
- throw new Error(portalCreateText('errors.invalidGitPath', { value }, '--git-path must be a relative path inside the Git repository.'));
269
- }
270
- return gitPath.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
271
- }
272
- function validatePortalSourceOptions(options, portal) {
273
- const sourceStorage = validatePortalSourceStorage(options.sourceStorage);
274
- const hasGitOption = Boolean(trimValue(options.gitRepo) || trimValue(options.gitBranch) || trimValue(options.gitPath));
275
- if (sourceStorage === 'nocobase') {
276
- if (hasGitOption) {
277
- throw new Error(portalCreateText('errors.gitOptionsForNocobaseStorage', undefined, '--git-repo, --git-branch, and --git-path can only be used with --source-storage git.'));
278
- }
279
- return { sourceStorage };
280
- }
281
- const repo = trimValue(options.gitRepo);
282
- if (!repo) {
283
- throw new Error(portalCreateText('errors.gitRepoRequired', undefined, '--git-repo is required when --source-storage is git.'));
284
- }
285
- if (!isFullGitRemoteUrl(repo)) {
286
- throw new Error(portalCreateText('errors.gitRepoInvalid', undefined, '--git-repo must be a full Git remote URL.'));
287
- }
288
- return {
289
- sourceStorage,
290
- git: {
291
- repo,
292
- branch: trimValue(options.gitBranch) || 'main',
293
- path: validateGitPath(options.gitPath ?? portal),
294
- },
295
- };
296
- }
297
256
  async function syncMultiPortalRecord(params) {
298
257
  const apiRequest = params.apiRequest ?? executeApiRequest;
299
258
  const response = await apiRequest({
300
259
  cliVersion: params.cliVersion ?? '',
301
260
  envName: params.envName,
302
261
  flags: {
303
- filterKeys: ['uid'],
262
+ filterKeys: ['portalName'],
304
263
  body: JSON.stringify({
305
264
  uid: params.portal,
306
265
  title: params.title,
307
- developmentMode: 'vibe-coding',
308
- routeName: params.portal,
266
+ portalType: 'ai',
267
+ portalName: params.portal,
309
268
  routePath: `/${params.portal}`,
310
269
  authCheck: true,
311
270
  enabled: true,
312
271
  uiLayoutUid: 'admin-layout-model',
313
272
  skipCreatePortalDirectory: true,
314
- options: {
315
- sourceStorage: params.sourceStorage,
316
- git: params.git,
317
- },
273
+ options: mergePortalConfigIntoOptions(params.config),
318
274
  }),
319
275
  },
320
276
  operation: FIRST_OR_CREATE_PORTAL_OPERATION,
@@ -326,7 +282,7 @@ async function syncMultiPortalRecord(params) {
326
282
  function validatePortalAppName(value) {
327
283
  const app = trimValue(value);
328
284
  if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(app)) {
329
- throw new Error(portalCreateText('errors.invalidPortalAppName', { value }, `Invalid Portal app name "${value}" from apiBaseUrl. Use letters, numbers, underscores, or hyphens, ` +
285
+ throw new Error(portalCreateText('errors.invalidPortalAppName', { value }, `Invalid portal app name "${value}" from apiBaseUrl. Use letters, numbers, underscores, or hyphens, ` +
330
286
  'and start with a letter or number.'));
331
287
  }
332
288
  return app;
@@ -341,7 +297,7 @@ export function titleFromPortalSlug(portal) {
341
297
  export function resolvePortalAppFromApiBaseUrl(apiBaseUrl, appPublicPath) {
342
298
  const normalizedApiBaseUrl = trimValue(apiBaseUrl);
343
299
  if (!normalizedApiBaseUrl) {
344
- throw new Error(portalCreateText('errors.missingApiBaseUrl', undefined, 'Cannot create a Portal workspace because the selected env has no apiBaseUrl.'));
300
+ throw new Error(portalCreateText('errors.missingApiBaseUrl', undefined, 'Cannot create a portal because the selected env has no apiBaseUrl.'));
345
301
  }
346
302
  const configuredPublicPath = trimValue(appPublicPath);
347
303
  let inferredPublicPath = '/';
@@ -371,7 +327,7 @@ export function buildPortalBasePath(params) {
371
327
  }
372
328
  export function resolvePortalStoragePath(env) {
373
329
  if (env.kind === 'ssh') {
374
- throw new Error(portalCreateText('errors.sshUnsupported', undefined, 'Cannot create a Portal workspace for ssh envs in the first version.'));
330
+ throw new Error(portalCreateText('errors.sshUnsupported', undefined, 'Cannot create a portal for ssh envs in the first version.'));
375
331
  }
376
332
  if (env.kind === 'http' && !trimValue(env.config.storagePath)) {
377
333
  const envName = trimValue(env.name);
@@ -397,7 +353,13 @@ export function resolvePortalStoragePath(env) {
397
353
  export async function createPortalWorkspace(options) {
398
354
  const portal = validatePortalSlug(options.portal);
399
355
  const title = trimValue(options.title) || titleFromPortalSlug(portal);
400
- const sourceOptions = validatePortalSourceOptions(options, portal);
356
+ const portalConfig = buildPortalConfig({
357
+ portal,
358
+ sourceStorage: options.sourceStorage,
359
+ gitRepo: options.gitRepo,
360
+ gitBranch: options.gitBranch,
361
+ gitPath: options.gitPath,
362
+ });
401
363
  const apiBaseUrl = trimValue(options.env.apiBaseUrl);
402
364
  const envApiUrl = resolvePortalEnvApiUrl(apiBaseUrl);
403
365
  const storagePath = resolvePortalStoragePath(options.env);
@@ -408,7 +370,7 @@ export async function createPortalWorkspace(options) {
408
370
  assertPortalDirIsInsideParent(portalParentDir, portalDir);
409
371
  const targetExists = await pathExists(portalDir);
410
372
  if (targetExists && !options.force) {
411
- throw new Error(portalCreateText('errors.workspaceExists', { portalDir }, `Portal workspace already exists: ${portalDir}\nPass --force to delete it and create a new workspace.`));
373
+ throw new Error(portalCreateText('errors.workspaceExists', { portalDir }, `Portal already exists: ${portalDir}\nPass --force to delete it and create a new portal.`));
412
374
  }
413
375
  const template = await resolvePortalTemplate(options.template, {
414
376
  npmRegistry: trimValue(options.env.config.npmRegistry),
@@ -421,23 +383,7 @@ export async function createPortalWorkspace(options) {
421
383
  await copyTemplate(template.dir, tempDir);
422
384
  await writeFile(path.join(tempDir, '.env'), [`NOCOBASE_API_URL=${envApiUrl}`, `NOCOBASE_PORTAL_BASE=${portalBase}`].join('\n') + '\n', 'utf-8');
423
385
  await writeFile(path.join(tempDir, '.env.local'), [`NOCOBASE_API_URL=${apiBaseUrl}`, `NOCOBASE_PORTAL_BASE=${portalBase}`].join('\n') + '\n', 'utf-8');
424
- const portalConfig = {
425
- schemaVersion: 1,
426
- app,
427
- name: portal,
428
- title,
429
- basePath: portalBase,
430
- apiBaseUrl,
431
- sourceStorage: sourceOptions.sourceStorage,
432
- git: sourceOptions.git,
433
- template: {
434
- type: template.type,
435
- source: template.source,
436
- },
437
- createdBy: 'nb portal create',
438
- createdAt: new Date().toISOString(),
439
- };
440
- await writeFile(path.join(tempDir, 'portal.config.json'), `${JSON.stringify(portalConfig, null, 2)}\n`, 'utf-8');
386
+ await writePortalConfig(tempDir, portalConfig);
441
387
  if (targetExists) {
442
388
  await rm(portalDir, { recursive: true, force: true });
443
389
  }
@@ -460,8 +406,7 @@ export async function createPortalWorkspace(options) {
460
406
  await syncMultiPortalRecord({
461
407
  portal,
462
408
  title,
463
- sourceStorage: sourceOptions.sourceStorage,
464
- git: sourceOptions.git,
409
+ config: portalConfig,
465
410
  envName: options.envName,
466
411
  cliVersion: options.cliVersion,
467
412
  apiRequest: options.apiRequest,
@@ -476,7 +421,7 @@ export async function createPortalWorkspace(options) {
476
421
  portalBase,
477
422
  template,
478
423
  installSkipped: !hasPackageJson,
479
- sourceStorage: sourceOptions.sourceStorage,
424
+ sourceStorage: portalConfig.sourceStorage,
480
425
  };
481
426
  }
482
427
  finally {
@@ -15,6 +15,7 @@ import { translateCli } from './cli-locale.js';
15
15
  import { buildPortalBasePath, resolvePortalAppFromApiBaseUrl, resolvePortalStoragePath, titleFromPortalSlug, validatePortalSlug, } from './portal-create.js';
16
16
  import { buildPortalCommandEnv } from './portal-command-env.js';
17
17
  import { updatePortalEnvFiles } from './portal-env-files.js';
18
+ import { mergePortalConfigIntoOptions, readPortalConfig } from './portal-config.js';
18
19
  import { run } from './run-npm.js';
19
20
  const portalDeployText = (key, values, fallback) => translateCli(`commands.portalDeploy.${key}`, values, { fallback });
20
21
  const DEPLOY_OPERATION = {
@@ -161,22 +162,26 @@ async function uploadPortalDist(params) {
161
162
  }
162
163
  async function syncMultiPortalRecord(params) {
163
164
  const apiRequest = params.apiRequest ?? executeApiRequest;
165
+ const body = {
166
+ uid: params.portal,
167
+ title: titleFromPortalSlug(params.portal),
168
+ portalType: 'ai',
169
+ portalName: params.portal,
170
+ routePath: `/${params.portal}`,
171
+ authCheck: true,
172
+ enabled: true,
173
+ uiLayoutUid: DEFAULT_PORTAL_UI_LAYOUT_UID,
174
+ skipCreatePortalDirectory: true,
175
+ };
176
+ if (params.config) {
177
+ body.options = mergePortalConfigIntoOptions(params.config);
178
+ }
164
179
  const response = await apiRequest({
165
180
  cliVersion: params.cliVersion ?? '',
166
181
  envName: params.envName,
167
182
  flags: {
168
- filterKeys: ['uid'],
169
- body: JSON.stringify({
170
- uid: params.portal,
171
- title: titleFromPortalSlug(params.portal),
172
- developmentMode: 'vibe-coding',
173
- routeName: params.portal,
174
- routePath: `/${params.portal}`,
175
- authCheck: true,
176
- enabled: true,
177
- uiLayoutUid: DEFAULT_PORTAL_UI_LAYOUT_UID,
178
- skipCreatePortalDirectory: true,
179
- }),
183
+ filterKeys: ['portalName'],
184
+ body: JSON.stringify(body),
180
185
  },
181
186
  operation: FIRST_OR_CREATE_PORTAL_OPERATION,
182
187
  });
@@ -193,9 +198,10 @@ export async function deployPortalWorkspace(options) {
193
198
  const portalDir = path.join(storagePath, 'portals', app, portal);
194
199
  const distDir = path.join(portalDir, 'dist');
195
200
  if (!(await pathExists(portalDir))) {
196
- throw new Error(portalDeployText('errors.workspaceMissing', { portalDir, portal }, `Portal workspace does not exist: ${portalDir}\nRun \`nb portal create ${portal}\` first.`));
201
+ throw new Error(portalDeployText('errors.workspaceMissing', { portalDir, portal }, `Portal does not exist: ${portalDir}\nRun \`nb portal create ${portal}\` first.`));
197
202
  }
198
- await assertFileExists(path.join(portalDir, 'package.json'), portalDeployText('errors.packageJsonMissing', { portalDir }, `Portal workspace is invalid: package.json is missing in ${portalDir}.`));
203
+ 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);
199
205
  await updatePortalEnvFiles({
200
206
  portalDir,
201
207
  apiBaseUrl,
@@ -221,6 +227,7 @@ export async function deployPortalWorkspace(options) {
221
227
  if (options.env.kind === 'local' || options.env.kind === 'docker') {
222
228
  await syncMultiPortalRecord({
223
229
  portal,
230
+ config: portalConfig,
224
231
  envName: options.envName,
225
232
  cliVersion: options.cliVersion,
226
233
  apiRequest: options.apiRequest,
@@ -237,7 +244,7 @@ export async function deployPortalWorkspace(options) {
237
244
  };
238
245
  }
239
246
  if (options.env.kind !== 'http') {
240
- throw new Error(portalDeployText('errors.unsupportedEnvKind', { kind: options.env.kind }, `Cannot deploy a Portal workspace for ${options.env.kind} envs in the first version.`));
247
+ throw new Error(portalDeployText('errors.unsupportedEnvKind', { kind: options.env.kind }, `Cannot deploy a portal for ${options.env.kind} envs in the first version.`));
241
248
  }
242
249
  const archive = await packPortalDist(distDir);
243
250
  let uploadResult;
@@ -257,6 +264,7 @@ export async function deployPortalWorkspace(options) {
257
264
  }
258
265
  await syncMultiPortalRecord({
259
266
  portal,
267
+ config: portalConfig,
260
268
  envName: options.envName,
261
269
  cliVersion: options.cliVersion,
262
270
  apiRequest: options.apiRequest,
@@ -39,7 +39,7 @@ async function pathExists(target) {
39
39
  function assertPortalDirIsInsideParent(parentDir, portalDir) {
40
40
  const relative = path.relative(parentDir, portalDir);
41
41
  if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
42
- throw new Error(portalDestroyText('errors.outsideParent', { parentDir, portalDir }, `Refusing to delete a Portal workspace outside ${parentDir}: ${portalDir}`));
42
+ throw new Error(portalDestroyText('errors.outsideParent', { parentDir, portalDir }, `Refusing to delete a portal outside ${parentDir}: ${portalDir}`));
43
43
  }
44
44
  }
45
45
  async function destroyMultiPortalRecord(params) {
@@ -70,13 +70,13 @@ export async function destroyPortalWorkspace(options) {
70
70
  const portalDir = path.join(portalParentDir, portal);
71
71
  const mode = options.env.kind;
72
72
  if (mode !== 'local' && mode !== 'docker' && mode !== 'http') {
73
- throw new Error(portalDestroyText('errors.unsupportedEnvKind', { kind: mode }, `Cannot destroy a Portal workspace for ${mode} envs in the first version.`));
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
76
  assertPortalDirIsInsideParent(portalParentDir, portalDir);
77
77
  const workspaceExists = await pathExists(portalDir);
78
78
  if (!workspaceExists && !options.force) {
79
- throw new Error(portalDestroyText('errors.workspaceMissing', { portalDir, portal }, `Portal workspace does not exist: ${portalDir}\nPass --force to ignore missing local files.`));
79
+ throw new Error(portalDestroyText('errors.workspaceMissing', { portalDir, portal }, `Portal does not exist: ${portalDir}\nPass --force to ignore missing local files.`));
80
80
  }
81
81
  const recordDeleted = await destroyMultiPortalRecord({
82
82
  portal,
@@ -42,16 +42,16 @@ export async function devPortalWorkspace(options) {
42
42
  const portal = validatePortalSlug(options.portal);
43
43
  const apiBaseUrl = trimValue(options.env.apiBaseUrl);
44
44
  if (options.env.kind === 'ssh') {
45
- throw new Error(portalDevText('errors.sshUnsupported', undefined, 'Cannot start a Portal workspace in dev mode for ssh envs in the first version.'));
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
47
  const storagePath = resolvePortalStoragePath(options.env);
48
48
  const { app, appPublicPath } = resolvePortalAppFromApiBaseUrl(apiBaseUrl, options.env.config.appPublicPath);
49
49
  const portalBase = buildPortalBasePath({ app, appPublicPath, portal });
50
50
  const portalDir = path.join(storagePath, 'portals', app, portal);
51
51
  if (!(await pathExists(portalDir))) {
52
- throw new Error(portalDevText('errors.workspaceMissing', { portalDir, portal }, `Portal workspace does not exist: ${portalDir}\nRun \`nb portal create ${portal}\` first.`));
52
+ throw new Error(portalDevText('errors.workspaceMissing', { portalDir, portal }, `Portal does not exist: ${portalDir}\nRun \`nb portal create ${portal}\` first.`));
53
53
  }
54
- await assertFileExists(path.join(portalDir, 'package.json'), portalDevText('errors.packageJsonMissing', { portalDir }, `Portal workspace is invalid: package.json is missing in ${portalDir}.`));
54
+ await assertFileExists(path.join(portalDir, 'package.json'), portalDevText('errors.packageJsonMissing', { portalDir }, `Portal is invalid: package.json is missing in ${portalDir}.`));
55
55
  await updatePortalEnvFiles({
56
56
  portalDir,
57
57
  apiBaseUrl,
@@ -16,14 +16,14 @@ function formatBoolean(value) {
16
16
  return value ? 'yes' : 'no';
17
17
  }
18
18
  export function findPortalListItem(items, portal) {
19
- return items.find((item) => item.routeName === portal || item.uid === portal);
19
+ return items.find((item) => item.portalName === portal || item.uid === portal);
20
20
  }
21
21
  export function formatPortalInfo(item) {
22
22
  const outputItem = toPortalOutputItem(item);
23
23
  return [
24
24
  `${portalInfoText('fields.name', undefined, 'Name')}: ${outputItem.name}`,
25
25
  `${portalInfoText('fields.url', undefined, 'URL')}: ${outputItem.url}`,
26
- `${portalInfoText('fields.developmentMode', undefined, 'Development mode')}: ${outputItem.developmentMode}`,
26
+ `${portalInfoText('fields.portalType', undefined, 'Portal type')}: ${outputItem.portalType}`,
27
27
  `${portalInfoText('fields.path', undefined, 'Local path')}: ${outputItem.localPath}`,
28
28
  `${portalInfoText('fields.enabled', undefined, 'Enabled')}: ${formatBoolean(outputItem.enabled)}`,
29
29
  `${portalInfoText('fields.localSynced', undefined, 'Local synced')}: ${formatBoolean(outputItem.localSynced)}`,
@@ -96,9 +96,18 @@ function stripBasePath(pathname, basePath) {
96
96
  }
97
97
  return pathValue;
98
98
  }
99
- function normalizeNoCodeRoutePath(routePath, appPublicPath) {
99
+ function normalizeNoCodeRoutePath(routePath, appPublicPath, app) {
100
100
  let normalizedRoutePath = normalizeRootPath(routePath);
101
+ const basePaths = app === 'main'
102
+ ? []
103
+ : [
104
+ appendAppPublicPath(appPublicPath, `v/apps/${app}`, { trailingSlash: false }),
105
+ appendAppPublicPath(appPublicPath, `x/apps/${app}`, { trailingSlash: false }),
106
+ `/v/apps/${app}`,
107
+ `/x/apps/${app}`,
108
+ ];
101
109
  for (const basePath of [
110
+ ...basePaths,
102
111
  appendAppPublicPath(appPublicPath, 'v', { trailingSlash: false }),
103
112
  appendAppPublicPath(appPublicPath, 'x', { trailingSlash: false }),
104
113
  '/v',
@@ -112,15 +121,19 @@ function buildNoCodePortalBasePath(params) {
112
121
  if (isAbsoluteUrl(params.routePath)) {
113
122
  return params.routePath;
114
123
  }
115
- const normalizedRoutePath = normalizeNoCodeRoutePath(params.routePath, params.appPublicPath);
116
- const segment = normalizedRoutePath === '/' ? 'v' : `v/${normalizedRoutePath.replace(/^\/+/, '')}`;
124
+ const normalizedRoutePath = normalizeNoCodeRoutePath(params.routePath, params.appPublicPath, params.app);
125
+ const routeSegment = normalizedRoutePath.replace(/^\/+/, '');
126
+ let segment = normalizedRoutePath === '/' ? 'v' : `v/${routeSegment}`;
127
+ if (params.app !== 'main') {
128
+ segment = normalizedRoutePath === '/' ? `v/apps/${params.app}` : `v/apps/${params.app}/${routeSegment}`;
129
+ }
117
130
  return appendAppPublicPath(params.appPublicPath, segment, { trailingSlash: normalizedRoutePath === '/' });
118
131
  }
119
132
  export function toPortalOutputItem(item) {
120
133
  return {
121
- name: item.routeName,
134
+ name: item.portalName,
122
135
  url: item.portalUrl,
123
- developmentMode: item.developmentMode,
136
+ portalType: item.portalType,
124
137
  localPath: item.localSynced === true ? item.portalDir : '',
125
138
  enabled: item.enabled,
126
139
  sourceStorage: item.sourceStorage,
@@ -134,7 +147,7 @@ async function listMultiPortalRecords(params) {
134
147
  envName: params.envName,
135
148
  flags: {
136
149
  pageSize: 200,
137
- sort: ['routeName'],
150
+ sort: ['portalName'],
138
151
  },
139
152
  operation: LIST_PORTALS_OPERATION,
140
153
  });
@@ -149,7 +162,7 @@ export async function listPortalWorkspaces(options) {
149
162
  const { app, appPublicPath } = resolvePortalAppFromApiBaseUrl(apiBaseUrl, options.env.config.appPublicPath);
150
163
  const mode = options.env.kind;
151
164
  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.`));
165
+ throw new Error(portalListText('errors.unsupportedEnvKind', { kind: mode }, `Cannot list portals for ${mode} envs in the first version.`));
153
166
  }
154
167
  const listMode = mode;
155
168
  const records = await listMultiPortalRecords({
@@ -159,33 +172,34 @@ export async function listPortalWorkspaces(options) {
159
172
  });
160
173
  const items = await Promise.all(records.map(async (record) => {
161
174
  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');
175
+ const portalName = readRecordString(record, 'portalName') || uid;
176
+ const routePath = readRecordString(record, 'routePath') || `/${portalName}`;
177
+ const portalType = readRecordString(record, 'portalType');
165
178
  const enabled = readRecordBoolean(record, 'enabled');
166
179
  const options = readRecordObject(record, 'options');
167
180
  const git = readRecordObject(options, 'git');
168
181
  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) : '';
182
+ const isAi = portalType === 'ai';
183
+ const portalDir = isAi ? path.join(storagePath, 'portals', app, portalName) : '';
171
184
  return {
172
185
  uid,
173
- routeName,
186
+ portalName,
174
187
  routePath,
175
- developmentMode,
188
+ portalType,
176
189
  enabled,
177
190
  sourceStorage,
178
191
  gitRepo: trimValue(git.repo) || readRecordString(record, 'gitRepo'),
179
192
  gitBranch: trimValue(git.branch) || readRecordString(record, 'gitBranch'),
180
193
  gitPath: trimValue(git.path) || readRecordString(record, 'gitPath'),
181
194
  sourceRevision: trimValue(options.sourceRevision) || readRecordString(record, 'sourceRevision'),
195
+ options,
182
196
  portalUrl: enabled
183
- ? buildPortalAccessUrl(apiBaseUrl, isVibeCoding
184
- ? buildPortalBasePath({ app, appPublicPath, portal: routeName })
185
- : buildNoCodePortalBasePath({ appPublicPath, routePath }))
197
+ ? buildPortalAccessUrl(apiBaseUrl, isAi
198
+ ? buildPortalBasePath({ app, appPublicPath, portal: portalName })
199
+ : buildNoCodePortalBasePath({ app, appPublicPath, routePath }))
186
200
  : '',
187
201
  portalDir,
188
- localSynced: isVibeCoding ? await pathExists(portalDir) : null,
202
+ localSynced: isAi ? await pathExists(portalDir) : null,
189
203
  };
190
204
  }));
191
205
  return {