@nocobase/cli 2.1.0-beta.46 → 2.1.0-beta.48

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 (48) hide show
  1. package/bin/run.js +2 -1
  2. package/dist/commands/app/destroy.js +3 -3
  3. package/dist/commands/app/restart.js +1 -2
  4. package/dist/commands/app/start.js +28 -3
  5. package/dist/commands/app/upgrade.js +1 -1
  6. package/dist/commands/env/update.js +11 -1
  7. package/dist/commands/init.js +44 -76
  8. package/dist/commands/install.js +68 -50
  9. package/dist/commands/license/activate.js +1 -1
  10. package/dist/commands/proxy/caddy/current.js +17 -0
  11. package/dist/commands/proxy/caddy/generate.js +69 -0
  12. package/dist/commands/proxy/caddy/index.js +28 -0
  13. package/dist/commands/proxy/caddy/info.js +31 -0
  14. package/dist/commands/proxy/caddy/reload.js +30 -0
  15. package/dist/commands/proxy/caddy/restart.js +28 -0
  16. package/dist/commands/proxy/caddy/start.js +30 -0
  17. package/dist/commands/proxy/caddy/status.js +19 -0
  18. package/dist/commands/proxy/caddy/stop.js +30 -0
  19. package/dist/commands/proxy/caddy/use.js +26 -0
  20. package/dist/commands/proxy/index.js +28 -0
  21. package/dist/commands/proxy/nginx/current.js +18 -0
  22. package/dist/commands/proxy/nginx/generate.js +68 -0
  23. package/dist/commands/proxy/nginx/index.js +28 -0
  24. package/dist/commands/proxy/nginx/info.js +34 -0
  25. package/dist/commands/proxy/nginx/reload.js +30 -0
  26. package/dist/commands/proxy/nginx/restart.js +28 -0
  27. package/dist/commands/proxy/nginx/start.js +30 -0
  28. package/dist/commands/proxy/nginx/status.js +19 -0
  29. package/dist/commands/proxy/nginx/stop.js +30 -0
  30. package/dist/commands/proxy/nginx/use.js +31 -0
  31. package/dist/commands/revision/create.js +31 -2
  32. package/dist/lib/app-managed-resources.js +7 -2
  33. package/dist/lib/auth-store.js +8 -0
  34. package/dist/lib/cli-config.js +73 -1
  35. package/dist/lib/env-config.js +8 -0
  36. package/dist/lib/env-proxy.js +105 -75
  37. package/dist/lib/managed-env-file.js +7 -4
  38. package/dist/lib/managed-init-env.js +32 -0
  39. package/dist/lib/prompt-catalog-terminal.js +1 -2
  40. package/dist/lib/prompt-validators.js +6 -0
  41. package/dist/lib/proxy-caddy.js +274 -0
  42. package/dist/lib/proxy-nginx.js +330 -0
  43. package/dist/locale/en-US.json +4 -3
  44. package/dist/locale/zh-CN.json +4 -3
  45. package/package.json +2 -2
  46. package/dist/commands/env/proxy/caddy.js +0 -28
  47. package/dist/commands/env/proxy/index.js +0 -353
  48. package/dist/commands/env/proxy/nginx.js +0 -28
@@ -18,6 +18,9 @@ const TCP_PORT_PROBE_HOSTS = ['127.0.0.1', '0.0.0.0', '::1'];
18
18
  function buildHealthCheckUrl(apiBaseUrl) {
19
19
  return `${apiBaseUrl.replace(/\/+$/, '')}/__health_check`;
20
20
  }
21
+ function hasSupportedApiBasePath(pathname) {
22
+ return /\/api(?:\/__app\/[^/]+)?\/?$/.test(pathname);
23
+ }
21
24
  function isMaintainingHealthCheckResponse(status, body) {
22
25
  if (status !== 503 || !body || typeof body !== 'object') {
23
26
  return false;
@@ -47,6 +50,9 @@ export async function validateApiBaseUrl(value) {
47
50
  if (/\/__health_check\/?$/i.test(url.pathname)) {
48
51
  return translateCli('validators.apiBaseUrl.healthCheckPathNotAllowed');
49
52
  }
53
+ if (!hasSupportedApiBasePath(url.pathname)) {
54
+ return translateCli('validators.apiBaseUrl.missingApiPrefix', { example: API_BASE_URL_EXAMPLE });
55
+ }
50
56
  const controller = new AbortController();
51
57
  const timeout = setTimeout(() => {
52
58
  controller.abort();
@@ -0,0 +1,274 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
10
+ import path from 'node:path';
11
+ import { dockerContainerExists, dockerContainerIsRunning, startDockerContainer, stopDockerContainer, } from './app-runtime.js';
12
+ import { CADDY_PROXY_DRIVER_OPTIONS, DEFAULT_CADDY_PROXY_DRIVER, getCliConfigValue, normalizeCaddyProxyDriver, resolveDockerContainerPrefix, setCliConfigValue, } from './cli-config.js';
13
+ import { resolveCliHomeRoot } from './cli-home.js';
14
+ import { applyEnvProxyAppEntryOptions, buildEnvProxyCaddyBundle, buildEnvProxyMainConfig, mapProxyPathFromCliRoot, resolveEnvProxyMainOutputPath, } from './env-proxy.js';
15
+ import { run } from './run-npm.js';
16
+ const DOCKER_CADDY_PROXY_CONTAINER_SUFFIX = 'caddy-proxy';
17
+ const DOCKER_CADDY_PROXY_IMAGE = 'caddy:latest';
18
+ const DOCKER_CADDY_PROXY_RUNTIME_ROOT = '/apps';
19
+ const DOCKER_CADDY_PROXY_CONF_DESTINATION = '/etc/caddy/Caddyfile';
20
+ async function readOptionalTextFile(filePath) {
21
+ try {
22
+ return await readFile(filePath, 'utf8');
23
+ }
24
+ catch (error) {
25
+ const code = error && typeof error === 'object' && 'code' in error ? error.code : undefined;
26
+ if (code === 'ENOENT') {
27
+ return undefined;
28
+ }
29
+ throw error;
30
+ }
31
+ }
32
+ export function isCaddyProxyDriver(value) {
33
+ return CADDY_PROXY_DRIVER_OPTIONS.includes(value);
34
+ }
35
+ export async function getCaddyProxyDriver() {
36
+ const configured = await getCliConfigValue('proxy.caddy-driver');
37
+ return normalizeCaddyProxyDriver(configured) ?? DEFAULT_CADDY_PROXY_DRIVER;
38
+ }
39
+ export async function setCaddyProxyDriver(driver) {
40
+ const normalized = await setCliConfigValue('proxy.caddy-driver', driver);
41
+ return normalizeCaddyProxyDriver(normalized) ?? DEFAULT_CADDY_PROXY_DRIVER;
42
+ }
43
+ export async function resolveCaddyProxyContainerName() {
44
+ const prefix = await resolveDockerContainerPrefix();
45
+ return `${prefix}-${DOCKER_CADDY_PROXY_CONTAINER_SUFFIX}`;
46
+ }
47
+ export function resolveCaddyProxyImage() {
48
+ return DOCKER_CADDY_PROXY_IMAGE;
49
+ }
50
+ export function resolveCaddyProxyRuntimeRoot(driver, cliRoot) {
51
+ return driver === 'docker' ? DOCKER_CADDY_PROXY_RUNTIME_ROOT : cliRoot;
52
+ }
53
+ export function resolveCaddyProxyUpstreamHost(driver) {
54
+ return driver === 'docker' ? 'host.docker.internal' : '127.0.0.1';
55
+ }
56
+ export async function resolveCaddyProxyRuntimeContext(options) {
57
+ const driver = await getCaddyProxyDriver();
58
+ const cliRoot = String(options?.cliRoot ?? process.env.NB_CLI_ROOT ?? resolveCliHomeRoot()).trim() || resolveCliHomeRoot();
59
+ return {
60
+ driver,
61
+ runtimeCliRoot: resolveCaddyProxyRuntimeRoot(driver, cliRoot),
62
+ upstreamHost: resolveCaddyProxyUpstreamHost(driver),
63
+ };
64
+ }
65
+ export async function writeCaddyProxyBundle(runtime, appEntryOptions, runtimeContext) {
66
+ const bundle = await buildEnvProxyCaddyBundle(runtime, {
67
+ runtimeCliRoot: runtimeContext.runtimeCliRoot,
68
+ upstreamHost: runtimeContext.upstreamHost,
69
+ });
70
+ const currentAppConfigContent = await readOptionalTextFile(bundle.appConfigPath);
71
+ const nextAppConfigContent = applyEnvProxyAppEntryOptions(bundle.appConfigContent, 'caddy', appEntryOptions);
72
+ const status = currentAppConfigContent ? 'updated' : 'created';
73
+ await Promise.all([mkdir(bundle.entryDir, { recursive: true }), mkdir(bundle.publicDir, { recursive: true })]);
74
+ await Promise.all([
75
+ writeFile(bundle.appConfigPath, nextAppConfigContent, 'utf8'),
76
+ writeFile(bundle.indexV1Path, bundle.indexV1Content, 'utf8'),
77
+ writeFile(bundle.indexV2Path, bundle.indexV2Content, 'utf8'),
78
+ writeFile(bundle.mainConfigPath, bundle.mainConfigContent, 'utf8'),
79
+ ]);
80
+ return {
81
+ bundle,
82
+ status,
83
+ };
84
+ }
85
+ function resolveLocalCaddyPidFilePath() {
86
+ return path.join(path.dirname(resolveEnvProxyMainOutputPath({ provider: 'caddy' })), 'caddy.pid');
87
+ }
88
+ async function isLocalCaddyRunning() {
89
+ const pidText = await readOptionalTextFile(resolveLocalCaddyPidFilePath());
90
+ const pid = Number.parseInt(String(pidText ?? '').trim(), 10);
91
+ if (!Number.isInteger(pid) || pid <= 0) {
92
+ return false;
93
+ }
94
+ try {
95
+ process.kill(pid, 0);
96
+ return true;
97
+ }
98
+ catch {
99
+ return false;
100
+ }
101
+ }
102
+ async function ensureCaddyProxyMainConfig(runtimeContext) {
103
+ const mainConfigPath = resolveEnvProxyMainOutputPath({ provider: 'caddy' });
104
+ const mainConfigContent = await buildEnvProxyMainConfig({
105
+ provider: 'caddy',
106
+ runtimeCliRoot: runtimeContext.runtimeCliRoot,
107
+ });
108
+ await mkdir(path.dirname(mainConfigPath), { recursive: true });
109
+ await writeFile(mainConfigPath, mainConfigContent, 'utf8');
110
+ return mainConfigPath;
111
+ }
112
+ async function startLocalCaddyProxy(runtimeContext) {
113
+ const mainConfigPath = await ensureCaddyProxyMainConfig(runtimeContext);
114
+ const pidFilePath = resolveLocalCaddyPidFilePath();
115
+ if (await isLocalCaddyRunning()) {
116
+ return 'already-running';
117
+ }
118
+ await run('caddy', ['start', '--config', mainConfigPath, '--adapter', 'caddyfile', '--pidfile', pidFilePath], {
119
+ errorName: 'caddy start',
120
+ stdio: 'ignore',
121
+ });
122
+ return 'started';
123
+ }
124
+ async function stopLocalCaddyProxy() {
125
+ const pidFilePath = resolveLocalCaddyPidFilePath();
126
+ const pidText = await readOptionalTextFile(pidFilePath);
127
+ const pid = Number.parseInt(String(pidText ?? '').trim(), 10);
128
+ if (!Number.isInteger(pid) || pid <= 0) {
129
+ return 'already-stopped';
130
+ }
131
+ try {
132
+ process.kill(pid, 'SIGTERM');
133
+ return 'stopped';
134
+ }
135
+ catch {
136
+ return 'already-stopped';
137
+ }
138
+ }
139
+ async function reloadLocalCaddyProxy(runtimeContext) {
140
+ const mainConfigPath = await ensureCaddyProxyMainConfig(runtimeContext);
141
+ if (!(await isLocalCaddyRunning())) {
142
+ return await startLocalCaddyProxy(runtimeContext);
143
+ }
144
+ await run('caddy', ['reload', '--config', mainConfigPath, '--adapter', 'caddyfile'], {
145
+ errorName: 'caddy reload',
146
+ stdio: 'ignore',
147
+ });
148
+ return 'reloaded';
149
+ }
150
+ async function ensureDockerCaddyProxyContainer(runtimeContext) {
151
+ const containerName = await resolveCaddyProxyContainerName();
152
+ if (await dockerContainerExists(containerName)) {
153
+ return;
154
+ }
155
+ const hostCliRoot = String(process.env.NB_CLI_ROOT ?? resolveCliHomeRoot()).trim() || resolveCliHomeRoot();
156
+ const mainConfigPath = await ensureCaddyProxyMainConfig(runtimeContext);
157
+ await run('docker', [
158
+ 'run',
159
+ '-d',
160
+ '--name',
161
+ containerName,
162
+ '--add-host',
163
+ 'host.docker.internal:host-gateway',
164
+ '-p',
165
+ '80:80',
166
+ '-v',
167
+ `${hostCliRoot}:${DOCKER_CADDY_PROXY_RUNTIME_ROOT}`,
168
+ '-v',
169
+ `${mainConfigPath}:${DOCKER_CADDY_PROXY_CONF_DESTINATION}:ro`,
170
+ DOCKER_CADDY_PROXY_IMAGE,
171
+ ], {
172
+ errorName: 'docker run',
173
+ stdio: 'ignore',
174
+ });
175
+ }
176
+ async function startDockerCaddyProxy(runtimeContext) {
177
+ const containerName = await resolveCaddyProxyContainerName();
178
+ await ensureCaddyProxyMainConfig(runtimeContext);
179
+ if (await dockerContainerExists(containerName)) {
180
+ const state = await startDockerContainer(containerName, { stdio: 'ignore' });
181
+ return state === 'already-running' ? 'already-running' : 'started';
182
+ }
183
+ await ensureDockerCaddyProxyContainer(runtimeContext);
184
+ return 'started';
185
+ }
186
+ async function stopDockerCaddyProxy() {
187
+ const containerName = await resolveCaddyProxyContainerName();
188
+ if (!(await dockerContainerExists(containerName))) {
189
+ return 'already-stopped';
190
+ }
191
+ return await stopDockerContainer(containerName, { stdio: 'ignore' });
192
+ }
193
+ async function reloadDockerCaddyProxy(runtimeContext) {
194
+ const containerName = await resolveCaddyProxyContainerName();
195
+ await ensureCaddyProxyMainConfig(runtimeContext);
196
+ if (!(await dockerContainerIsRunning(containerName))) {
197
+ return await startDockerCaddyProxy(runtimeContext);
198
+ }
199
+ await run('docker', ['exec', containerName, 'caddy', 'reload', '--config', DOCKER_CADDY_PROXY_CONF_DESTINATION, '--adapter', 'caddyfile'], {
200
+ errorName: 'docker exec caddy reload',
201
+ stdio: 'ignore',
202
+ });
203
+ return 'reloaded';
204
+ }
205
+ export async function startCaddyProxy(runtimeContext) {
206
+ return runtimeContext.driver === 'docker'
207
+ ? await startDockerCaddyProxy(runtimeContext)
208
+ : await startLocalCaddyProxy(runtimeContext);
209
+ }
210
+ export async function stopCaddyProxy(runtimeContext) {
211
+ return runtimeContext.driver === 'docker' ? await stopDockerCaddyProxy() : await stopLocalCaddyProxy();
212
+ }
213
+ export async function reloadCaddyProxy(runtimeContext) {
214
+ return runtimeContext.driver === 'docker'
215
+ ? await reloadDockerCaddyProxy(runtimeContext)
216
+ : await reloadLocalCaddyProxy(runtimeContext);
217
+ }
218
+ export async function restartCaddyProxy(runtimeContext) {
219
+ await stopCaddyProxy(runtimeContext);
220
+ await startCaddyProxy(runtimeContext);
221
+ return 'restarted';
222
+ }
223
+ export async function getCaddyProxyStatus(runtimeContext) {
224
+ const configFile = await mapProxyPathFromCliRoot(resolveEnvProxyMainOutputPath({ provider: 'caddy' }), {
225
+ runtimeCliRoot: runtimeContext.runtimeCliRoot,
226
+ });
227
+ if (runtimeContext.driver === 'docker') {
228
+ const containerName = await resolveCaddyProxyContainerName();
229
+ return {
230
+ driver: runtimeContext.driver,
231
+ state: (await dockerContainerIsRunning(containerName)) ? 'running' : 'stopped',
232
+ configFile,
233
+ runtimeRoot: runtimeContext.runtimeCliRoot,
234
+ upstreamHost: runtimeContext.upstreamHost,
235
+ containerName,
236
+ image: resolveCaddyProxyImage(),
237
+ };
238
+ }
239
+ return {
240
+ driver: runtimeContext.driver,
241
+ state: (await isLocalCaddyRunning()) ? 'running' : 'stopped',
242
+ configFile,
243
+ runtimeRoot: runtimeContext.runtimeCliRoot,
244
+ upstreamHost: runtimeContext.upstreamHost,
245
+ caddyBinary: await getCliConfigValue('bin.caddy'),
246
+ };
247
+ }
248
+ export function formatCaddyProxyInfoLines(info) {
249
+ const lines = [
250
+ `driver: ${info.driver}`,
251
+ `configFile: ${info.configFile}`,
252
+ `runtimeRoot: ${info.runtimeRoot}`,
253
+ `upstreamHost:${info.upstreamHost}`,
254
+ ];
255
+ if (info.driver === 'local') {
256
+ lines.push(`caddyBin: ${info.caddyBinary}`);
257
+ }
258
+ else {
259
+ lines.push(`container: ${info.containerName}`);
260
+ lines.push(`image: ${info.image}`);
261
+ }
262
+ return lines;
263
+ }
264
+ export function formatCaddyProxyStatusLines(status) {
265
+ const lines = [`driver: ${status.driver}`, `status: ${status.state}`, `config: ${status.configFile}`];
266
+ if (status.driver === 'local') {
267
+ lines.push(`caddy: ${status.caddyBinary}`);
268
+ }
269
+ else {
270
+ lines.push(`container: ${status.containerName}`);
271
+ lines.push(`image: ${status.image}`);
272
+ }
273
+ return lines;
274
+ }
@@ -0,0 +1,330 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
10
+ import path from 'node:path';
11
+ import { dockerContainerExists, dockerContainerIsRunning, startDockerContainer, stopDockerContainer, } from './app-runtime.js';
12
+ import { DEFAULT_NGINX_PROXY_DRIVER, getCliConfigValue, NGINX_PROXY_DRIVER_OPTIONS, normalizeNginxProxyDriver, resolveDockerContainerPrefix, setCliConfigValue, } from './cli-config.js';
13
+ import { resolveCliHomeRoot } from './cli-home.js';
14
+ import { applyEnvProxyAppEntryOptions, appConfigHasManagedNginxBlock, buildEnvProxyMainConfig, buildEnvProxyNginxBundle, extractManagedNginxConfigBlock, installEnvProxyProvider, mapProxyPathFromCliRoot, reloadEnvProxyProvider, resolveEnvProxyMainOutputPath, replaceManagedNginxConfigBlock, syncEnvProxyNginxSnippets, } from './env-proxy.js';
15
+ import { run } from './run-npm.js';
16
+ const DOCKER_NGINX_PROXY_CONTAINER_SUFFIX = 'nginx-proxy';
17
+ const DOCKER_NGINX_PROXY_IMAGE = 'nginx:latest';
18
+ const DOCKER_NGINX_PROXY_RUNTIME_ROOT = '/apps';
19
+ const DOCKER_NGINX_PROXY_CONF_DESTINATION = '/etc/nginx/conf.d/default.conf';
20
+ async function readOptionalTextFile(filePath) {
21
+ try {
22
+ return await readFile(filePath, 'utf8');
23
+ }
24
+ catch (error) {
25
+ const code = error && typeof error === 'object' && 'code' in error ? error.code : undefined;
26
+ if (code === 'ENOENT') {
27
+ return undefined;
28
+ }
29
+ throw error;
30
+ }
31
+ }
32
+ export function isNginxProxyDriver(value) {
33
+ return NGINX_PROXY_DRIVER_OPTIONS.includes(value);
34
+ }
35
+ export async function getNginxProxyDriver() {
36
+ const configured = await getCliConfigValue('proxy.nginx-driver');
37
+ return normalizeNginxProxyDriver(configured) ?? DEFAULT_NGINX_PROXY_DRIVER;
38
+ }
39
+ export async function setNginxProxyDriver(driver) {
40
+ const normalized = await setCliConfigValue('proxy.nginx-driver', driver);
41
+ return normalizeNginxProxyDriver(normalized) ?? DEFAULT_NGINX_PROXY_DRIVER;
42
+ }
43
+ export async function resolveNginxProxyContainerName() {
44
+ const prefix = await resolveDockerContainerPrefix();
45
+ return `${prefix}-${DOCKER_NGINX_PROXY_CONTAINER_SUFFIX}`;
46
+ }
47
+ export function resolveNginxProxyImage() {
48
+ return DOCKER_NGINX_PROXY_IMAGE;
49
+ }
50
+ export function resolveNginxProxyRuntimeRoot(driver, cliRoot) {
51
+ return driver === 'docker' ? DOCKER_NGINX_PROXY_RUNTIME_ROOT : cliRoot;
52
+ }
53
+ export function resolveNginxProxyUpstreamHost(driver) {
54
+ return driver === 'docker' ? 'host.docker.internal' : '127.0.0.1';
55
+ }
56
+ export async function resolveNginxProxyRuntimeContext(options) {
57
+ const driver = await getNginxProxyDriver();
58
+ const cliRoot = String(options?.cliRoot ?? process.env.NB_CLI_ROOT ?? resolveCliHomeRoot()).trim() || resolveCliHomeRoot();
59
+ return {
60
+ driver,
61
+ runtimeCliRoot: resolveNginxProxyRuntimeRoot(driver, cliRoot),
62
+ upstreamHost: resolveNginxProxyUpstreamHost(driver),
63
+ };
64
+ }
65
+ function buildNginxManagedBlockMissingMessage(appConfigPath) {
66
+ return (`The editable nginx app entry config at ${appConfigPath} does not contain the NocoBase managed block. ` +
67
+ 'Restore the managed block or delete the file and regenerate the proxy config.');
68
+ }
69
+ export async function writeNginxProxyBundle(runtime, appEntryOptions, runtimeContext) {
70
+ const bundle = await buildEnvProxyNginxBundle(runtime, {
71
+ runtimeCliRoot: runtimeContext.runtimeCliRoot,
72
+ upstreamHost: runtimeContext.upstreamHost,
73
+ });
74
+ const managedConfigBlock = extractManagedNginxConfigBlock(bundle.appConfigContent);
75
+ if (!managedConfigBlock) {
76
+ throw new Error('Failed to render the managed nginx config block.');
77
+ }
78
+ const currentAppConfigContent = await readOptionalTextFile(bundle.appConfigPath);
79
+ let nextAppConfigContent = applyEnvProxyAppEntryOptions(bundle.appConfigContent, 'nginx', appEntryOptions);
80
+ let status = 'created';
81
+ if (currentAppConfigContent) {
82
+ if (!appConfigHasManagedNginxBlock(currentAppConfigContent)) {
83
+ throw new Error(buildNginxManagedBlockMissingMessage(bundle.appConfigPath));
84
+ }
85
+ nextAppConfigContent = applyEnvProxyAppEntryOptions(replaceManagedNginxConfigBlock(currentAppConfigContent, managedConfigBlock), 'nginx', appEntryOptions);
86
+ status = 'updated';
87
+ }
88
+ await Promise.all([mkdir(bundle.entryDir, { recursive: true }), mkdir(bundle.publicDir, { recursive: true })]);
89
+ await Promise.all([
90
+ writeFile(bundle.appConfigPath, nextAppConfigContent, 'utf8'),
91
+ writeFile(bundle.indexV1Path, bundle.indexV1Content, 'utf8'),
92
+ writeFile(bundle.indexV2Path, bundle.indexV2Content, 'utf8'),
93
+ writeFile(bundle.mainConfigPath, bundle.mainConfigContent, 'utf8'),
94
+ syncEnvProxyNginxSnippets(),
95
+ ]);
96
+ return {
97
+ bundle,
98
+ status,
99
+ };
100
+ }
101
+ export function normalizeProxyListenPort(value) {
102
+ const normalized = value?.trim() || undefined;
103
+ if (!normalized || !/^\d+$/.test(normalized)) {
104
+ return undefined;
105
+ }
106
+ const port = Number(normalized);
107
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
108
+ return undefined;
109
+ }
110
+ return normalized;
111
+ }
112
+ export function resolveProxyAppEntryOptions(flags) {
113
+ return {
114
+ host: flags.host?.trim() || undefined,
115
+ port: normalizeProxyListenPort(flags.port?.trim()),
116
+ };
117
+ }
118
+ export function formatNginxProxyInfoLines(info) {
119
+ const lines = [
120
+ `driver: ${info.driver}`,
121
+ `configFile: ${info.configFile}`,
122
+ `snippetsDir: ${info.snippetsDir}`,
123
+ `runtimeRoot: ${info.runtimeRoot}`,
124
+ `upstreamHost:${info.upstreamHost}`,
125
+ ];
126
+ if (info.driver === 'local') {
127
+ lines.push(`nginxBin: ${info.nginxBinary}`);
128
+ }
129
+ else {
130
+ lines.push(`container: ${info.containerName}`);
131
+ lines.push(`image: ${info.image}`);
132
+ }
133
+ return lines;
134
+ }
135
+ function parseNginxPidPathFromVersionOutput(output) {
136
+ const match = output.match(/--pid-path=(?:"([^"]+)"|'([^']+)'|([^\s]+))/);
137
+ const pidPath = String(match?.[1] ?? match?.[2] ?? match?.[3] ?? '').trim();
138
+ if (!pidPath) {
139
+ throw new Error('Failed to detect the nginx pid path from `nginx -V`.');
140
+ }
141
+ return pidPath;
142
+ }
143
+ async function captureNginxVersionOutput() {
144
+ let stdout = '';
145
+ let stderr = '';
146
+ await run('nginx', ['-V'], {
147
+ errorName: 'nginx -V',
148
+ stdio: 'pipe',
149
+ onStdout: (chunk) => {
150
+ stdout += chunk;
151
+ },
152
+ onStderr: (chunk) => {
153
+ stderr += chunk;
154
+ },
155
+ });
156
+ return `${stdout}${stderr}`.trim();
157
+ }
158
+ async function resolveLocalNginxPidPath() {
159
+ return parseNginxPidPathFromVersionOutput(await captureNginxVersionOutput());
160
+ }
161
+ async function isLocalNginxRunning() {
162
+ const pidPath = await resolveLocalNginxPidPath();
163
+ const pidText = await readOptionalTextFile(pidPath);
164
+ const pid = Number.parseInt(String(pidText ?? '').trim(), 10);
165
+ if (!Number.isInteger(pid) || pid <= 0) {
166
+ return false;
167
+ }
168
+ try {
169
+ process.kill(pid, 0);
170
+ return true;
171
+ }
172
+ catch {
173
+ return false;
174
+ }
175
+ }
176
+ async function ensureNginxProxyMainConfig(runtimeContext) {
177
+ const mainConfigPath = resolveEnvProxyMainOutputPath({ provider: 'nginx' });
178
+ const mainConfigContent = await buildEnvProxyMainConfig({
179
+ provider: 'nginx',
180
+ runtimeCliRoot: runtimeContext.runtimeCliRoot,
181
+ });
182
+ await mkdir(path.dirname(mainConfigPath), { recursive: true });
183
+ await writeFile(mainConfigPath, mainConfigContent, 'utf8');
184
+ await syncEnvProxyNginxSnippets();
185
+ return mainConfigPath;
186
+ }
187
+ async function startLocalNginxProxy(runtimeContext) {
188
+ await ensureNginxProxyMainConfig(runtimeContext);
189
+ await installEnvProxyProvider('nginx', { runtimeCliRoot: runtimeContext.runtimeCliRoot });
190
+ if (await isLocalNginxRunning()) {
191
+ return 'already-running';
192
+ }
193
+ await run('nginx', [], {
194
+ errorName: 'nginx',
195
+ stdio: 'ignore',
196
+ });
197
+ return 'started';
198
+ }
199
+ async function stopLocalNginxProxy() {
200
+ if (!(await isLocalNginxRunning())) {
201
+ return 'already-stopped';
202
+ }
203
+ await run('nginx', ['-s', 'stop'], {
204
+ errorName: 'nginx -s stop',
205
+ stdio: 'ignore',
206
+ });
207
+ return 'stopped';
208
+ }
209
+ async function reloadLocalNginxProxy(runtimeContext) {
210
+ await ensureNginxProxyMainConfig(runtimeContext);
211
+ await installEnvProxyProvider('nginx', { runtimeCliRoot: runtimeContext.runtimeCliRoot });
212
+ if (!(await isLocalNginxRunning())) {
213
+ return await startLocalNginxProxy(runtimeContext);
214
+ }
215
+ await reloadEnvProxyProvider('nginx', { runtimeCliRoot: runtimeContext.runtimeCliRoot });
216
+ return 'reloaded';
217
+ }
218
+ async function ensureDockerNginxProxyContainer(runtimeContext) {
219
+ const containerName = await resolveNginxProxyContainerName();
220
+ if (await dockerContainerExists(containerName)) {
221
+ return;
222
+ }
223
+ const hostCliRoot = String(process.env.NB_CLI_ROOT ?? resolveCliHomeRoot()).trim() || resolveCliHomeRoot();
224
+ const mainConfigPath = await ensureNginxProxyMainConfig(runtimeContext);
225
+ await run('docker', [
226
+ 'run',
227
+ '-d',
228
+ '--name',
229
+ containerName,
230
+ '--add-host',
231
+ 'host.docker.internal:host-gateway',
232
+ '-p',
233
+ '80:80',
234
+ '-v',
235
+ `${hostCliRoot}:${DOCKER_NGINX_PROXY_RUNTIME_ROOT}`,
236
+ '-v',
237
+ `${mainConfigPath}:${DOCKER_NGINX_PROXY_CONF_DESTINATION}:ro`,
238
+ DOCKER_NGINX_PROXY_IMAGE,
239
+ ], {
240
+ errorName: 'docker run',
241
+ stdio: 'ignore',
242
+ });
243
+ }
244
+ async function startDockerNginxProxy(runtimeContext) {
245
+ const containerName = await resolveNginxProxyContainerName();
246
+ await ensureNginxProxyMainConfig(runtimeContext);
247
+ if (await dockerContainerExists(containerName)) {
248
+ const state = await startDockerContainer(containerName, { stdio: 'ignore' });
249
+ return state === 'already-running' ? 'already-running' : 'started';
250
+ }
251
+ await ensureDockerNginxProxyContainer(runtimeContext);
252
+ return 'started';
253
+ }
254
+ async function stopDockerNginxProxy() {
255
+ const containerName = await resolveNginxProxyContainerName();
256
+ if (!(await dockerContainerExists(containerName))) {
257
+ return 'already-stopped';
258
+ }
259
+ return await stopDockerContainer(containerName, { stdio: 'ignore' });
260
+ }
261
+ async function reloadDockerNginxProxy(runtimeContext) {
262
+ const containerName = await resolveNginxProxyContainerName();
263
+ await ensureNginxProxyMainConfig(runtimeContext);
264
+ if (!(await dockerContainerIsRunning(containerName))) {
265
+ return await startDockerNginxProxy(runtimeContext);
266
+ }
267
+ await run('docker', ['exec', containerName, 'nginx', '-s', 'reload'], {
268
+ errorName: 'docker exec nginx -s reload',
269
+ stdio: 'ignore',
270
+ });
271
+ return 'reloaded';
272
+ }
273
+ export async function startNginxProxy(runtimeContext) {
274
+ return runtimeContext.driver === 'docker'
275
+ ? await startDockerNginxProxy(runtimeContext)
276
+ : await startLocalNginxProxy(runtimeContext);
277
+ }
278
+ export async function stopNginxProxy(runtimeContext) {
279
+ return runtimeContext.driver === 'docker' ? await stopDockerNginxProxy() : await stopLocalNginxProxy();
280
+ }
281
+ export async function reloadNginxProxy(runtimeContext) {
282
+ return runtimeContext.driver === 'docker'
283
+ ? await reloadDockerNginxProxy(runtimeContext)
284
+ : await reloadLocalNginxProxy(runtimeContext);
285
+ }
286
+ export async function restartNginxProxy(runtimeContext) {
287
+ await stopNginxProxy(runtimeContext);
288
+ await startNginxProxy(runtimeContext);
289
+ return 'restarted';
290
+ }
291
+ export async function getNginxProxyStatus(runtimeContext) {
292
+ const configFile = await mapProxyPathFromCliRoot(resolveEnvProxyMainOutputPath({ provider: 'nginx' }), {
293
+ runtimeCliRoot: runtimeContext.runtimeCliRoot,
294
+ });
295
+ if (runtimeContext.driver === 'docker') {
296
+ const containerName = await resolveNginxProxyContainerName();
297
+ return {
298
+ driver: runtimeContext.driver,
299
+ state: (await dockerContainerIsRunning(containerName)) ? 'running' : 'stopped',
300
+ configFile,
301
+ runtimeRoot: runtimeContext.runtimeCliRoot,
302
+ upstreamHost: runtimeContext.upstreamHost,
303
+ containerName,
304
+ image: resolveNginxProxyImage(),
305
+ };
306
+ }
307
+ return {
308
+ driver: runtimeContext.driver,
309
+ state: (await isLocalNginxRunning()) ? 'running' : 'stopped',
310
+ configFile,
311
+ runtimeRoot: runtimeContext.runtimeCliRoot,
312
+ upstreamHost: runtimeContext.upstreamHost,
313
+ nginxBinary: await getCliConfigValue('bin.nginx'),
314
+ };
315
+ }
316
+ export function formatNginxProxyStatusLines(status) {
317
+ const lines = [
318
+ `driver: ${status.driver}`,
319
+ `status: ${status.state}`,
320
+ `config: ${status.configFile}`,
321
+ ];
322
+ if (status.driver === 'local') {
323
+ lines.push(`nginx: ${status.nginxBinary}`);
324
+ }
325
+ else {
326
+ lines.push(`container: ${status.containerName}`);
327
+ lines.push(`image: ${status.image}`);
328
+ }
329
+ return lines;
330
+ }
@@ -54,6 +54,7 @@
54
54
  "apiBaseUrl": {
55
55
  "invalid": "Enter a valid URL, for example {{example}}.",
56
56
  "invalidProtocol": "URL must start with http:// or https://, for example {{example}}.",
57
+ "missingApiPrefix": "The API base URL must include the /api prefix, for example {{example}}.",
57
58
  "healthCheckPathNotAllowed": "Do not include /__health_check in the API base URL. Enter the application API base URL, for example http://localhost:13000/api.",
58
59
  "maintaining": "The API base URL is reachable, but the app is still starting or in maintenance mode. Please try again later.",
59
60
  "healthCheckFailed": "The API base URL did not pass the health check (HTTP {{status}}). Make sure it points to a running NocoBase API endpoint.",
@@ -149,7 +150,7 @@
149
150
  "appEntryMissingInclude": "The editable app entry config at {{appConfigPath}} does not reference {{generatedConfigPath}}. Add the managed generated-config reference back into the editable app entry and rerun the command.",
150
151
  "nginxOutputUnsupported": "The nginx provider does not support `--output`. It writes `app.conf`, `public/index-v1.html`, `public/index-v2.html`, and `nocobase.conf` into `~/.nocobase/proxy/nginx/<env>/`.",
151
152
  "caddyOutputUnsupported": "The caddy provider does not support `--output`. It writes `app.caddy`, `generated.caddy`, `public/index-v1.html`, `public/index-v2.html`, and `nocobase.caddy` into `~/.nocobase/proxy/caddy/<env>/`.",
152
- "nginxAppEntryMissingManagedBlock": "The editable nginx app entry config at {{appConfigPath}} does not contain the NocoBase managed block. Restore the managed block or delete the file and rerun `nb env proxy`."
153
+ "nginxAppEntryMissingManagedBlock": "The editable nginx app entry config at {{appConfigPath}} does not contain the NocoBase managed block. Restore the managed block or delete the file and rerun `nb proxy nginx generate`."
153
154
  }
154
155
  },
155
156
  "shared": {
@@ -201,7 +202,7 @@
201
202
  },
202
203
  "messages": {
203
204
  "activated": "Activated the license for env \"{{envName}}\".",
204
- "savedLicenseKey": "Saved license key at {{licenseKeyPath}}"
205
+ "savedLicenseKey": "Saved the license key securely for this env"
205
206
  }
206
207
  }
207
208
  },
@@ -320,7 +321,7 @@
320
321
  "placeholder": "local"
321
322
  },
322
323
  "lang": {
323
- "message": "Which language would you like to use?"
324
+ "message": "Which language would you like to use for the app?"
324
325
  },
325
326
  "appPath": {
326
327
  "message": "Where should this app be stored? (relative to {{root}})",