@nocobase/cli 2.1.0-alpha.45 → 2.1.0-alpha.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +63 -380
- package/assets/env-proxy/nginx/app.conf.tpl +23 -0
- package/assets/env-proxy/nginx/nocobase.conf.tpl +5 -0
- package/assets/env-proxy/nginx/snippets/dist-location.conf +5 -0
- package/assets/env-proxy/nginx/snippets/gzip.conf +17 -0
- package/assets/env-proxy/nginx/snippets/log-format-http.conf +13 -0
- package/assets/env-proxy/nginx/snippets/maps-http.conf +14 -0
- package/assets/env-proxy/nginx/snippets/mime-types.conf +98 -0
- package/assets/env-proxy/nginx/snippets/proxy-location.conf +17 -0
- package/assets/env-proxy/nginx/snippets/spa-location.conf +6 -0
- package/assets/env-proxy/nginx/snippets/uploads-location.conf +21 -0
- package/bin/run.js +40 -32
- package/dist/commands/app/autostart/disable.js +55 -0
- package/dist/commands/app/autostart/enable.js +55 -0
- package/dist/commands/app/autostart/list.js +37 -0
- package/dist/commands/app/autostart/run.js +84 -0
- package/dist/commands/app/autostart/shared.js +49 -0
- package/dist/commands/app/destroy.js +21 -12
- package/dist/commands/app/down.js +2 -2
- package/dist/commands/app/logs.js +2 -1
- package/dist/commands/app/restart.js +92 -20
- package/dist/commands/app/shared.js +3 -2
- package/dist/commands/app/start.js +143 -38
- package/dist/commands/app/stop.js +31 -2
- package/dist/commands/app/upgrade.js +11 -7
- package/dist/commands/config/delete.js +10 -2
- package/dist/commands/config/get.js +7 -1
- package/dist/commands/config/set.js +12 -2
- package/dist/commands/db/ps.js +5 -18
- package/dist/commands/env/add.js +24 -44
- package/dist/commands/env/auth.js +1 -1
- package/dist/commands/env/info.js +3 -2
- package/dist/commands/env/list.js +4 -5
- package/dist/commands/env/proxy/caddy.js +28 -0
- package/dist/commands/env/proxy/index.js +353 -0
- package/dist/commands/env/proxy/nginx.js +28 -0
- package/dist/commands/env/remove.js +112 -22
- package/dist/commands/env/shared.js +17 -9
- package/dist/commands/env/status.js +10 -7
- package/dist/commands/env/update.js +385 -21
- package/dist/commands/init.js +368 -98
- package/dist/commands/install.js +271 -104
- package/dist/commands/license/activate.js +63 -244
- package/dist/commands/license/plugins/shared.js +64 -13
- package/dist/commands/plugin/import.js +108 -0
- package/dist/commands/revision/create.js +89 -0
- package/dist/commands/self/update.js +58 -1
- package/dist/commands/source/dev.js +5 -5
- package/dist/commands/source/test.js +13 -14
- package/dist/lib/app-health.js +39 -32
- package/dist/lib/app-managed-resources.js +25 -9
- package/dist/lib/app-public-path.js +80 -0
- package/dist/lib/app-runtime.js +12 -3
- package/dist/lib/auth-store.js +128 -4
- package/dist/lib/backup.js +3 -3
- package/dist/lib/bootstrap.js +14 -8
- package/dist/lib/cli-config.js +229 -3
- package/dist/lib/cli-entry-error.js +52 -0
- package/dist/lib/command-log.js +284 -0
- package/dist/lib/docker-env-file.js +10 -2
- package/dist/lib/docker-log-stream.js +45 -0
- package/dist/lib/env-auth.js +25 -14
- package/dist/lib/env-command-config.js +45 -0
- package/dist/lib/env-config.js +7 -2
- package/dist/lib/env-guard.js +9 -10
- package/dist/lib/env-paths.js +101 -0
- package/dist/lib/env-proxy.js +1295 -0
- package/dist/lib/inquirer.js +4 -5
- package/dist/lib/managed-env-file.js +98 -0
- package/dist/lib/plugin-import.js +279 -0
- package/dist/lib/prompt-catalog-core.js +3 -2
- package/dist/lib/prompt-validators.js +14 -0
- package/dist/lib/prompt-web-ui.js +8 -1
- package/dist/lib/run-npm.js +56 -12
- package/dist/lib/skills-manager.js +3 -1
- package/dist/lib/source-publish.js +22 -2
- package/dist/lib/startup-update.js +74 -50
- package/dist/lib/ui.js +2 -1
- package/dist/locale/en-US.json +122 -22
- package/dist/locale/zh-CN.json +119 -19
- package/package.json +5 -7
- package/scripts/build.mjs +34 -0
- package/scripts/clean.mjs +9 -0
- package/tsconfig.json +19 -0
- package/LICENSE.txt +0 -107
- package/README.zh-CN.md +0 -355
package/dist/lib/inquirer.js
CHANGED
|
@@ -103,7 +103,7 @@ export const input = createPrompt((config, done) => {
|
|
|
103
103
|
rl.write(config.default);
|
|
104
104
|
setValue(config.default);
|
|
105
105
|
}
|
|
106
|
-
}, []);
|
|
106
|
+
}, [config.default]);
|
|
107
107
|
useKeypress(async (key, rl) => {
|
|
108
108
|
if (status !== 'idle')
|
|
109
109
|
return;
|
|
@@ -232,11 +232,10 @@ export const password = createPrompt((config, done) => {
|
|
|
232
232
|
});
|
|
233
233
|
const message = theme.style.message(config.message, status);
|
|
234
234
|
const maskChar = resolveMaskChar(config.mask);
|
|
235
|
+
const transformedValue = value ? config.transformer?.(value) : undefined;
|
|
235
236
|
const displayValue = status === 'done'
|
|
236
|
-
? theme.style.answer(maskChar.repeat(value.length))
|
|
237
|
-
: maskChar
|
|
238
|
-
? maskChar.repeat(value.length)
|
|
239
|
-
: '';
|
|
237
|
+
? theme.style.answer(transformedValue ?? maskChar.repeat(value.length))
|
|
238
|
+
: transformedValue ?? (maskChar ? maskChar.repeat(value.length) : '');
|
|
240
239
|
const headerLine = [prefix, message].filter(Boolean).join(' ');
|
|
241
240
|
const inputLine = buildInputLine(displayValue);
|
|
242
241
|
const prompt = headerLine.endsWith('\n') ? `${headerLine}${inputLine}` : `${headerLine}\n${inputLine}`;
|
|
@@ -0,0 +1,98 @@
|
|
|
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 { readFile } from 'node:fs/promises';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { resolveConfiguredEnvPath } from './cli-home.js';
|
|
12
|
+
import { resolveDockerEnvFileArg } from "./docker-env-file.js";
|
|
13
|
+
import { resolveConfiguredAppPath } from './env-paths.js';
|
|
14
|
+
function trimValue(value) {
|
|
15
|
+
const text = String(value ?? '').trim();
|
|
16
|
+
return text || undefined;
|
|
17
|
+
}
|
|
18
|
+
function stripWrappingQuotes(value) {
|
|
19
|
+
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
20
|
+
return value
|
|
21
|
+
.slice(1, -1)
|
|
22
|
+
.replace(/\\n/g, '\n')
|
|
23
|
+
.replace(/\\r/g, '\r')
|
|
24
|
+
.replace(/\\t/g, '\t')
|
|
25
|
+
.replace(/\\"/g, '"')
|
|
26
|
+
.replace(/\\\\/g, '\\');
|
|
27
|
+
}
|
|
28
|
+
if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
|
|
29
|
+
return value.slice(1, -1);
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
export function parseSimpleEnvFile(content) {
|
|
34
|
+
const values = {};
|
|
35
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
36
|
+
const trimmed = rawLine.trim();
|
|
37
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const line = trimmed.startsWith('export ') ? trimmed.slice('export '.length).trim() : trimmed;
|
|
41
|
+
const separatorIndex = line.indexOf('=');
|
|
42
|
+
if (separatorIndex <= 0) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const key = line.slice(0, separatorIndex).trim();
|
|
46
|
+
if (!key) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
values[key] = stripWrappingQuotes(line.slice(separatorIndex + 1).trim());
|
|
50
|
+
}
|
|
51
|
+
return values;
|
|
52
|
+
}
|
|
53
|
+
export function resolveManagedLocalEnvFilePath(runtime) {
|
|
54
|
+
const config = runtime.env.config ?? {};
|
|
55
|
+
const explicitEnvFile = trimValue(config.envFile);
|
|
56
|
+
if (explicitEnvFile) {
|
|
57
|
+
return resolveConfiguredEnvPath(explicitEnvFile) ?? explicitEnvFile;
|
|
58
|
+
}
|
|
59
|
+
const configuredAppPath = resolveConfiguredAppPath(config);
|
|
60
|
+
if (configuredAppPath) {
|
|
61
|
+
return path.join(configuredAppPath, '.env');
|
|
62
|
+
}
|
|
63
|
+
if (path.basename(runtime.projectRoot) === 'source') {
|
|
64
|
+
return path.resolve(runtime.projectRoot, '..', '.env');
|
|
65
|
+
}
|
|
66
|
+
return path.join(runtime.projectRoot, '.env');
|
|
67
|
+
}
|
|
68
|
+
export async function resolveManagedRuntimeEnvFilePath(runtime) {
|
|
69
|
+
if (runtime.kind === 'local') {
|
|
70
|
+
return resolveManagedLocalEnvFilePath(runtime);
|
|
71
|
+
}
|
|
72
|
+
return await resolveDockerEnvFileArg(runtime.envName, runtime.env.config ?? {});
|
|
73
|
+
}
|
|
74
|
+
export async function readManagedRuntimeEnvValues(runtime) {
|
|
75
|
+
const envFilePath = await resolveManagedRuntimeEnvFilePath(runtime);
|
|
76
|
+
if (!envFilePath) {
|
|
77
|
+
return {
|
|
78
|
+
envFilePath: undefined,
|
|
79
|
+
envValues: {},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
return {
|
|
84
|
+
envFilePath,
|
|
85
|
+
envValues: parseSimpleEnvFile(await readFile(envFilePath, 'utf8')),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : '';
|
|
90
|
+
if (code === 'ENOENT') {
|
|
91
|
+
return {
|
|
92
|
+
envFilePath,
|
|
93
|
+
envValues: {},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
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 fs from 'node:fs';
|
|
10
|
+
import fsp from 'node:fs/promises';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { Readable } from 'node:stream';
|
|
14
|
+
import { pipeline } from 'node:stream/promises';
|
|
15
|
+
import { createGunzip } from 'node:zlib';
|
|
16
|
+
import * as tar from 'tar';
|
|
17
|
+
import { fetchWithPreservedAuthRedirect } from './http-request.js';
|
|
18
|
+
import { resolvePluginStoragePath } from './plugin-storage.js';
|
|
19
|
+
import { run } from './run-npm.js';
|
|
20
|
+
const NPM_PACK_TIMEOUT_MS = 30_000;
|
|
21
|
+
const NPM_AUTH_ERROR_PATTERNS = [
|
|
22
|
+
'e401',
|
|
23
|
+
'e403',
|
|
24
|
+
'unauthorized',
|
|
25
|
+
'authorization',
|
|
26
|
+
'forbidden',
|
|
27
|
+
'need auth',
|
|
28
|
+
'requires authentication',
|
|
29
|
+
'unable to authenticate',
|
|
30
|
+
'authentication token',
|
|
31
|
+
];
|
|
32
|
+
const NPM_NOT_FOUND_ERROR_PATTERNS = [
|
|
33
|
+
'e404',
|
|
34
|
+
'etarget',
|
|
35
|
+
'not found',
|
|
36
|
+
'not in this registry',
|
|
37
|
+
'no matching version found',
|
|
38
|
+
'no match found',
|
|
39
|
+
];
|
|
40
|
+
const NPM_NETWORK_ERROR_PATTERNS = [
|
|
41
|
+
'enotfound',
|
|
42
|
+
'eai_again',
|
|
43
|
+
'etimedout',
|
|
44
|
+
'esockettimedout',
|
|
45
|
+
'econnreset',
|
|
46
|
+
'econnrefused',
|
|
47
|
+
'enetunreach',
|
|
48
|
+
'ehostunreach',
|
|
49
|
+
'fetch failed',
|
|
50
|
+
'getaddrinfo',
|
|
51
|
+
'timed out',
|
|
52
|
+
];
|
|
53
|
+
function trimValue(value) {
|
|
54
|
+
const text = String(value ?? '').trim();
|
|
55
|
+
return text || undefined;
|
|
56
|
+
}
|
|
57
|
+
async function pathExists(target) {
|
|
58
|
+
try {
|
|
59
|
+
await fsp.access(target);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function isHttpArchiveSource(value) {
|
|
67
|
+
try {
|
|
68
|
+
const url = new URL(value);
|
|
69
|
+
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function responseBodyToNodeReadable(body) {
|
|
76
|
+
return Readable.fromWeb(body);
|
|
77
|
+
}
|
|
78
|
+
function looksLikeLocalArchivePath(value) {
|
|
79
|
+
return (path.isAbsolute(value) ||
|
|
80
|
+
value.startsWith('./') ||
|
|
81
|
+
value.startsWith('../') ||
|
|
82
|
+
value.endsWith('.tgz') ||
|
|
83
|
+
value.endsWith('.tar.gz'));
|
|
84
|
+
}
|
|
85
|
+
function resolvePluginOutputDir(storagePluginsPath, packageName) {
|
|
86
|
+
const outputDir = path.resolve(storagePluginsPath, packageName);
|
|
87
|
+
const relative = path.relative(storagePluginsPath, outputDir);
|
|
88
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
89
|
+
throw new Error(`Imported archive package name "${packageName}" resolves outside plugin storage.`);
|
|
90
|
+
}
|
|
91
|
+
return outputDir;
|
|
92
|
+
}
|
|
93
|
+
function normalizeNpmRegistry(value) {
|
|
94
|
+
const text = trimValue(value);
|
|
95
|
+
return text ? text.replace(/\/+$/, '') : undefined;
|
|
96
|
+
}
|
|
97
|
+
function formatNpmPackFailureMessage(source, registry, details) {
|
|
98
|
+
const normalizedDetails = details.toLowerCase();
|
|
99
|
+
const lines = [
|
|
100
|
+
`Failed to import npm package "${source}" with \`npm pack\`.`,
|
|
101
|
+
details,
|
|
102
|
+
];
|
|
103
|
+
const loginCommand = registry ? `npm login --registry=${registry}` : 'npm login';
|
|
104
|
+
if (NPM_AUTH_ERROR_PATTERNS.some((pattern) => normalizedDetails.includes(pattern))) {
|
|
105
|
+
lines.push(`Hint: If this is a private registry, run \`${loginCommand}\` first and retry.`);
|
|
106
|
+
return lines.join('\n');
|
|
107
|
+
}
|
|
108
|
+
if (NPM_NOT_FOUND_ERROR_PATTERNS.some((pattern) => normalizedDetails.includes(pattern))) {
|
|
109
|
+
lines.push(registry
|
|
110
|
+
? `Hint: Check that "${source}" exists in ${registry} and that the package name or tag is correct.`
|
|
111
|
+
: `Hint: Check that "${source}" exists and that the package name or tag is correct.`);
|
|
112
|
+
return lines.join('\n');
|
|
113
|
+
}
|
|
114
|
+
if (NPM_NETWORK_ERROR_PATTERNS.some((pattern) => normalizedDetails.includes(pattern))) {
|
|
115
|
+
lines.push(registry
|
|
116
|
+
? `Hint: Check that the npm registry ${registry} is reachable from this machine.`
|
|
117
|
+
: 'Hint: Check that the npm registry is reachable from this machine.');
|
|
118
|
+
return lines.join('\n');
|
|
119
|
+
}
|
|
120
|
+
if (registry) {
|
|
121
|
+
lines.push(`Hint: If this is a private registry, make sure \`${loginCommand}\` has completed successfully.`);
|
|
122
|
+
}
|
|
123
|
+
return lines.join('\n');
|
|
124
|
+
}
|
|
125
|
+
async function resolvePackedPluginTarball(packRoot, sourceLabel) {
|
|
126
|
+
const entries = await fsp.readdir(packRoot, { withFileTypes: true });
|
|
127
|
+
const tarballs = entries
|
|
128
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.tgz'))
|
|
129
|
+
.map((entry) => path.join(packRoot, entry.name))
|
|
130
|
+
.sort();
|
|
131
|
+
if (tarballs.length === 1) {
|
|
132
|
+
return tarballs[0];
|
|
133
|
+
}
|
|
134
|
+
if (tarballs.length === 0) {
|
|
135
|
+
throw new Error(`npm pack did not produce a local tarball for ${sourceLabel}.`);
|
|
136
|
+
}
|
|
137
|
+
throw new Error(`npm pack produced multiple tarballs for ${sourceLabel}.`);
|
|
138
|
+
}
|
|
139
|
+
async function packNpmPluginSource(source, npmRegistry, runFn = run) {
|
|
140
|
+
const packRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'nocobase-cli-plugin-pack-'));
|
|
141
|
+
const args = ['pack', '--silent'];
|
|
142
|
+
const registry = normalizeNpmRegistry(npmRegistry);
|
|
143
|
+
if (registry) {
|
|
144
|
+
args.push(`--registry=${registry}`);
|
|
145
|
+
}
|
|
146
|
+
args.push(source);
|
|
147
|
+
let stdout = '';
|
|
148
|
+
let stderr = '';
|
|
149
|
+
try {
|
|
150
|
+
await runFn('npm', args, {
|
|
151
|
+
cwd: packRoot,
|
|
152
|
+
stdio: 'pipe',
|
|
153
|
+
errorName: 'npm pack',
|
|
154
|
+
timeoutMs: NPM_PACK_TIMEOUT_MS,
|
|
155
|
+
onStdout: (chunk) => {
|
|
156
|
+
stdout += chunk;
|
|
157
|
+
},
|
|
158
|
+
onStderr: (chunk) => {
|
|
159
|
+
stderr += chunk;
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
const tarballPath = await resolvePackedPluginTarball(packRoot, source);
|
|
163
|
+
return {
|
|
164
|
+
stream: fs.createReadStream(tarballPath),
|
|
165
|
+
source,
|
|
166
|
+
sourceType: 'npm',
|
|
167
|
+
cleanup: async () => {
|
|
168
|
+
await fsp.rm(packRoot, { recursive: true, force: true });
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
await fsp.rm(packRoot, { recursive: true, force: true });
|
|
174
|
+
const originalMessage = error instanceof Error ? error.message : String(error);
|
|
175
|
+
const details = trimValue(stderr) || trimValue(stdout) || trimValue(originalMessage) || 'npm pack failed.';
|
|
176
|
+
throw new Error(formatNpmPackFailureMessage(source, registry, details));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async function openPluginSource(source, npmRegistry, runFn = run) {
|
|
180
|
+
if (isHttpArchiveSource(source)) {
|
|
181
|
+
const response = await fetchWithPreservedAuthRedirect(source);
|
|
182
|
+
if (!response.ok) {
|
|
183
|
+
const statusText = trimValue(response.statusText);
|
|
184
|
+
throw new Error(`Failed to download plugin archive from ${source}: ${response.status}${statusText ? ` ${statusText}` : ''}.`);
|
|
185
|
+
}
|
|
186
|
+
if (!response.body) {
|
|
187
|
+
throw new Error(`Downloaded plugin archive from ${source} does not include a response body.`);
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
stream: responseBodyToNodeReadable(response.body),
|
|
191
|
+
source,
|
|
192
|
+
sourceType: 'url',
|
|
193
|
+
cleanup: async () => undefined,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const resolvedPath = path.resolve(process.cwd(), source);
|
|
197
|
+
if (await pathExists(resolvedPath)) {
|
|
198
|
+
return {
|
|
199
|
+
stream: fs.createReadStream(resolvedPath),
|
|
200
|
+
source: resolvedPath,
|
|
201
|
+
sourceType: 'file',
|
|
202
|
+
cleanup: async () => undefined,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (looksLikeLocalArchivePath(source)) {
|
|
206
|
+
throw new Error(`Plugin archive file does not exist: ${resolvedPath}`);
|
|
207
|
+
}
|
|
208
|
+
return await packNpmPluginSource(source, npmRegistry, runFn);
|
|
209
|
+
}
|
|
210
|
+
async function readPluginMetadata(extractRoot, sourceLabel) {
|
|
211
|
+
const packageJsonPath = path.join(extractRoot, 'package.json');
|
|
212
|
+
let content;
|
|
213
|
+
try {
|
|
214
|
+
content = await fsp.readFile(packageJsonPath, 'utf8');
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
throw new Error(`Imported archive from ${sourceLabel} does not contain a package.json at the package root.`);
|
|
218
|
+
}
|
|
219
|
+
let metadata;
|
|
220
|
+
try {
|
|
221
|
+
metadata = JSON.parse(content);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
throw new Error(`Imported archive from ${sourceLabel} contains an invalid package.json.`);
|
|
225
|
+
}
|
|
226
|
+
const packageName = trimValue(metadata.name);
|
|
227
|
+
if (!packageName) {
|
|
228
|
+
throw new Error(`Imported archive from ${sourceLabel} is missing "package.json.name".`);
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
packageName,
|
|
232
|
+
packageVersion: trimValue(metadata.version),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
export async function importPluginSource(source, options = {}) {
|
|
236
|
+
const normalizedSource = trimValue(source);
|
|
237
|
+
if (!normalizedSource) {
|
|
238
|
+
throw new Error('Pass a plugin archive path, URL, or npm package spec.');
|
|
239
|
+
}
|
|
240
|
+
const storagePluginsPath = resolvePluginStoragePath(options.storagePath);
|
|
241
|
+
await fsp.mkdir(storagePluginsPath, { recursive: true });
|
|
242
|
+
const archive = await openPluginSource(normalizedSource, options.npmRegistry, options.runFn);
|
|
243
|
+
const stageDir = await fsp.mkdtemp(path.join(storagePluginsPath, '.nb-plugin-import-'));
|
|
244
|
+
let stageMoved = false;
|
|
245
|
+
try {
|
|
246
|
+
try {
|
|
247
|
+
await pipeline(archive.stream, createGunzip(), tar.extract({ cwd: stageDir, strip: 1 }));
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
251
|
+
throw new Error(`Failed to extract plugin archive from ${archive.source}: ${message}`);
|
|
252
|
+
}
|
|
253
|
+
const { packageName, packageVersion } = await readPluginMetadata(stageDir, archive.source);
|
|
254
|
+
const outputDir = resolvePluginOutputDir(storagePluginsPath, packageName);
|
|
255
|
+
const action = (await pathExists(outputDir)) ? 'updated' : 'installed';
|
|
256
|
+
await fsp.mkdir(path.dirname(outputDir), { recursive: true });
|
|
257
|
+
await fsp.rm(outputDir, { recursive: true, force: true });
|
|
258
|
+
await fsp.rename(stageDir, outputDir);
|
|
259
|
+
stageMoved = true;
|
|
260
|
+
return {
|
|
261
|
+
action,
|
|
262
|
+
packageName,
|
|
263
|
+
packageVersion,
|
|
264
|
+
outputDir,
|
|
265
|
+
source: archive.source,
|
|
266
|
+
sourceType: archive.sourceType,
|
|
267
|
+
storagePluginsPath,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
if (!stageMoved) {
|
|
272
|
+
await fsp.rm(stageDir, { recursive: true, force: true });
|
|
273
|
+
}
|
|
274
|
+
await archive.cleanup();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
export async function importPluginArchive(source, storagePath) {
|
|
278
|
+
return await importPluginSource(source, { storagePath });
|
|
279
|
+
}
|
|
@@ -88,14 +88,15 @@ export function mergedInteger(key, def, iv, useYesInitial) {
|
|
|
88
88
|
}
|
|
89
89
|
return def.initialValue;
|
|
90
90
|
}
|
|
91
|
-
export function mergedPassword(key, def, iv, useYesInitial) {
|
|
91
|
+
export function mergedPassword(key, def, iv, useYesInitial, valuesSoFar = {}) {
|
|
92
92
|
if (hasIvKey(iv, key)) {
|
|
93
93
|
return String(iv[key] ?? '');
|
|
94
94
|
}
|
|
95
95
|
if (useYesInitial && def.yesInitialValue !== undefined) {
|
|
96
96
|
return def.yesInitialValue;
|
|
97
97
|
}
|
|
98
|
-
|
|
98
|
+
const initialValue = def.initialValue;
|
|
99
|
+
return typeof initialValue === 'function' ? initialValue(valuesSoFar) : initialValue;
|
|
99
100
|
}
|
|
100
101
|
export function isBlankText(value) {
|
|
101
102
|
return value.trim() === '';
|
|
@@ -11,6 +11,7 @@ import net from 'node:net';
|
|
|
11
11
|
import { translateCli } from "./cli-locale.js";
|
|
12
12
|
const API_BASE_URL_EXAMPLE = 'http://localhost:13000/api';
|
|
13
13
|
const ENV_KEY_PATTERN = /^[A-Za-z0-9]+$/;
|
|
14
|
+
const APP_PUBLIC_PATH_PATTERN = /^\/(?:[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*)?\/?$/;
|
|
14
15
|
const TCP_PORT_EXAMPLE = '13000';
|
|
15
16
|
const API_BASE_URL_REQUEST_TIMEOUT_MS = 5_000;
|
|
16
17
|
const TCP_PORT_PROBE_HOSTS = ['127.0.0.1', '0.0.0.0', '::1'];
|
|
@@ -95,6 +96,19 @@ export function validateEnvKey(value) {
|
|
|
95
96
|
}
|
|
96
97
|
return undefined;
|
|
97
98
|
}
|
|
99
|
+
export function validateAppPublicPath(value) {
|
|
100
|
+
const raw = String(value ?? '').trim();
|
|
101
|
+
if (raw === '') {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
if (!raw.startsWith('/')) {
|
|
105
|
+
return translateCli('validators.appPublicPath.invalid');
|
|
106
|
+
}
|
|
107
|
+
if (!APP_PUBLIC_PATH_PATTERN.test(raw)) {
|
|
108
|
+
return translateCli('validators.appPublicPath.invalid');
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
98
112
|
function parseTcpPort(value) {
|
|
99
113
|
const raw = String(value ?? '').trim();
|
|
100
114
|
if (raw === '') {
|
|
@@ -33,6 +33,13 @@ function resolveTextDefault(def, out) {
|
|
|
33
33
|
}
|
|
34
34
|
return String(iv ?? '');
|
|
35
35
|
}
|
|
36
|
+
function resolvePasswordDefault(def, out) {
|
|
37
|
+
const iv = def.initialValue;
|
|
38
|
+
if (typeof iv === 'function') {
|
|
39
|
+
return String(iv(out) ?? '');
|
|
40
|
+
}
|
|
41
|
+
return String(iv ?? '');
|
|
42
|
+
}
|
|
36
43
|
function isInputBlock(def) {
|
|
37
44
|
return (def.type === 'text' ||
|
|
38
45
|
def.type === 'boolean' ||
|
|
@@ -89,7 +96,7 @@ function defaultValueForInput(key, def, out) {
|
|
|
89
96
|
return firstValue ?? '';
|
|
90
97
|
}
|
|
91
98
|
case 'password':
|
|
92
|
-
return def
|
|
99
|
+
return resolvePasswordDefault(def, out);
|
|
93
100
|
case 'integer':
|
|
94
101
|
return def.initialValue !== undefined && Number.isFinite(def.initialValue) ? def.initialValue : 0;
|
|
95
102
|
default:
|
package/dist/lib/run-npm.js
CHANGED
|
@@ -28,18 +28,35 @@ const MISSING_COMMAND_SPECS = {
|
|
|
28
28
|
displayName: 'Docker',
|
|
29
29
|
configKey: 'bin.docker',
|
|
30
30
|
},
|
|
31
|
+
caddy: {
|
|
32
|
+
displayName: 'Caddy',
|
|
33
|
+
configKey: 'bin.caddy',
|
|
34
|
+
},
|
|
31
35
|
git: {
|
|
32
36
|
displayName: 'Git',
|
|
33
37
|
configKey: 'bin.git',
|
|
34
38
|
},
|
|
39
|
+
nginx: {
|
|
40
|
+
displayName: 'Nginx',
|
|
41
|
+
configKey: 'bin.nginx',
|
|
42
|
+
},
|
|
35
43
|
yarn: {
|
|
36
44
|
displayName: 'Yarn',
|
|
37
45
|
configKey: 'bin.yarn',
|
|
38
46
|
},
|
|
39
47
|
};
|
|
48
|
+
const DOCKER_DAEMON_UNAVAILABLE_PATTERNS = [
|
|
49
|
+
/cannot connect to the docker daemon/i,
|
|
50
|
+
/is the docker daemon running\?/i,
|
|
51
|
+
/error during connect/i,
|
|
52
|
+
/docker daemon is not running/i,
|
|
53
|
+
];
|
|
40
54
|
async function resolveCommandName(name) {
|
|
41
55
|
return await resolveConfiguredCommandName(name);
|
|
42
56
|
}
|
|
57
|
+
function shouldTeeInheritedOutput(options) {
|
|
58
|
+
return options?.stdio === 'inherit' && Boolean(String(process.env.NB_CLI_ACTIVE_LOG_FILE ?? '').trim());
|
|
59
|
+
}
|
|
43
60
|
function createMissingCommandError(name, label, error) {
|
|
44
61
|
const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : undefined;
|
|
45
62
|
if (code !== 'ENOENT') {
|
|
@@ -53,6 +70,17 @@ function createMissingCommandError(name, label, error) {
|
|
|
53
70
|
fallback: `Couldn't run \`${label}\` because the ${spec.displayName} executable could not be found. Install ${spec.displayName} or update \`nb config set ${spec.configKey} <path>\` and try again.`,
|
|
54
71
|
}));
|
|
55
72
|
}
|
|
73
|
+
function isDockerDaemonUnavailableError(error) {
|
|
74
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
75
|
+
return DOCKER_DAEMON_UNAVAILABLE_PATTERNS.some((pattern) => pattern.test(message));
|
|
76
|
+
}
|
|
77
|
+
function createDockerDaemonUnavailableError(action, error) {
|
|
78
|
+
const details = error instanceof Error ? error.message : String(error);
|
|
79
|
+
return new Error(translateCli('commands.shared.dockerDaemonUnavailable', { action, details }, {
|
|
80
|
+
fallback: `Couldn't run \`${action}\` because Docker is installed but the Docker daemon is not running.` +
|
|
81
|
+
` Start Docker Desktop or the Docker daemon service, then try again. Details: ${details}`,
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
56
84
|
function pathExists(candidate) {
|
|
57
85
|
try {
|
|
58
86
|
return Boolean(candidate) && fs.statSync(candidate) !== undefined;
|
|
@@ -107,9 +135,12 @@ export async function run(name, args, options) {
|
|
|
107
135
|
const cwd = resolveCwd(options?.cwd);
|
|
108
136
|
const label = options?.errorName ?? name;
|
|
109
137
|
const command = await resolveCommandName(name);
|
|
138
|
+
const stdio = shouldTeeInheritedOutput(options)
|
|
139
|
+
? ['inherit', 'pipe', 'pipe']
|
|
140
|
+
: (options?.stdio ?? 'inherit');
|
|
110
141
|
return await new Promise((resolve, reject) => {
|
|
111
142
|
const child = spawn(command, [...args], {
|
|
112
|
-
stdio
|
|
143
|
+
stdio,
|
|
113
144
|
cwd,
|
|
114
145
|
env: {
|
|
115
146
|
...process.env,
|
|
@@ -117,19 +148,21 @@ export async function run(name, args, options) {
|
|
|
117
148
|
},
|
|
118
149
|
windowsHide: process.platform === 'win32',
|
|
119
150
|
});
|
|
120
|
-
if (options?.stdio === 'pipe') {
|
|
151
|
+
if (options?.stdio === 'pipe' || shouldTeeInheritedOutput(options)) {
|
|
121
152
|
child.stdout?.setEncoding('utf8');
|
|
122
153
|
child.stderr?.setEncoding('utf8');
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
154
|
+
child.stdout?.on('data', (chunk) => {
|
|
155
|
+
if (shouldTeeInheritedOutput(options)) {
|
|
156
|
+
process.stdout.write(chunk);
|
|
157
|
+
}
|
|
158
|
+
options.onStdout?.(String(chunk));
|
|
159
|
+
});
|
|
160
|
+
child.stderr?.on('data', (chunk) => {
|
|
161
|
+
if (shouldTeeInheritedOutput(options)) {
|
|
162
|
+
process.stderr.write(chunk);
|
|
163
|
+
}
|
|
164
|
+
options.onStderr?.(String(chunk));
|
|
165
|
+
});
|
|
133
166
|
}
|
|
134
167
|
const cleanupSignalForwarding = forwardSignalsToChild(child);
|
|
135
168
|
const timeoutController = attachProcessTimeout(child, options?.timeoutMs);
|
|
@@ -370,6 +403,17 @@ export async function commandOutputViaFile(name, args, options) {
|
|
|
370
403
|
export function runNpm(args, options) {
|
|
371
404
|
return run('yarn', [...args], { ...options, errorName: 'npm' });
|
|
372
405
|
}
|
|
406
|
+
export async function ensureDockerDaemonRunning(action = 'use Docker') {
|
|
407
|
+
try {
|
|
408
|
+
await commandOutput('docker', ['info'], { errorName: 'docker info', timeoutMs: 5000 });
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
if (isDockerDaemonUnavailableError(error)) {
|
|
412
|
+
throw createDockerDaemonUnavailableError(action, error);
|
|
413
|
+
}
|
|
414
|
+
throw error;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
373
417
|
export function runNocoBaseCommand(args, options) {
|
|
374
418
|
const cwd = resolveProjectCwd(options?.cwd);
|
|
375
419
|
const localBin = path.join(cwd, 'node_modules', '.bin');
|
|
@@ -17,7 +17,9 @@ import { commandOutput, commandOutputViaFile, run } from './run-npm.js';
|
|
|
17
17
|
export const NOCOBASE_SKILLS_SOURCE = 'nocobase/skills';
|
|
18
18
|
export const NOCOBASE_SKILLS_PACKAGE_NAME = '@nocobase/skills';
|
|
19
19
|
const NOCOBASE_SKILLS_NAME_PREFIX = 'nocobase-';
|
|
20
|
-
|
|
20
|
+
// `npx skills list` may take several seconds on cold starts while `npx`
|
|
21
|
+
// resolves and boots the package, even when the local skills installation is healthy.
|
|
22
|
+
const SKILLS_LIST_TIMEOUT_MS = 15000;
|
|
21
23
|
const SKILLS_NPM_VIEW_TIMEOUT_MS = 3000;
|
|
22
24
|
const SKILLS_PACK_TIMEOUT_MS = 30000;
|
|
23
25
|
const SKILLS_ADD_TIMEOUT_MS = 20000;
|
|
@@ -15,6 +15,9 @@ function trimValue(value) {
|
|
|
15
15
|
function sanitizeEnvSegment(value) {
|
|
16
16
|
return trimValue(value).replace(/[^A-Za-z0-9]+/g, '').slice(0, 16);
|
|
17
17
|
}
|
|
18
|
+
function normalizeSnapshotBaseVersion(baseVersion) {
|
|
19
|
+
return trimValue(baseVersion).replace(/(?:-snapshot\.\d{8}\.[A-Za-z0-9]+)+$/, '');
|
|
20
|
+
}
|
|
18
21
|
export async function resolveSourcePublishRegistry(explicitRegistry) {
|
|
19
22
|
const normalized = trimValue(explicitRegistry);
|
|
20
23
|
if (normalized) {
|
|
@@ -50,7 +53,7 @@ export function buildSnapshotVersion(baseVersion, gitSha, now = new Date()) {
|
|
|
50
53
|
const yyyy = String(now.getFullYear()).padStart(4, '0');
|
|
51
54
|
const mm = String(now.getMonth() + 1).padStart(2, '0');
|
|
52
55
|
const dd = String(now.getDate()).padStart(2, '0');
|
|
53
|
-
return `${baseVersion}-snapshot.${yyyy}${mm}${dd}.${gitSha}`;
|
|
56
|
+
return `${normalizeSnapshotBaseVersion(baseVersion)}-snapshot.${yyyy}${mm}${dd}.${gitSha}`;
|
|
54
57
|
}
|
|
55
58
|
export async function resolveGitBranch(cwd) {
|
|
56
59
|
const branch = trimValue(await commandOutput('git', ['branch', '--show-current'], {
|
|
@@ -60,6 +63,20 @@ export async function resolveGitBranch(cwd) {
|
|
|
60
63
|
if (!branch) {
|
|
61
64
|
throw new Error('`nb source publish --snapshot` requires a named Git branch. Detached HEAD is not supported.');
|
|
62
65
|
}
|
|
66
|
+
const branchRef = `refs/heads/${branch}`;
|
|
67
|
+
try {
|
|
68
|
+
await commandOutput('git', ['rev-parse', '--verify', branchRef], {
|
|
69
|
+
cwd,
|
|
70
|
+
errorName: `git rev-parse --verify ${branchRef}`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
throw new Error([
|
|
75
|
+
`The current Git branch reference is invalid: ${branch}.`,
|
|
76
|
+
'This usually means a previous source publish cleanup left HEAD pointing at a missing temporary branch.',
|
|
77
|
+
'Switch to a real local branch, then run `nb source publish --snapshot` again.',
|
|
78
|
+
].join('\n'), { cause: error });
|
|
79
|
+
}
|
|
63
80
|
return branch;
|
|
64
81
|
}
|
|
65
82
|
export async function hasLocalGitChanges(cwd) {
|
|
@@ -292,6 +309,9 @@ export async function publishSourceSnapshot(params) {
|
|
|
292
309
|
if (publishError) {
|
|
293
310
|
throw publishError;
|
|
294
311
|
}
|
|
312
|
+
if (!result) {
|
|
313
|
+
throw new Error('Source snapshot publishing finished without a result.');
|
|
314
|
+
}
|
|
295
315
|
return result;
|
|
296
316
|
}
|
|
297
317
|
export function buildSuggestedInitCommand(result) {
|
|
@@ -299,7 +319,7 @@ export function buildSuggestedInitCommand(result) {
|
|
|
299
319
|
const normalizedRegistry = result.npmRegistry || `http://${host}:${port || DEFAULT_SOURCE_REGISTRY_PORT}`;
|
|
300
320
|
const suggestedEnv = ['snapshot', sanitizeEnvSegment(result.gitSha)].filter(Boolean).join('');
|
|
301
321
|
return [
|
|
302
|
-
`nb init --env ${suggestedEnv} --yes --source npm`,
|
|
322
|
+
`nb init --ui --env ${suggestedEnv} --yes --source npm`,
|
|
303
323
|
`--version ${result.version}`,
|
|
304
324
|
`--npm-registry=${normalizedRegistry}`,
|
|
305
325
|
].join(' ');
|