@nocobase/cli 2.2.0-alpha.5 → 2.2.0-alpha.7
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.
- package/assets/env-proxy/nginx/snippets/proxy-location.conf +1 -0
- package/dist/commands/app/start.js +4 -1
- package/dist/commands/env/info.js +11 -1
- package/dist/commands/init.js +11 -1
- package/dist/commands/install.js +27 -95
- package/dist/commands/proxy/caddy/generate.js +12 -4
- package/dist/commands/proxy/nginx/generate.js +12 -4
- package/dist/commands/source/download.js +2 -2
- package/dist/lib/auth-store.js +61 -0
- package/dist/lib/cli-config.js +3 -1
- package/dist/lib/env-config.js +5 -0
- package/dist/lib/env-proxy-config.js +48 -0
- package/dist/lib/env-proxy.js +10 -0
- package/dist/lib/prompt-catalog-terminal.js +32 -19
- package/dist/lib/prompt-web-ui.js +13 -2
- package/dist/lib/proxy-caddy.js +53 -7
- package/dist/lib/proxy-nginx.js +53 -7
- package/dist/locale/en-US.json +37 -37
- package/dist/locale/zh-CN.json +37 -37
- package/package.json +2 -2
|
@@ -11,6 +11,17 @@ import { exit, stdin as stdinStream, stdout as stdoutStream } from 'node:process
|
|
|
11
11
|
import { createCliTranslate } from "./cli-locale.js";
|
|
12
12
|
import { confirm, select, input, password } from "./inquirer.js";
|
|
13
13
|
import { createPromptCatalogHooks, hasIvKey, isBlankText, isPromptBlockSkipped, mergedBoolean, mergedInteger, mergedPassword, mergedSelect, mergedText, resolvePromptCatalogLocale, resolvePromptText, runPromptFieldValidate, selectOptionValues, tryApplyPreset, } from "./prompt-catalog-core.js";
|
|
14
|
+
function buildPromptComputationSeed(catalog, initialValues) {
|
|
15
|
+
const catalogKeys = new Set(Object.keys(catalog));
|
|
16
|
+
const seed = {};
|
|
17
|
+
for (const [key, value] of Object.entries(initialValues)) {
|
|
18
|
+
if (catalogKeys.has(key) || value === undefined || value === null) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
seed[key] = value;
|
|
22
|
+
}
|
|
23
|
+
return seed;
|
|
24
|
+
}
|
|
14
25
|
function adaptInquirerValidate(validate) {
|
|
15
26
|
if (!validate) {
|
|
16
27
|
return undefined;
|
|
@@ -113,14 +124,16 @@ export async function runPromptCatalog(catalog, options = {}) {
|
|
|
113
124
|
const hooks = createTerminalHooks(locale, options.hooks);
|
|
114
125
|
const interactive = Boolean(stdinStream.isTTY && stdoutStream.isTTY && !options.yes);
|
|
115
126
|
const preset = options.values ?? {};
|
|
127
|
+
const computationSeed = buildPromptComputationSeed(catalog, resolveIv);
|
|
116
128
|
const out = {};
|
|
117
129
|
const renderer = createInquirerRenderer();
|
|
118
130
|
for (const [key, def] of Object.entries(catalog)) {
|
|
119
|
-
|
|
131
|
+
const valuesSoFar = { ...computationSeed, ...out };
|
|
132
|
+
if (isPromptBlockSkipped(def, valuesSoFar)) {
|
|
120
133
|
continue;
|
|
121
134
|
}
|
|
122
135
|
if (tryApplyPreset(key, def, preset, out, hooks, locale)) {
|
|
123
|
-
const errV = await runPromptFieldValidate(def, out[key], out);
|
|
136
|
+
const errV = await runPromptFieldValidate(def, out[key], { ...computationSeed, ...out });
|
|
124
137
|
if (errV) {
|
|
125
138
|
hooks.onMissingNonInteractive(errV);
|
|
126
139
|
}
|
|
@@ -135,7 +148,7 @@ export async function runPromptCatalog(catalog, options = {}) {
|
|
|
135
148
|
continue;
|
|
136
149
|
}
|
|
137
150
|
if (def.type === 'run') {
|
|
138
|
-
await def.run(out, options.command);
|
|
151
|
+
await def.run({ ...computationSeed, ...out }, options.command);
|
|
139
152
|
continue;
|
|
140
153
|
}
|
|
141
154
|
if (def.type === 'text') {
|
|
@@ -144,18 +157,18 @@ export async function runPromptCatalog(catalog, options = {}) {
|
|
|
144
157
|
? resolvePromptText(def.placeholder, locale)
|
|
145
158
|
: undefined;
|
|
146
159
|
if (!interactive) {
|
|
147
|
-
const merged = mergedText(key, def, resolveIv, useYesInitial,
|
|
160
|
+
const merged = mergedText(key, def, resolveIv, useYesInitial, valuesSoFar);
|
|
148
161
|
if (def.required && isBlankText(merged)) {
|
|
149
162
|
hooks.onMissingNonInteractive(t('promptCatalog.nonInteractive.textRequired', { key }));
|
|
150
163
|
}
|
|
151
164
|
out[key] = merged;
|
|
152
|
-
const errT = await runPromptFieldValidate(def, merged, { ...out, [key]: merged });
|
|
165
|
+
const errT = await runPromptFieldValidate(def, merged, { ...computationSeed, ...out, [key]: merged });
|
|
153
166
|
if (errT) {
|
|
154
167
|
hooks.onMissingNonInteractive(errT);
|
|
155
168
|
}
|
|
156
169
|
continue;
|
|
157
170
|
}
|
|
158
|
-
const merged = mergedText(key, def, promptIv, false,
|
|
171
|
+
const merged = mergedText(key, def, promptIv, false, valuesSoFar);
|
|
159
172
|
const raw = await callPrompt(() => renderer.text({
|
|
160
173
|
message,
|
|
161
174
|
initialValue: merged,
|
|
@@ -168,7 +181,7 @@ export async function runPromptCatalog(catalog, options = {}) {
|
|
|
168
181
|
return undefined;
|
|
169
182
|
}
|
|
170
183
|
const currentValue = typeof value === 'string' ? value : String(value ?? '');
|
|
171
|
-
const result = runPromptFieldValidate(def, currentValue, { ...out, [key]: currentValue });
|
|
184
|
+
const result = runPromptFieldValidate(def, currentValue, { ...computationSeed, ...out, [key]: currentValue });
|
|
172
185
|
return result;
|
|
173
186
|
},
|
|
174
187
|
}), renderer, hooks);
|
|
@@ -180,7 +193,7 @@ export async function runPromptCatalog(catalog, options = {}) {
|
|
|
180
193
|
if (!interactive) {
|
|
181
194
|
const b = mergedBoolean(key, def, resolveIv, useYesInitial);
|
|
182
195
|
out[key] = b;
|
|
183
|
-
const errB = await runPromptFieldValidate(def, b, { ...out, [key]: b });
|
|
196
|
+
const errB = await runPromptFieldValidate(def, b, { ...computationSeed, ...out, [key]: b });
|
|
184
197
|
if (errB) {
|
|
185
198
|
hooks.onMissingNonInteractive(errB);
|
|
186
199
|
}
|
|
@@ -191,7 +204,7 @@ export async function runPromptCatalog(catalog, options = {}) {
|
|
|
191
204
|
for (;;) {
|
|
192
205
|
const raw = await callPrompt(() => renderer.confirm({ message, initialValue: merged }), renderer, hooks);
|
|
193
206
|
const b = Boolean(raw);
|
|
194
|
-
const errB = await runPromptFieldValidate(def, b, { ...out, [key]: b });
|
|
207
|
+
const errB = await runPromptFieldValidate(def, b, { ...computationSeed, ...out, [key]: b });
|
|
195
208
|
if (errB) {
|
|
196
209
|
renderer.error(errB);
|
|
197
210
|
continue;
|
|
@@ -224,7 +237,7 @@ export async function runPromptCatalog(catalog, options = {}) {
|
|
|
224
237
|
: t('promptCatalog.nonInteractive.selectMissingDefault', { key }));
|
|
225
238
|
}
|
|
226
239
|
out[key] = merged;
|
|
227
|
-
const errS = await runPromptFieldValidate(def, merged, { ...out, [key]: merged });
|
|
240
|
+
const errS = await runPromptFieldValidate(def, merged, { ...computationSeed, ...out, [key]: merged });
|
|
228
241
|
if (errS) {
|
|
229
242
|
hooks.onMissingNonInteractive(errS);
|
|
230
243
|
}
|
|
@@ -248,7 +261,7 @@ export async function runPromptCatalog(catalog, options = {}) {
|
|
|
248
261
|
initialValue: uiInitial,
|
|
249
262
|
}), renderer, hooks);
|
|
250
263
|
const picked = raw;
|
|
251
|
-
const errS = await runPromptFieldValidate(def, picked, { ...out, [key]: picked });
|
|
264
|
+
const errS = await runPromptFieldValidate(def, picked, { ...computationSeed, ...out, [key]: picked });
|
|
252
265
|
if (errS) {
|
|
253
266
|
renderer.error(errS);
|
|
254
267
|
continue;
|
|
@@ -269,13 +282,13 @@ export async function runPromptCatalog(catalog, options = {}) {
|
|
|
269
282
|
if (def.type === 'password') {
|
|
270
283
|
const message = resolvePromptText(def.message, locale, key);
|
|
271
284
|
if (!interactive) {
|
|
272
|
-
const merged = mergedPassword(key, def, resolveIv, useYesInitial);
|
|
285
|
+
const merged = mergedPassword(key, def, resolveIv, useYesInitial, valuesSoFar);
|
|
273
286
|
if (merged === undefined) {
|
|
274
287
|
if (def.required) {
|
|
275
288
|
hooks.onMissingNonInteractive(t('promptCatalog.nonInteractive.passwordRequired', { key }));
|
|
276
289
|
}
|
|
277
290
|
out[key] = '';
|
|
278
|
-
const errPE = await runPromptFieldValidate(def, '', { ...out, [key]: '' });
|
|
291
|
+
const errPE = await runPromptFieldValidate(def, '', { ...computationSeed, ...out, [key]: '' });
|
|
279
292
|
if (errPE) {
|
|
280
293
|
hooks.onMissingNonInteractive(errPE);
|
|
281
294
|
}
|
|
@@ -285,7 +298,7 @@ export async function runPromptCatalog(catalog, options = {}) {
|
|
|
285
298
|
hooks.onMissingNonInteractive(t('promptCatalog.nonInteractive.passwordRequiredNonEmpty', { key }));
|
|
286
299
|
}
|
|
287
300
|
out[key] = merged;
|
|
288
|
-
const errP = await runPromptFieldValidate(def, merged, { ...out, [key]: merged });
|
|
301
|
+
const errP = await runPromptFieldValidate(def, merged, { ...computationSeed, ...out, [key]: merged });
|
|
289
302
|
if (errP) {
|
|
290
303
|
hooks.onMissingNonInteractive(errP);
|
|
291
304
|
}
|
|
@@ -302,7 +315,7 @@ export async function runPromptCatalog(catalog, options = {}) {
|
|
|
302
315
|
return undefined;
|
|
303
316
|
}
|
|
304
317
|
const currentValue = typeof value === 'string' ? value : String(value ?? '');
|
|
305
|
-
const result = runPromptFieldValidate(def, currentValue, { ...out, [key]: currentValue });
|
|
318
|
+
const result = runPromptFieldValidate(def, currentValue, { ...computationSeed, ...out, [key]: currentValue });
|
|
306
319
|
return result;
|
|
307
320
|
},
|
|
308
321
|
}), renderer, hooks);
|
|
@@ -322,14 +335,14 @@ export async function runPromptCatalog(catalog, options = {}) {
|
|
|
322
335
|
}
|
|
323
336
|
const z = def.initialValue ?? 0;
|
|
324
337
|
out[key] = z;
|
|
325
|
-
const errI = await runPromptFieldValidate(def, z, { ...out, [key]: z });
|
|
338
|
+
const errI = await runPromptFieldValidate(def, z, { ...computationSeed, ...out, [key]: z });
|
|
326
339
|
if (errI) {
|
|
327
340
|
hooks.onMissingNonInteractive(errI);
|
|
328
341
|
}
|
|
329
342
|
continue;
|
|
330
343
|
}
|
|
331
344
|
out[key] = merged;
|
|
332
|
-
const errI2 = await runPromptFieldValidate(def, merged, { ...out, [key]: merged });
|
|
345
|
+
const errI2 = await runPromptFieldValidate(def, merged, { ...computationSeed, ...out, [key]: merged });
|
|
333
346
|
if (errI2) {
|
|
334
347
|
hooks.onMissingNonInteractive(errI2);
|
|
335
348
|
}
|
|
@@ -349,7 +362,7 @@ export async function runPromptCatalog(catalog, options = {}) {
|
|
|
349
362
|
}
|
|
350
363
|
if (def.validate) {
|
|
351
364
|
const z = def.initialValue ?? 0;
|
|
352
|
-
return runPromptFieldValidate(def, z, { ...out, [key]: z });
|
|
365
|
+
return runPromptFieldValidate(def, z, { ...computationSeed, ...out, [key]: z });
|
|
353
366
|
}
|
|
354
367
|
return undefined;
|
|
355
368
|
}
|
|
@@ -360,7 +373,7 @@ export async function runPromptCatalog(catalog, options = {}) {
|
|
|
360
373
|
return undefined;
|
|
361
374
|
}
|
|
362
375
|
const n = Number.parseInt(trimmed, 10);
|
|
363
|
-
return runPromptFieldValidate(def, n, { ...out, [key]: n });
|
|
376
|
+
return runPromptFieldValidate(def, n, { ...computationSeed, ...out, [key]: n });
|
|
364
377
|
},
|
|
365
378
|
}), renderer, hooks);
|
|
366
379
|
if (typeof raw === 'string' && raw.trim() === '' && !def.required) {
|
|
@@ -48,13 +48,24 @@ function isInputBlock(def) {
|
|
|
48
48
|
def.type === 'password' ||
|
|
49
49
|
def.type === 'integer');
|
|
50
50
|
}
|
|
51
|
+
function buildPromptComputationSeed(catalog, userPreset) {
|
|
52
|
+
const catalogKeys = new Set(Object.keys(catalog));
|
|
53
|
+
const seed = {};
|
|
54
|
+
for (const [key, value] of Object.entries(userPreset)) {
|
|
55
|
+
if (catalogKeys.has(key) || value === undefined || value === null) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
seed[key] = value;
|
|
59
|
+
}
|
|
60
|
+
return seed;
|
|
61
|
+
}
|
|
51
62
|
/**
|
|
52
63
|
* Merges CLI/env **`userPreset`** with catalog block defaults, in the same key order and with the
|
|
53
64
|
* same `hidden` / `run` semantics as {@link isPromptBlockSkipped}, so the web form can prefill
|
|
54
65
|
* and reflow `hidden` fields (e.g. `integer` when `select` changes).
|
|
55
66
|
*/
|
|
56
67
|
export function buildWebFormValuesFromCatalog(catalog, userPreset = {}) {
|
|
57
|
-
const out =
|
|
68
|
+
const out = buildPromptComputationSeed(catalog, userPreset);
|
|
58
69
|
for (const [key, def] of Object.entries(catalog)) {
|
|
59
70
|
if (def.type === 'intro' || def.type === 'outro') {
|
|
60
71
|
continue;
|
|
@@ -109,7 +120,7 @@ function defaultValueForInput(key, def, out) {
|
|
|
109
120
|
* from current raw form data (e.g. after changing `select`). Matches how {@link isPromptBlockSkipped} uses `out` while iterating the catalog.
|
|
110
121
|
*/
|
|
111
122
|
export function reflowWebFormState(catalog, raw, userSeed = {}) {
|
|
112
|
-
const out =
|
|
123
|
+
const out = buildPromptComputationSeed(catalog, userSeed);
|
|
113
124
|
const show = {};
|
|
114
125
|
for (const [key, def] of Object.entries(catalog)) {
|
|
115
126
|
if (def.type === 'intro' || def.type === 'outro') {
|
package/dist/lib/proxy-caddy.js
CHANGED
|
@@ -9,14 +9,17 @@
|
|
|
9
9
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { dockerContainerExists, dockerContainerIsRunning, startDockerContainer, stopDockerContainer, } from './app-runtime.js';
|
|
12
|
+
import { loadAuthConfig } from './auth-store.js';
|
|
12
13
|
import { CADDY_PROXY_DRIVER_OPTIONS, DEFAULT_CADDY_PROXY_DRIVER, getCliConfigValue, normalizeCaddyProxyDriver, resolveDockerContainerPrefix, setCliConfigValue, } from './cli-config.js';
|
|
13
14
|
import { resolveCliHomeRoot } from './cli-home.js';
|
|
14
15
|
import { applyEnvProxyAppEntryOptions, buildManualEnvProxyCaddyBundle, buildEnvProxyCaddyBundle, buildEnvProxyMainConfig, mapProxyPathFromCliRoot, resolveEnvProxyMainOutputPath, } from './env-proxy.js';
|
|
15
|
-
import {
|
|
16
|
+
import { normalizeEnvProxyConfig } from './env-proxy-config.js';
|
|
17
|
+
import { commandOutput, run } from './run-npm.js';
|
|
16
18
|
const DOCKER_CADDY_PROXY_CONTAINER_SUFFIX = 'caddy-proxy';
|
|
17
19
|
const DOCKER_CADDY_PROXY_IMAGE = 'caddy:latest';
|
|
18
20
|
const DOCKER_CADDY_PROXY_RUNTIME_ROOT = '/apps';
|
|
19
21
|
const DOCKER_CADDY_PROXY_CONF_DESTINATION = '/etc/caddy/Caddyfile';
|
|
22
|
+
const DEFAULT_DOCKER_CADDY_PROXY_PUBLISHED_PORTS = [80, 443];
|
|
20
23
|
async function readOptionalTextFile(filePath) {
|
|
21
24
|
try {
|
|
22
25
|
return await readFile(filePath, 'utf8');
|
|
@@ -171,11 +174,16 @@ async function reloadLocalCaddyProxy(runtimeContext) {
|
|
|
171
174
|
}
|
|
172
175
|
async function ensureDockerCaddyProxyContainer(runtimeContext) {
|
|
173
176
|
const containerName = await resolveCaddyProxyContainerName();
|
|
177
|
+
const mainConfigPath = await ensureCaddyProxyMainConfig(runtimeContext);
|
|
178
|
+
const publishedPorts = await resolveDockerCaddyPublishedPorts();
|
|
174
179
|
if (await dockerContainerExists(containerName)) {
|
|
175
|
-
|
|
180
|
+
if (await dockerCaddyProxyContainerMatchesPublishedPorts(containerName, publishedPorts)) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
await removeDockerCaddyProxyContainer(containerName);
|
|
176
184
|
}
|
|
177
185
|
const hostCliRoot = String(process.env.NB_CLI_ROOT ?? resolveCliHomeRoot()).trim() || resolveCliHomeRoot();
|
|
178
|
-
const
|
|
186
|
+
const dockerPortArgs = publishedPorts.flatMap((port) => ['-p', `${port}:${port}`]);
|
|
179
187
|
await run('docker', [
|
|
180
188
|
'run',
|
|
181
189
|
'-d',
|
|
@@ -183,8 +191,7 @@ async function ensureDockerCaddyProxyContainer(runtimeContext) {
|
|
|
183
191
|
containerName,
|
|
184
192
|
'--add-host',
|
|
185
193
|
'host.docker.internal:host-gateway',
|
|
186
|
-
|
|
187
|
-
'80:80',
|
|
194
|
+
...dockerPortArgs,
|
|
188
195
|
'-v',
|
|
189
196
|
`${hostCliRoot}:${DOCKER_CADDY_PROXY_RUNTIME_ROOT}`,
|
|
190
197
|
'-v',
|
|
@@ -195,14 +202,53 @@ async function ensureDockerCaddyProxyContainer(runtimeContext) {
|
|
|
195
202
|
stdio: 'ignore',
|
|
196
203
|
});
|
|
197
204
|
}
|
|
205
|
+
async function resolveDockerCaddyPublishedPorts() {
|
|
206
|
+
const config = await loadAuthConfig();
|
|
207
|
+
const ports = new Set(DEFAULT_DOCKER_CADDY_PROXY_PUBLISHED_PORTS);
|
|
208
|
+
for (const envConfig of Object.values(config.envs)) {
|
|
209
|
+
const port = normalizeEnvProxyConfig(envConfig.proxy)?.port;
|
|
210
|
+
if (port !== undefined) {
|
|
211
|
+
ports.add(port);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return Array.from(ports).sort((left, right) => left - right);
|
|
215
|
+
}
|
|
216
|
+
async function readDockerCaddyPublishedPorts(containerName) {
|
|
217
|
+
const output = await commandOutput('docker', ['inspect', '--format', '{{json .HostConfig.PortBindings}}', containerName], { errorName: 'docker inspect' });
|
|
218
|
+
const parsed = JSON.parse(output.trim() || '{}');
|
|
219
|
+
const ports = new Set();
|
|
220
|
+
for (const bindings of Object.values(parsed)) {
|
|
221
|
+
if (!Array.isArray(bindings)) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
for (const binding of bindings) {
|
|
225
|
+
const port = Number.parseInt(String(binding?.HostPort ?? '').trim(), 10);
|
|
226
|
+
if (Number.isInteger(port) && port > 0) {
|
|
227
|
+
ports.add(port);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return Array.from(ports).sort((left, right) => left - right);
|
|
232
|
+
}
|
|
233
|
+
async function dockerCaddyProxyContainerMatchesPublishedPorts(containerName, expectedPorts) {
|
|
234
|
+
const currentPorts = await readDockerCaddyPublishedPorts(containerName);
|
|
235
|
+
return currentPorts.length === expectedPorts.length && currentPorts.every((port, index) => port === expectedPorts[index]);
|
|
236
|
+
}
|
|
237
|
+
async function removeDockerCaddyProxyContainer(containerName) {
|
|
238
|
+
await run('docker', ['rm', '-f', containerName], {
|
|
239
|
+
errorName: 'docker rm',
|
|
240
|
+
stdio: 'ignore',
|
|
241
|
+
});
|
|
242
|
+
}
|
|
198
243
|
async function startDockerCaddyProxy(runtimeContext) {
|
|
199
244
|
const containerName = await resolveCaddyProxyContainerName();
|
|
200
245
|
await ensureCaddyProxyMainConfig(runtimeContext);
|
|
201
|
-
|
|
246
|
+
const existedBeforeEnsure = await dockerContainerExists(containerName);
|
|
247
|
+
await ensureDockerCaddyProxyContainer(runtimeContext);
|
|
248
|
+
if (existedBeforeEnsure && (await dockerContainerExists(containerName))) {
|
|
202
249
|
const state = await startDockerContainer(containerName, { stdio: 'ignore' });
|
|
203
250
|
return state === 'already-running' ? 'already-running' : 'started';
|
|
204
251
|
}
|
|
205
|
-
await ensureDockerCaddyProxyContainer(runtimeContext);
|
|
206
252
|
return 'started';
|
|
207
253
|
}
|
|
208
254
|
async function stopDockerCaddyProxy() {
|
package/dist/lib/proxy-nginx.js
CHANGED
|
@@ -9,14 +9,17 @@
|
|
|
9
9
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { dockerContainerExists, dockerContainerIsRunning, startDockerContainer, stopDockerContainer, } from './app-runtime.js';
|
|
12
|
+
import { loadAuthConfig } from './auth-store.js';
|
|
12
13
|
import { DEFAULT_NGINX_PROXY_DRIVER, getCliConfigValue, NGINX_PROXY_DRIVER_OPTIONS, normalizeNginxProxyDriver, resolveDockerContainerPrefix, setCliConfigValue, } from './cli-config.js';
|
|
13
14
|
import { resolveCliHomeRoot } from './cli-home.js';
|
|
14
15
|
import { applyEnvProxyAppEntryOptions, appConfigHasManagedNginxBlock, buildManualEnvProxyNginxBundle, buildEnvProxyMainConfig, buildEnvProxyNginxBundle, extractManagedNginxConfigBlock, installEnvProxyProvider, mapProxyPathFromCliRoot, reloadEnvProxyProvider, resolveEnvProxyMainOutputPath, replaceManagedNginxConfigBlock, syncEnvProxyNginxSnippets, } from './env-proxy.js';
|
|
15
|
-
import {
|
|
16
|
+
import { normalizeEnvProxyConfig } from './env-proxy-config.js';
|
|
17
|
+
import { commandOutput, run } from './run-npm.js';
|
|
16
18
|
const DOCKER_NGINX_PROXY_CONTAINER_SUFFIX = 'nginx-proxy';
|
|
17
19
|
const DOCKER_NGINX_PROXY_IMAGE = 'nginx:latest';
|
|
18
20
|
const DOCKER_NGINX_PROXY_RUNTIME_ROOT = '/apps';
|
|
19
21
|
const DOCKER_NGINX_PROXY_CONF_DESTINATION = '/etc/nginx/conf.d/default.conf';
|
|
22
|
+
const DEFAULT_DOCKER_NGINX_PROXY_PUBLISHED_PORTS = [80, 443];
|
|
20
23
|
async function readOptionalTextFile(filePath) {
|
|
21
24
|
try {
|
|
22
25
|
return await readFile(filePath, 'utf8');
|
|
@@ -231,11 +234,16 @@ async function reloadLocalNginxProxy(runtimeContext) {
|
|
|
231
234
|
}
|
|
232
235
|
async function ensureDockerNginxProxyContainer(runtimeContext) {
|
|
233
236
|
const containerName = await resolveNginxProxyContainerName();
|
|
237
|
+
const mainConfigPath = await ensureNginxProxyMainConfig(runtimeContext);
|
|
238
|
+
const publishedPorts = await resolveDockerNginxPublishedPorts();
|
|
234
239
|
if (await dockerContainerExists(containerName)) {
|
|
235
|
-
|
|
240
|
+
if (await dockerNginxProxyContainerMatchesPublishedPorts(containerName, publishedPorts)) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
await removeDockerNginxProxyContainer(containerName);
|
|
236
244
|
}
|
|
237
245
|
const hostCliRoot = String(process.env.NB_CLI_ROOT ?? resolveCliHomeRoot()).trim() || resolveCliHomeRoot();
|
|
238
|
-
const
|
|
246
|
+
const dockerPortArgs = publishedPorts.flatMap((port) => ['-p', `${port}:${port}`]);
|
|
239
247
|
await run('docker', [
|
|
240
248
|
'run',
|
|
241
249
|
'-d',
|
|
@@ -243,8 +251,7 @@ async function ensureDockerNginxProxyContainer(runtimeContext) {
|
|
|
243
251
|
containerName,
|
|
244
252
|
'--add-host',
|
|
245
253
|
'host.docker.internal:host-gateway',
|
|
246
|
-
|
|
247
|
-
'80:80',
|
|
254
|
+
...dockerPortArgs,
|
|
248
255
|
'-v',
|
|
249
256
|
`${hostCliRoot}:${DOCKER_NGINX_PROXY_RUNTIME_ROOT}`,
|
|
250
257
|
'-v',
|
|
@@ -255,14 +262,53 @@ async function ensureDockerNginxProxyContainer(runtimeContext) {
|
|
|
255
262
|
stdio: 'ignore',
|
|
256
263
|
});
|
|
257
264
|
}
|
|
265
|
+
async function resolveDockerNginxPublishedPorts() {
|
|
266
|
+
const config = await loadAuthConfig();
|
|
267
|
+
const ports = new Set(DEFAULT_DOCKER_NGINX_PROXY_PUBLISHED_PORTS);
|
|
268
|
+
for (const envConfig of Object.values(config.envs)) {
|
|
269
|
+
const port = normalizeEnvProxyConfig(envConfig.proxy)?.port;
|
|
270
|
+
if (port !== undefined) {
|
|
271
|
+
ports.add(port);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return Array.from(ports).sort((left, right) => left - right);
|
|
275
|
+
}
|
|
276
|
+
async function readDockerNginxPublishedPorts(containerName) {
|
|
277
|
+
const output = await commandOutput('docker', ['inspect', '--format', '{{json .HostConfig.PortBindings}}', containerName], { errorName: 'docker inspect' });
|
|
278
|
+
const parsed = JSON.parse(output.trim() || '{}');
|
|
279
|
+
const ports = new Set();
|
|
280
|
+
for (const bindings of Object.values(parsed)) {
|
|
281
|
+
if (!Array.isArray(bindings)) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
for (const binding of bindings) {
|
|
285
|
+
const port = Number.parseInt(String(binding?.HostPort ?? '').trim(), 10);
|
|
286
|
+
if (Number.isInteger(port) && port > 0) {
|
|
287
|
+
ports.add(port);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return Array.from(ports).sort((left, right) => left - right);
|
|
292
|
+
}
|
|
293
|
+
async function dockerNginxProxyContainerMatchesPublishedPorts(containerName, expectedPorts) {
|
|
294
|
+
const currentPorts = await readDockerNginxPublishedPorts(containerName);
|
|
295
|
+
return currentPorts.length === expectedPorts.length && currentPorts.every((port, index) => port === expectedPorts[index]);
|
|
296
|
+
}
|
|
297
|
+
async function removeDockerNginxProxyContainer(containerName) {
|
|
298
|
+
await run('docker', ['rm', '-f', containerName], {
|
|
299
|
+
errorName: 'docker rm',
|
|
300
|
+
stdio: 'ignore',
|
|
301
|
+
});
|
|
302
|
+
}
|
|
258
303
|
async function startDockerNginxProxy(runtimeContext) {
|
|
259
304
|
const containerName = await resolveNginxProxyContainerName();
|
|
260
305
|
await ensureNginxProxyMainConfig(runtimeContext);
|
|
261
|
-
|
|
306
|
+
const existedBeforeEnsure = await dockerContainerExists(containerName);
|
|
307
|
+
await ensureDockerNginxProxyContainer(runtimeContext);
|
|
308
|
+
if (existedBeforeEnsure && (await dockerContainerExists(containerName))) {
|
|
262
309
|
const state = await startDockerContainer(containerName, { stdio: 'ignore' });
|
|
263
310
|
return state === 'already-running' ? 'already-running' : 'started';
|
|
264
311
|
}
|
|
265
|
-
await ensureDockerNginxProxyContainer(runtimeContext);
|
|
266
312
|
return 'started';
|
|
267
313
|
}
|
|
268
314
|
async function stopDockerNginxProxy() {
|
package/dist/locale/en-US.json
CHANGED
|
@@ -93,20 +93,20 @@
|
|
|
93
93
|
"envAdd": {
|
|
94
94
|
"prompts": {
|
|
95
95
|
"name": {
|
|
96
|
-
"message": "
|
|
96
|
+
"message": "App environment identifier",
|
|
97
97
|
"placeholder": "default"
|
|
98
98
|
},
|
|
99
99
|
"scope": {
|
|
100
|
-
"message": "
|
|
100
|
+
"message": "Connection save location",
|
|
101
101
|
"globalLabel": "Global",
|
|
102
102
|
"globalHint": "user-level config"
|
|
103
103
|
},
|
|
104
104
|
"apiBaseUrl": {
|
|
105
|
-
"message": "
|
|
105
|
+
"message": "API base URL",
|
|
106
106
|
"placeholder": "https://demo.example.com/api or https://demo.example.com/api/__app/<subapp>"
|
|
107
107
|
},
|
|
108
108
|
"authType": {
|
|
109
|
-
"message": "
|
|
109
|
+
"message": "Authentication method",
|
|
110
110
|
"basicLabel": "Basic authentication (username + password)",
|
|
111
111
|
"basicHint": "uses your credentials to fetch a token after save",
|
|
112
112
|
"oauthLabel": "OAuth (browser authentication)",
|
|
@@ -114,14 +114,14 @@
|
|
|
114
114
|
"tokenLabel": "API token / API key"
|
|
115
115
|
},
|
|
116
116
|
"username": {
|
|
117
|
-
"message": "
|
|
117
|
+
"message": "Basic login username",
|
|
118
118
|
"placeholder": "admin"
|
|
119
119
|
},
|
|
120
120
|
"password": {
|
|
121
|
-
"message": "
|
|
121
|
+
"message": "Basic login password"
|
|
122
122
|
},
|
|
123
123
|
"accessToken": {
|
|
124
|
-
"message": "
|
|
124
|
+
"message": "API token or API key",
|
|
125
125
|
"placeholder": "Enter your API token / API key"
|
|
126
126
|
}
|
|
127
127
|
}
|
|
@@ -179,7 +179,7 @@
|
|
|
179
179
|
},
|
|
180
180
|
"prompts": {
|
|
181
181
|
"provideMethod": {
|
|
182
|
-
"message": "
|
|
182
|
+
"message": "License key input method",
|
|
183
183
|
"keyOption": "Paste the license key",
|
|
184
184
|
"fileOption": "Read the key from a file"
|
|
185
185
|
},
|
|
@@ -252,7 +252,7 @@
|
|
|
252
252
|
},
|
|
253
253
|
"prompts": {
|
|
254
254
|
"source": {
|
|
255
|
-
"message": "
|
|
255
|
+
"message": "NocoBase install source",
|
|
256
256
|
"dockerLabel": "Docker install (Recommended)",
|
|
257
257
|
"dockerHint": "Best for no-code and low-maintenance setups. You can upgrade later by pulling a newer image and restarting the app.",
|
|
258
258
|
"npmLabel": "create-nocobase-app install",
|
|
@@ -261,7 +261,7 @@
|
|
|
261
261
|
"gitHint": "Best when you want to try the latest unreleased changes, contribute code, or debug and modify NocoBase source directly. This option is more developer-oriented."
|
|
262
262
|
},
|
|
263
263
|
"version": {
|
|
264
|
-
"message": "
|
|
264
|
+
"message": "NocoBase version",
|
|
265
265
|
"latestLabel": "latest",
|
|
266
266
|
"latestHint": "Stable release. Best for production use and the most predictable experience.",
|
|
267
267
|
"betaLabel": "beta",
|
|
@@ -272,15 +272,15 @@
|
|
|
272
272
|
"otherHint": "Enter another package version, Docker tag, or Git ref manually, such as a branch name."
|
|
273
273
|
},
|
|
274
274
|
"otherVersion": {
|
|
275
|
-
"message": "
|
|
275
|
+
"message": "Custom version, Docker tag, or Git ref",
|
|
276
276
|
"placeholder": "For example: fix/cli-v2"
|
|
277
277
|
},
|
|
278
278
|
"dockerRegistry": {
|
|
279
|
-
"message": "
|
|
279
|
+
"message": "Docker registry (image tag is set in Version)",
|
|
280
280
|
"placeholder": "nocobase/nocobase"
|
|
281
281
|
},
|
|
282
282
|
"dockerPlatform": {
|
|
283
|
-
"message": "
|
|
283
|
+
"message": "Architecture",
|
|
284
284
|
"autoLabel": "Auto",
|
|
285
285
|
"autoHint": "Use Docker default for this machine"
|
|
286
286
|
},
|
|
@@ -323,79 +323,79 @@
|
|
|
323
323
|
},
|
|
324
324
|
"prompts": {
|
|
325
325
|
"env": {
|
|
326
|
-
"message": "
|
|
326
|
+
"message": "App environment identifier",
|
|
327
327
|
"placeholder": "local"
|
|
328
328
|
},
|
|
329
329
|
"lang": {
|
|
330
|
-
"message": "
|
|
330
|
+
"message": "App language"
|
|
331
331
|
},
|
|
332
332
|
"appPath": {
|
|
333
|
-
"message": "
|
|
333
|
+
"message": "App directory (relative to {{root}})",
|
|
334
334
|
"placeholder": "./<env>/"
|
|
335
335
|
},
|
|
336
336
|
"appPort": {
|
|
337
|
-
"message": "
|
|
337
|
+
"message": "App port",
|
|
338
338
|
"placeholder": "13000"
|
|
339
339
|
},
|
|
340
340
|
"appPublicPath": {
|
|
341
|
-
"message": "
|
|
341
|
+
"message": "App subpath (for example, /nocobase/)",
|
|
342
342
|
"placeholder": "/ or /nocobase/"
|
|
343
343
|
},
|
|
344
344
|
"storagePath": {
|
|
345
|
-
"message": "
|
|
345
|
+
"message": "Uploads and local files directory",
|
|
346
346
|
"placeholder": "./<env>/storage/"
|
|
347
347
|
},
|
|
348
348
|
"dbDialect": {
|
|
349
|
-
"message": "
|
|
349
|
+
"message": "Database type"
|
|
350
350
|
},
|
|
351
351
|
"builtinDb": {
|
|
352
|
-
"message": "
|
|
352
|
+
"message": "Use built-in database"
|
|
353
353
|
},
|
|
354
354
|
"builtinDbImage": {
|
|
355
|
-
"message": "
|
|
355
|
+
"message": "Built-in database Docker image",
|
|
356
356
|
"placeholder": "postgres:16"
|
|
357
357
|
},
|
|
358
358
|
"dbHost": {
|
|
359
|
-
"message": "
|
|
359
|
+
"message": "Database host",
|
|
360
360
|
"placeholder": "127.0.0.1"
|
|
361
361
|
},
|
|
362
362
|
"dbPort": {
|
|
363
|
-
"message": "
|
|
363
|
+
"message": "Database port",
|
|
364
364
|
"placeholder": "5432"
|
|
365
365
|
},
|
|
366
366
|
"dbDatabase": {
|
|
367
|
-
"message": "
|
|
367
|
+
"message": "Database name"
|
|
368
368
|
},
|
|
369
369
|
"dbUser": {
|
|
370
|
-
"message": "
|
|
370
|
+
"message": "Database username"
|
|
371
371
|
},
|
|
372
372
|
"dbPassword": {
|
|
373
|
-
"message": "
|
|
373
|
+
"message": "Database password"
|
|
374
374
|
},
|
|
375
375
|
"dbSchema": {
|
|
376
|
-
"message": "
|
|
376
|
+
"message": "Database schema (PostgreSQL/KingbaseES only, optional)",
|
|
377
377
|
"placeholder": "Leave empty to use the default schema"
|
|
378
378
|
},
|
|
379
379
|
"dbTablePrefix": {
|
|
380
|
-
"message": "
|
|
380
|
+
"message": "Database table prefix (optional)",
|
|
381
381
|
"placeholder": "For example: nb_"
|
|
382
382
|
},
|
|
383
383
|
"dbUnderscored": {
|
|
384
|
-
"message": "Use underscored names for database tables and columns
|
|
384
|
+
"message": "Use underscored names for database tables and columns"
|
|
385
385
|
},
|
|
386
386
|
"rootUsername": {
|
|
387
|
-
"message": "
|
|
387
|
+
"message": "Initial admin username",
|
|
388
388
|
"placeholder": "nocobase"
|
|
389
389
|
},
|
|
390
390
|
"rootEmail": {
|
|
391
|
-
"message": "
|
|
391
|
+
"message": "Initial admin email",
|
|
392
392
|
"placeholder": "admin@nocobase.com"
|
|
393
393
|
},
|
|
394
394
|
"rootPassword": {
|
|
395
|
-
"message": "
|
|
395
|
+
"message": "Initial admin password"
|
|
396
396
|
},
|
|
397
397
|
"rootNickname": {
|
|
398
|
-
"message": "
|
|
398
|
+
"message": "Initial admin display name",
|
|
399
399
|
"placeholder": "Super Admin"
|
|
400
400
|
}
|
|
401
401
|
}
|
|
@@ -419,11 +419,11 @@
|
|
|
419
419
|
},
|
|
420
420
|
"prompts": {
|
|
421
421
|
"appName": {
|
|
422
|
-
"message": "
|
|
422
|
+
"message": "Unique app environment identifier (used by the CLI to locate and manage this environment)",
|
|
423
423
|
"placeholder": "local"
|
|
424
424
|
},
|
|
425
425
|
"setupMode": {
|
|
426
|
-
"message": "
|
|
426
|
+
"message": "App environment setup method",
|
|
427
427
|
"installNewLabel": "Install a new app",
|
|
428
428
|
"installNewHint": "Install a brand-new app from scratch. Best for first-time setup, evaluation, or a fresh local environment.",
|
|
429
429
|
"manageLocalLabel": "Manage an app already on this machine",
|
|
@@ -432,7 +432,7 @@
|
|
|
432
432
|
"connectRemoteHint": "Save only the remote app connection. Nothing will be installed or taken over on this machine."
|
|
433
433
|
},
|
|
434
434
|
"installSkills": {
|
|
435
|
-
"message": "Install NocoBase AI coding skills (nocobase/skills)
|
|
435
|
+
"message": "Install NocoBase AI coding skills (nocobase/skills)"
|
|
436
436
|
},
|
|
437
437
|
"apiBaseUrl": {
|
|
438
438
|
"message": "API base URL",
|