@nocobase/cli 2.1.0-alpha.19 → 2.1.0-alpha.20
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/README.md +15 -9
- package/bin/run.js +9 -3
- package/dist/commands/api/resource/index.js +20 -0
- package/dist/commands/build.js +2 -2
- package/dist/commands/db/start.js +22 -0
- package/dist/commands/dev.js +1 -1
- package/dist/commands/download.js +221 -49
- package/dist/commands/env/add.js +179 -34
- package/dist/commands/env/auth.js +31 -6
- package/dist/commands/env/list.js +12 -2
- package/dist/commands/env/remove.js +12 -1
- package/dist/commands/env/update.js +24 -9
- package/dist/commands/env/use.js +11 -1
- package/dist/commands/init.js +186 -0
- package/dist/commands/install.js +660 -12
- package/dist/commands/pm/disable.js +14 -15
- package/dist/commands/pm/enable.js +14 -15
- package/dist/commands/pm/list.js +5 -16
- package/dist/commands/scaffold/migration.js +1 -1
- package/dist/commands/scaffold/plugin.js +1 -1
- package/dist/commands/start.js +1 -1
- package/dist/commands/upgrade.js +1 -1
- package/dist/generated/command-registry.js +57 -11
- package/dist/help/runtime-help.js +20 -0
- package/dist/lib/auth-store.js +48 -3
- package/dist/lib/bootstrap.js +14 -9
- package/dist/lib/command-discovery.js +4 -4
- package/dist/lib/env-auth.js +95 -15
- package/dist/lib/init-browser-wizard.js +431 -0
- package/dist/lib/openapi.js +8 -200
- package/dist/lib/run-npm.js +27 -42
- package/nocobase-ctl.config.json +28 -68
- package/package.json +7 -6
- package/dist/commands/self-update.js +0 -46
|
@@ -0,0 +1,431 @@
|
|
|
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
|
+
* Local HTTP wizard for `nb init --ui`: binds a TCP server (default 0.0.0.0, ephemeral port) and returns user choices.
|
|
11
|
+
*/
|
|
12
|
+
import http from 'node:http';
|
|
13
|
+
import { execFile } from 'node:child_process';
|
|
14
|
+
import { promisify } from 'node:util';
|
|
15
|
+
const execFileAsync = promisify(execFile);
|
|
16
|
+
const WIZARD_TIMEOUT_MS = 600_000;
|
|
17
|
+
/** Avoid hanging on `server.close()`: browsers keep HTTP/1.1 connections alive after GET /, so close waits until idle timeout unless we close sockets. */
|
|
18
|
+
function closeWizardServer(server, onClosed) {
|
|
19
|
+
server.close(onClosed);
|
|
20
|
+
server.closeAllConnections?.();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Host fragment for `http://…` (not `0.0.0.0` / `::`).
|
|
24
|
+
*/
|
|
25
|
+
function wizardUrlHost(bindHost) {
|
|
26
|
+
if (bindHost === '0.0.0.0' || bindHost === '::') {
|
|
27
|
+
return '127.0.0.1';
|
|
28
|
+
}
|
|
29
|
+
return formatHostForHttpUrl(bindHost);
|
|
30
|
+
}
|
|
31
|
+
function formatHostForHttpUrl(host) {
|
|
32
|
+
if (host.includes(':') && !(host.startsWith('[') && host.endsWith(']'))) {
|
|
33
|
+
return `[${host}]`;
|
|
34
|
+
}
|
|
35
|
+
return host;
|
|
36
|
+
}
|
|
37
|
+
function wizardOpenUrl(bindHost, port) {
|
|
38
|
+
return `http://${wizardUrlHost(bindHost)}:${port}/`;
|
|
39
|
+
}
|
|
40
|
+
function initWizardHtml() {
|
|
41
|
+
return `<!DOCTYPE html>
|
|
42
|
+
<html lang="en">
|
|
43
|
+
<head>
|
|
44
|
+
<meta charset="utf-8" />
|
|
45
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
46
|
+
<title>Initialize the NocoBase AI setup environment</title>
|
|
47
|
+
<style>
|
|
48
|
+
:root { font-family: system-ui, sans-serif; color: #0f172a; background: #f8fafc; }
|
|
49
|
+
body { max-width: 36rem; margin: 2rem auto; padding: 0 1rem; }
|
|
50
|
+
h1 { font-size: 1.35rem; font-weight: 600; margin-bottom: 0.25rem; }
|
|
51
|
+
p.sub { color: #64748b; font-size: 0.9rem; margin-top: 0; margin-bottom: 1.5rem; }
|
|
52
|
+
fieldset { border: 1px solid #e2e8f0; border-radius: 8px; padding: 1rem 1.1rem; margin: 0 0 1rem; background: #fff; }
|
|
53
|
+
legend { font-weight: 600; padding: 0 0.35rem; }
|
|
54
|
+
label.opt { display: flex; gap: 0.6rem; align-items: flex-start; margin: 0.5rem 0; cursor: pointer; }
|
|
55
|
+
label.opt input { margin-top: 0.2rem; }
|
|
56
|
+
label.field { display: block; margin: 0.65rem 0; }
|
|
57
|
+
label.field span { display: block; font-size: 0.85rem; font-weight: 500; margin-bottom: 0.25rem; }
|
|
58
|
+
input.txt { width: 100%; max-width: 100%; box-sizing: border-box; font: inherit; padding: 0.45rem 0.5rem; border: 1px solid #cbd5e1; border-radius: 6px; }
|
|
59
|
+
.actions { display: flex; gap: 0.75rem; flex-wrap: wrap; margin-top: 1.25rem; }
|
|
60
|
+
button { font: inherit; padding: 0.5rem 1rem; border-radius: 6px; border: none; cursor: pointer; }
|
|
61
|
+
button.primary { background: #2563eb; color: #fff; }
|
|
62
|
+
button.primary:hover { background: #1d4ed8; }
|
|
63
|
+
button.secondary { background: #e2e8f0; color: #334155; }
|
|
64
|
+
button.secondary:hover { background: #cbd5e1; }
|
|
65
|
+
#status { margin-top: 1rem; font-size: 0.9rem; min-height: 1.25rem; }
|
|
66
|
+
#status.err { color: #b91c1c; }
|
|
67
|
+
#status.ok { color: #15803d; }
|
|
68
|
+
#status.wait { color: #b45309; }
|
|
69
|
+
button:disabled { opacity: 0.65; cursor: not-allowed; }
|
|
70
|
+
#envPanel { display: none; }
|
|
71
|
+
#envPanel.visible { display: block; }
|
|
72
|
+
#tokenRow { display: none; }
|
|
73
|
+
#tokenRow.visible { display: block; }
|
|
74
|
+
.hint { font-size: 0.8rem; color: #64748b; margin-top: 0.15rem; }
|
|
75
|
+
</style>
|
|
76
|
+
</head>
|
|
77
|
+
<body>
|
|
78
|
+
<h1>Initialize the NocoBase AI setup environment</h1>
|
|
79
|
+
<p class="sub">Pick your options and submit. This page only talks to the <code>nb init</code> process on your machine (<code>127.0.0.1</code>).</p>
|
|
80
|
+
<form id="f">
|
|
81
|
+
<fieldset>
|
|
82
|
+
<legend>Agent skills</legend>
|
|
83
|
+
<label class="opt">
|
|
84
|
+
<input type="checkbox" name="installSkills" id="installSkills" checked />
|
|
85
|
+
<span>Install NocoBase agent skills (<code>nocobase/skills</code>) for Cursor and Codex</span>
|
|
86
|
+
</label>
|
|
87
|
+
</fieldset>
|
|
88
|
+
<fieldset>
|
|
89
|
+
<legend>What happens next</legend>
|
|
90
|
+
<label class="opt">
|
|
91
|
+
<input type="radio" name="path" value="install" checked />
|
|
92
|
+
<span>New project — run <strong>nb install</strong> (full setup in the terminal)</span>
|
|
93
|
+
</label>
|
|
94
|
+
<label class="opt">
|
|
95
|
+
<input type="radio" name="path" value="env_add" />
|
|
96
|
+
<span>Existing deployment — run <strong>nb env add</strong> (fill in the section below)</span>
|
|
97
|
+
</label>
|
|
98
|
+
</fieldset>
|
|
99
|
+
<div id="envPanel">
|
|
100
|
+
<fieldset>
|
|
101
|
+
<legend>CLI environment</legend>
|
|
102
|
+
<p class="sub" style="margin-bottom:0.75rem">Matches the terminal prompts for <code>nb env add</code>. Values are sent to the CLI when you click Continue.</p>
|
|
103
|
+
<label class="field">
|
|
104
|
+
<span>Environment name</span>
|
|
105
|
+
<input class="txt" type="text" id="envName" value="default" autocomplete="off" />
|
|
106
|
+
</label>
|
|
107
|
+
<fieldset>
|
|
108
|
+
<legend>Where to store this env</legend>
|
|
109
|
+
<label class="opt">
|
|
110
|
+
<input type="radio" name="scope" value="project" checked />
|
|
111
|
+
<span>Project <span class="hint"><code>.nocobase</code> in this directory</span></span>
|
|
112
|
+
</label>
|
|
113
|
+
<label class="opt">
|
|
114
|
+
<input type="radio" name="scope" value="global" />
|
|
115
|
+
<span>Global <span class="hint">your user config</span></span>
|
|
116
|
+
</label>
|
|
117
|
+
</fieldset>
|
|
118
|
+
<label class="field">
|
|
119
|
+
<span>API base URL</span>
|
|
120
|
+
<input class="txt" type="url" id="apiBaseUrl" value="http://localhost:13000/api" autocomplete="off" />
|
|
121
|
+
</label>
|
|
122
|
+
<fieldset>
|
|
123
|
+
<legend>Authentication</legend>
|
|
124
|
+
<label class="opt">
|
|
125
|
+
<input type="radio" name="authType" value="oauth" checked />
|
|
126
|
+
<span>OAuth — after saving, the CLI runs <code>nb env auth</code></span>
|
|
127
|
+
</label>
|
|
128
|
+
<label class="opt">
|
|
129
|
+
<input type="radio" name="authType" value="token" />
|
|
130
|
+
<span>Token — API key or bearer token</span>
|
|
131
|
+
</label>
|
|
132
|
+
</fieldset>
|
|
133
|
+
<div id="tokenRow">
|
|
134
|
+
<label class="field">
|
|
135
|
+
<span>Access token / API key</span>
|
|
136
|
+
<input class="txt" type="password" id="accessToken" autocomplete="off" />
|
|
137
|
+
</label>
|
|
138
|
+
</div>
|
|
139
|
+
</fieldset>
|
|
140
|
+
</div>
|
|
141
|
+
<div class="actions">
|
|
142
|
+
<button type="submit" class="primary" id="btnContinue">Continue</button>
|
|
143
|
+
<button type="button" class="secondary" id="cancel">Cancel</button>
|
|
144
|
+
</div>
|
|
145
|
+
</form>
|
|
146
|
+
<p id="status"></p>
|
|
147
|
+
<script>
|
|
148
|
+
const status = document.getElementById('status');
|
|
149
|
+
const envPanel = document.getElementById('envPanel');
|
|
150
|
+
const tokenRow = document.getElementById('tokenRow');
|
|
151
|
+
function setErr(msg) { status.className = 'err'; status.textContent = msg; }
|
|
152
|
+
function setOk(msg) { status.className = 'ok'; status.textContent = msg; }
|
|
153
|
+
function setWait(msg) { status.className = 'wait'; status.textContent = msg; }
|
|
154
|
+
function syncPath() {
|
|
155
|
+
const path = document.querySelector('input[name="path"]:checked').value;
|
|
156
|
+
envPanel.classList.toggle('visible', path === 'env_add');
|
|
157
|
+
}
|
|
158
|
+
function syncAuth() {
|
|
159
|
+
const t = document.querySelector('input[name="authType"]:checked').value;
|
|
160
|
+
tokenRow.classList.toggle('visible', t === 'token');
|
|
161
|
+
}
|
|
162
|
+
document.querySelectorAll('input[name="path"]').forEach(function (el) {
|
|
163
|
+
el.addEventListener('change', syncPath);
|
|
164
|
+
});
|
|
165
|
+
document.querySelectorAll('input[name="authType"]').forEach(function (el) {
|
|
166
|
+
el.addEventListener('change', syncAuth);
|
|
167
|
+
});
|
|
168
|
+
syncPath();
|
|
169
|
+
syncAuth();
|
|
170
|
+
document.getElementById('f').addEventListener('submit', async function (e) {
|
|
171
|
+
e.preventDefault();
|
|
172
|
+
const btnContinue = document.getElementById('btnContinue');
|
|
173
|
+
const btnCancel = document.getElementById('cancel');
|
|
174
|
+
const installSkills = document.getElementById('installSkills').checked;
|
|
175
|
+
const path = document.querySelector('input[name="path"]:checked').value;
|
|
176
|
+
const hasNocobase = path === 'env_add';
|
|
177
|
+
let envAdd = undefined;
|
|
178
|
+
if (hasNocobase) {
|
|
179
|
+
const envName = document.getElementById('envName').value.trim();
|
|
180
|
+
const scope = document.querySelector('input[name="scope"]:checked').value;
|
|
181
|
+
const apiBaseUrl = document.getElementById('apiBaseUrl').value.trim();
|
|
182
|
+
const authType = document.querySelector('input[name="authType"]:checked').value;
|
|
183
|
+
const accessToken = document.getElementById('accessToken').value.trim();
|
|
184
|
+
if (!envName) { setErr('Please enter an environment name.'); return; }
|
|
185
|
+
if (!apiBaseUrl) { setErr('Please enter the API base URL.'); return; }
|
|
186
|
+
if (authType === 'token' && !accessToken) { setErr('Please enter an access token for token authentication.'); return; }
|
|
187
|
+
envAdd = {
|
|
188
|
+
envName: envName,
|
|
189
|
+
scope: scope,
|
|
190
|
+
apiBaseUrl: apiBaseUrl,
|
|
191
|
+
authType: authType,
|
|
192
|
+
accessToken: authType === 'token' ? accessToken : undefined,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
btnContinue.disabled = true;
|
|
196
|
+
btnCancel.disabled = true;
|
|
197
|
+
setWait('Submitting to the CLI…');
|
|
198
|
+
try {
|
|
199
|
+
const r = await fetch('/submit', {
|
|
200
|
+
method: 'POST',
|
|
201
|
+
headers: { 'Content-Type': 'application/json' },
|
|
202
|
+
body: JSON.stringify({ installSkills, hasNocobase, envAdd }),
|
|
203
|
+
});
|
|
204
|
+
const data = await r.json().catch(function () { return {}; });
|
|
205
|
+
if (!r.ok) throw new Error(data.error || 'Request failed');
|
|
206
|
+
setOk(
|
|
207
|
+
'Submitted. Continue in your terminal. This window will try to close automatically in 5 seconds.',
|
|
208
|
+
);
|
|
209
|
+
setTimeout(function () {
|
|
210
|
+
window.close();
|
|
211
|
+
setTimeout(function () {
|
|
212
|
+
if (document.visibilityState === 'visible') {
|
|
213
|
+
setOk(
|
|
214
|
+
'This tab could not be closed automatically. Please close it manually—the CLI is still running in your terminal.',
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}, 600);
|
|
218
|
+
}, 5000);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
setErr(err.message || 'Could not reach the CLI. Keep this terminal command running: nb init --ui');
|
|
221
|
+
btnContinue.disabled = false;
|
|
222
|
+
btnCancel.disabled = false;
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
document.getElementById('cancel').addEventListener('click', async function () {
|
|
226
|
+
try {
|
|
227
|
+
await fetch('/cancel', { method: 'POST' });
|
|
228
|
+
} catch (_) {}
|
|
229
|
+
setOk('Cancelled. You may close this tab.');
|
|
230
|
+
});
|
|
231
|
+
</script>
|
|
232
|
+
</body>
|
|
233
|
+
</html>`;
|
|
234
|
+
}
|
|
235
|
+
export async function openBrowser(url) {
|
|
236
|
+
try {
|
|
237
|
+
if (process.platform === 'darwin') {
|
|
238
|
+
await execFileAsync('open', [url]);
|
|
239
|
+
}
|
|
240
|
+
else if (process.platform === 'win32') {
|
|
241
|
+
await execFileAsync('cmd', ['/c', 'start', '', url]);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
await execFileAsync('xdg-open', [url]);
|
|
245
|
+
}
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
export class InitWizardCancelledError extends Error {
|
|
253
|
+
constructor() {
|
|
254
|
+
super('Init cancelled from browser.');
|
|
255
|
+
this.name = 'InitWizardCancelledError';
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function parseAndValidateSubmit(raw) {
|
|
259
|
+
const data = JSON.parse(raw);
|
|
260
|
+
const installSkills = Boolean(data.installSkills);
|
|
261
|
+
const hasNocobase = Boolean(data.hasNocobase);
|
|
262
|
+
if (!hasNocobase) {
|
|
263
|
+
return { installSkills, hasNocobase: false };
|
|
264
|
+
}
|
|
265
|
+
const ea = data.envAdd;
|
|
266
|
+
if (!ea || typeof ea !== 'object') {
|
|
267
|
+
throw new Error('Environment fields are required when you choose an existing deployment.');
|
|
268
|
+
}
|
|
269
|
+
const rec = ea;
|
|
270
|
+
const envName = typeof rec.envName === 'string' ? rec.envName.trim() : '';
|
|
271
|
+
const scope = rec.scope === 'global' || rec.scope === 'project' ? rec.scope : '';
|
|
272
|
+
const apiBaseUrl = typeof rec.apiBaseUrl === 'string' ? rec.apiBaseUrl.trim() : '';
|
|
273
|
+
const authType = rec.authType === 'token' || rec.authType === 'oauth' ? rec.authType : '';
|
|
274
|
+
const accessToken = typeof rec.accessToken === 'string' && rec.accessToken.trim() ? rec.accessToken.trim() : undefined;
|
|
275
|
+
if (!envName) {
|
|
276
|
+
throw new Error('Environment name cannot be empty.');
|
|
277
|
+
}
|
|
278
|
+
if (!scope) {
|
|
279
|
+
throw new Error('Choose project or global scope.');
|
|
280
|
+
}
|
|
281
|
+
if (!apiBaseUrl) {
|
|
282
|
+
throw new Error('API base URL cannot be empty.');
|
|
283
|
+
}
|
|
284
|
+
if (!authType) {
|
|
285
|
+
throw new Error('Choose token or OAuth authentication.');
|
|
286
|
+
}
|
|
287
|
+
if (authType === 'token' && !accessToken) {
|
|
288
|
+
throw new Error('Token authentication requires an access token.');
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
installSkills,
|
|
292
|
+
hasNocobase: true,
|
|
293
|
+
envAdd: {
|
|
294
|
+
envName,
|
|
295
|
+
scope,
|
|
296
|
+
apiBaseUrl,
|
|
297
|
+
authType,
|
|
298
|
+
...(authType === 'token' ? { accessToken } : {}),
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function buildEnvAddArgv(fields) {
|
|
303
|
+
const argv = [
|
|
304
|
+
fields.envName,
|
|
305
|
+
'--scope',
|
|
306
|
+
fields.scope,
|
|
307
|
+
'--api-base-url',
|
|
308
|
+
fields.apiBaseUrl,
|
|
309
|
+
'--auth-type',
|
|
310
|
+
fields.authType,
|
|
311
|
+
];
|
|
312
|
+
if (fields.authType === 'token' && fields.accessToken) {
|
|
313
|
+
argv.push('--access-token', fields.accessToken);
|
|
314
|
+
}
|
|
315
|
+
return argv;
|
|
316
|
+
}
|
|
317
|
+
export { buildEnvAddArgv };
|
|
318
|
+
function resolveWizardListenOptions(options) {
|
|
319
|
+
const bindHost = options?.bindHost?.trim() || '0.0.0.0';
|
|
320
|
+
let port;
|
|
321
|
+
if (options?.port !== undefined) {
|
|
322
|
+
port = options.port;
|
|
323
|
+
}
|
|
324
|
+
else if (process.env.NOCOBASE_INIT_UI_PORT !== undefined) {
|
|
325
|
+
const n = Number(process.env.NOCOBASE_INIT_UI_PORT);
|
|
326
|
+
port = Number.isFinite(n) ? n : 0;
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
port = 0;
|
|
330
|
+
}
|
|
331
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
332
|
+
throw new Error('Wizard port must be an integer from 0 to 65535 (0 = random).');
|
|
333
|
+
}
|
|
334
|
+
return { bindHost, port };
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Binds a local HTTP server, opens the wizard URL, resolves when the form is submitted
|
|
338
|
+
* (then closes the server). The CLI continues in the terminal.
|
|
339
|
+
*/
|
|
340
|
+
export function runInitBrowserWizard(log, options) {
|
|
341
|
+
const { bindHost, port: listenPort } = resolveWizardListenOptions(options);
|
|
342
|
+
const html = initWizardHtml();
|
|
343
|
+
return new Promise((resolve, reject) => {
|
|
344
|
+
let settled = false;
|
|
345
|
+
const timeout = setTimeout(() => {
|
|
346
|
+
if (settled) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
settled = true;
|
|
350
|
+
closeWizardServer(server, () => {
|
|
351
|
+
reject(new Error('Browser wizard timed out after 10 minutes with no submission.'));
|
|
352
|
+
});
|
|
353
|
+
}, WIZARD_TIMEOUT_MS);
|
|
354
|
+
function done(choice) {
|
|
355
|
+
if (settled) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
settled = true;
|
|
359
|
+
clearTimeout(timeout);
|
|
360
|
+
closeWizardServer(server, () => resolve(choice));
|
|
361
|
+
}
|
|
362
|
+
function fail(err) {
|
|
363
|
+
if (settled) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
settled = true;
|
|
367
|
+
clearTimeout(timeout);
|
|
368
|
+
closeWizardServer(server, () => reject(err));
|
|
369
|
+
}
|
|
370
|
+
const server = http.createServer((req, res) => {
|
|
371
|
+
const u = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
372
|
+
if (req.method === 'GET' && (u.pathname === '/' || u.pathname === '/index.html')) {
|
|
373
|
+
res.writeHead(200, {
|
|
374
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
375
|
+
'Cache-Control': 'no-store',
|
|
376
|
+
Connection: 'close',
|
|
377
|
+
});
|
|
378
|
+
res.end(html);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (req.method === 'POST' && u.pathname === '/cancel') {
|
|
382
|
+
res.writeHead(200, { 'Content-Type': 'application/json', Connection: 'close' });
|
|
383
|
+
res.end(JSON.stringify({ ok: true }), () => {
|
|
384
|
+
fail(new InitWizardCancelledError());
|
|
385
|
+
});
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (req.method === 'POST' && u.pathname === '/submit') {
|
|
389
|
+
let raw = '';
|
|
390
|
+
req.on('data', (c) => {
|
|
391
|
+
raw += c;
|
|
392
|
+
});
|
|
393
|
+
req.on('end', () => {
|
|
394
|
+
try {
|
|
395
|
+
const choice = parseAndValidateSubmit(raw);
|
|
396
|
+
res.writeHead(200, { 'Content-Type': 'application/json', Connection: 'close' });
|
|
397
|
+
res.end(JSON.stringify({ ok: true }), () => {
|
|
398
|
+
done(choice);
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
catch (e) {
|
|
402
|
+
const msg = e instanceof Error ? e.message : 'Invalid submission.';
|
|
403
|
+
res.writeHead(400, {
|
|
404
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
405
|
+
Connection: 'close',
|
|
406
|
+
});
|
|
407
|
+
res.end(JSON.stringify({ ok: false, error: msg }));
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
res.writeHead(404, { Connection: 'close' }).end();
|
|
413
|
+
});
|
|
414
|
+
server.on('error', (err) => {
|
|
415
|
+
fail(err instanceof Error ? err : new Error(String(err)));
|
|
416
|
+
});
|
|
417
|
+
server.listen(listenPort, bindHost, async () => {
|
|
418
|
+
const addr = server.address();
|
|
419
|
+
if (!addr || typeof addr === 'string') {
|
|
420
|
+
fail(new Error('Failed to bind local wizard server.'));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const openUrl = wizardOpenUrl(bindHost, addr.port);
|
|
424
|
+
log(`Local wizard: ${openUrl}`);
|
|
425
|
+
const opened = await openBrowser(openUrl);
|
|
426
|
+
if (!opened) {
|
|
427
|
+
log('Could not open a browser automatically. Open the URL above manually.');
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
}
|
package/dist/lib/openapi.js
CHANGED
|
@@ -1,200 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
+
*/
|
|
7
9
|
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete'];
|
|
8
|
-
function isPluginSwaggerSource(filePath) {
|
|
9
|
-
return (filePath.includes(`${path.sep}packages${path.sep}plugins${path.sep}`) &&
|
|
10
|
-
filePath.includes(`${path.sep}src${path.sep}swagger${path.sep}`));
|
|
11
|
-
}
|
|
12
|
-
function isCoreSwaggerSource(filePath) {
|
|
13
|
-
return filePath.includes(`${path.sep}packages${path.sep}core${path.sep}server${path.sep}src${path.sep}swagger${path.sep}`);
|
|
14
|
-
}
|
|
15
|
-
function toModuleName(filePath) {
|
|
16
|
-
if (isCoreSwaggerSource(filePath)) {
|
|
17
|
-
return 'core';
|
|
18
|
-
}
|
|
19
|
-
const normalized = filePath.replace(/\\/g, '/');
|
|
20
|
-
const pluginMatch = normalized.match(/packages\/plugins\/@nocobase\/plugin-([^/]+)\//);
|
|
21
|
-
return pluginMatch?.[1] ?? '';
|
|
22
|
-
}
|
|
23
|
-
function toPackageFile(filePath) {
|
|
24
|
-
if (isCoreSwaggerSource(filePath)) {
|
|
25
|
-
return '';
|
|
26
|
-
}
|
|
27
|
-
const normalized = filePath.replace(/\\/g, '/');
|
|
28
|
-
const match = normalized.match(/^(.*\/packages\/plugins\/@nocobase\/plugin-[^/]+)\//);
|
|
29
|
-
return match ? `${match[1]}/package.json` : '';
|
|
30
|
-
}
|
|
31
|
-
function toPackageName(filePath) {
|
|
32
|
-
if (isCoreSwaggerSource(filePath)) {
|
|
33
|
-
return '@nocobase/server';
|
|
34
|
-
}
|
|
35
|
-
const normalized = filePath.replace(/\\/g, '/');
|
|
36
|
-
const match = normalized.match(/packages\/plugins\/(@nocobase\/plugin-[^/]+)\//);
|
|
37
|
-
return match?.[1] ?? '';
|
|
38
|
-
}
|
|
39
|
-
async function walk(dir, result) {
|
|
40
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
41
|
-
for (const entry of entries) {
|
|
42
|
-
const fullPath = path.join(dir, entry.name);
|
|
43
|
-
if (entry.isDirectory()) {
|
|
44
|
-
await walk(fullPath, result);
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
if (!SWAGGER_FILENAMES.has(entry.name)) {
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
if (!isPluginSwaggerSource(fullPath) && !isCoreSwaggerSource(fullPath)) {
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
result.push(fullPath);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
export async function discoverSwaggerSources(sourceRoot) {
|
|
57
|
-
const files = [];
|
|
58
|
-
await walk(path.join(sourceRoot, 'packages'), files);
|
|
59
|
-
return files
|
|
60
|
-
.sort()
|
|
61
|
-
.map((sourceFile) => ({
|
|
62
|
-
moduleName: toModuleName(sourceFile),
|
|
63
|
-
sourceFile,
|
|
64
|
-
sourceId: path.relative(sourceRoot, sourceFile).replace(/\\/g, '/'),
|
|
65
|
-
packageFile: toPackageFile(sourceFile) || undefined,
|
|
66
|
-
packageName: toPackageName(sourceFile) || undefined,
|
|
67
|
-
format: sourceFile.endsWith('.json') ? 'json' : 'ts',
|
|
68
|
-
}))
|
|
69
|
-
.filter((item) => item.moduleName);
|
|
70
|
-
}
|
|
71
|
-
function getPropertyName(name) {
|
|
72
|
-
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
|
|
73
|
-
return name.text;
|
|
74
|
-
}
|
|
75
|
-
if (ts.isComputedPropertyName(name) && ts.isStringLiteral(name.expression)) {
|
|
76
|
-
return name.expression.text;
|
|
77
|
-
}
|
|
78
|
-
throw new Error('Unsupported computed property in swagger object.');
|
|
79
|
-
}
|
|
80
|
-
function evaluateExpression(node) {
|
|
81
|
-
if (ts.isObjectLiteralExpression(node)) {
|
|
82
|
-
const value = {};
|
|
83
|
-
for (const property of node.properties) {
|
|
84
|
-
if (ts.isPropertyAssignment(property)) {
|
|
85
|
-
value[getPropertyName(property.name)] = evaluateExpression(property.initializer);
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
if (ts.isShorthandPropertyAssignment(property)) {
|
|
89
|
-
throw new Error(`Unsupported shorthand property "${property.name.text}" in swagger object.`);
|
|
90
|
-
}
|
|
91
|
-
if (ts.isSpreadAssignment(property)) {
|
|
92
|
-
throw new Error('Unsupported spread assignment in swagger object.');
|
|
93
|
-
}
|
|
94
|
-
if (ts.isMethodDeclaration(property) || ts.isAccessor(property)) {
|
|
95
|
-
throw new Error('Unsupported method/accessor in swagger object.');
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return value;
|
|
99
|
-
}
|
|
100
|
-
if (ts.isArrayLiteralExpression(node)) {
|
|
101
|
-
return node.elements.map((element) => {
|
|
102
|
-
if (ts.isSpreadElement(element)) {
|
|
103
|
-
throw new Error('Unsupported spread element in swagger array.');
|
|
104
|
-
}
|
|
105
|
-
return evaluateExpression(element);
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
109
|
-
return node.text;
|
|
110
|
-
}
|
|
111
|
-
if (ts.isNumericLiteral(node)) {
|
|
112
|
-
return Number(node.text);
|
|
113
|
-
}
|
|
114
|
-
if (node.kind === ts.SyntaxKind.TrueKeyword) {
|
|
115
|
-
return true;
|
|
116
|
-
}
|
|
117
|
-
if (node.kind === ts.SyntaxKind.FalseKeyword) {
|
|
118
|
-
return false;
|
|
119
|
-
}
|
|
120
|
-
if (node.kind === ts.SyntaxKind.NullKeyword) {
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
if (ts.isParenthesizedExpression(node)) {
|
|
124
|
-
return evaluateExpression(node.expression);
|
|
125
|
-
}
|
|
126
|
-
if (ts.isPropertyAccessExpression(node)) {
|
|
127
|
-
return {
|
|
128
|
-
__target__: evaluateExpression(node.expression),
|
|
129
|
-
__property__: node.name.text,
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
if (ts.isCallExpression(node)) {
|
|
133
|
-
const callee = evaluateExpression(node.expression);
|
|
134
|
-
const args = node.arguments.map((argument) => evaluateExpression(argument));
|
|
135
|
-
if (callee &&
|
|
136
|
-
typeof callee === 'object' &&
|
|
137
|
-
'__target__' in callee &&
|
|
138
|
-
'__property__' in callee &&
|
|
139
|
-
callee.__property__ === 'join' &&
|
|
140
|
-
Array.isArray(callee.__target__)) {
|
|
141
|
-
return callee.__target__.join(args[0] ?? ',');
|
|
142
|
-
}
|
|
143
|
-
throw new Error('Unsupported call expression in swagger object.');
|
|
144
|
-
}
|
|
145
|
-
if (ts.isPrefixUnaryExpression(node)) {
|
|
146
|
-
const operand = evaluateExpression(node.operand);
|
|
147
|
-
if (node.operator === ts.SyntaxKind.MinusToken) {
|
|
148
|
-
return -operand;
|
|
149
|
-
}
|
|
150
|
-
if (node.operator === ts.SyntaxKind.PlusToken) {
|
|
151
|
-
return +operand;
|
|
152
|
-
}
|
|
153
|
-
if (node.operator === ts.SyntaxKind.ExclamationToken) {
|
|
154
|
-
return !operand;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
throw new Error(`Unsupported swagger expression kind: ${ts.SyntaxKind[node.kind]}`);
|
|
158
|
-
}
|
|
159
|
-
function parseTypeScriptSwagger(sourceText, fileName) {
|
|
160
|
-
const sourceFile = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
161
|
-
for (const statement of sourceFile.statements) {
|
|
162
|
-
if (!ts.isExportAssignment(statement)) {
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
if (!ts.isObjectLiteralExpression(statement.expression)) {
|
|
166
|
-
throw new Error('Expected `export default` to be an object literal.');
|
|
167
|
-
}
|
|
168
|
-
return evaluateExpression(statement.expression);
|
|
169
|
-
}
|
|
170
|
-
throw new Error('Missing `export default` in swagger source.');
|
|
171
|
-
}
|
|
172
|
-
function normalizeDocument(document) {
|
|
173
|
-
return {
|
|
174
|
-
...document,
|
|
175
|
-
openapi: document.openapi ?? '3.0.2',
|
|
176
|
-
info: {
|
|
177
|
-
...(document.info ?? {}),
|
|
178
|
-
title: document.info?.title ?? 'NocoBase API',
|
|
179
|
-
version: document.info?.version ?? '1.0.0',
|
|
180
|
-
},
|
|
181
|
-
paths: document.paths ?? {},
|
|
182
|
-
components: document.components ?? {},
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
export async function loadSwaggerDocument(source) {
|
|
186
|
-
const content = await fs.readFile(source.sourceFile, 'utf8');
|
|
187
|
-
const document = normalizeDocument(source.format === 'json'
|
|
188
|
-
? JSON.parse(content)
|
|
189
|
-
: parseTypeScriptSwagger(content, source.sourceFile));
|
|
190
|
-
try {
|
|
191
|
-
await SwaggerParser.validate(document);
|
|
192
|
-
return (await SwaggerParser.dereference(document));
|
|
193
|
-
}
|
|
194
|
-
catch (error) {
|
|
195
|
-
return document;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
10
|
function resolveLocalRef(document, ref) {
|
|
199
11
|
if (!ref.startsWith('#/')) {
|
|
200
12
|
return undefined;
|
|
@@ -224,10 +36,6 @@ function dereferenceNode(node, document, seen = new Set()) {
|
|
|
224
36
|
}
|
|
225
37
|
return Object.fromEntries(Object.entries(node).map(([key, value]) => [key, dereferenceNode(value, document, seen)]));
|
|
226
38
|
}
|
|
227
|
-
export async function sha1File(filePath) {
|
|
228
|
-
const content = await fs.readFile(filePath);
|
|
229
|
-
return createHash('sha1').update(content).digest('hex');
|
|
230
|
-
}
|
|
231
39
|
export function collectOperations(document) {
|
|
232
40
|
const operations = [];
|
|
233
41
|
for (const [pathTemplate, pathItem] of Object.entries(document.paths ?? {})) {
|