@nocobase/ctl 0.1.5
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 +164 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +14 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +20 -0
- package/dist/commands/env/add.js +51 -0
- package/dist/commands/env/index.js +27 -0
- package/dist/commands/env/list.js +31 -0
- package/dist/commands/env/remove.js +54 -0
- package/dist/commands/env/update.js +54 -0
- package/dist/commands/env/use.js +26 -0
- package/dist/commands/resource/create.js +15 -0
- package/dist/commands/resource/destroy.js +15 -0
- package/dist/commands/resource/get.js +15 -0
- package/dist/commands/resource/index.js +7 -0
- package/dist/commands/resource/list.js +16 -0
- package/dist/commands/resource/query.js +15 -0
- package/dist/commands/resource/update.js +15 -0
- package/dist/generated/command-registry.js +81 -0
- package/dist/lib/api-client.js +196 -0
- package/dist/lib/auth-store.js +92 -0
- package/dist/lib/bootstrap.js +263 -0
- package/dist/lib/build-config.js +10 -0
- package/dist/lib/cli-home.js +30 -0
- package/dist/lib/generated-command.js +113 -0
- package/dist/lib/naming.js +70 -0
- package/dist/lib/openapi.js +254 -0
- package/dist/lib/post-processors.js +23 -0
- package/dist/lib/resource-command.js +331 -0
- package/dist/lib/resource-request.js +103 -0
- package/dist/lib/runtime-generator.js +383 -0
- package/dist/lib/runtime-store.js +56 -0
- package/dist/lib/ui.js +154 -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 +327 -0
- package/package.json +61 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { getCurrentEnvName, getEnv } from "./auth-store.js";
|
|
3
|
+
function normalizeBaseUrl(baseUrl) {
|
|
4
|
+
return baseUrl.replace(/\/+$/, '');
|
|
5
|
+
}
|
|
6
|
+
async function resolveServerRequestTarget(options) {
|
|
7
|
+
const envName = options.envName ?? (await getCurrentEnvName());
|
|
8
|
+
const env = await getEnv(envName);
|
|
9
|
+
const baseUrl = options.baseUrl ?? env?.baseUrl;
|
|
10
|
+
const token = options.token ?? env?.auth?.accessToken;
|
|
11
|
+
if (!baseUrl) {
|
|
12
|
+
throw new Error('Missing base URL. Use --base-url or configure one with `nocobase env add`.');
|
|
13
|
+
}
|
|
14
|
+
return { baseUrl, token };
|
|
15
|
+
}
|
|
16
|
+
async function parseResponse(response) {
|
|
17
|
+
const text = await response.text();
|
|
18
|
+
let data = text;
|
|
19
|
+
if (text) {
|
|
20
|
+
try {
|
|
21
|
+
data = JSON.parse(text);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
data = text;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
ok: response.ok,
|
|
29
|
+
status: response.status,
|
|
30
|
+
data,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function parseScalarValue(value, type) {
|
|
34
|
+
if (value === undefined) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
if (type === 'boolean') {
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
if (type === 'integer' || type === 'number') {
|
|
41
|
+
return Number(value);
|
|
42
|
+
}
|
|
43
|
+
if (typeof value !== 'string') {
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
const trimmed = value.trim();
|
|
47
|
+
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(trimmed);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
function hasParameterValue(flags, parameter) {
|
|
58
|
+
const value = flags[parameter.flagName];
|
|
59
|
+
if (parameter.type === 'boolean') {
|
|
60
|
+
return value !== undefined;
|
|
61
|
+
}
|
|
62
|
+
if (Array.isArray(value)) {
|
|
63
|
+
return value.length > 0;
|
|
64
|
+
}
|
|
65
|
+
return value !== undefined && value !== '';
|
|
66
|
+
}
|
|
67
|
+
async function parseBody(flags, operation) {
|
|
68
|
+
const inlineBody = flags.body;
|
|
69
|
+
const bodyFile = flags['body-file'];
|
|
70
|
+
const bodyParameters = operation.parameters.filter((parameter) => parameter.in === 'body');
|
|
71
|
+
const hasBodyFlags = bodyParameters.some((parameter) => hasParameterValue(flags, parameter));
|
|
72
|
+
if ((inlineBody || bodyFile) && hasBodyFlags) {
|
|
73
|
+
throw new Error('Use body field flags or --body/--body-file, not both.');
|
|
74
|
+
}
|
|
75
|
+
if (inlineBody) {
|
|
76
|
+
return JSON.parse(inlineBody);
|
|
77
|
+
}
|
|
78
|
+
if (bodyFile) {
|
|
79
|
+
return fs.readFile(bodyFile, 'utf8').then((content) => JSON.parse(content));
|
|
80
|
+
}
|
|
81
|
+
if (!bodyParameters.length) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
const body = {};
|
|
85
|
+
for (const parameter of bodyParameters) {
|
|
86
|
+
const rawValue = flags[parameter.flagName];
|
|
87
|
+
const value = parameter.isArray && !parameter.jsonEncoded
|
|
88
|
+
? (Array.isArray(rawValue) ? rawValue : rawValue ? [rawValue] : undefined)
|
|
89
|
+
: parseScalarValue(rawValue, parameter.type);
|
|
90
|
+
if (parameter.required && (value === undefined || value === '')) {
|
|
91
|
+
throw new Error(`Missing required body field --${parameter.flagName}`);
|
|
92
|
+
}
|
|
93
|
+
if (value === undefined) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
body[parameter.name] = value;
|
|
97
|
+
}
|
|
98
|
+
if (Object.keys(body).length > 0) {
|
|
99
|
+
return body;
|
|
100
|
+
}
|
|
101
|
+
if (operation.hasBody && operation.bodyRequired) {
|
|
102
|
+
throw new Error('Missing request body. Use body field flags or --body/--body-file.');
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
export async function executeApiRequest(options) {
|
|
107
|
+
const { baseUrl, token } = await resolveServerRequestTarget(options);
|
|
108
|
+
const headers = new Headers();
|
|
109
|
+
if (token) {
|
|
110
|
+
headers.set('authorization', `Bearer ${token}`);
|
|
111
|
+
}
|
|
112
|
+
const query = new URLSearchParams();
|
|
113
|
+
let requestPath = options.operation.pathTemplate;
|
|
114
|
+
for (const parameter of options.operation.parameters) {
|
|
115
|
+
if (parameter.in === 'body') {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const rawValue = options.flags[parameter.flagName];
|
|
119
|
+
const value = parameter.isArray
|
|
120
|
+
? (Array.isArray(rawValue) ? rawValue : rawValue ? [rawValue] : undefined)
|
|
121
|
+
: parseScalarValue(rawValue, parameter.type);
|
|
122
|
+
if (parameter.required && (value === undefined || value === '')) {
|
|
123
|
+
throw new Error(`Missing required parameter --${parameter.flagName}`);
|
|
124
|
+
}
|
|
125
|
+
if (value === undefined) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (parameter.in === 'path') {
|
|
129
|
+
requestPath = requestPath.replace(`{${parameter.name}}`, encodeURIComponent(String(value)));
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (parameter.in === 'query') {
|
|
133
|
+
if (Array.isArray(value)) {
|
|
134
|
+
value.forEach((item) => query.append(parameter.name, String(parseScalarValue(item, parameter.type))));
|
|
135
|
+
}
|
|
136
|
+
else if (typeof value === 'object') {
|
|
137
|
+
query.set(parameter.name, JSON.stringify(value));
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
query.set(parameter.name, String(value));
|
|
141
|
+
}
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (parameter.in === 'header') {
|
|
145
|
+
headers.set(parameter.name, typeof value === 'object' ? JSON.stringify(value) : String(value));
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const body = await parseBody(options.flags, options.operation);
|
|
150
|
+
if (body !== undefined) {
|
|
151
|
+
headers.set('content-type', 'application/json');
|
|
152
|
+
}
|
|
153
|
+
const url = new URL(`${normalizeBaseUrl(baseUrl)}${requestPath}`);
|
|
154
|
+
query.forEach((value, key) => url.searchParams.append(key, value));
|
|
155
|
+
const response = await fetch(url, {
|
|
156
|
+
method: options.operation.method.toUpperCase(),
|
|
157
|
+
headers,
|
|
158
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
159
|
+
});
|
|
160
|
+
return parseResponse(response);
|
|
161
|
+
}
|
|
162
|
+
export async function executeRawApiRequest(options) {
|
|
163
|
+
const { baseUrl, token } = await resolveServerRequestTarget(options);
|
|
164
|
+
const headers = new Headers();
|
|
165
|
+
if (token) {
|
|
166
|
+
headers.set('authorization', `Bearer ${token}`);
|
|
167
|
+
}
|
|
168
|
+
for (const [name, value] of Object.entries(options.headers ?? {})) {
|
|
169
|
+
if (value === undefined || value === null || value === '') {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
headers.set(name, typeof value === 'object' ? JSON.stringify(value) : String(value));
|
|
173
|
+
}
|
|
174
|
+
if (options.body !== undefined) {
|
|
175
|
+
headers.set('content-type', 'application/json');
|
|
176
|
+
}
|
|
177
|
+
const url = new URL(`${normalizeBaseUrl(baseUrl)}${options.path}`);
|
|
178
|
+
for (const [key, value] of Object.entries(options.query ?? {})) {
|
|
179
|
+
if (value === undefined) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (Array.isArray(value)) {
|
|
183
|
+
for (const item of value) {
|
|
184
|
+
url.searchParams.append(key, typeof item === 'object' ? JSON.stringify(item) : String(item));
|
|
185
|
+
}
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
url.searchParams.set(key, typeof value === 'object' ? JSON.stringify(value) : String(value));
|
|
189
|
+
}
|
|
190
|
+
const response = await fetch(url, {
|
|
191
|
+
method: options.method.toUpperCase(),
|
|
192
|
+
headers,
|
|
193
|
+
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
194
|
+
});
|
|
195
|
+
return parseResponse(response);
|
|
196
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { resolveCliHomeDir } from "./cli-home.js";
|
|
4
|
+
const DEFAULT_CONFIG = {
|
|
5
|
+
currentEnv: 'default',
|
|
6
|
+
envs: {},
|
|
7
|
+
};
|
|
8
|
+
function getConfigFile(options = {}) {
|
|
9
|
+
return path.join(resolveCliHomeDir(options.scope), 'config.json');
|
|
10
|
+
}
|
|
11
|
+
export async function loadAuthConfig(options = {}) {
|
|
12
|
+
try {
|
|
13
|
+
const content = await fs.readFile(getConfigFile(options), 'utf8');
|
|
14
|
+
const parsed = JSON.parse(content);
|
|
15
|
+
return {
|
|
16
|
+
currentEnv: parsed.currentEnv || 'default',
|
|
17
|
+
envs: parsed.envs || {},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
return DEFAULT_CONFIG;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export async function saveAuthConfig(config, options = {}) {
|
|
25
|
+
const filePath = getConfigFile(options);
|
|
26
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
27
|
+
await fs.writeFile(filePath, JSON.stringify(config, null, 2));
|
|
28
|
+
}
|
|
29
|
+
export async function listEnvs(options = {}) {
|
|
30
|
+
const config = await loadAuthConfig(options);
|
|
31
|
+
return {
|
|
32
|
+
currentEnv: config.currentEnv || 'default',
|
|
33
|
+
envs: config.envs,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export async function getCurrentEnvName(options = {}) {
|
|
37
|
+
const config = await loadAuthConfig(options);
|
|
38
|
+
return config.currentEnv || 'default';
|
|
39
|
+
}
|
|
40
|
+
export async function setCurrentEnv(envName, options = {}) {
|
|
41
|
+
const config = await loadAuthConfig(options);
|
|
42
|
+
if (!config.envs[envName]) {
|
|
43
|
+
throw new Error(`Env "${envName}" is not configured`);
|
|
44
|
+
}
|
|
45
|
+
config.currentEnv = envName;
|
|
46
|
+
await saveAuthConfig(config, options);
|
|
47
|
+
}
|
|
48
|
+
export async function getEnv(envName, options = {}) {
|
|
49
|
+
const config = await loadAuthConfig(options);
|
|
50
|
+
const resolved = envName || config.currentEnv || 'default';
|
|
51
|
+
return config.envs[resolved];
|
|
52
|
+
}
|
|
53
|
+
export async function upsertEnv(envName, baseUrl, accessToken, options = {}) {
|
|
54
|
+
const config = await loadAuthConfig(options);
|
|
55
|
+
config.envs[envName] = {
|
|
56
|
+
...(config.envs[envName] ?? {}),
|
|
57
|
+
baseUrl,
|
|
58
|
+
auth: {
|
|
59
|
+
type: 'token',
|
|
60
|
+
accessToken,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
config.currentEnv = envName;
|
|
64
|
+
await saveAuthConfig(config, options);
|
|
65
|
+
}
|
|
66
|
+
export async function setEnvRuntime(envName, runtime, options = {}) {
|
|
67
|
+
const config = await loadAuthConfig(options);
|
|
68
|
+
const current = config.envs[envName] ?? {};
|
|
69
|
+
config.envs[envName] = {
|
|
70
|
+
...current,
|
|
71
|
+
runtime,
|
|
72
|
+
};
|
|
73
|
+
config.currentEnv = envName;
|
|
74
|
+
await saveAuthConfig(config, options);
|
|
75
|
+
}
|
|
76
|
+
export async function removeEnv(envName, options = {}) {
|
|
77
|
+
const config = await loadAuthConfig(options);
|
|
78
|
+
if (!config.envs[envName]) {
|
|
79
|
+
throw new Error(`Env "${envName}" is not configured`);
|
|
80
|
+
}
|
|
81
|
+
delete config.envs[envName];
|
|
82
|
+
if (config.currentEnv === envName) {
|
|
83
|
+
const nextEnv = Object.keys(config.envs).sort()[0];
|
|
84
|
+
config.currentEnv = nextEnv ?? 'default';
|
|
85
|
+
}
|
|
86
|
+
await saveAuthConfig(config, options);
|
|
87
|
+
return {
|
|
88
|
+
removed: envName,
|
|
89
|
+
currentEnv: config.currentEnv || 'default',
|
|
90
|
+
hasEnvs: Object.keys(config.envs).length > 0,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { getCurrentEnvName, getEnv, setEnvRuntime } from "./auth-store.js";
|
|
2
|
+
import { generateRuntime } from "./runtime-generator.js";
|
|
3
|
+
import { hasRuntimeSync, saveRuntime } from "./runtime-store.js";
|
|
4
|
+
import { confirmAction, printInfo, printVerbose, setVerboseMode, stopTask, updateTask } from "./ui.js";
|
|
5
|
+
const APP_RETRY_INTERVAL = 2000;
|
|
6
|
+
const APP_RETRY_TIMEOUT = 120000;
|
|
7
|
+
function readFlag(argv, name) {
|
|
8
|
+
const exact = `--${name}`;
|
|
9
|
+
const prefix = `--${name}=`;
|
|
10
|
+
const alias = name === 'env' ? '-e' : name === 'scope' ? '-s' : undefined;
|
|
11
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
12
|
+
const value = argv[index];
|
|
13
|
+
if (value === exact) {
|
|
14
|
+
return argv[index + 1];
|
|
15
|
+
}
|
|
16
|
+
if (alias && value === alias) {
|
|
17
|
+
return argv[index + 1];
|
|
18
|
+
}
|
|
19
|
+
if (value.startsWith(prefix)) {
|
|
20
|
+
return value.slice(prefix.length);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
function hasBooleanFlag(argv, name) {
|
|
26
|
+
const exact = `--${name}`;
|
|
27
|
+
const negated = `--no-${name}`;
|
|
28
|
+
const prefix = `--${name}=`;
|
|
29
|
+
const alias = name === 'verbose' ? '-V' : undefined;
|
|
30
|
+
for (const value of argv) {
|
|
31
|
+
if (value === exact) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (alias && value === alias) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
if (value === negated) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
if (value.startsWith(prefix)) {
|
|
41
|
+
return value.slice(prefix.length) !== 'false';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
function getCommandToken(argv) {
|
|
47
|
+
for (const token of argv) {
|
|
48
|
+
if (!token || token.startsWith('-')) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
return token;
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
function hasHelpFlag(argv) {
|
|
56
|
+
return argv.includes('--help') || argv.includes('-h');
|
|
57
|
+
}
|
|
58
|
+
function hasVersionFlag(argv) {
|
|
59
|
+
return argv.includes('--version') || argv.includes('-v');
|
|
60
|
+
}
|
|
61
|
+
function isRootHelp(argv) {
|
|
62
|
+
const commandToken = getCommandToken(argv);
|
|
63
|
+
return !commandToken && hasHelpFlag(argv);
|
|
64
|
+
}
|
|
65
|
+
function isBuiltinCommand(argv) {
|
|
66
|
+
const commandToken = getCommandToken(argv);
|
|
67
|
+
return commandToken === 'env' || commandToken === 'resource';
|
|
68
|
+
}
|
|
69
|
+
async function requestJson(url, options) {
|
|
70
|
+
const headers = new Headers();
|
|
71
|
+
if (options.token) {
|
|
72
|
+
headers.set('authorization', `Bearer ${options.token}`);
|
|
73
|
+
}
|
|
74
|
+
let response;
|
|
75
|
+
try {
|
|
76
|
+
response = await fetch(url, {
|
|
77
|
+
method: options.method ?? 'GET',
|
|
78
|
+
headers,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
return {
|
|
83
|
+
status: 0,
|
|
84
|
+
ok: false,
|
|
85
|
+
data: {
|
|
86
|
+
error: {
|
|
87
|
+
message: error?.message ?? 'fetch failed',
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const text = await response.text();
|
|
93
|
+
let data = undefined;
|
|
94
|
+
if (text) {
|
|
95
|
+
try {
|
|
96
|
+
data = JSON.parse(text);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
data = text;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
status: response.status,
|
|
104
|
+
ok: response.ok,
|
|
105
|
+
data,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function sleep(ms) {
|
|
109
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
110
|
+
}
|
|
111
|
+
function isAppRestarting(response) {
|
|
112
|
+
return response.status === 503 && response.data?.error?.code === 'APP_COMMANDING';
|
|
113
|
+
}
|
|
114
|
+
function shouldRetryAppAvailability(response) {
|
|
115
|
+
return isAppRestarting(response) || response.status === 0;
|
|
116
|
+
}
|
|
117
|
+
function getSwaggerUrl(baseUrl) {
|
|
118
|
+
return `${baseUrl.replace(/\/+$/, '')}/swagger:get`;
|
|
119
|
+
}
|
|
120
|
+
function getHealthCheckUrl(baseUrl) {
|
|
121
|
+
return `${baseUrl.replace(/\/+$/, '')}/__health_check`;
|
|
122
|
+
}
|
|
123
|
+
async function waitForServiceReady(baseUrl, token) {
|
|
124
|
+
const healthCheckUrl = getHealthCheckUrl(baseUrl);
|
|
125
|
+
const startedAt = Date.now();
|
|
126
|
+
let notified = false;
|
|
127
|
+
while (Date.now() - startedAt < APP_RETRY_TIMEOUT) {
|
|
128
|
+
const response = await fetch(healthCheckUrl, {
|
|
129
|
+
method: 'GET',
|
|
130
|
+
headers: token ? { authorization: `Bearer ${token}` } : undefined,
|
|
131
|
+
}).catch((error) => {
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
status: 0,
|
|
135
|
+
text: async () => error?.message ?? 'fetch failed',
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
const text = await response.text();
|
|
139
|
+
if (response.ok && text.trim().toLowerCase() === 'ok') {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (!notified) {
|
|
143
|
+
printVerbose(`Waiting for health check: ${healthCheckUrl}`);
|
|
144
|
+
updateTask(`Waiting for application readiness (${healthCheckUrl})`);
|
|
145
|
+
notified = true;
|
|
146
|
+
}
|
|
147
|
+
await sleep(APP_RETRY_INTERVAL);
|
|
148
|
+
}
|
|
149
|
+
throw new Error(`The application did not become ready in time. Expected \`${healthCheckUrl}\` to respond with \`ok\`.`);
|
|
150
|
+
}
|
|
151
|
+
async function waitForSwaggerSchema(baseUrl, token) {
|
|
152
|
+
const swaggerUrl = getSwaggerUrl(baseUrl);
|
|
153
|
+
const startedAt = Date.now();
|
|
154
|
+
printVerbose(`Checking swagger schema: ${swaggerUrl}`);
|
|
155
|
+
while (Date.now() - startedAt < APP_RETRY_TIMEOUT) {
|
|
156
|
+
const response = await requestJson(swaggerUrl, { token });
|
|
157
|
+
if (response.ok) {
|
|
158
|
+
return response;
|
|
159
|
+
}
|
|
160
|
+
if (!shouldRetryAppAvailability(response)) {
|
|
161
|
+
return response;
|
|
162
|
+
}
|
|
163
|
+
await waitForServiceReady(baseUrl, token);
|
|
164
|
+
}
|
|
165
|
+
return await requestJson(swaggerUrl, { token });
|
|
166
|
+
}
|
|
167
|
+
async function confirmEnableApiDoc() {
|
|
168
|
+
return confirmAction('Enable the API documentation plugin now?', { defaultValue: false });
|
|
169
|
+
}
|
|
170
|
+
async function fetchSwaggerSchema(baseUrl, token) {
|
|
171
|
+
let response = await waitForSwaggerSchema(baseUrl, token);
|
|
172
|
+
if (response.status === 404) {
|
|
173
|
+
printInfo('The API documentation plugin is not enabled.');
|
|
174
|
+
const shouldEnable = await confirmEnableApiDoc();
|
|
175
|
+
if (!shouldEnable) {
|
|
176
|
+
throw new Error('`swagger:get` returned 404. Enable the `API documentation plugin` first.');
|
|
177
|
+
}
|
|
178
|
+
const enableUrl = `${baseUrl.replace(/\/+$/, '')}/pm:enable?filterByTk=api-doc`;
|
|
179
|
+
printVerbose(`Enabling API documentation plugin via ${enableUrl}`);
|
|
180
|
+
const enableResponse = await requestJson(enableUrl, { method: 'POST', token });
|
|
181
|
+
if (!enableResponse.ok) {
|
|
182
|
+
throw new Error(`Failed to enable the \`API documentation plugin\` via \`pm:enable\`.\n${JSON.stringify(enableResponse.data, null, 2)}`);
|
|
183
|
+
}
|
|
184
|
+
updateTask('Enabled the API documentation plugin. Waiting for application readiness...');
|
|
185
|
+
await waitForServiceReady(baseUrl, token);
|
|
186
|
+
response = await waitForSwaggerSchema(baseUrl, token);
|
|
187
|
+
}
|
|
188
|
+
if (!response.ok) {
|
|
189
|
+
throw new Error(`Failed to load swagger schema from \`swagger:get\`.\n${JSON.stringify(response.data, null, 2)}`);
|
|
190
|
+
}
|
|
191
|
+
return (response.data?.data ?? response.data);
|
|
192
|
+
}
|
|
193
|
+
export async function ensureRuntimeFromArgv(argv, options) {
|
|
194
|
+
const commandToken = getCommandToken(argv);
|
|
195
|
+
setVerboseMode(hasBooleanFlag(argv, 'verbose'));
|
|
196
|
+
if (hasVersionFlag(argv) || isBuiltinCommand(argv)) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const envName = readFlag(argv, 'env') ?? (await getCurrentEnvName());
|
|
200
|
+
const env = await getEnv(envName);
|
|
201
|
+
const baseUrl = readFlag(argv, 'base-url') ?? env?.baseUrl;
|
|
202
|
+
const token = readFlag(argv, 'token') ?? env?.auth?.accessToken;
|
|
203
|
+
const runtimeVersion = env?.runtime?.version;
|
|
204
|
+
if (!commandToken || isRootHelp(argv)) {
|
|
205
|
+
if (!baseUrl) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (runtimeVersion && hasRuntimeSync(runtimeVersion)) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (!baseUrl) {
|
|
213
|
+
throw new Error([
|
|
214
|
+
'No env is configured for runtime commands.',
|
|
215
|
+
'Run `nocobase env add --name <name> --base-url <url> --token <token>` first.',
|
|
216
|
+
'If you configure multiple environments later, switch with `nocobase env use <name>`.',
|
|
217
|
+
].join('\n'));
|
|
218
|
+
}
|
|
219
|
+
updateTask('Loading command runtime...');
|
|
220
|
+
try {
|
|
221
|
+
printVerbose(`Runtime source: ${baseUrl}`);
|
|
222
|
+
const document = await fetchSwaggerSchema(baseUrl, token);
|
|
223
|
+
const runtime = await generateRuntime(document, options.configFile, baseUrl);
|
|
224
|
+
await saveRuntime(runtime);
|
|
225
|
+
await setEnvRuntime(envName, {
|
|
226
|
+
version: runtime.version,
|
|
227
|
+
schemaHash: runtime.schemaHash,
|
|
228
|
+
generatedAt: runtime.generatedAt,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
finally {
|
|
232
|
+
stopTask();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
export async function updateEnvRuntime(options) {
|
|
236
|
+
setVerboseMode(Boolean(options.verbose));
|
|
237
|
+
const envName = options.envName ?? (await getCurrentEnvName({ scope: options.scope }));
|
|
238
|
+
const env = await getEnv(envName, { scope: options.scope });
|
|
239
|
+
const baseUrl = options.baseUrl ?? env?.baseUrl;
|
|
240
|
+
const token = options.token ?? env?.auth?.accessToken;
|
|
241
|
+
if (!baseUrl) {
|
|
242
|
+
throw new Error([
|
|
243
|
+
`Env "${envName}" is missing a base URL.`,
|
|
244
|
+
'Update it with `nocobase env add --name <name> --base-url <url>` first.',
|
|
245
|
+
].join('\n'));
|
|
246
|
+
}
|
|
247
|
+
updateTask('Loading command runtime...');
|
|
248
|
+
try {
|
|
249
|
+
printVerbose(`Runtime source: ${baseUrl}`);
|
|
250
|
+
const document = await fetchSwaggerSchema(baseUrl, token);
|
|
251
|
+
const runtime = await generateRuntime(document, options.configFile, baseUrl);
|
|
252
|
+
await saveRuntime(runtime, { scope: options.scope });
|
|
253
|
+
await setEnvRuntime(envName, {
|
|
254
|
+
version: runtime.version,
|
|
255
|
+
schemaHash: runtime.schemaHash,
|
|
256
|
+
generatedAt: runtime.generatedAt,
|
|
257
|
+
}, { scope: options.scope });
|
|
258
|
+
return runtime;
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
stopTask();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
export const CLI_HOME_DIRNAME = '.nocobase-ctl';
|
|
5
|
+
function resolveGlobalCliHomeRoot() {
|
|
6
|
+
if (process.env.NOCOBASE_CTL_HOME) {
|
|
7
|
+
return process.env.NOCOBASE_CTL_HOME;
|
|
8
|
+
}
|
|
9
|
+
return os.homedir();
|
|
10
|
+
}
|
|
11
|
+
export function resolveCliHomeRoot(scope = 'auto') {
|
|
12
|
+
const cwdRoot = process.cwd();
|
|
13
|
+
if (scope === 'project') {
|
|
14
|
+
return cwdRoot;
|
|
15
|
+
}
|
|
16
|
+
if (scope === 'global') {
|
|
17
|
+
return resolveGlobalCliHomeRoot();
|
|
18
|
+
}
|
|
19
|
+
const cwdCliHome = path.join(cwdRoot, CLI_HOME_DIRNAME);
|
|
20
|
+
if (fs.existsSync(cwdCliHome)) {
|
|
21
|
+
return cwdRoot;
|
|
22
|
+
}
|
|
23
|
+
return resolveGlobalCliHomeRoot();
|
|
24
|
+
}
|
|
25
|
+
export function resolveCliHomeDir(scope = 'auto') {
|
|
26
|
+
return path.join(resolveCliHomeRoot(scope), CLI_HOME_DIRNAME);
|
|
27
|
+
}
|
|
28
|
+
export function formatCliHomeScope(scope) {
|
|
29
|
+
return scope === 'project' ? 'project' : 'global';
|
|
30
|
+
}
|