@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
@@ -14,9 +14,12 @@ import path from 'node:path';
14
14
  import * as tar from 'tar';
15
15
  import { executeApiRequest } from './api-client.js';
16
16
  import { translateCli } from './cli-locale.js';
17
+ import { buildPortalConfig, buildPortalConfigFromOptions, DEFAULT_PORTAL_GIT_PATH, readPortalConfig, syncPortalConfigToRemote, writePortalConfig, } from './portal-config.js';
18
+ import { buildPortalCommandEnv } from './portal-command-env.js';
17
19
  import { buildPortalBasePath, resolvePortalAppFromApiBaseUrl, resolvePortalStoragePath, validatePortalSlug, } from './portal-create.js';
18
20
  import { listPortalWorkspaces } from './portal-list.js';
19
21
  import { findPortalListItem } from './portal-info.js';
22
+ import { run } from './run-npm.js';
20
23
  const execFileAsync = promisify(execFile);
21
24
  const portalSourceText = (key, values, fallback) => translateCli(`commands.portalSource.${key}`, values, { fallback });
22
25
  const PULL_SOURCE_OPERATION = {
@@ -85,8 +88,18 @@ async function pathExists(target) {
85
88
  return false;
86
89
  }
87
90
  }
91
+ async function isFile(target) {
92
+ try {
93
+ return (await stat(target)).isFile();
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ }
88
99
  function shouldPackPortalSourceEntry(entryName) {
89
- return !entryName.split('/').some((segment) => ['.git', 'node_modules', 'dist', '.DS_Store'].includes(segment));
100
+ return !entryName
101
+ .split('/')
102
+ .some((segment) => segment.startsWith('._') || ['.git', 'node_modules', 'dist', '.DS_Store'].includes(segment));
90
103
  }
91
104
  function validatePortalSourceTarEntry(entryPath, entry) {
92
105
  if (path.isAbsolute(entryPath) || entryPath.split(/[\\/]+/).includes('..')) {
@@ -118,7 +131,7 @@ async function packPortalSource(portalDir) {
118
131
  async function replacePortalSourceFromArchive(params) {
119
132
  const targetExists = await pathExists(params.portalDir);
120
133
  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.`));
134
+ throw new Error(portalSourceText('errors.workspaceExists', { portalDir: params.portalDir }, `Portal already exists: ${params.portalDir}\nPass --force to delete it and pull again.`));
122
135
  }
123
136
  const parentDir = path.dirname(params.portalDir);
124
137
  const tempDir = await mkdtemp(path.join(parentDir, `.${path.basename(params.portalDir)}-pull-`));
@@ -142,7 +155,7 @@ async function replacePortalSourceFromArchive(params) {
142
155
  async function replacePortalSourceFromDirectory(params) {
143
156
  const targetExists = await pathExists(params.portalDir);
144
157
  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.`));
158
+ throw new Error(portalSourceText('errors.workspaceExists', { portalDir: params.portalDir }, `Portal already exists: ${params.portalDir}\nPass --force to delete it and pull again.`));
146
159
  }
147
160
  const parentDir = path.dirname(params.portalDir);
148
161
  const tempDir = await mkdtemp(path.join(parentDir, `.${path.basename(params.portalDir)}-pull-`));
@@ -167,6 +180,31 @@ async function runGit(args, cwd) {
167
180
  maxBuffer: 10 * 1024 * 1024,
168
181
  });
169
182
  }
183
+ async function installPortalDependencies(params) {
184
+ if (params.installDependencies === false) {
185
+ return {
186
+ dependenciesInstalled: false,
187
+ installSkipped: true,
188
+ };
189
+ }
190
+ if (!(await isFile(path.join(params.portalDir, 'package.json')))) {
191
+ return {
192
+ dependenciesInstalled: false,
193
+ installSkipped: true,
194
+ };
195
+ }
196
+ const runCommand = params.runCommand ?? run;
197
+ await runCommand('pnpm', ['install'], {
198
+ cwd: params.portalDir,
199
+ env: buildPortalCommandEnv(),
200
+ envMode: 'replace',
201
+ errorName: 'pnpm install',
202
+ });
203
+ return {
204
+ dependenciesInstalled: true,
205
+ installSkipped: false,
206
+ };
207
+ }
170
208
  function readSourceRevision(data) {
171
209
  if (!data || typeof data !== 'object' || Array.isArray(data)) {
172
210
  return undefined;
@@ -186,7 +224,7 @@ async function resolvePortalSourceContext(options) {
186
224
  const portalBase = buildPortalBasePath({ app, appPublicPath, portal });
187
225
  const mode = options.env.kind;
188
226
  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.`));
227
+ throw new Error(portalSourceText('errors.unsupportedEnvKind', { kind: mode }, `Cannot sync portal source for ${mode} envs in the first version.`));
190
228
  }
191
229
  const list = await listPortalWorkspaces({
192
230
  env: options.env,
@@ -207,7 +245,30 @@ async function resolvePortalSourceContext(options) {
207
245
  sourceStorage: item.sourceStorage || 'nocobase',
208
246
  gitRepo: item.gitRepo,
209
247
  gitBranch: item.gitBranch || 'main',
210
- gitPath: item.gitPath || portal,
248
+ gitPath: item.gitPath || DEFAULT_PORTAL_GIT_PATH,
249
+ options: item.options,
250
+ };
251
+ }
252
+ function buildPortalConfigFromContext(context) {
253
+ if (Object.keys(context.options).length > 0) {
254
+ return buildPortalConfigFromOptions(context.options, context.portal);
255
+ }
256
+ const sourceStorage = context.sourceStorage || 'nocobase';
257
+ return buildPortalConfig({
258
+ portal: context.portal,
259
+ sourceStorage,
260
+ gitRepo: sourceStorage === 'git' ? context.gitRepo : undefined,
261
+ gitBranch: sourceStorage === 'git' ? context.gitBranch : undefined,
262
+ gitPath: sourceStorage === 'git' ? context.gitPath : undefined,
263
+ });
264
+ }
265
+ function applyPortalConfigToContext(context, config) {
266
+ return {
267
+ ...context,
268
+ sourceStorage: config.sourceStorage,
269
+ gitRepo: config.git?.repo ?? '',
270
+ gitBranch: config.git?.branch ?? 'main',
271
+ gitPath: config.git?.path ?? DEFAULT_PORTAL_GIT_PATH,
211
272
  };
212
273
  }
213
274
  function assertGitSourceConfig(context) {
@@ -217,9 +278,33 @@ function assertGitSourceConfig(context) {
217
278
  return {
218
279
  repo: context.gitRepo,
219
280
  branch: context.gitBranch || 'main',
220
- gitPath: context.gitPath || context.portal,
281
+ gitPath: context.gitPath || DEFAULT_PORTAL_GIT_PATH,
221
282
  };
222
283
  }
284
+ function isGitRepositoryRootPath(gitPath) {
285
+ return gitPath === DEFAULT_PORTAL_GIT_PATH;
286
+ }
287
+ async function copyPortalSourceToGitPath(params) {
288
+ if (isGitRepositoryRootPath(params.gitPath)) {
289
+ const existingEntries = await readdir(params.repoDir);
290
+ await Promise.all(existingEntries
291
+ .filter((entry) => entry !== '.git')
292
+ .map((entry) => rm(path.join(params.repoDir, entry), { recursive: true, force: true })));
293
+ const sourceEntries = (await readdir(params.portalDir)).filter(shouldPackPortalSourceEntry);
294
+ await Promise.all(sourceEntries.map((entry) => cp(path.join(params.portalDir, entry), path.join(params.repoDir, entry), {
295
+ recursive: true,
296
+ filter: (source) => shouldPackPortalSourceEntry(path.relative(params.portalDir, source)),
297
+ })));
298
+ return;
299
+ }
300
+ const targetDir = path.join(params.repoDir, params.gitPath);
301
+ await rm(targetDir, { recursive: true, force: true });
302
+ await mkdir(path.dirname(targetDir), { recursive: true });
303
+ await cp(params.portalDir, targetDir, {
304
+ recursive: true,
305
+ filter: (source) => shouldPackPortalSourceEntry(path.relative(params.portalDir, source)),
306
+ });
307
+ }
223
308
  async function cloneGitSource(params) {
224
309
  const repoDir = path.join(params.cwd, 'repo');
225
310
  try {
@@ -267,13 +352,12 @@ async function pushGitPortalSource(params) {
267
352
  repo: git.repo,
268
353
  branch: git.branch,
269
354
  cwd: tempDir,
355
+ createBranch: true,
270
356
  });
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)),
357
+ await copyPortalSourceToGitPath({
358
+ portalDir: params.context.portalDir,
359
+ repoDir,
360
+ gitPath: git.gitPath,
277
361
  });
278
362
  await runGit(['add', git.gitPath], repoDir);
279
363
  const status = await runGit(['status', '--porcelain', '--', git.gitPath], repoDir);
@@ -299,24 +383,33 @@ async function pushGitPortalSource(params) {
299
383
  }
300
384
  export async function pullPortalSource(options) {
301
385
  const context = await resolvePortalSourceContext(options);
302
- if (context.sourceStorage === 'git') {
386
+ const portalConfig = buildPortalConfigFromContext(context);
387
+ const sourceContext = applyPortalConfigToContext(context, portalConfig);
388
+ if (sourceContext.sourceStorage === 'git') {
303
389
  await pullGitPortalSource({
304
- context,
390
+ context: sourceContext,
305
391
  force: options.force,
306
392
  });
393
+ await writePortalConfig(sourceContext.portalDir, portalConfig);
394
+ const installResult = await installPortalDependencies({
395
+ portalDir: sourceContext.portalDir,
396
+ installDependencies: options.installDependencies,
397
+ runCommand: options.runCommand,
398
+ });
307
399
  return {
308
- ...context,
400
+ ...sourceContext,
309
401
  changed: true,
402
+ ...installResult,
310
403
  };
311
404
  }
312
- if (context.sourceStorage !== 'nocobase') {
313
- throw new Error(portalSourceText('errors.unsupportedSourceStorage', { sourceStorage: context.sourceStorage }, `Unsupported Portal source storage: ${context.sourceStorage}`));
405
+ if (sourceContext.sourceStorage !== 'nocobase') {
406
+ throw new Error(portalSourceText('errors.unsupportedSourceStorage', { sourceStorage: sourceContext.sourceStorage }, `Unsupported portal source storage: ${sourceContext.sourceStorage}`));
314
407
  }
315
- if (context.mode === 'local' || context.mode === 'docker') {
408
+ if (sourceContext.mode === 'local' || sourceContext.mode === 'docker') {
316
409
  return {
317
- ...context,
410
+ ...sourceContext,
318
411
  changed: false,
319
- noopReason: context.mode === 'local'
412
+ noopReason: sourceContext.mode === 'local'
320
413
  ? portalSourceText('messages.localPullNoop', undefined, 'Portal source is already local.')
321
414
  : portalSourceText('messages.dockerPullNoop', undefined, 'Portal source is already available through the Docker volume.'),
322
415
  };
@@ -329,8 +422,8 @@ export async function pullPortalSource(options) {
329
422
  cliVersion: options.cliVersion ?? '',
330
423
  envName: options.envName,
331
424
  flags: {
332
- app: context.app,
333
- portal: context.portal,
425
+ app: sourceContext.app,
426
+ portal: sourceContext.portal,
334
427
  output: archivePath,
335
428
  },
336
429
  operation: PULL_SOURCE_OPERATION,
@@ -338,15 +431,22 @@ export async function pullPortalSource(options) {
338
431
  if (!response.ok) {
339
432
  throw new Error(portalSourceText('errors.pullFailed', { status: response.status, details: JSON.stringify(response.data, null, 2) }, `Portal source pull failed with status ${response.status}\n${JSON.stringify(response.data, null, 2)}`));
340
433
  }
341
- await mkdir(path.dirname(context.portalDir), { recursive: true });
434
+ await mkdir(path.dirname(sourceContext.portalDir), { recursive: true });
342
435
  await replacePortalSourceFromArchive({
343
436
  archivePath,
344
- portalDir: context.portalDir,
437
+ portalDir: sourceContext.portalDir,
345
438
  force: options.force,
346
439
  });
440
+ await writePortalConfig(sourceContext.portalDir, portalConfig);
441
+ const installResult = await installPortalDependencies({
442
+ portalDir: sourceContext.portalDir,
443
+ installDependencies: options.installDependencies,
444
+ runCommand: options.runCommand,
445
+ });
347
446
  return {
348
- ...context,
447
+ ...sourceContext,
349
448
  changed: true,
449
+ ...installResult,
350
450
  };
351
451
  }
352
452
  finally {
@@ -355,16 +455,26 @@ export async function pullPortalSource(options) {
355
455
  }
356
456
  export async function pushPortalSource(options) {
357
457
  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
- }
458
+ if (!(await pathExists(context.portalDir))) {
459
+ throw new Error(portalSourceText('errors.workspaceMissing', { portalDir: context.portalDir, portal: context.portal }, `Portal does not exist: ${context.portalDir}\nRun \`nb portal create ${context.portal}\` first.`));
460
+ }
461
+ const portalConfig = await readPortalConfig(context.portalDir);
462
+ await syncPortalConfigToRemote({
463
+ portal: context.portal,
464
+ config: portalConfig,
465
+ currentOptions: context.options,
466
+ envName: options.envName,
467
+ cliVersion: options.cliVersion,
468
+ apiRequest: options.apiRequest,
469
+ });
470
+ const sourceContext = applyPortalConfigToContext(context, portalConfig);
471
+ if (sourceContext.sourceStorage === 'git') {
362
472
  const revision = await pushGitPortalSource({
363
- context,
473
+ context: sourceContext,
364
474
  message: options.message,
365
475
  });
366
476
  return {
367
- ...context,
477
+ ...sourceContext,
368
478
  changed: Boolean(revision),
369
479
  sourceRevision: revision,
370
480
  noopReason: revision
@@ -372,22 +482,19 @@ export async function pushPortalSource(options) {
372
482
  : portalSourceText('messages.gitPushNoop', undefined, 'No local source changes to push.'),
373
483
  };
374
484
  }
375
- if (context.sourceStorage !== 'nocobase') {
376
- throw new Error(portalSourceText('errors.unsupportedSourceStorage', { sourceStorage: context.sourceStorage }, `Unsupported Portal source storage: ${context.sourceStorage}`));
485
+ if (sourceContext.sourceStorage !== 'nocobase') {
486
+ throw new Error(portalSourceText('errors.unsupportedSourceStorage', { sourceStorage: sourceContext.sourceStorage }, `Unsupported portal source storage: ${sourceContext.sourceStorage}`));
377
487
  }
378
- if (context.mode === 'local' || context.mode === 'docker') {
488
+ if (sourceContext.mode === 'local' || sourceContext.mode === 'docker') {
379
489
  return {
380
- ...context,
490
+ ...sourceContext,
381
491
  changed: false,
382
- noopReason: context.mode === 'local'
492
+ noopReason: sourceContext.mode === 'local'
383
493
  ? portalSourceText('messages.localPushNoop', undefined, 'Portal source is already local.')
384
494
  : portalSourceText('messages.dockerPushNoop', undefined, 'Portal source is already available through the Docker volume.'),
385
495
  };
386
496
  }
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);
497
+ const archive = await packPortalSource(sourceContext.portalDir);
391
498
  const apiRequest = options.apiRequest ?? executeApiRequest;
392
499
  try {
393
500
  const response = await apiRequest({
@@ -395,8 +502,8 @@ export async function pushPortalSource(options) {
395
502
  envName: options.envName,
396
503
  flags: {
397
504
  file: archive.archivePath,
398
- app: context.app,
399
- portal: context.portal,
505
+ app: sourceContext.app,
506
+ portal: sourceContext.portal,
400
507
  message: options.message,
401
508
  },
402
509
  operation: PUSH_SOURCE_OPERATION,
@@ -405,7 +512,7 @@ export async function pushPortalSource(options) {
405
512
  throw new Error(portalSourceText('errors.pushFailed', { status: response.status, details: JSON.stringify(response.data, null, 2) }, `Portal source push failed with status ${response.status}\n${JSON.stringify(response.data, null, 2)}`));
406
513
  }
407
514
  return {
408
- ...context,
515
+ ...sourceContext,
409
516
  changed: true,
410
517
  sourceRevision: readSourceRevision(response.data),
411
518
  };
@@ -79,6 +79,7 @@ export async function writeCaddyProxyBundle(runtime, appEntryOptions, runtimeCon
79
79
  writeFile(bundle.appConfigPath, nextAppConfigContent, 'utf8'),
80
80
  writeFile(bundle.indexV1Path, bundle.indexV1Content, 'utf8'),
81
81
  writeFile(bundle.indexV2Path, bundle.indexV2Content, 'utf8'),
82
+ writeFile(bundle.indexSettingsPath, bundle.indexSettingsContent, 'utf8'),
82
83
  writeFile(bundle.mainConfigPath, bundle.mainConfigContent, 'utf8'),
83
84
  ]);
84
85
  return {
@@ -100,6 +101,7 @@ export async function writeManualCaddyProxyBundle(input, appEntryOptions, runtim
100
101
  writeFile(bundle.appConfigPath, nextAppConfigContent, 'utf8'),
101
102
  writeFile(bundle.indexV1Path, bundle.indexV1Content, 'utf8'),
102
103
  writeFile(bundle.indexV2Path, bundle.indexV2Content, 'utf8'),
104
+ writeFile(bundle.indexSettingsPath, bundle.indexSettingsContent, 'utf8'),
103
105
  writeFile(bundle.mainConfigPath, bundle.mainConfigContent, 'utf8'),
104
106
  ]);
105
107
  return {
@@ -107,6 +107,7 @@ async function writeResolvedNginxProxyBundle(bundle, appEntryOptions, options) {
107
107
  writeFile(bundle.appConfigPath, nextAppConfigContent, 'utf8'),
108
108
  writeFile(bundle.indexV1Path, bundle.indexV1Content, 'utf8'),
109
109
  writeFile(bundle.indexV2Path, bundle.indexV2Content, 'utf8'),
110
+ writeFile(bundle.indexSettingsPath, bundle.indexSettingsContent, 'utf8'),
110
111
  writeFile(bundle.mainConfigPath, bundle.mainConfigContent, 'utf8'),
111
112
  syncEnvProxyNginxSnippets(),
112
113
  ]);
@@ -175,33 +175,57 @@
175
175
  "templateInvalidDirectory": "Portal template \"{{source}}\" is invalid: expected a directory.",
176
176
  "localTemplateMissing": "Portal template directory does not exist: {{source}}",
177
177
  "templateUnresolved": "Portal template \"{{source}}\" could not be resolved. {{details}}",
178
- "templateDownloadFailed": "Failed to download Portal template \"{{source}}\" with npm pack. {{details}}",
179
- "templateExtractFailed": "Failed to extract Portal template \"{{source}}\": {{details}}",
178
+ "templateDownloadFailed": "Failed to download portal template \"{{source}}\" with npm pack. {{details}}",
179
+ "templateExtractFailed": "Failed to extract portal template \"{{source}}\": {{details}}",
180
180
  "templateMissingPackageJson": "Portal template \"{{source}}\" is invalid: package.json is missing.",
181
181
  "npmPackNoTarball": "npm pack did not produce a local tarball for {{source}}.",
182
182
  "npmPackMultipleTarballs": "npm pack produced multiple tarballs for {{source}}.",
183
- "invalidPortalName": "Invalid Portal name \"{{value}}\". Use lowercase letters, numbers, underscores, or hyphens, and start with a lowercase letter or number.",
184
- "invalidPortalAppName": "Invalid Portal app name \"{{value}}\" from apiBaseUrl. Use letters, numbers, underscores, or hyphens, and start with a letter or number.",
185
- "missingApiBaseUrl": "Cannot create a Portal workspace because the selected env has no apiBaseUrl.",
186
- "outsideParent": "Refusing to modify a Portal workspace outside {{parentDir}}: {{portalDir}}",
187
- "sshUnsupported": "Cannot create a Portal workspace for ssh envs in the first version.",
188
- "workspaceExists": "Portal workspace already exists: {{portalDir}}\nPass --force to delete it and create a new workspace."
183
+ "invalidPortalName": "Invalid portal name \"{{value}}\". Use lowercase letters, numbers, underscores, or hyphens, and start with a lowercase letter or number.",
184
+ "invalidPortalAppName": "Invalid portal app name \"{{value}}\" from apiBaseUrl. Use letters, numbers, underscores, or hyphens, and start with a letter or number.",
185
+ "missingApiBaseUrl": "Cannot create a portal because the selected env has no apiBaseUrl.",
186
+ "outsideParent": "Refusing to modify a portal outside {{parentDir}}: {{portalDir}}",
187
+ "sshUnsupported": "Cannot create a portal for ssh envs in the first version.",
188
+ "workspaceExists": "Portal already exists: {{portalDir}}\nPass --force to delete it and create a new portal."
189
189
  },
190
190
  "messages": {
191
191
  "skipInstall": "Skipped pnpm install because package.json was not found in {{portalDir}}.",
192
192
  "created": "Portal \"{{portal}}\" created at {{portalDir}}.",
193
193
  "app": "App: {{app}}",
194
- "base": "Base: {{base}}"
194
+ "base": "Base: {{base}}",
195
+ "sourceStorage": "Source storage: {{sourceStorage}}"
196
+ }
197
+ },
198
+ "portalConfig": {
199
+ "errors": {
200
+ "invalidSourceStorage": "Invalid source storage \"{{value}}\". Use \"nocobase\" or \"git\".",
201
+ "invalidGitPath": "--git-path must be a relative path inside the Git repository.",
202
+ "gitOptionsForNocobaseStorage": "--git-repo, --git-branch, and --git-path can only be used with --source-storage git.",
203
+ "gitRepoRequired": "--git-repo is required when --source-storage is git.",
204
+ "gitRepoInvalid": "--git-repo must be a full Git remote URL.",
205
+ "updateFailed": "Portal config update failed with status {{status}}\n{{details}}"
206
+ }
207
+ },
208
+ "portalConfigure": {
209
+ "errors": {
210
+ "envNotConfigured": "Env \"{{envName}}\" is not configured. Run `nb env add {{envName}} --api-base-url <url>` first.",
211
+ "noEnvConfigured": "No NocoBase env is configured yet. Run `nb init --ui` to create one first.",
212
+ "noChanges": "No portal configuration changes were provided. Pass --source-storage or a --git-* flag.",
213
+ "workspaceMissing": "Portal does not exist: {{portalDir}}\nRun `nb portal create {{portal}}` or `nb portal pull {{portal}}` first."
214
+ },
215
+ "messages": {
216
+ "updated": "Portal \"{{portal}}\" configuration updated at {{portalDir}}/portal.config.json.",
217
+ "remoteSynced": "Remote portal record: synced",
218
+ "remoteSkipped": "Remote portal record: not found; local config only"
195
219
  }
196
220
  },
197
221
  "portalDeploy": {
198
222
  "errors": {
199
223
  "envNotConfigured": "Env \"{{envName}}\" is not configured. Run `nb env add {{envName}} --api-base-url <url>` first.",
200
224
  "noEnvConfigured": "No NocoBase env is configured yet. Run `nb init --ui` to create one first.",
201
- "workspaceMissing": "Portal workspace does not exist: {{portalDir}}\nRun `nb portal create {{portal}}` first.",
202
- "packageJsonMissing": "Portal workspace is invalid: package.json is missing in {{portalDir}}.",
225
+ "workspaceMissing": "Portal does not exist: {{portalDir}}\nRun `nb portal create {{portal}}` first.",
226
+ "packageJsonMissing": "Portal is invalid: package.json is missing in {{portalDir}}.",
203
227
  "distMissing": "Portal build did not produce {{distDir}}/index.html.",
204
- "unsupportedEnvKind": "Cannot deploy a Portal workspace for {{kind}} envs in the first version.",
228
+ "unsupportedEnvKind": "Cannot deploy a portal for {{kind}} envs in the first version.",
205
229
  "uploadFailed": "Portal dist upload failed with status {{status}}\n{{details}}",
206
230
  "recordSyncFailed": "Portal record sync failed with status {{status}}\n{{details}}"
207
231
  },
@@ -220,16 +244,16 @@
220
244
  "errors": {
221
245
  "envNotConfigured": "Env \"{{envName}}\" is not configured. Run `nb env add {{envName}} --api-base-url <url>` first.",
222
246
  "noEnvConfigured": "No NocoBase env is configured yet. Run `nb init --ui` to create one first.",
223
- "unsupportedEnvKind": "Cannot list Portal workspaces for {{kind}} envs in the first version.",
247
+ "unsupportedEnvKind": "Cannot list portals for {{kind}} envs in the first version.",
224
248
  "listFailed": "Portal list failed with status {{status}}\n{{details}}"
225
249
  },
226
250
  "messages": {
227
- "empty": "No Portal records found."
251
+ "empty": "No portal records found."
228
252
  },
229
253
  "table": {
230
254
  "name": "Name",
231
255
  "url": "URL",
232
- "developmentMode": "Development mode",
256
+ "portalType": "Portal type",
233
257
  "path": "Local path",
234
258
  "enabled": "Enabled",
235
259
  "localSynced": "Local synced"
@@ -244,7 +268,7 @@
244
268
  "fields": {
245
269
  "name": "Name",
246
270
  "url": "URL",
247
- "developmentMode": "Development mode",
271
+ "portalType": "Portal type",
248
272
  "path": "Local path",
249
273
  "enabled": "Enabled",
250
274
  "localSynced": "Local synced"
@@ -254,14 +278,14 @@
254
278
  "errors": {
255
279
  "envNotConfigured": "Env \"{{envName}}\" is not configured. Run `nb env add {{envName}} --api-base-url <url>` first.",
256
280
  "noEnvConfigured": "No NocoBase env is configured yet. Run `nb init --ui` to create one first.",
257
- "confirmationRequired": "Refusing to destroy a Portal in non-interactive mode without --yes.",
258
- "outsideParent": "Refusing to delete a Portal workspace outside {{parentDir}}: {{portalDir}}",
259
- "workspaceMissing": "Portal workspace does not exist: {{portalDir}}\nPass --force to ignore missing local files.",
260
- "unsupportedEnvKind": "Cannot destroy a Portal workspace for {{kind}} envs in the first version.",
281
+ "confirmationRequired": "Refusing to destroy a portal in non-interactive mode without --yes.",
282
+ "outsideParent": "Refusing to delete a portal outside {{parentDir}}: {{portalDir}}",
283
+ "workspaceMissing": "Portal does not exist: {{portalDir}}\nPass --force to ignore missing local files.",
284
+ "unsupportedEnvKind": "Cannot destroy a portal for {{kind}} envs in the first version.",
261
285
  "recordDestroyFailed": "Portal record destroy failed with status {{status}}\n{{details}}"
262
286
  },
263
287
  "prompts": {
264
- "confirm": "Destroy Portal \"{{portal}}\" and delete its storage directory?"
288
+ "confirm": "Destroy portal \"{{portal}}\" and delete its storage directory?"
265
289
  },
266
290
  "messages": {
267
291
  "destroyed": "Portal \"{{portal}}\" destroyed.",
@@ -269,19 +293,19 @@
269
293
  "app": "App: {{app}}",
270
294
  "base": "Base: {{base}}",
271
295
  "record": "Record: {{status}}",
272
- "workspace": "Workspace: {{status}} ({{dir}})"
296
+ "workspace": "Portal files: {{status}} ({{dir}})"
273
297
  }
274
298
  },
275
299
  "portalDev": {
276
300
  "errors": {
277
301
  "envNotConfigured": "Env \"{{envName}}\" is not configured. Run `nb env add {{envName}} --api-base-url <url>` first.",
278
302
  "noEnvConfigured": "No NocoBase env is configured yet. Run `nb init --ui` to create one first.",
279
- "workspaceMissing": "Portal workspace does not exist: {{portalDir}}\nRun `nb portal create {{portal}}` first.",
280
- "packageJsonMissing": "Portal workspace is invalid: package.json is missing in {{portalDir}}.",
281
- "sshUnsupported": "Cannot start a Portal workspace in dev mode for ssh envs in the first version."
303
+ "workspaceMissing": "Portal does not exist: {{portalDir}}\nRun `nb portal create {{portal}}` first.",
304
+ "packageJsonMissing": "Portal is invalid: package.json is missing in {{portalDir}}.",
305
+ "sshUnsupported": "Cannot start a portal in dev mode for ssh envs in the first version."
282
306
  },
283
307
  "messages": {
284
- "starting": "Starting Portal \"{{portal}}\"...",
308
+ "starting": "Starting portal \"{{portal}}\"...",
285
309
  "mode": "Mode: {{mode}}",
286
310
  "app": "App: {{app}}",
287
311
  "base": "Base: {{base}}",
@@ -461,15 +485,15 @@
461
485
  "message": "App subpath (for example, /nocobase/)",
462
486
  "placeholder": "/ or /nocobase/"
463
487
  },
464
- "developmentMode": {
465
- "message": "Who will lead development?",
466
- "noCodeLabel": "Human-led development",
467
- "noCodeHint": "Build your application using configuration and low-code tools, with AI as your assistant.",
468
- "vibeCodingLabel": "AI-led development",
469
- "vibeCodingHint": "You describe what you need, and AI writes the code and builds the application for you."
488
+ "portalType": {
489
+ "message": "Portal type",
490
+ "noCodeLabel": "No-code portal",
491
+ "noCodeHint": "Create with visual configuration. AI can help adjust the configuration. Path: /v/<name>",
492
+ "aiLabel": "AI portal",
493
+ "aiHint": "Create with AI Agent and code. Users can request changes in natural language. Path: /x/<name>"
470
494
  },
471
495
  "portalName": {
472
- "message": "Initial portal name",
496
+ "message": "Portal name",
473
497
  "placeholder": "admin"
474
498
  },
475
499
  "portalTemplate": {
@@ -576,15 +600,15 @@
576
600
  "skipDownload": {
577
601
  "message": "Skip downloading NocoBase and reuse existing local app files or Docker images"
578
602
  },
579
- "developmentMode": {
580
- "message": "Who will lead development?",
581
- "noCodeLabel": "Human-led development",
582
- "noCodeHint": "Build your application using configuration and low-code tools, with AI as your assistant.",
583
- "vibeCodingLabel": "AI-led development",
584
- "vibeCodingHint": "You describe what you need, and AI writes the code and builds the application for you."
603
+ "portalType": {
604
+ "message": "Portal type",
605
+ "noCodeLabel": "No-code portal",
606
+ "noCodeHint": "Create with visual configuration. AI can help adjust the configuration. Path: /v/<name>",
607
+ "aiLabel": "AI portal",
608
+ "aiHint": "Create with AI Agent and code. Users can request changes in natural language. Path: /x/<name>"
585
609
  },
586
610
  "portalName": {
587
- "message": "Initial portal name",
611
+ "message": "Portal name",
588
612
  "placeholder": "admin"
589
613
  },
590
614
  "portalTemplate": {
@@ -612,9 +636,9 @@
612
636
  "title": "App source and version",
613
637
  "description": "Choose how to get the app and which source and version to use."
614
638
  },
615
- "developmentMode": {
616
- "title": "Development approach",
617
- "description": "Choose the development approach for your application."
639
+ "portalType": {
640
+ "title": "Configure portal",
641
+ "description": "Set the default portal name and type."
618
642
  },
619
643
  "configureDatabase": {
620
644
  "title": "Configure the database",