@nocobase/cli 2.1.0-alpha.3 → 2.1.0-alpha.31
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.txt +107 -0
- package/README.md +379 -19
- package/README.zh-CN.md +329 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +131 -0
- package/dist/commands/api/resource/create.js +15 -0
- package/dist/commands/api/resource/destroy.js +15 -0
- package/dist/commands/api/resource/get.js +15 -0
- package/dist/commands/api/resource/index.js +20 -0
- package/dist/commands/api/resource/list.js +16 -0
- package/dist/commands/api/resource/query.js +15 -0
- package/dist/commands/api/resource/update.js +15 -0
- package/dist/commands/app/down.js +266 -0
- package/dist/commands/app/logs.js +98 -0
- package/dist/commands/app/restart.js +75 -0
- package/dist/commands/app/start.js +253 -0
- package/dist/commands/app/stop.js +99 -0
- package/dist/commands/app/upgrade.js +582 -0
- package/{src/cli.js → dist/commands/build.js} +4 -11
- package/dist/commands/config/delete.js +30 -0
- package/dist/commands/config/get.js +29 -0
- package/dist/commands/config/index.js +20 -0
- package/dist/commands/config/list.js +29 -0
- package/dist/commands/config/set.js +35 -0
- package/dist/commands/db/check.js +238 -0
- package/dist/commands/db/logs.js +85 -0
- package/dist/commands/db/ps.js +60 -0
- package/dist/commands/db/shared.js +96 -0
- package/dist/commands/db/start.js +71 -0
- package/dist/commands/db/stop.js +71 -0
- package/{templates/plugin/src/client/models/index.ts → dist/commands/dev.js} +4 -4
- package/{src/index.js → dist/commands/down.js} +4 -6
- package/{src/commands/locale/react-js-cron/index.js → dist/commands/download.js} +4 -8
- package/dist/commands/env/add.js +312 -0
- package/dist/commands/env/auth.js +55 -0
- package/dist/commands/env/info.js +156 -0
- package/dist/commands/env/list.js +50 -0
- package/dist/commands/env/remove.js +59 -0
- package/dist/commands/env/shared.js +158 -0
- package/dist/commands/env/update.js +67 -0
- package/dist/commands/env/use.js +28 -0
- package/dist/commands/examples/prompts-stages.js +150 -0
- package/dist/commands/examples/prompts-test.js +181 -0
- package/dist/commands/init.js +1027 -0
- package/dist/commands/install.js +2206 -0
- package/dist/commands/license/activate.js +360 -0
- package/dist/commands/license/env.js +94 -0
- package/dist/commands/license/generate-id.js +108 -0
- package/dist/commands/license/id.js +56 -0
- package/dist/commands/license/index.js +20 -0
- package/dist/commands/license/plugins/clean.js +101 -0
- package/dist/commands/license/plugins/index.js +20 -0
- package/dist/commands/license/plugins/list.js +50 -0
- package/dist/commands/license/plugins/shared.js +325 -0
- package/dist/commands/license/plugins/sync.js +269 -0
- package/dist/commands/license/shared.js +414 -0
- package/dist/commands/license/status.js +50 -0
- package/dist/commands/logs.js +12 -0
- package/dist/commands/plugin/disable.js +66 -0
- package/dist/commands/plugin/enable.js +66 -0
- package/dist/commands/plugin/list.js +62 -0
- package/dist/commands/pm/disable.js +12 -0
- package/dist/commands/pm/enable.js +12 -0
- package/dist/commands/pm/list.js +12 -0
- package/dist/commands/restart.js +12 -0
- package/dist/commands/scaffold/migration.js +38 -0
- package/dist/commands/scaffold/plugin.js +37 -0
- package/dist/commands/self/check.js +71 -0
- package/dist/commands/self/index.js +20 -0
- package/dist/commands/self/update.js +86 -0
- package/dist/commands/skills/check.js +69 -0
- package/dist/commands/skills/index.js +20 -0
- package/dist/commands/skills/install.js +71 -0
- package/dist/commands/skills/remove.js +71 -0
- package/dist/commands/skills/update.js +78 -0
- package/dist/commands/source/build.js +58 -0
- package/dist/commands/source/dev.js +158 -0
- package/dist/commands/source/download.js +866 -0
- package/dist/commands/source/test.js +477 -0
- package/dist/commands/start.js +12 -0
- package/dist/commands/stop.js +12 -0
- package/dist/commands/test.js +12 -0
- package/dist/commands/upgrade.js +12 -0
- package/dist/generated/command-registry.js +133 -0
- package/dist/help/runtime-help.js +23 -0
- package/dist/lib/api-client.js +329 -0
- package/dist/lib/app-health.js +126 -0
- package/dist/lib/app-managed-resources.js +268 -0
- package/dist/lib/app-runtime.js +171 -0
- package/dist/lib/auth-store.js +328 -0
- package/dist/lib/bootstrap.js +384 -0
- package/dist/lib/build-config.js +18 -0
- package/dist/lib/builtin-db.js +86 -0
- package/dist/lib/cli-config.js +176 -0
- package/dist/lib/cli-home.js +47 -0
- package/dist/lib/cli-locale.js +129 -0
- package/dist/lib/command-discovery.js +39 -0
- package/dist/lib/db-connection-check.js +178 -0
- package/dist/lib/env-auth.js +872 -0
- package/dist/lib/env-config.js +87 -0
- package/dist/lib/generated-command.js +171 -0
- package/dist/lib/http-request.js +49 -0
- package/dist/lib/naming.js +70 -0
- package/dist/lib/openapi.js +62 -0
- package/dist/lib/plugin-storage.js +127 -0
- package/dist/lib/post-processors.js +23 -0
- package/dist/lib/prompt-catalog.js +581 -0
- package/dist/lib/prompt-validators.js +185 -0
- package/dist/lib/prompt-web-ui.js +2103 -0
- package/dist/lib/resource-command.js +343 -0
- package/dist/lib/resource-request.js +104 -0
- package/dist/lib/run-npm.js +250 -0
- package/dist/lib/runtime-env-vars.js +32 -0
- package/dist/lib/runtime-generator.js +498 -0
- package/dist/lib/runtime-store.js +56 -0
- package/dist/lib/self-manager.js +301 -0
- package/dist/lib/skills-manager.js +296 -0
- package/dist/lib/startup-update.js +281 -0
- package/dist/lib/ui.js +178 -0
- package/dist/locale/en-US.json +339 -0
- package/dist/locale/zh-CN.json +339 -0
- package/dist/post-processors/data-modeling.js +66 -0
- package/dist/post-processors/data-source-manager.js +114 -0
- package/dist/post-processors/index.js +19 -0
- package/nocobase-ctl.config.json +369 -0
- package/package.json +95 -26
- package/LICENSE +0 -661
- package/bin/index.js +0 -39
- package/nocobase.conf.tpl +0 -95
- package/src/commands/benchmark.js +0 -73
- package/src/commands/build.js +0 -49
- package/src/commands/clean.js +0 -30
- package/src/commands/client.js +0 -166
- package/src/commands/create-nginx-conf.js +0 -37
- package/src/commands/create-plugin.js +0 -33
- package/src/commands/dev.js +0 -200
- package/src/commands/doc.js +0 -76
- package/src/commands/e2e.js +0 -265
- package/src/commands/global.js +0 -43
- package/src/commands/index.js +0 -45
- package/src/commands/instance-id.js +0 -47
- package/src/commands/locale/cronstrue.js +0 -122
- package/src/commands/locale/react-js-cron/en-US.json +0 -75
- package/src/commands/locale/react-js-cron/zh-CN.json +0 -33
- package/src/commands/locale/react-js-cron/zh-TW.json +0 -33
- package/src/commands/locale.js +0 -81
- package/src/commands/p-test.js +0 -88
- package/src/commands/perf.js +0 -63
- package/src/commands/pkg.js +0 -321
- package/src/commands/pm2.js +0 -37
- package/src/commands/postinstall.js +0 -88
- package/src/commands/start.js +0 -148
- package/src/commands/tar.js +0 -36
- package/src/commands/test-coverage.js +0 -55
- package/src/commands/test.js +0 -107
- package/src/commands/umi.js +0 -33
- package/src/commands/update-deps.js +0 -72
- package/src/commands/upgrade.js +0 -47
- package/src/commands/view-license-key.js +0 -44
- package/src/license.js +0 -76
- package/src/logger.js +0 -75
- package/src/plugin-generator.js +0 -80
- package/src/util.js +0 -517
- package/templates/bundle-status.html +0 -338
- package/templates/create-app-package.json +0 -39
- package/templates/plugin/.npmignore.tpl +0 -2
- package/templates/plugin/README.md.tpl +0 -1
- package/templates/plugin/client.d.ts +0 -2
- package/templates/plugin/client.js +0 -1
- package/templates/plugin/package.json.tpl +0 -11
- package/templates/plugin/server.d.ts +0 -2
- package/templates/plugin/server.js +0 -1
- package/templates/plugin/src/client/client.d.ts +0 -249
- package/templates/plugin/src/client/index.tsx.tpl +0 -1
- package/templates/plugin/src/client/locale.ts +0 -21
- package/templates/plugin/src/client/plugin.tsx.tpl +0 -10
- package/templates/plugin/src/index.ts +0 -2
- package/templates/plugin/src/locale/en-US.json +0 -1
- package/templates/plugin/src/locale/zh-CN.json +0 -1
- package/templates/plugin/src/server/collections/.gitkeep +0 -0
- package/templates/plugin/src/server/index.ts.tpl +0 -1
- package/templates/plugin/src/server/plugin.ts.tpl +0 -19
|
@@ -0,0 +1,329 @@
|
|
|
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
|
+
/**
|
|
10
|
+
* This file is part of the NocoBase (R) project.
|
|
11
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
12
|
+
* Authors: NocoBase Team.
|
|
13
|
+
*
|
|
14
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
15
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
16
|
+
*/
|
|
17
|
+
import { createWriteStream } from 'node:fs';
|
|
18
|
+
import { promises as fs } from 'node:fs';
|
|
19
|
+
import { basename, dirname } from 'node:path';
|
|
20
|
+
import { Readable } from 'node:stream';
|
|
21
|
+
import { pipeline } from 'node:stream/promises';
|
|
22
|
+
import { resolveServerRequestTarget } from './env-auth.js';
|
|
23
|
+
import { fetchWithPreservedAuthRedirect } from './http-request.js';
|
|
24
|
+
const CLI_REQUEST_SOURCE_HEADER = 'x-request-source';
|
|
25
|
+
const CLI_REQUEST_SOURCE_VALUE = 'cli';
|
|
26
|
+
function stripUtf8Bom(text) {
|
|
27
|
+
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
28
|
+
}
|
|
29
|
+
function parseJsonInput(raw, flagName) {
|
|
30
|
+
const content = stripUtf8Bom(raw);
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(content);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
throw new Error(`Invalid JSON for --${flagName}: ${error?.message ?? 'parse failed'}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function normalizeBaseUrl(baseUrl) {
|
|
39
|
+
return baseUrl.replace(/\/+$/, '');
|
|
40
|
+
}
|
|
41
|
+
async function parseResponse(response) {
|
|
42
|
+
const text = await response.text();
|
|
43
|
+
let data = text;
|
|
44
|
+
if (text) {
|
|
45
|
+
try {
|
|
46
|
+
data = JSON.parse(text);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
data = text;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
ok: response.ok,
|
|
54
|
+
status: response.status,
|
|
55
|
+
data,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async function parseBinaryResponse(response, outputPath) {
|
|
59
|
+
if (response.ok && response.body) {
|
|
60
|
+
await fs.mkdir(dirname(outputPath), { recursive: true }).catch(() => undefined);
|
|
61
|
+
await pipeline(Readable.fromWeb(response.body), createWriteStream(outputPath));
|
|
62
|
+
return {
|
|
63
|
+
ok: response.ok,
|
|
64
|
+
status: response.status,
|
|
65
|
+
data: {
|
|
66
|
+
output: outputPath,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return parseResponse(response);
|
|
71
|
+
}
|
|
72
|
+
function parseScalarValue(value, type) {
|
|
73
|
+
if (value === undefined) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
if (type === 'boolean') {
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
if (type === 'integer' || type === 'number') {
|
|
80
|
+
return Number(value);
|
|
81
|
+
}
|
|
82
|
+
if (typeof value !== 'string') {
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
const trimmed = value.trim();
|
|
86
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(trimmed);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
function hasParameterValue(flags, parameter) {
|
|
97
|
+
const value = flags[parameter.flagName];
|
|
98
|
+
if (parameter.type === 'boolean') {
|
|
99
|
+
return value !== undefined;
|
|
100
|
+
}
|
|
101
|
+
if (Array.isArray(value)) {
|
|
102
|
+
return value.length > 0;
|
|
103
|
+
}
|
|
104
|
+
return value !== undefined && value !== '';
|
|
105
|
+
}
|
|
106
|
+
function listProvidedBodyFlags(flags, parameters) {
|
|
107
|
+
return parameters
|
|
108
|
+
.filter((parameter) => hasParameterValue(flags, parameter))
|
|
109
|
+
.map((parameter) => `--${parameter.flagName}`);
|
|
110
|
+
}
|
|
111
|
+
function parseBodyFieldValue(rawValue, parameter) {
|
|
112
|
+
if (rawValue === undefined) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
if (parameter.isArray && !parameter.jsonEncoded) {
|
|
116
|
+
return Array.isArray(rawValue) ? rawValue : rawValue ? [rawValue] : undefined;
|
|
117
|
+
}
|
|
118
|
+
if (parameter.jsonEncoded || parameter.type === 'object' || parameter.type === 'array') {
|
|
119
|
+
if (typeof rawValue !== 'string') {
|
|
120
|
+
return rawValue;
|
|
121
|
+
}
|
|
122
|
+
const parsed = parseJsonInput(rawValue, parameter.flagName);
|
|
123
|
+
if (parameter.type === 'array' && !Array.isArray(parsed)) {
|
|
124
|
+
throw new Error(`--${parameter.flagName} must be a JSON array`);
|
|
125
|
+
}
|
|
126
|
+
if (parameter.type === 'object' && (parsed === null || Array.isArray(parsed) || typeof parsed !== 'object')) {
|
|
127
|
+
throw new Error(`--${parameter.flagName} must be a JSON object`);
|
|
128
|
+
}
|
|
129
|
+
return parsed;
|
|
130
|
+
}
|
|
131
|
+
return parseScalarValue(rawValue, parameter.type);
|
|
132
|
+
}
|
|
133
|
+
export async function parseBody(flags, operation) {
|
|
134
|
+
if (operation.requestContentType === 'multipart/form-data') {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
const inlineBody = flags.body;
|
|
138
|
+
const bodyFile = flags['body-file'];
|
|
139
|
+
const bodyParameters = operation.parameters.filter((parameter) => parameter.in === 'body');
|
|
140
|
+
const hasBodyFlags = bodyParameters.some((parameter) => hasParameterValue(flags, parameter));
|
|
141
|
+
if ((inlineBody || bodyFile) && hasBodyFlags) {
|
|
142
|
+
const providedBodyFlags = listProvidedBodyFlags(flags, bodyParameters);
|
|
143
|
+
const rawBodyInput = inlineBody ? '--body' : '--body-file';
|
|
144
|
+
throw new Error(`Conflicting request body inputs: received ${rawBodyInput} together with body field flags (${providedBodyFlags.join(', ')}). Use either body field flags or --body/--body-file.`);
|
|
145
|
+
}
|
|
146
|
+
if (inlineBody) {
|
|
147
|
+
return parseJsonInput(inlineBody, 'body');
|
|
148
|
+
}
|
|
149
|
+
if (bodyFile) {
|
|
150
|
+
return fs.readFile(bodyFile, 'utf8').then((content) => parseJsonInput(content, 'body-file'));
|
|
151
|
+
}
|
|
152
|
+
if (!bodyParameters.length) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
const body = {};
|
|
156
|
+
for (const parameter of bodyParameters) {
|
|
157
|
+
const rawValue = flags[parameter.flagName];
|
|
158
|
+
const value = parseBodyFieldValue(rawValue, parameter);
|
|
159
|
+
if (parameter.required && (value === undefined || value === '')) {
|
|
160
|
+
throw new Error(`Missing required body field --${parameter.flagName}`);
|
|
161
|
+
}
|
|
162
|
+
if (value === undefined) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
body[parameter.name] = value;
|
|
166
|
+
}
|
|
167
|
+
if (Object.keys(body).length > 0) {
|
|
168
|
+
return body;
|
|
169
|
+
}
|
|
170
|
+
if (operation.hasBody && operation.bodyRequired) {
|
|
171
|
+
throw new Error('Missing request body. Use body field flags or --body/--body-file.');
|
|
172
|
+
}
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
async function createMultipartBody(flags, operation) {
|
|
176
|
+
const bodyParameters = operation.parameters.filter((parameter) => parameter.in === 'body');
|
|
177
|
+
const formData = new FormData();
|
|
178
|
+
let hasValues = false;
|
|
179
|
+
for (const parameter of bodyParameters) {
|
|
180
|
+
const rawValue = flags[parameter.flagName];
|
|
181
|
+
const hasValue = hasParameterValue(flags, parameter);
|
|
182
|
+
if (parameter.required && !hasValue) {
|
|
183
|
+
throw new Error(`Missing required body field --${parameter.flagName}`);
|
|
184
|
+
}
|
|
185
|
+
if (!hasValue) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (parameter.isFile) {
|
|
189
|
+
const filePath = String(rawValue);
|
|
190
|
+
const content = await fs.readFile(filePath);
|
|
191
|
+
const arrayBuffer = content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength);
|
|
192
|
+
formData.append(parameter.name, new Blob([arrayBuffer]), basename(filePath));
|
|
193
|
+
hasValues = true;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const value = parseBodyFieldValue(rawValue, parameter);
|
|
197
|
+
if (value === undefined) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
formData.append(parameter.name, typeof value === 'object' ? JSON.stringify(value) : String(value));
|
|
201
|
+
hasValues = true;
|
|
202
|
+
}
|
|
203
|
+
if (!hasValues && operation.bodyRequired) {
|
|
204
|
+
throw new Error('Missing multipart request body.');
|
|
205
|
+
}
|
|
206
|
+
return hasValues ? formData : undefined;
|
|
207
|
+
}
|
|
208
|
+
export async function executeApiRequest(options) {
|
|
209
|
+
const { baseUrl, token } = await resolveServerRequestTarget(options);
|
|
210
|
+
const headers = new Headers();
|
|
211
|
+
headers.set(CLI_REQUEST_SOURCE_HEADER, CLI_REQUEST_SOURCE_VALUE);
|
|
212
|
+
if (token) {
|
|
213
|
+
headers.set('authorization', `Bearer ${token}`);
|
|
214
|
+
}
|
|
215
|
+
if (options.role) {
|
|
216
|
+
headers.set('x-role', options.role);
|
|
217
|
+
}
|
|
218
|
+
const query = new URLSearchParams();
|
|
219
|
+
let requestPath = options.operation.pathTemplate;
|
|
220
|
+
for (const parameter of options.operation.parameters) {
|
|
221
|
+
if (parameter.in === 'body') {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const rawValue = options.flags[parameter.flagName];
|
|
225
|
+
const value = parameter.isArray
|
|
226
|
+
? (Array.isArray(rawValue) ? rawValue : rawValue ? [rawValue] : undefined)
|
|
227
|
+
: parseScalarValue(rawValue, parameter.type);
|
|
228
|
+
if (parameter.required && (value === undefined || value === '')) {
|
|
229
|
+
throw new Error(`Missing required parameter --${parameter.flagName}`);
|
|
230
|
+
}
|
|
231
|
+
if (value === undefined) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (parameter.in === 'path') {
|
|
235
|
+
requestPath = requestPath.replace(`{${parameter.name}}`, encodeURIComponent(String(value)));
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (parameter.in === 'query') {
|
|
239
|
+
if (Array.isArray(value)) {
|
|
240
|
+
value.forEach((item) => query.append(parameter.name, String(parseScalarValue(item, parameter.type))));
|
|
241
|
+
}
|
|
242
|
+
else if (typeof value === 'object') {
|
|
243
|
+
query.set(parameter.name, JSON.stringify(value));
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
query.set(parameter.name, String(value));
|
|
247
|
+
}
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (parameter.in === 'header') {
|
|
251
|
+
headers.set(parameter.name, typeof value === 'object' ? JSON.stringify(value) : String(value));
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const body = options.operation.requestContentType === 'multipart/form-data'
|
|
256
|
+
? await createMultipartBody(options.flags, options.operation)
|
|
257
|
+
: await parseBody(options.flags, options.operation);
|
|
258
|
+
if (body !== undefined && options.operation.requestContentType !== 'multipart/form-data') {
|
|
259
|
+
headers.set('content-type', 'application/json');
|
|
260
|
+
}
|
|
261
|
+
const url = new URL(`${normalizeBaseUrl(baseUrl)}${requestPath}`);
|
|
262
|
+
query.forEach((value, key) => url.searchParams.append(key, value));
|
|
263
|
+
const response = await fetchWithPreservedAuthRedirect(url.toString(), {
|
|
264
|
+
method: options.operation.method.toUpperCase(),
|
|
265
|
+
headers,
|
|
266
|
+
body: body === undefined ? undefined : body instanceof FormData ? body : JSON.stringify(body),
|
|
267
|
+
});
|
|
268
|
+
if (options.operation.responseType === 'binary') {
|
|
269
|
+
const outputPath = options.flags.output;
|
|
270
|
+
if (!outputPath) {
|
|
271
|
+
throw new Error('Missing required output path --output');
|
|
272
|
+
}
|
|
273
|
+
return parseBinaryResponse(response, outputPath);
|
|
274
|
+
}
|
|
275
|
+
return parseResponse(response);
|
|
276
|
+
}
|
|
277
|
+
export async function executeRawApiRequest(options) {
|
|
278
|
+
const { baseUrl, token } = await resolveServerRequestTarget(options);
|
|
279
|
+
const headers = new Headers();
|
|
280
|
+
headers.set(CLI_REQUEST_SOURCE_HEADER, CLI_REQUEST_SOURCE_VALUE);
|
|
281
|
+
if (token) {
|
|
282
|
+
headers.set('authorization', `Bearer ${token}`);
|
|
283
|
+
}
|
|
284
|
+
if (options.role) {
|
|
285
|
+
headers.set('x-role', options.role);
|
|
286
|
+
}
|
|
287
|
+
for (const [name, value] of Object.entries(options.headers ?? {})) {
|
|
288
|
+
if (value === undefined || value === null || value === '') {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
headers.set(name, typeof value === 'object' ? JSON.stringify(value) : String(value));
|
|
292
|
+
}
|
|
293
|
+
if (options.body !== undefined) {
|
|
294
|
+
headers.set('content-type', 'application/json');
|
|
295
|
+
}
|
|
296
|
+
const url = new URL(`${normalizeBaseUrl(baseUrl)}${options.path}`);
|
|
297
|
+
for (const [key, value] of Object.entries(options.query ?? {})) {
|
|
298
|
+
if (value === undefined) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (Array.isArray(value)) {
|
|
302
|
+
for (const item of value) {
|
|
303
|
+
url.searchParams.append(key, typeof item === 'object' ? JSON.stringify(item) : String(item));
|
|
304
|
+
}
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
url.searchParams.set(key, typeof value === 'object' ? JSON.stringify(value) : String(value));
|
|
308
|
+
}
|
|
309
|
+
const controller = options.timeoutMs && options.timeoutMs > 0 ? new AbortController() : undefined;
|
|
310
|
+
const timeout = controller
|
|
311
|
+
? setTimeout(() => {
|
|
312
|
+
controller.abort();
|
|
313
|
+
}, options.timeoutMs)
|
|
314
|
+
: undefined;
|
|
315
|
+
try {
|
|
316
|
+
const response = await fetchWithPreservedAuthRedirect(url.toString(), {
|
|
317
|
+
method: options.method.toUpperCase(),
|
|
318
|
+
headers,
|
|
319
|
+
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
320
|
+
signal: controller?.signal,
|
|
321
|
+
});
|
|
322
|
+
return parseResponse(response);
|
|
323
|
+
}
|
|
324
|
+
finally {
|
|
325
|
+
if (timeout) {
|
|
326
|
+
clearTimeout(timeout);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
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 { printInfo, startTask, stopTask, updateTask } from './ui.js';
|
|
10
|
+
const APP_HEALTH_CHECK_INTERVAL_MS = 2_000;
|
|
11
|
+
const APP_HEALTH_CHECK_TIMEOUT_MS = 600_000;
|
|
12
|
+
const APP_HEALTH_CHECK_REQUEST_TIMEOUT_MS = 5_000;
|
|
13
|
+
function trimValue(value) {
|
|
14
|
+
return String(value ?? '').trim();
|
|
15
|
+
}
|
|
16
|
+
function buildHealthCheckUrl(apiBaseUrl) {
|
|
17
|
+
return `${apiBaseUrl.replace(/\/+$/, '')}/__health_check`;
|
|
18
|
+
}
|
|
19
|
+
async function sleep(ms) {
|
|
20
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
|
+
}
|
|
22
|
+
async function requestAppHealthCheck(params) {
|
|
23
|
+
const controller = new AbortController();
|
|
24
|
+
const timeout = setTimeout(() => {
|
|
25
|
+
controller.abort();
|
|
26
|
+
}, params.requestTimeoutMs ?? APP_HEALTH_CHECK_REQUEST_TIMEOUT_MS);
|
|
27
|
+
try {
|
|
28
|
+
const response = await (params.fetchImpl ?? fetch)(params.healthCheckUrl, {
|
|
29
|
+
method: 'GET',
|
|
30
|
+
signal: controller.signal,
|
|
31
|
+
});
|
|
32
|
+
const text = await response.text().catch(() => '');
|
|
33
|
+
const body = text.replace(/\s+/g, ' ').trim() || 'No response yet';
|
|
34
|
+
return {
|
|
35
|
+
ok: response.ok && text.trim().toLowerCase() === 'ok',
|
|
36
|
+
message: `HTTP ${response.status}: ${body}`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
message: `No response within ${Math.ceil((params.requestTimeoutMs ?? APP_HEALTH_CHECK_REQUEST_TIMEOUT_MS) / 1000)}s`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
message: error instanceof Error ? error.message : String(error),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
clearTimeout(timeout);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export class AppHealthCheckError extends Error {
|
|
56
|
+
}
|
|
57
|
+
export function formatAppUrl(port) {
|
|
58
|
+
const value = trimValue(port);
|
|
59
|
+
return value ? `http://127.0.0.1:${value}` : undefined;
|
|
60
|
+
}
|
|
61
|
+
export function resolveManagedAppApiBaseUrl(runtime, options) {
|
|
62
|
+
const override = trimValue(options?.portOverride);
|
|
63
|
+
if (override) {
|
|
64
|
+
return `http://127.0.0.1:${override}/api`;
|
|
65
|
+
}
|
|
66
|
+
const baseUrl = trimValue(runtime.env.baseUrl);
|
|
67
|
+
if (baseUrl) {
|
|
68
|
+
return baseUrl.replace(/\/+$/, '');
|
|
69
|
+
}
|
|
70
|
+
const appPort = runtime.env.appPort === undefined || runtime.env.appPort === null
|
|
71
|
+
? ''
|
|
72
|
+
: trimValue(runtime.env.appPort);
|
|
73
|
+
return appPort ? `http://127.0.0.1:${appPort}/api` : undefined;
|
|
74
|
+
}
|
|
75
|
+
export async function isAppReady(apiBaseUrl, options) {
|
|
76
|
+
const baseUrl = trimValue(apiBaseUrl);
|
|
77
|
+
if (!baseUrl) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const result = await requestAppHealthCheck({
|
|
81
|
+
healthCheckUrl: buildHealthCheckUrl(baseUrl),
|
|
82
|
+
fetchImpl: options?.fetchImpl,
|
|
83
|
+
requestTimeoutMs: options?.requestTimeoutMs,
|
|
84
|
+
});
|
|
85
|
+
return result.ok;
|
|
86
|
+
}
|
|
87
|
+
export async function waitForAppReady(params) {
|
|
88
|
+
const apiBaseUrl = trimValue(params.apiBaseUrl);
|
|
89
|
+
if (!apiBaseUrl) {
|
|
90
|
+
printInfo(`Skipping health check for "${params.envName}" because no local API URL is saved for this env.`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const healthCheckUrl = buildHealthCheckUrl(apiBaseUrl);
|
|
94
|
+
const startedAt = Date.now();
|
|
95
|
+
let lastMessage = 'No response yet';
|
|
96
|
+
let spinnerActive = true;
|
|
97
|
+
startTask(`Waiting for NocoBase to become ready for "${params.envName}"...`);
|
|
98
|
+
try {
|
|
99
|
+
while (Date.now() - startedAt < APP_HEALTH_CHECK_TIMEOUT_MS) {
|
|
100
|
+
const result = await requestAppHealthCheck({
|
|
101
|
+
healthCheckUrl,
|
|
102
|
+
fetchImpl: params.fetchImpl,
|
|
103
|
+
});
|
|
104
|
+
if (result.ok) {
|
|
105
|
+
stopTask();
|
|
106
|
+
spinnerActive = false;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
lastMessage = result.message;
|
|
110
|
+
const elapsedSeconds = Math.max(1, Math.floor((Date.now() - startedAt) / 1000));
|
|
111
|
+
updateTask(`Waiting for NocoBase to become ready for "${params.envName}"... (${elapsedSeconds}s elapsed, last status: ${lastMessage})`);
|
|
112
|
+
await sleep(APP_HEALTH_CHECK_INTERVAL_MS);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
if (spinnerActive) {
|
|
117
|
+
stopTask();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const hints = [
|
|
121
|
+
params.logHint,
|
|
122
|
+
params.containerName ? `docker logs ${params.containerName}` : undefined,
|
|
123
|
+
].filter(Boolean);
|
|
124
|
+
const hintText = hints.length > 0 ? ` ${hints.join(' ')}` : '';
|
|
125
|
+
throw new AppHealthCheckError(`NocoBase did not become ready in time for "${params.envName}". Expected \`${healthCheckUrl}\` to respond with \`ok\`, but the last status was: ${lastMessage}.${hintText}`);
|
|
126
|
+
}
|