@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.
Files changed (87) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +63 -380
  3. package/assets/env-proxy/nginx/app.conf.tpl +23 -0
  4. package/assets/env-proxy/nginx/nocobase.conf.tpl +5 -0
  5. package/assets/env-proxy/nginx/snippets/dist-location.conf +5 -0
  6. package/assets/env-proxy/nginx/snippets/gzip.conf +17 -0
  7. package/assets/env-proxy/nginx/snippets/log-format-http.conf +13 -0
  8. package/assets/env-proxy/nginx/snippets/maps-http.conf +14 -0
  9. package/assets/env-proxy/nginx/snippets/mime-types.conf +98 -0
  10. package/assets/env-proxy/nginx/snippets/proxy-location.conf +17 -0
  11. package/assets/env-proxy/nginx/snippets/spa-location.conf +6 -0
  12. package/assets/env-proxy/nginx/snippets/uploads-location.conf +21 -0
  13. package/bin/run.js +40 -32
  14. package/dist/commands/app/autostart/disable.js +55 -0
  15. package/dist/commands/app/autostart/enable.js +55 -0
  16. package/dist/commands/app/autostart/list.js +37 -0
  17. package/dist/commands/app/autostart/run.js +84 -0
  18. package/dist/commands/app/autostart/shared.js +49 -0
  19. package/dist/commands/app/destroy.js +21 -12
  20. package/dist/commands/app/down.js +2 -2
  21. package/dist/commands/app/logs.js +2 -1
  22. package/dist/commands/app/restart.js +92 -20
  23. package/dist/commands/app/shared.js +3 -2
  24. package/dist/commands/app/start.js +143 -38
  25. package/dist/commands/app/stop.js +31 -2
  26. package/dist/commands/app/upgrade.js +11 -7
  27. package/dist/commands/config/delete.js +10 -2
  28. package/dist/commands/config/get.js +7 -1
  29. package/dist/commands/config/set.js +12 -2
  30. package/dist/commands/db/ps.js +5 -18
  31. package/dist/commands/env/add.js +24 -44
  32. package/dist/commands/env/auth.js +1 -1
  33. package/dist/commands/env/info.js +3 -2
  34. package/dist/commands/env/list.js +4 -5
  35. package/dist/commands/env/proxy/caddy.js +28 -0
  36. package/dist/commands/env/proxy/index.js +353 -0
  37. package/dist/commands/env/proxy/nginx.js +28 -0
  38. package/dist/commands/env/remove.js +112 -22
  39. package/dist/commands/env/shared.js +17 -9
  40. package/dist/commands/env/status.js +10 -7
  41. package/dist/commands/env/update.js +385 -21
  42. package/dist/commands/init.js +368 -98
  43. package/dist/commands/install.js +271 -104
  44. package/dist/commands/license/activate.js +63 -244
  45. package/dist/commands/license/plugins/shared.js +64 -13
  46. package/dist/commands/plugin/import.js +108 -0
  47. package/dist/commands/revision/create.js +89 -0
  48. package/dist/commands/self/update.js +58 -1
  49. package/dist/commands/source/dev.js +5 -5
  50. package/dist/commands/source/test.js +13 -14
  51. package/dist/lib/app-health.js +39 -32
  52. package/dist/lib/app-managed-resources.js +25 -9
  53. package/dist/lib/app-public-path.js +80 -0
  54. package/dist/lib/app-runtime.js +12 -3
  55. package/dist/lib/auth-store.js +128 -4
  56. package/dist/lib/backup.js +3 -3
  57. package/dist/lib/bootstrap.js +14 -8
  58. package/dist/lib/cli-config.js +229 -3
  59. package/dist/lib/cli-entry-error.js +52 -0
  60. package/dist/lib/command-log.js +284 -0
  61. package/dist/lib/docker-env-file.js +10 -2
  62. package/dist/lib/docker-log-stream.js +45 -0
  63. package/dist/lib/env-auth.js +25 -14
  64. package/dist/lib/env-command-config.js +45 -0
  65. package/dist/lib/env-config.js +7 -2
  66. package/dist/lib/env-guard.js +9 -10
  67. package/dist/lib/env-paths.js +101 -0
  68. package/dist/lib/env-proxy.js +1295 -0
  69. package/dist/lib/inquirer.js +4 -5
  70. package/dist/lib/managed-env-file.js +98 -0
  71. package/dist/lib/plugin-import.js +279 -0
  72. package/dist/lib/prompt-catalog-core.js +3 -2
  73. package/dist/lib/prompt-validators.js +14 -0
  74. package/dist/lib/prompt-web-ui.js +8 -1
  75. package/dist/lib/run-npm.js +56 -12
  76. package/dist/lib/skills-manager.js +3 -1
  77. package/dist/lib/source-publish.js +22 -2
  78. package/dist/lib/startup-update.js +74 -50
  79. package/dist/lib/ui.js +2 -1
  80. package/dist/locale/en-US.json +122 -22
  81. package/dist/locale/zh-CN.json +119 -19
  82. package/package.json +5 -7
  83. package/scripts/build.mjs +34 -0
  84. package/scripts/clean.mjs +9 -0
  85. package/tsconfig.json +19 -0
  86. package/LICENSE.txt +0 -107
  87. package/README.zh-CN.md +0 -355
@@ -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
- return def.initialValue;
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.initialValue ?? '';
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:
@@ -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: options?.stdio ?? 'inherit',
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
- if (options.onStdout) {
124
- child.stdout?.on('data', (chunk) => {
125
- options.onStdout?.(String(chunk));
126
- });
127
- }
128
- if (options.onStderr) {
129
- child.stderr?.on('data', (chunk) => {
130
- options.onStderr?.(String(chunk));
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
- const SKILLS_LIST_TIMEOUT_MS = 5000;
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(' ');