@nocobase/cli 2.1.0-alpha.46 → 2.1.0-alpha.47
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/LICENSE +201 -0
- package/README.md +63 -380
- package/assets/env-proxy/nginx/app.conf.tpl +23 -0
- package/assets/env-proxy/nginx/nocobase.conf.tpl +5 -0
- package/assets/env-proxy/nginx/snippets/dist-location.conf +5 -0
- package/assets/env-proxy/nginx/snippets/gzip.conf +17 -0
- package/assets/env-proxy/nginx/snippets/log-format-http.conf +13 -0
- package/assets/env-proxy/nginx/snippets/maps-http.conf +14 -0
- package/assets/env-proxy/nginx/snippets/mime-types.conf +98 -0
- package/assets/env-proxy/nginx/snippets/proxy-location.conf +17 -0
- package/assets/env-proxy/nginx/snippets/spa-location.conf +6 -0
- package/assets/env-proxy/nginx/snippets/uploads-location.conf +21 -0
- package/bin/run.js +40 -3
- package/dist/commands/app/autostart/disable.js +55 -0
- package/dist/commands/app/autostart/enable.js +55 -0
- package/dist/commands/app/autostart/list.js +37 -0
- package/dist/commands/app/autostart/run.js +84 -0
- package/dist/commands/app/autostart/shared.js +49 -0
- package/dist/commands/app/destroy.js +21 -12
- package/dist/commands/app/down.js +2 -2
- package/dist/commands/app/logs.js +2 -1
- package/dist/commands/app/restart.js +89 -24
- package/dist/commands/app/shared.js +3 -2
- package/dist/commands/app/start.js +142 -38
- package/dist/commands/app/stop.js +31 -2
- package/dist/commands/app/upgrade.js +9 -5
- package/dist/commands/config/delete.js +6 -1
- package/dist/commands/config/get.js +6 -1
- package/dist/commands/config/set.js +7 -2
- package/dist/commands/env/add.js +24 -44
- package/dist/commands/env/info.js +3 -2
- package/dist/commands/env/proxy/caddy.js +28 -0
- package/dist/commands/env/proxy/index.js +353 -0
- package/dist/commands/env/proxy/nginx.js +28 -0
- package/dist/commands/env/remove.js +112 -22
- package/dist/commands/env/shared.js +17 -9
- package/dist/commands/env/update.js +385 -21
- package/dist/commands/init.js +350 -98
- package/dist/commands/install.js +218 -75
- package/dist/commands/license/activate.js +63 -244
- package/dist/commands/license/plugins/shared.js +64 -13
- package/dist/commands/plugin/import.js +108 -0
- package/dist/commands/revision/create.js +89 -0
- package/dist/commands/source/test.js +13 -14
- package/dist/lib/app-health.js +5 -5
- package/dist/lib/app-managed-resources.js +24 -8
- package/dist/lib/app-public-path.js +80 -0
- package/dist/lib/app-runtime.js +10 -1
- package/dist/lib/auth-store.js +119 -4
- package/dist/lib/cli-config.js +191 -3
- package/dist/lib/cli-entry-error.js +8 -0
- package/dist/lib/command-log.js +284 -0
- package/dist/lib/docker-env-file.js +10 -2
- package/dist/lib/env-auth.js +4 -1
- package/dist/lib/env-command-config.js +45 -0
- package/dist/lib/env-config.js +7 -2
- package/dist/lib/env-guard.js +9 -10
- package/dist/lib/env-paths.js +101 -0
- package/dist/lib/env-proxy.js +1295 -0
- package/dist/lib/inquirer.js +4 -5
- package/dist/lib/managed-env-file.js +98 -0
- package/dist/lib/plugin-import.js +279 -0
- package/dist/lib/prompt-catalog-core.js +3 -2
- package/dist/lib/prompt-validators.js +14 -0
- package/dist/lib/prompt-web-ui.js +8 -1
- package/dist/lib/run-npm.js +56 -12
- package/dist/lib/skills-manager.js +3 -1
- package/dist/lib/source-publish.js +18 -1
- package/dist/lib/ui.js +2 -1
- package/dist/locale/en-US.json +116 -22
- package/dist/locale/zh-CN.json +113 -19
- package/package.json +5 -7
- package/scripts/build.mjs +34 -0
- package/scripts/clean.mjs +9 -0
- package/tsconfig.json +19 -0
- package/LICENSE.txt +0 -107
- package/README.zh-CN.md +0 -355
|
@@ -0,0 +1,353 @@
|
|
|
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 { Args, Command, Flags, loadHelpClass } from '@oclif/core';
|
|
12
|
+
import { formatMissingManagedAppEnvMessage, resolveManagedAppRuntime, } from '../../../lib/app-runtime.js';
|
|
13
|
+
import { resolveDefaultConfigScope } from '../../../lib/cli-home.js';
|
|
14
|
+
import { translateCli } from '../../../lib/cli-locale.js';
|
|
15
|
+
import { applyEnvProxyAppEntryOptions, appConfigIncludesGeneratedConfig, appConfigHasManagedGeneratedConfigBlock, appConfigHasManagedNginxBlock, buildManagedAppEntryGeneratedConfigBlock, buildEnvProxyCaddyBundle, buildEnvProxyNginxBundle, extractManagedNginxConfigBlock, installEnvProxyProvider, reloadEnvProxyProvider, replaceManagedAppEntryGeneratedConfigBlock, replaceManagedNginxConfigBlock, syncEnvProxyNginxSnippets, } from '../../../lib/env-proxy.js';
|
|
16
|
+
import { announceTargetEnv, failTask, startTask, succeedTask } from '../../../lib/ui.js';
|
|
17
|
+
async function readOptionalTextFile(filePath) {
|
|
18
|
+
try {
|
|
19
|
+
return await readFile(filePath, 'utf8');
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
const code = error && typeof error === 'object' && 'code' in error ? error.code : undefined;
|
|
23
|
+
if (code === 'ENOENT') {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function buildEditableAppEntryReferenceLine(provider, generatedConfigPath) {
|
|
30
|
+
if (provider === 'caddy') {
|
|
31
|
+
return `import ./${path.basename(generatedConfigPath)}`;
|
|
32
|
+
}
|
|
33
|
+
return `include ${generatedConfigPath};`;
|
|
34
|
+
}
|
|
35
|
+
function replaceEditableAppEntryReferenceWithManagedBlock(content, provider, currentGeneratedConfigPath, nextGeneratedConfigPath) {
|
|
36
|
+
const currentReferenceLine = buildEditableAppEntryReferenceLine(provider, currentGeneratedConfigPath);
|
|
37
|
+
const nextBlock = buildManagedAppEntryGeneratedConfigBlock(provider, nextGeneratedConfigPath);
|
|
38
|
+
const escapedComment = provider === 'caddy'
|
|
39
|
+
? '# Keep this import so `nb env proxy` can refresh managed routes.'
|
|
40
|
+
: '# Keep this include so `nb env proxy` can refresh managed routes.';
|
|
41
|
+
const escapedCommentPattern = escapedComment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
42
|
+
const escapedLinePattern = currentReferenceLine.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
43
|
+
const legacyBlockPattern = new RegExp(`[ \\t]*${escapedCommentPattern}\\n[ \\t]*${escapedLinePattern.replace(/;$/, ';?')}`, 'm');
|
|
44
|
+
if (legacyBlockPattern.test(content)) {
|
|
45
|
+
return content.replace(legacyBlockPattern, nextBlock);
|
|
46
|
+
}
|
|
47
|
+
return content.replace(currentReferenceLine, nextBlock);
|
|
48
|
+
}
|
|
49
|
+
function normalizeProxyListenPort(value) {
|
|
50
|
+
const normalized = value?.trim() || undefined;
|
|
51
|
+
if (!normalized || !/^\d+$/.test(normalized)) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const port = Number(normalized);
|
|
55
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
return normalized;
|
|
59
|
+
}
|
|
60
|
+
function buildNginxManagedBlockMissingMessage(appConfigPath) {
|
|
61
|
+
return translateCli('commands.envProxy.errors.nginxAppEntryMissingManagedBlock', {
|
|
62
|
+
appConfigPath,
|
|
63
|
+
}, {
|
|
64
|
+
fallback: `The editable nginx app entry config at ${appConfigPath} does not contain the NocoBase managed block. ` +
|
|
65
|
+
'Restore the managed block or delete the file and rerun `nb env proxy`.',
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async function writeNginxProxyBundle(runtime, appEntryOptions, options) {
|
|
69
|
+
const bundle = await buildEnvProxyNginxBundle(runtime, { scope: options?.scope });
|
|
70
|
+
const managedConfigBlock = extractManagedNginxConfigBlock(bundle.appConfigContent);
|
|
71
|
+
if (!managedConfigBlock) {
|
|
72
|
+
throw new Error('Failed to render the managed nginx config block.');
|
|
73
|
+
}
|
|
74
|
+
const currentAppConfigContent = await readOptionalTextFile(bundle.appConfigPath);
|
|
75
|
+
let nextAppConfigContent = applyEnvProxyAppEntryOptions(bundle.appConfigContent, 'nginx', appEntryOptions);
|
|
76
|
+
let status = 'created';
|
|
77
|
+
if (currentAppConfigContent) {
|
|
78
|
+
if (!appConfigHasManagedNginxBlock(currentAppConfigContent)) {
|
|
79
|
+
throw new Error(buildNginxManagedBlockMissingMessage(bundle.appConfigPath));
|
|
80
|
+
}
|
|
81
|
+
nextAppConfigContent = applyEnvProxyAppEntryOptions(replaceManagedNginxConfigBlock(currentAppConfigContent, managedConfigBlock), 'nginx', appEntryOptions);
|
|
82
|
+
status = 'updated';
|
|
83
|
+
}
|
|
84
|
+
await Promise.all([mkdir(bundle.entryDir, { recursive: true }), mkdir(bundle.publicDir, { recursive: true })]);
|
|
85
|
+
await Promise.all([
|
|
86
|
+
writeFile(bundle.appConfigPath, nextAppConfigContent, 'utf8'),
|
|
87
|
+
writeFile(bundle.indexV1Path, bundle.indexV1Content, 'utf8'),
|
|
88
|
+
writeFile(bundle.indexV2Path, bundle.indexV2Content, 'utf8'),
|
|
89
|
+
writeFile(bundle.mainConfigPath, bundle.mainConfigContent, 'utf8'),
|
|
90
|
+
syncEnvProxyNginxSnippets({ scope: options?.scope }),
|
|
91
|
+
]);
|
|
92
|
+
return {
|
|
93
|
+
bundle,
|
|
94
|
+
status,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
async function writeCaddyProxyBundle(runtime, appEntryOptions, options) {
|
|
98
|
+
const bundle = await buildEnvProxyCaddyBundle(runtime, { scope: options?.scope });
|
|
99
|
+
const currentAppConfigContent = await readOptionalTextFile(bundle.appConfigPath);
|
|
100
|
+
let nextAppConfigContent = applyEnvProxyAppEntryOptions(bundle.appConfigContent, 'caddy', appEntryOptions);
|
|
101
|
+
let status = 'created';
|
|
102
|
+
if (currentAppConfigContent) {
|
|
103
|
+
if (appConfigHasManagedGeneratedConfigBlock(currentAppConfigContent)) {
|
|
104
|
+
nextAppConfigContent = applyEnvProxyAppEntryOptions(replaceManagedAppEntryGeneratedConfigBlock(currentAppConfigContent, 'caddy', bundle.generatedConfigPath), 'caddy', appEntryOptions);
|
|
105
|
+
status = 'updated';
|
|
106
|
+
}
|
|
107
|
+
else if (appConfigIncludesGeneratedConfig(currentAppConfigContent, bundle.generatedConfigPath, 'caddy')) {
|
|
108
|
+
nextAppConfigContent = applyEnvProxyAppEntryOptions(replaceEditableAppEntryReferenceWithManagedBlock(currentAppConfigContent, 'caddy', bundle.generatedConfigPath, bundle.generatedConfigPath), 'caddy', appEntryOptions);
|
|
109
|
+
status = 'migrated';
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
throw new Error(translateCli('commands.envProxy.errors.appEntryMissingInclude', {
|
|
113
|
+
envName: runtime.envName,
|
|
114
|
+
appConfigPath: bundle.appConfigPath,
|
|
115
|
+
generatedConfigPath: bundle.generatedConfigPath,
|
|
116
|
+
}, {
|
|
117
|
+
fallback: `The editable app entry config at ${bundle.appConfigPath} does not reference ${bundle.generatedConfigPath}. ` +
|
|
118
|
+
'Add the managed generated-config reference back into the editable app entry and rerun the command.',
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
await Promise.all([mkdir(bundle.entryDir, { recursive: true }), mkdir(bundle.publicDir, { recursive: true })]);
|
|
123
|
+
await Promise.all([
|
|
124
|
+
writeFile(bundle.appConfigPath, nextAppConfigContent, 'utf8'),
|
|
125
|
+
writeFile(bundle.generatedConfigPath, bundle.generatedConfigContent, 'utf8'),
|
|
126
|
+
writeFile(bundle.indexV1Path, bundle.indexV1Content, 'utf8'),
|
|
127
|
+
writeFile(bundle.indexV2Path, bundle.indexV2Content, 'utf8'),
|
|
128
|
+
writeFile(bundle.mainConfigPath, bundle.mainConfigContent, 'utf8'),
|
|
129
|
+
]);
|
|
130
|
+
return {
|
|
131
|
+
bundle,
|
|
132
|
+
status,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
export const ENV_PROXY_NAME_ARG = Args.string({
|
|
136
|
+
description: 'Configured environment name to generate a proxy config for. Defaults to the current env when omitted',
|
|
137
|
+
required: false,
|
|
138
|
+
});
|
|
139
|
+
function createEnvProxySharedFlags() {
|
|
140
|
+
return {
|
|
141
|
+
env: Flags.string({
|
|
142
|
+
char: 'e',
|
|
143
|
+
description: 'Configured environment name to generate a proxy config for. Defaults to the current env when omitted',
|
|
144
|
+
}),
|
|
145
|
+
host: Flags.string({
|
|
146
|
+
description: 'Host exposed by the proxy entry config, such as example.com or localhost',
|
|
147
|
+
}),
|
|
148
|
+
port: Flags.string({
|
|
149
|
+
description: 'Port exposed by the proxy entry config, not the upstream NocoBase app port',
|
|
150
|
+
}),
|
|
151
|
+
install: Flags.boolean({
|
|
152
|
+
description: 'Install the rendered provider config into the provider main config when supported',
|
|
153
|
+
default: false,
|
|
154
|
+
}),
|
|
155
|
+
reload: Flags.boolean({
|
|
156
|
+
description: 'Validate and reload the provider after writing the proxy config',
|
|
157
|
+
default: false,
|
|
158
|
+
}),
|
|
159
|
+
print: Flags.boolean({
|
|
160
|
+
description: 'Print the rendered config to stdout instead of writing files',
|
|
161
|
+
default: false,
|
|
162
|
+
}),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
export function createEnvProxyNginxFlags() {
|
|
166
|
+
return {
|
|
167
|
+
...createEnvProxySharedFlags(),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
export function createEnvProxyCaddyFlags() {
|
|
171
|
+
return {
|
|
172
|
+
...createEnvProxySharedFlags(),
|
|
173
|
+
output: Flags.string({
|
|
174
|
+
char: 'o',
|
|
175
|
+
description: 'Output file path for providers that emit a single generated config file',
|
|
176
|
+
}),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function resolveRequestedEnv(command, args, flags) {
|
|
180
|
+
const envNameArg = args.name?.trim() || undefined;
|
|
181
|
+
const envNameFlag = flags.env?.trim() || undefined;
|
|
182
|
+
if (envNameArg && envNameFlag && envNameArg !== envNameFlag) {
|
|
183
|
+
command.error(`Environment name was provided both as the argument ("${envNameArg}") and as --env ("${envNameFlag}"). Please use only one.`);
|
|
184
|
+
}
|
|
185
|
+
return envNameArg || envNameFlag;
|
|
186
|
+
}
|
|
187
|
+
function resolveAppEntryOptions(command, flags) {
|
|
188
|
+
const requestedHost = flags.host?.trim() || undefined;
|
|
189
|
+
const requestedPort = flags.port?.trim() || undefined;
|
|
190
|
+
if (requestedPort && !normalizeProxyListenPort(requestedPort)) {
|
|
191
|
+
command.error(translateCli('commands.envProxy.errors.invalidProxyPort', { port: requestedPort }, {
|
|
192
|
+
fallback: `Invalid proxy entry port "${requestedPort}". Use an integer between 1 and 65535.`,
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
host: requestedHost,
|
|
197
|
+
port: normalizeProxyListenPort(requestedPort),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
export async function runEnvProxyCommand(command, args, flags, provider) {
|
|
201
|
+
const scope = resolveDefaultConfigScope();
|
|
202
|
+
const requestedEnv = resolveRequestedEnv(command, args, flags);
|
|
203
|
+
if (flags.print && (flags.install || flags.reload)) {
|
|
204
|
+
command.error('`--print` cannot be combined with `--install` or `--reload`.');
|
|
205
|
+
}
|
|
206
|
+
if (flags.output && (flags.install || flags.reload)) {
|
|
207
|
+
command.error('`--output` cannot be combined with `--install` or `--reload` in the current implementation.');
|
|
208
|
+
}
|
|
209
|
+
const appEntryOptions = resolveAppEntryOptions(command, flags);
|
|
210
|
+
const runtime = await resolveManagedAppRuntime(requestedEnv);
|
|
211
|
+
if (!runtime) {
|
|
212
|
+
command.error(formatMissingManagedAppEnvMessage(requestedEnv));
|
|
213
|
+
}
|
|
214
|
+
if (runtime.kind === 'http') {
|
|
215
|
+
command.error(translateCli('commands.envProxy.errors.httpEnvUnsupported', { envName: runtime.envName }, {
|
|
216
|
+
fallback: `Can't generate a proxy config for "${runtime.envName}" from this machine because the env only has an API connection.`,
|
|
217
|
+
}));
|
|
218
|
+
}
|
|
219
|
+
if (runtime.kind === 'ssh') {
|
|
220
|
+
command.error(translateCli('commands.envProxy.errors.sshEnvUnsupported', { envName: runtime.envName }, {
|
|
221
|
+
fallback: `Can't generate a proxy config for "${runtime.envName}" yet because SSH envs are not implemented.`,
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
announceTargetEnv(runtime.envName);
|
|
225
|
+
if (flags.output) {
|
|
226
|
+
command.error(translateCli(provider === 'nginx'
|
|
227
|
+
? 'commands.envProxy.errors.nginxOutputUnsupported'
|
|
228
|
+
: 'commands.envProxy.errors.caddyOutputUnsupported', {
|
|
229
|
+
envName: runtime.envName,
|
|
230
|
+
}, {
|
|
231
|
+
fallback: provider === 'nginx'
|
|
232
|
+
? '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>/`.'
|
|
233
|
+
: '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>/`.',
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
startTask(translateCli('commands.envProxy.messages.generating', { envName: runtime.envName }, {
|
|
237
|
+
fallback: `Generating proxy config for env "${runtime.envName}"...`,
|
|
238
|
+
}));
|
|
239
|
+
const generationFailureMessage = translateCli('commands.envProxy.messages.failed', { envName: runtime.envName }, {
|
|
240
|
+
fallback: `Failed to generate proxy config for env "${runtime.envName}".`,
|
|
241
|
+
});
|
|
242
|
+
const installStartMessage = translateCli('commands.envProxy.messages.installingProvider', { provider }, {
|
|
243
|
+
fallback: `Installing the shared proxy config into ${provider}...`,
|
|
244
|
+
});
|
|
245
|
+
const installFailureMessage = translateCli('commands.envProxy.messages.installProviderFailed', { provider }, {
|
|
246
|
+
fallback: `Failed to install the shared proxy config into ${provider}.`,
|
|
247
|
+
});
|
|
248
|
+
const reloadStartMessage = translateCli('commands.envProxy.messages.reloadingProvider', { provider }, {
|
|
249
|
+
fallback: `Validating and reloading ${provider}...`,
|
|
250
|
+
});
|
|
251
|
+
const reloadFailureMessage = translateCli('commands.envProxy.messages.reloadProviderFailed', { provider }, {
|
|
252
|
+
fallback: `Failed to validate and reload ${provider}.`,
|
|
253
|
+
});
|
|
254
|
+
try {
|
|
255
|
+
if (flags.print) {
|
|
256
|
+
if (provider === 'nginx') {
|
|
257
|
+
const bundle = await buildEnvProxyNginxBundle(runtime, { scope });
|
|
258
|
+
process.stdout.write(applyEnvProxyAppEntryOptions(bundle.appConfigContent, provider, appEntryOptions));
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
const bundle = await buildEnvProxyCaddyBundle(runtime, { scope });
|
|
262
|
+
process.stdout.write(applyEnvProxyAppEntryOptions(bundle.appConfigContent, provider, appEntryOptions));
|
|
263
|
+
}
|
|
264
|
+
succeedTask(translateCli('commands.envProxy.messages.printed', { envName: runtime.envName }, {
|
|
265
|
+
fallback: `Printed proxy config for env "${runtime.envName}".`,
|
|
266
|
+
}));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (provider === 'nginx') {
|
|
270
|
+
const { bundle, status } = await writeNginxProxyBundle(runtime, appEntryOptions, { scope });
|
|
271
|
+
succeedTask(translateCli(status === 'created'
|
|
272
|
+
? 'commands.envProxy.messages.savedNginxBundleCreated'
|
|
273
|
+
: 'commands.envProxy.messages.savedNginxBundleUpdated', {
|
|
274
|
+
envName: runtime.envName,
|
|
275
|
+
appConfigPath: bundle.appConfigPath,
|
|
276
|
+
entryDir: bundle.entryDir,
|
|
277
|
+
}, {
|
|
278
|
+
fallback: status === 'created'
|
|
279
|
+
? `Saved nginx proxy files for env "${runtime.envName}" under ${bundle.entryDir}, and created editable app entry config at ${bundle.appConfigPath}.`
|
|
280
|
+
: `Saved nginx proxy files for env "${runtime.envName}" under ${bundle.entryDir}, and refreshed editable app entry config at ${bundle.appConfigPath}.`,
|
|
281
|
+
}));
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
const { bundle, status } = await writeCaddyProxyBundle(runtime, appEntryOptions, { scope });
|
|
285
|
+
succeedTask(translateCli(status === 'created'
|
|
286
|
+
? 'commands.envProxy.messages.savedCaddyBundleCreated'
|
|
287
|
+
: status === 'migrated'
|
|
288
|
+
? 'commands.envProxy.messages.savedCaddyBundleMigrated'
|
|
289
|
+
: 'commands.envProxy.messages.savedCaddyBundleUpdated', { envName: runtime.envName, entryDir: bundle.entryDir, appConfigPath: bundle.appConfigPath }, {
|
|
290
|
+
fallback: status === 'created'
|
|
291
|
+
? `Saved caddy proxy files for env "${runtime.envName}" under ${bundle.entryDir}, and created editable app entry config at ${bundle.appConfigPath}.`
|
|
292
|
+
: status === 'migrated'
|
|
293
|
+
? `Saved caddy proxy files for env "${runtime.envName}" under ${bundle.entryDir}, and migrated the app entry config at ${bundle.appConfigPath}.`
|
|
294
|
+
: `Saved caddy proxy files for env "${runtime.envName}" under ${bundle.entryDir}, and refreshed editable app entry config at ${bundle.appConfigPath}.`,
|
|
295
|
+
}));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
failTask(generationFailureMessage);
|
|
300
|
+
command.error(error instanceof Error ? error.message : String(error));
|
|
301
|
+
}
|
|
302
|
+
if (flags.install) {
|
|
303
|
+
startTask(installStartMessage);
|
|
304
|
+
try {
|
|
305
|
+
const installResult = await installEnvProxyProvider(provider, { scope });
|
|
306
|
+
succeedTask(installResult.status === 'already-installed'
|
|
307
|
+
? translateCli('commands.envProxy.messages.providerAlreadyInstalled', { provider, configPath: installResult.configPath }, {
|
|
308
|
+
fallback: `The shared proxy config is already installed in the ${provider} main config at ${installResult.configPath}.`,
|
|
309
|
+
})
|
|
310
|
+
: translateCli('commands.envProxy.messages.providerInstalled', { provider, configPath: installResult.configPath }, {
|
|
311
|
+
fallback: `Installed the shared proxy config into the ${provider} main config at ${installResult.configPath}.`,
|
|
312
|
+
}));
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
failTask(installFailureMessage);
|
|
316
|
+
command.error(error instanceof Error ? error.message : String(error));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (flags.reload) {
|
|
320
|
+
startTask(reloadStartMessage);
|
|
321
|
+
try {
|
|
322
|
+
await reloadEnvProxyProvider(provider, { scope });
|
|
323
|
+
succeedTask(translateCli('commands.envProxy.messages.providerReloaded', { provider }, {
|
|
324
|
+
fallback: `Validated and reloaded ${provider}.`,
|
|
325
|
+
}));
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
failTask(reloadFailureMessage);
|
|
329
|
+
command.error(error instanceof Error ? error.message : String(error));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
export default class EnvProxy extends Command {
|
|
334
|
+
static summary = 'Generate proxy configs for one managed env through provider subcommands';
|
|
335
|
+
static description = 'Use `nb env proxy nginx` or `nb env proxy caddy` to generate provider-specific proxy files.';
|
|
336
|
+
static examples = [
|
|
337
|
+
'<%= config.bin %> env proxy nginx',
|
|
338
|
+
'<%= config.bin %> env proxy nginx --env app1',
|
|
339
|
+
'<%= config.bin %> env proxy nginx --env app1 --install --reload',
|
|
340
|
+
'<%= config.bin %> env proxy caddy',
|
|
341
|
+
'<%= config.bin %> env proxy caddy --env app1',
|
|
342
|
+
'<%= config.bin %> env proxy caddy --env app1 --install --reload',
|
|
343
|
+
'<%= config.bin %> env proxy caddy --env app1 --print',
|
|
344
|
+
];
|
|
345
|
+
async run() {
|
|
346
|
+
await this.parse(EnvProxy);
|
|
347
|
+
const Help = await loadHelpClass(this.config);
|
|
348
|
+
await new Help(this.config, this.config.pjson.oclif.helpOptions ?? this.config.pjson.helpOptions).showHelp([
|
|
349
|
+
this.id ?? 'env proxy',
|
|
350
|
+
...this.argv,
|
|
351
|
+
]);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
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 { Command } from '@oclif/core';
|
|
10
|
+
import { createEnvProxyNginxFlags, ENV_PROXY_NAME_ARG, runEnvProxyCommand } from './index.js';
|
|
11
|
+
export default class EnvProxyNginx extends Command {
|
|
12
|
+
static summary = 'Generate nginx proxy files for one managed env';
|
|
13
|
+
static examples = [
|
|
14
|
+
'<%= config.bin %> <%= command.id %>',
|
|
15
|
+
'<%= config.bin %> <%= command.id %> --env app1',
|
|
16
|
+
'<%= config.bin %> <%= command.id %> --env app1 --host a.local.nocobase.com --port 8080',
|
|
17
|
+
'<%= config.bin %> <%= command.id %> --env app1 --install --reload',
|
|
18
|
+
'<%= config.bin %> <%= command.id %> --env app1 --print',
|
|
19
|
+
];
|
|
20
|
+
static args = {
|
|
21
|
+
name: ENV_PROXY_NAME_ARG,
|
|
22
|
+
};
|
|
23
|
+
static flags = createEnvProxyNginxFlags();
|
|
24
|
+
async run() {
|
|
25
|
+
const { args, flags } = await this.parse(EnvProxyNginx);
|
|
26
|
+
await runEnvProxyCommand(this, args, flags, 'nginx');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -7,26 +7,104 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
import { Args, Command, Flags } from '@oclif/core';
|
|
10
|
+
import { resolveManagedAppRuntime } from '../../lib/app-runtime.js';
|
|
10
11
|
import { getCurrentEnvName, loadAuthConfig, removeEnv } from '../../lib/auth-store.js';
|
|
11
12
|
import { resolveDefaultConfigScope } from '../../lib/cli-home.js';
|
|
12
|
-
import { confirm } from "../../lib/inquirer.js";
|
|
13
|
-
import { isInteractiveTerminal, printVerbose, setVerboseMode } from '../../lib/ui.js';
|
|
13
|
+
import { confirm, input } from "../../lib/inquirer.js";
|
|
14
|
+
import { isInteractiveTerminal, printInfo, printVerbose, setVerboseMode } from '../../lib/ui.js';
|
|
15
|
+
function formatRemoveForceRequiredMessage(envName, purge) {
|
|
16
|
+
if (purge) {
|
|
17
|
+
return [
|
|
18
|
+
`Refusing to purge env "${envName}" without confirmation in non-interactive mode.`,
|
|
19
|
+
'Re-run with `--purge --force` to continue.',
|
|
20
|
+
].join('\n');
|
|
21
|
+
}
|
|
22
|
+
return [
|
|
23
|
+
`Refusing to remove env "${envName}" without confirmation in non-interactive mode.`,
|
|
24
|
+
'Re-run with `--force` to continue.',
|
|
25
|
+
].join('\n');
|
|
26
|
+
}
|
|
27
|
+
function buildRemovePrompt(runtime, options) {
|
|
28
|
+
const subject = options.isCurrent ? `current env "${runtime.envName}"` : `env "${runtime.envName}"`;
|
|
29
|
+
if (options.purge) {
|
|
30
|
+
const lines = [`Purge ${subject}?`];
|
|
31
|
+
if (runtime.kind === 'local' || runtime.kind === 'docker') {
|
|
32
|
+
lines.push('This removes CLI-managed local runtime resources for this env on this machine.');
|
|
33
|
+
lines.push('Storage data will be removed, and downloaded local app files will be removed when applicable.');
|
|
34
|
+
lines.push('External database services are not managed by the CLI and will be left untouched.');
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
lines.push('This env has no CLI-managed local runtime resources on this machine.');
|
|
38
|
+
lines.push('Only the saved CLI env config will be removed. External services are not touched.');
|
|
39
|
+
}
|
|
40
|
+
lines.push(`Type "${runtime.envName}" to confirm:`);
|
|
41
|
+
return lines.join('\n');
|
|
42
|
+
}
|
|
43
|
+
const lines = [`Remove ${subject}?`];
|
|
44
|
+
if (runtime.kind === 'local' || runtime.kind === 'docker') {
|
|
45
|
+
lines.push('NocoBase and any CLI-managed built-in database for this env will be stopped on this machine.');
|
|
46
|
+
lines.push('The saved CLI env config will then be removed. Storage data and local app files will be kept.');
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
lines.push('Only the saved CLI env config will be removed.');
|
|
50
|
+
}
|
|
51
|
+
return lines.join('\n');
|
|
52
|
+
}
|
|
53
|
+
async function confirmEnvRemoval(runtime, options) {
|
|
54
|
+
if (!isInteractiveTerminal()) {
|
|
55
|
+
if (options.force) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
throw new Error(formatRemoveForceRequiredMessage(runtime.envName, options.purge));
|
|
59
|
+
}
|
|
60
|
+
if (options.force) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
if (options.purge) {
|
|
64
|
+
try {
|
|
65
|
+
await input({
|
|
66
|
+
message: buildRemovePrompt(runtime, options),
|
|
67
|
+
required: true,
|
|
68
|
+
validate: (value) => (value.trim() === runtime.envName ? true : `Type "${runtime.envName}" to confirm.`),
|
|
69
|
+
placeholder: runtime.envName,
|
|
70
|
+
});
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
return await confirm({
|
|
79
|
+
message: buildRemovePrompt(runtime, options),
|
|
80
|
+
default: false,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
14
87
|
export default class EnvRemove extends Command {
|
|
15
88
|
static summary = 'Remove a configured environment';
|
|
16
|
-
static description = 'Remove
|
|
89
|
+
static description = 'Remove a configured env. Local and Docker envs stop CLI-managed runtime resources on this machine first; pass `--purge` to also delete managed local resources, storage data, and downloaded app files when applicable.';
|
|
17
90
|
static examples = [
|
|
18
91
|
'<%= config.bin %> <%= command.id %> staging',
|
|
19
|
-
'<%= config.bin %> <%= command.id %> staging --
|
|
92
|
+
'<%= config.bin %> <%= command.id %> staging --force',
|
|
93
|
+
'<%= config.bin %> <%= command.id %> staging --purge --force',
|
|
20
94
|
];
|
|
21
95
|
static flags = {
|
|
22
96
|
yes: Flags.boolean({
|
|
23
97
|
char: 'y',
|
|
24
|
-
|
|
98
|
+
hidden: true,
|
|
25
99
|
default: false,
|
|
26
100
|
}),
|
|
27
101
|
force: Flags.boolean({
|
|
28
102
|
char: 'f',
|
|
29
|
-
|
|
103
|
+
description: 'Skip confirmation for the selected remove mode',
|
|
104
|
+
default: false,
|
|
105
|
+
}),
|
|
106
|
+
purge: Flags.boolean({
|
|
107
|
+
description: 'Also remove CLI-managed local runtime resources, storage data, and downloaded app files when applicable. For remote API envs, only the saved CLI env config will be removed.',
|
|
30
108
|
default: false,
|
|
31
109
|
}),
|
|
32
110
|
verbose: Flags.boolean({
|
|
@@ -49,25 +127,37 @@ export default class EnvRemove extends Command {
|
|
|
49
127
|
this.error(`Env "${args.name}" is not configured`);
|
|
50
128
|
}
|
|
51
129
|
const currentEnv = await getCurrentEnvName({ scope });
|
|
130
|
+
const runtime = await resolveManagedAppRuntime(args.name);
|
|
131
|
+
if (!runtime) {
|
|
132
|
+
this.error(`Env "${args.name}" is not configured`);
|
|
133
|
+
}
|
|
52
134
|
const skipConfirmation = flags.yes || flags.force;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
135
|
+
let confirmed = false;
|
|
136
|
+
try {
|
|
137
|
+
confirmed = await confirmEnvRemoval(runtime, {
|
|
138
|
+
force: skipConfirmation,
|
|
139
|
+
isCurrent: args.name === currentEnv,
|
|
140
|
+
purge: flags.purge,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
this.error(error instanceof Error ? error.message : String(error));
|
|
145
|
+
}
|
|
146
|
+
if (!confirmed) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const runCommand = this.config.runCommand.bind(this.config);
|
|
150
|
+
const verboseArgv = flags.verbose ? ['--verbose'] : [];
|
|
151
|
+
if (flags.purge) {
|
|
152
|
+
if (runtime.kind === 'local' || runtime.kind === 'docker') {
|
|
153
|
+
await runCommand('app:destroy', ['--env', runtime.envName, '--force', ...verboseArgv]);
|
|
69
154
|
return;
|
|
70
155
|
}
|
|
156
|
+
printInfo(`No local CLI-managed resources were found for "${runtime.envName}". Removing the saved CLI env config only.`);
|
|
157
|
+
}
|
|
158
|
+
else if (runtime.kind === 'local' || runtime.kind === 'docker') {
|
|
159
|
+
printVerbose(`Stopping CLI-managed runtime resources for "${runtime.envName}" before removing the env config.`);
|
|
160
|
+
await runCommand('app:stop', ['--env', runtime.envName, '--with-db', '--yes', ...verboseArgv]);
|
|
71
161
|
}
|
|
72
162
|
printVerbose(`Removing env "${args.name}"`);
|
|
73
163
|
const result = await removeEnv(args.name, { scope });
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { buildDockerDbContainerName, dockerContainerExists, dockerContainerIsRunning, } from '../../lib/app-runtime.js';
|
|
10
10
|
import { executeRawApiRequest } from '../../lib/api-client.js';
|
|
11
|
+
import { buildLocalAppUrl } from '../../lib/app-public-path.js';
|
|
11
12
|
export function resolveApiBaseUrl(config) {
|
|
12
13
|
return String(config.apiBaseUrl ?? config.baseUrl ?? config.apibaseUrl ?? '').trim();
|
|
13
14
|
}
|
|
@@ -53,16 +54,23 @@ export function appUrl(runtime) {
|
|
|
53
54
|
}
|
|
54
55
|
const port = String(runtime.env.config.appPort ?? '').trim();
|
|
55
56
|
if (port) {
|
|
56
|
-
return
|
|
57
|
+
return buildLocalAppUrl(port, runtime.env.config?.appPublicPath) ?? '';
|
|
57
58
|
}
|
|
58
59
|
return '';
|
|
59
60
|
}
|
|
60
|
-
export function
|
|
61
|
+
export function appPath(runtime) {
|
|
62
|
+
if (runtime.kind === 'http') {
|
|
63
|
+
return '-';
|
|
64
|
+
}
|
|
65
|
+
const value = String(runtime.env.appPath ?? runtime.env.config.appPath ?? '').trim();
|
|
66
|
+
return value || '-';
|
|
67
|
+
}
|
|
68
|
+
export function sourcePath(runtime) {
|
|
61
69
|
if (runtime.kind === 'http' || runtime.kind === 'docker') {
|
|
62
70
|
return '-';
|
|
63
71
|
}
|
|
64
72
|
if (runtime.kind === 'local') {
|
|
65
|
-
return String(runtime.projectRoot ?? runtime.env.appRootPath ?? '').trim() || '-';
|
|
73
|
+
return String(runtime.projectRoot ?? runtime.env.sourcePath ?? runtime.env.appRootPath ?? '').trim() || '-';
|
|
66
74
|
}
|
|
67
75
|
return String(runtime.env.config.appRootPath ?? '').trim() || '-';
|
|
68
76
|
}
|
|
@@ -98,15 +106,15 @@ function isNetworkFailure(error) {
|
|
|
98
106
|
if (!(error instanceof Error)) {
|
|
99
107
|
return false;
|
|
100
108
|
}
|
|
101
|
-
return (error.name === 'AbortError'
|
|
102
|
-
|
|
109
|
+
return (error.name === 'AbortError' ||
|
|
110
|
+
/fetch failed|network|timeout|timed out|abort|ECONNREFUSED|ECONNRESET|ENOTFOUND|ETIMEDOUT|EAI_AGAIN|ENETUNREACH/i.test(error.message));
|
|
103
111
|
}
|
|
104
112
|
function isUnconfiguredFailure(error) {
|
|
105
113
|
return error instanceof Error && /missing (a )?base url|missing base URL/i.test(error.message);
|
|
106
114
|
}
|
|
107
115
|
function isAuthFailure(error) {
|
|
108
|
-
return (error instanceof Error
|
|
109
|
-
|
|
116
|
+
return (error instanceof Error &&
|
|
117
|
+
/EMPTY_TOKEN|INVALID_TOKEN|EXPIRED_TOKEN|BLOCKED_TOKEN|EXPIRED_SESSION|NOT_EXIST_USER|invalid_grant|sign in|signin|authentication failed/i.test(error.message));
|
|
110
118
|
}
|
|
111
119
|
export async function apiStatus(envName, config, options = {}) {
|
|
112
120
|
if (!resolveApiBaseUrl(config)) {
|
|
@@ -166,7 +174,7 @@ async function dockerStatus(containerName) {
|
|
|
166
174
|
if (!(await dockerContainerExists(containerName))) {
|
|
167
175
|
return 'missing';
|
|
168
176
|
}
|
|
169
|
-
return await dockerContainerIsRunning(containerName) ? 'running' : 'stopped';
|
|
177
|
+
return (await dockerContainerIsRunning(containerName)) ? 'running' : 'stopped';
|
|
170
178
|
}
|
|
171
179
|
export async function dbStatus(runtime) {
|
|
172
180
|
if (!runtime.env.config.builtinDb) {
|
|
@@ -192,5 +200,5 @@ export async function runtimeStatus(runtime) {
|
|
|
192
200
|
if (runtime.kind === 'docker') {
|
|
193
201
|
return await dockerStatus(runtime.containerName);
|
|
194
202
|
}
|
|
195
|
-
return await isLocalAppHealthy(runtime) ? 'running' : 'stopped';
|
|
203
|
+
return (await isLocalAppHealthy(runtime)) ? 'running' : 'stopped';
|
|
196
204
|
}
|