@nocobase/cli 2.1.0-alpha.20 → 2.1.0-alpha.21
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 +256 -89
- package/README.zh-CN.md +332 -0
- package/bin/run.js +21 -2
- package/dist/commands/build.js +7 -1
- package/dist/commands/db/logs.js +85 -0
- package/dist/commands/db/ps.js +60 -0
- package/dist/commands/db/shared.js +81 -0
- package/dist/commands/db/start.js +55 -7
- package/dist/commands/db/stop.js +70 -0
- package/dist/commands/dev.js +112 -21
- package/dist/commands/down.js +193 -0
- package/dist/commands/download.js +622 -183
- package/dist/commands/env/add.js +233 -131
- package/dist/commands/env/auth.js +9 -8
- package/dist/commands/init.js +696 -103
- package/dist/commands/install.js +1588 -566
- package/dist/commands/logs.js +90 -0
- package/dist/commands/pm/disable.js +35 -3
- package/dist/commands/pm/enable.js +35 -3
- package/dist/commands/pm/list.js +37 -4
- package/dist/commands/prompts-stages.js +144 -0
- package/dist/commands/prompts-test.js +175 -0
- package/dist/commands/ps.js +116 -0
- package/dist/commands/start.js +171 -15
- package/dist/commands/stop.js +90 -0
- package/dist/commands/upgrade.js +559 -11
- package/dist/lib/app-runtime.js +142 -0
- package/dist/lib/auth-store.js +44 -3
- package/dist/lib/bootstrap.js +7 -3
- package/dist/lib/env-auth.js +427 -82
- package/dist/lib/prompt-catalog.js +552 -0
- package/dist/lib/prompt-validators.js +184 -0
- package/dist/lib/prompt-web-ui.js +2027 -0
- package/dist/lib/run-npm.js +71 -7
- package/package.json +3 -3
- package/dist/commands/restart.js +0 -32
- package/dist/lib/init-browser-wizard.js +0 -431
package/dist/lib/run-npm.js
CHANGED
|
@@ -8,16 +8,28 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { spawn } from 'node:child_process';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
function resolveCommandName(name) {
|
|
12
|
+
if (process.platform !== 'win32' || path.extname(name) || name.includes(path.sep)) {
|
|
13
|
+
return name;
|
|
14
|
+
}
|
|
15
|
+
if (['npm', 'npx', 'pnpm', 'yarn'].includes(name)) {
|
|
16
|
+
return `${name}.cmd`;
|
|
15
17
|
}
|
|
18
|
+
return name;
|
|
19
|
+
}
|
|
20
|
+
function resolveCwd(cwd) {
|
|
21
|
+
const next = cwd ?? process.cwd();
|
|
22
|
+
if (path.isAbsolute(next)) {
|
|
23
|
+
return next;
|
|
24
|
+
}
|
|
25
|
+
return path.resolve(process.cwd(), next);
|
|
26
|
+
}
|
|
27
|
+
export function run(name, args, options) {
|
|
28
|
+
const cwd = resolveCwd(options?.cwd);
|
|
16
29
|
const label = options?.errorName ?? name;
|
|
17
30
|
return new Promise((resolve, reject) => {
|
|
18
|
-
const child = spawn(name, [...args], {
|
|
19
|
-
stdio: 'inherit',
|
|
20
|
-
shell: true,
|
|
31
|
+
const child = spawn(resolveCommandName(name), [...args], {
|
|
32
|
+
stdio: options?.stdio ?? 'inherit',
|
|
21
33
|
cwd,
|
|
22
34
|
env: {
|
|
23
35
|
...process.env,
|
|
@@ -38,6 +50,58 @@ export function run(name, args, options) {
|
|
|
38
50
|
});
|
|
39
51
|
});
|
|
40
52
|
}
|
|
53
|
+
export function commandSucceeds(name, args, options) {
|
|
54
|
+
const cwd = resolveCwd(options?.cwd);
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
const child = spawn(resolveCommandName(name), [...args], {
|
|
57
|
+
cwd,
|
|
58
|
+
env: {
|
|
59
|
+
...process.env,
|
|
60
|
+
...options?.env,
|
|
61
|
+
},
|
|
62
|
+
stdio: 'ignore',
|
|
63
|
+
});
|
|
64
|
+
child.once('error', () => resolve(false));
|
|
65
|
+
child.once('close', (code) => resolve(code === 0));
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
export function commandOutput(name, args, options) {
|
|
69
|
+
const cwd = resolveCwd(options?.cwd);
|
|
70
|
+
const label = options?.errorName ?? name;
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const child = spawn(resolveCommandName(name), [...args], {
|
|
73
|
+
cwd,
|
|
74
|
+
env: {
|
|
75
|
+
...process.env,
|
|
76
|
+
...options?.env,
|
|
77
|
+
},
|
|
78
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
79
|
+
});
|
|
80
|
+
let stdout = '';
|
|
81
|
+
let stderr = '';
|
|
82
|
+
child.stdout.setEncoding('utf8');
|
|
83
|
+
child.stderr.setEncoding('utf8');
|
|
84
|
+
child.stdout.on('data', (chunk) => {
|
|
85
|
+
stdout += chunk;
|
|
86
|
+
});
|
|
87
|
+
child.stderr.on('data', (chunk) => {
|
|
88
|
+
stderr += chunk;
|
|
89
|
+
});
|
|
90
|
+
child.once('error', reject);
|
|
91
|
+
child.once('close', (code, signal) => {
|
|
92
|
+
if (code === 0) {
|
|
93
|
+
resolve(stdout.trim());
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (signal) {
|
|
97
|
+
reject(new Error(`${label} exited due to signal ${signal}`));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const details = stderr.trim() || stdout.trim();
|
|
101
|
+
reject(new Error(details ? `${label} exited with code ${code}: ${details}` : `${label} exited with code ${code}`));
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
41
105
|
/** Run `yarn` with the given argument list, inheriting stdio (errors label as `npm` for compatibility). */
|
|
42
106
|
export function runNpm(args, options) {
|
|
43
107
|
return run('yarn', [...args], { ...options, errorName: 'npm' });
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/cli",
|
|
3
|
-
"version": "2.1.0-alpha.
|
|
3
|
+
"version": "2.1.0-alpha.21",
|
|
4
4
|
"description": "NocoBase Command Line Tool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/generated/command-registry.js",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"clean": "rm -rf dist",
|
|
9
9
|
"build": "yarn clean && tsc -p tsconfig.json",
|
|
10
|
-
"test": "
|
|
10
|
+
"test": "TEST_ENV=server-side yarn --cwd ../../.. vitest run packages/core/cli"
|
|
11
11
|
},
|
|
12
12
|
"keywords": [],
|
|
13
13
|
"author": "",
|
|
@@ -60,5 +60,5 @@
|
|
|
60
60
|
"type": "git",
|
|
61
61
|
"url": "git+https://github.com/nocobase/nocobase.git"
|
|
62
62
|
},
|
|
63
|
-
"gitHead": "
|
|
63
|
+
"gitHead": "b4c2b469f321ecaec7863a8ae371a02fe6a35aa2"
|
|
64
64
|
}
|
package/dist/commands/restart.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
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 { Args, Command, Flags } from '@oclif/core';
|
|
10
|
-
export default class Restart extends Command {
|
|
11
|
-
static args = {
|
|
12
|
-
file: Args.string({ description: 'file to read' }),
|
|
13
|
-
};
|
|
14
|
-
static description = 'describe the command here';
|
|
15
|
-
static examples = [
|
|
16
|
-
'<%= config.bin %> <%= command.id %>',
|
|
17
|
-
];
|
|
18
|
-
static flags = {
|
|
19
|
-
// flag with no value (-f, --force)
|
|
20
|
-
force: Flags.boolean({ char: 'f' }),
|
|
21
|
-
// flag with a value (-n, --name=VALUE)
|
|
22
|
-
name: Flags.string({ char: 'n', description: 'name to print' }),
|
|
23
|
-
};
|
|
24
|
-
async run() {
|
|
25
|
-
const { args, flags } = await this.parse(Restart);
|
|
26
|
-
const name = flags.name ?? 'world';
|
|
27
|
-
this.log(`hello ${name} from packages/core/cli/src/commands/restart.ts`);
|
|
28
|
-
if (args.file && flags.force) {
|
|
29
|
-
this.log(`you input --force and --file: ${args.file}`);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
@@ -1,431 +0,0 @@
|
|
|
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
|
-
}
|