@nocobase/cli 2.2.0-beta.8 → 2.2.0-test.15

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 (56) hide show
  1. package/assets/env-proxy/nginx/app.conf.tpl +23 -0
  2. package/assets/env-proxy/nginx/nocobase.conf.tpl +5 -0
  3. package/assets/env-proxy/nginx/snippets/dist-location.conf +5 -0
  4. package/assets/env-proxy/nginx/snippets/gzip.conf +17 -0
  5. package/assets/env-proxy/nginx/snippets/log-format-http.conf +13 -0
  6. package/assets/env-proxy/nginx/snippets/maps-http.conf +14 -0
  7. package/assets/env-proxy/nginx/snippets/mime-types.conf +98 -0
  8. package/assets/env-proxy/nginx/snippets/proxy-location.conf +18 -0
  9. package/assets/env-proxy/nginx/snippets/spa-location.conf +6 -0
  10. package/assets/env-proxy/nginx/snippets/uploads-location.conf +24 -0
  11. package/dist/commands/app/start.js +25 -2
  12. package/dist/commands/config/set.js +1 -0
  13. package/dist/commands/env/info.js +11 -1
  14. package/dist/commands/init.js +131 -4
  15. package/dist/commands/install.js +179 -131
  16. package/dist/commands/portal/create.js +105 -0
  17. package/dist/commands/portal/deploy.js +81 -0
  18. package/dist/commands/portal/destroy.js +104 -0
  19. package/dist/commands/portal/dev.js +71 -0
  20. package/dist/commands/portal/index.js +20 -0
  21. package/dist/commands/portal/info.js +82 -0
  22. package/dist/commands/portal/list.js +98 -0
  23. package/dist/commands/portal/pull.js +77 -0
  24. package/dist/commands/portal/push.js +79 -0
  25. package/dist/commands/proxy/caddy/generate.js +93 -7
  26. package/dist/commands/proxy/nginx/generate.js +98 -7
  27. package/dist/commands/revision/create.js +1 -1
  28. package/dist/commands/source/dev.js +1 -1
  29. package/dist/commands/source/download.js +18 -14
  30. package/dist/lib/app-managed-resources.js +3 -2
  31. package/dist/lib/auth-store.js +71 -1
  32. package/dist/lib/cli-config.js +74 -2
  33. package/dist/lib/docker-image.js +94 -6
  34. package/dist/lib/env-config.js +8 -0
  35. package/dist/lib/env-proxy-config.js +48 -0
  36. package/dist/lib/env-proxy.js +266 -61
  37. package/dist/lib/managed-init-env.js +6 -1
  38. package/dist/lib/portal-command-env.js +31 -0
  39. package/dist/lib/portal-create.js +488 -0
  40. package/dist/lib/portal-deploy.js +275 -0
  41. package/dist/lib/portal-destroy.js +100 -0
  42. package/dist/lib/portal-dev.js +79 -0
  43. package/dist/lib/portal-env-files.js +53 -0
  44. package/dist/lib/portal-info.js +31 -0
  45. package/dist/lib/portal-list.js +197 -0
  46. package/dist/lib/portal-source.js +416 -0
  47. package/dist/lib/portal-template.js +190 -0
  48. package/dist/lib/prompt-catalog-terminal.js +32 -19
  49. package/dist/lib/prompt-web-ui.js +13 -2
  50. package/dist/lib/proxy-caddy.js +77 -9
  51. package/dist/lib/proxy-nginx.js +71 -11
  52. package/dist/lib/run-npm.js +17 -16
  53. package/dist/lib/ui.js +28 -1
  54. package/dist/locale/en-US.json +192 -38
  55. package/dist/locale/zh-CN.json +192 -38
  56. package/package.json +6 -3
@@ -0,0 +1,190 @@
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 { cp, mkdir, mkdtemp, rm, stat } from 'node:fs/promises';
10
+ import os from 'node:os';
11
+ import path from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { buildPortalCommandEnv } from './portal-command-env.js';
14
+ import { resolvePortalTemplate } from './portal-create.js';
15
+ import { run } from './run-npm.js';
16
+ const DEFAULT_PORTAL_APP_NAME = 'main';
17
+ const DEFAULT_PORTAL_NAME = 'admin';
18
+ const PORTAL_CLIENT_PREFIX = 'x';
19
+ function trimValue(value) {
20
+ return String(value ?? '').trim();
21
+ }
22
+ function normalizePortalName(value) {
23
+ const segment = String(value || '')
24
+ .trim()
25
+ .replace(/^\/+|\/+$/g, '');
26
+ return segment || DEFAULT_PORTAL_NAME;
27
+ }
28
+ function normalizePortalAppName(value) {
29
+ const segment = String(value || '')
30
+ .trim()
31
+ .replace(/^\/+|\/+$/g, '');
32
+ return segment || DEFAULT_PORTAL_APP_NAME;
33
+ }
34
+ function validatePortalSegment(kind, value) {
35
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(value)) {
36
+ throw new Error(`Invalid ${kind} "${value}". Use letters, numbers, underscores, or hyphens, and start with a letter or number.`);
37
+ }
38
+ }
39
+ async function pathExists(filePath) {
40
+ try {
41
+ await stat(filePath);
42
+ return true;
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }
48
+ function resolveLocalTemplatePath(templateSource) {
49
+ if (templateSource.startsWith('file://')) {
50
+ return fileURLToPath(templateSource);
51
+ }
52
+ return templateSource;
53
+ }
54
+ function isGitTemplateSource(templateSource) {
55
+ return (templateSource.startsWith('git@') ||
56
+ templateSource.startsWith('git+') ||
57
+ /^https?:\/\//i.test(templateSource) ||
58
+ templateSource.endsWith('.git'));
59
+ }
60
+ async function getLocalTemplateDir(templateSource) {
61
+ let localPath;
62
+ try {
63
+ localPath = resolveLocalTemplatePath(templateSource);
64
+ }
65
+ catch {
66
+ return undefined;
67
+ }
68
+ let result;
69
+ try {
70
+ result = await stat(localPath);
71
+ }
72
+ catch {
73
+ return undefined;
74
+ }
75
+ if (!result.isDirectory()) {
76
+ throw new Error(`Portal template "${templateSource}" is invalid: expected a directory.`);
77
+ }
78
+ return localPath;
79
+ }
80
+ async function resolveInitialPortalTemplate(params) {
81
+ const localTemplateDir = await getLocalTemplateDir(params.templateSource);
82
+ if (localTemplateDir) {
83
+ return {
84
+ dir: localTemplateDir,
85
+ source: params.templateSource,
86
+ type: 'local',
87
+ };
88
+ }
89
+ if (!isGitTemplateSource(params.templateSource)) {
90
+ return resolvePortalTemplate(params.templateSource, {
91
+ npmRegistry: params.npmRegistry,
92
+ runCommand: params.runCommand,
93
+ });
94
+ }
95
+ await params.runCommand('git', ['clone', '--depth', '1', params.templateSource, params.tempDir], {
96
+ errorName: 'git clone',
97
+ stdio: params.verbose ? 'inherit' : 'ignore',
98
+ });
99
+ return {
100
+ dir: params.tempDir,
101
+ source: params.templateSource,
102
+ type: 'local',
103
+ };
104
+ }
105
+ async function copyTemplate(sourceDir, targetDir) {
106
+ await mkdir(path.dirname(targetDir), { recursive: true });
107
+ await cp(sourceDir, targetDir, {
108
+ recursive: true,
109
+ filter: (source) => !source.split(path.sep).includes('.git'),
110
+ });
111
+ }
112
+ async function buildPortalHtml(params) {
113
+ const stdio = params.verbose ? 'inherit' : 'ignore';
114
+ await params.runCommand('yarn', ['build:html'], {
115
+ cwd: params.portalDir,
116
+ env: buildPortalCommandEnv({
117
+ NOCOBASE_API_URL: '/api',
118
+ NOCOBASE_PORTAL_BASE: `/${PORTAL_CLIENT_PREFIX}/${params.portalName}/`,
119
+ }),
120
+ envMode: 'replace',
121
+ errorName: 'yarn build:html',
122
+ stdio,
123
+ });
124
+ }
125
+ export async function prepareInitialPortalTemplate(options) {
126
+ const developmentMode = trimValue(options.developmentMode);
127
+ if (developmentMode !== 'vibe-coding') {
128
+ return { prepared: false, skippedReason: 'no-code' };
129
+ }
130
+ const storagePath = trimValue(options.storagePath);
131
+ if (!storagePath) {
132
+ throw new Error('Cannot prepare an initial Portal template without a storage path.');
133
+ }
134
+ const templateUrl = trimValue(options.portalTemplate);
135
+ if (!templateUrl) {
136
+ throw new Error('Initial Portal template is required when development mode is "vibe-coding".');
137
+ }
138
+ const appName = normalizePortalAppName(options.appName);
139
+ const portalName = normalizePortalName(options.portalName);
140
+ validatePortalSegment('portal app name', appName);
141
+ validatePortalSegment('Portal name', portalName);
142
+ const portalDir = path.join(storagePath, 'portals', appName, portalName);
143
+ if (await pathExists(portalDir)) {
144
+ if (await pathExists(path.join(portalDir, 'dist', 'index.html'))) {
145
+ return { prepared: false, skippedReason: 'already-prepared' };
146
+ }
147
+ await rm(portalDir, { recursive: true, force: true });
148
+ }
149
+ options.onStartTask?.(`Preparing Portal "${portalName}" from template...`);
150
+ const runCommand = options.runCommand ?? run;
151
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), 'nocobase-portal-template-'));
152
+ let cleanupPortalDir = false;
153
+ let template;
154
+ try {
155
+ template = await resolveInitialPortalTemplate({
156
+ templateSource: templateUrl,
157
+ tempDir,
158
+ npmRegistry: options.npmRegistry,
159
+ verbose: options.verbose,
160
+ runCommand,
161
+ });
162
+ const templateDir = template.dir;
163
+ if (!(await pathExists(path.join(templateDir, 'package.json')))) {
164
+ throw new Error(`Portal template "${templateUrl}" is invalid: package.json is missing.`);
165
+ }
166
+ cleanupPortalDir = true;
167
+ await copyTemplate(templateDir, portalDir);
168
+ await rm(path.join(portalDir, 'node_modules'), { recursive: true, force: true });
169
+ await buildPortalHtml({
170
+ portalDir,
171
+ portalName,
172
+ verbose: options.verbose,
173
+ runCommand,
174
+ });
175
+ cleanupPortalDir = false;
176
+ options.onSucceedTask?.(`Portal "${portalName}" is ready.`);
177
+ return { prepared: true };
178
+ }
179
+ catch (error) {
180
+ if (cleanupPortalDir) {
181
+ await rm(portalDir, { recursive: true, force: true });
182
+ }
183
+ options.onFailTask?.(`Failed to prepare Portal "${portalName}".`);
184
+ throw error;
185
+ }
186
+ finally {
187
+ await rm(tempDir, { recursive: true, force: true });
188
+ await template?.cleanup?.();
189
+ }
190
+ }
@@ -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
- if (isPromptBlockSkipped(def, out)) {
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, out);
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, out);
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') {
@@ -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
- import { applyEnvProxyAppEntryOptions, buildEnvProxyCaddyBundle, buildEnvProxyMainConfig, mapProxyPathFromCliRoot, resolveEnvProxyMainOutputPath, } from './env-proxy.js';
15
- import { run } from './run-npm.js';
15
+ import { applyEnvProxyAppEntryOptions, buildManualEnvProxyCaddyBundle, buildEnvProxyCaddyBundle, buildEnvProxyMainConfig, mapProxyPathFromCliRoot, resolveEnvProxyMainOutputPath, } from './env-proxy.js';
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');
@@ -62,8 +65,9 @@ export async function resolveCaddyProxyRuntimeContext(options) {
62
65
  upstreamHost: resolveCaddyProxyUpstreamHost(driver),
63
66
  };
64
67
  }
65
- export async function writeCaddyProxyBundle(runtime, appEntryOptions, runtimeContext) {
68
+ export async function writeCaddyProxyBundle(runtime, appEntryOptions, runtimeContext, options) {
66
69
  const bundle = await buildEnvProxyCaddyBundle(runtime, {
70
+ cdnBaseUrl: options?.cdnBaseUrl,
67
71
  runtimeCliRoot: runtimeContext.runtimeCliRoot,
68
72
  upstreamHost: runtimeContext.upstreamHost,
69
73
  });
@@ -82,6 +86,27 @@ export async function writeCaddyProxyBundle(runtime, appEntryOptions, runtimeCon
82
86
  status,
83
87
  };
84
88
  }
89
+ export async function writeManualCaddyProxyBundle(input, appEntryOptions, runtimeContext, options) {
90
+ const bundle = await buildManualEnvProxyCaddyBundle(input, {
91
+ cdnBaseUrl: options?.cdnBaseUrl,
92
+ runtimeCliRoot: runtimeContext.runtimeCliRoot,
93
+ upstreamHost: input.upstreamHost || runtimeContext.upstreamHost,
94
+ });
95
+ const currentAppConfigContent = await readOptionalTextFile(bundle.appConfigPath);
96
+ const nextAppConfigContent = applyEnvProxyAppEntryOptions(bundle.appConfigContent, 'caddy', appEntryOptions);
97
+ const status = currentAppConfigContent ? 'updated' : 'created';
98
+ await Promise.all([mkdir(bundle.entryDir, { recursive: true }), mkdir(bundle.publicDir, { recursive: true })]);
99
+ await Promise.all([
100
+ writeFile(bundle.appConfigPath, nextAppConfigContent, 'utf8'),
101
+ writeFile(bundle.indexV1Path, bundle.indexV1Content, 'utf8'),
102
+ writeFile(bundle.indexV2Path, bundle.indexV2Content, 'utf8'),
103
+ writeFile(bundle.mainConfigPath, bundle.mainConfigContent, 'utf8'),
104
+ ]);
105
+ return {
106
+ bundle,
107
+ status,
108
+ };
109
+ }
85
110
  function resolveLocalCaddyPidFilePath() {
86
111
  return path.join(path.dirname(resolveEnvProxyMainOutputPath({ provider: 'caddy' })), 'caddy.pid');
87
112
  }
@@ -149,11 +174,16 @@ async function reloadLocalCaddyProxy(runtimeContext) {
149
174
  }
150
175
  async function ensureDockerCaddyProxyContainer(runtimeContext) {
151
176
  const containerName = await resolveCaddyProxyContainerName();
177
+ const mainConfigPath = await ensureCaddyProxyMainConfig(runtimeContext);
178
+ const publishedPorts = await resolveDockerCaddyPublishedPorts();
152
179
  if (await dockerContainerExists(containerName)) {
153
- return;
180
+ if (await dockerCaddyProxyContainerMatchesPublishedPorts(containerName, publishedPorts)) {
181
+ return;
182
+ }
183
+ await removeDockerCaddyProxyContainer(containerName);
154
184
  }
155
185
  const hostCliRoot = String(process.env.NB_CLI_ROOT ?? resolveCliHomeRoot()).trim() || resolveCliHomeRoot();
156
- const mainConfigPath = await ensureCaddyProxyMainConfig(runtimeContext);
186
+ const dockerPortArgs = publishedPorts.flatMap((port) => ['-p', `${port}:${port}`]);
157
187
  await run('docker', [
158
188
  'run',
159
189
  '-d',
@@ -161,8 +191,7 @@ async function ensureDockerCaddyProxyContainer(runtimeContext) {
161
191
  containerName,
162
192
  '--add-host',
163
193
  'host.docker.internal:host-gateway',
164
- '-p',
165
- '80:80',
194
+ ...dockerPortArgs,
166
195
  '-v',
167
196
  `${hostCliRoot}:${DOCKER_CADDY_PROXY_RUNTIME_ROOT}`,
168
197
  '-v',
@@ -173,14 +202,53 @@ async function ensureDockerCaddyProxyContainer(runtimeContext) {
173
202
  stdio: 'ignore',
174
203
  });
175
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
+ }
176
243
  async function startDockerCaddyProxy(runtimeContext) {
177
244
  const containerName = await resolveCaddyProxyContainerName();
178
245
  await ensureCaddyProxyMainConfig(runtimeContext);
179
- if (await dockerContainerExists(containerName)) {
246
+ const existedBeforeEnsure = await dockerContainerExists(containerName);
247
+ await ensureDockerCaddyProxyContainer(runtimeContext);
248
+ if (existedBeforeEnsure && (await dockerContainerExists(containerName))) {
180
249
  const state = await startDockerContainer(containerName, { stdio: 'ignore' });
181
250
  return state === 'already-running' ? 'already-running' : 'started';
182
251
  }
183
- await ensureDockerCaddyProxyContainer(runtimeContext);
184
252
  return 'started';
185
253
  }
186
254
  async function stopDockerCaddyProxy() {