@nocobase/cli 2.1.0-alpha.26 → 2.1.0-alpha.28
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 +24 -0
- package/README.zh-CN.md +4 -0
- package/dist/commands/app/down.js +2 -3
- package/dist/commands/app/logs.js +2 -2
- package/dist/commands/app/upgrade.js +114 -128
- package/dist/commands/config/delete.js +30 -0
- package/dist/commands/config/get.js +29 -0
- package/dist/commands/config/index.js +20 -0
- package/dist/commands/config/list.js +29 -0
- package/dist/commands/config/set.js +35 -0
- package/dist/commands/db/check.js +238 -0
- package/dist/commands/db/logs.js +2 -2
- package/dist/commands/db/shared.js +6 -5
- package/dist/commands/env/info.js +6 -2
- package/dist/commands/env/shared.js +1 -1
- package/dist/commands/init.js +0 -1
- package/dist/commands/install.js +87 -35
- package/dist/commands/license/activate.js +357 -0
- package/dist/commands/license/env.js +94 -0
- package/dist/commands/license/generate-id.js +107 -0
- package/dist/commands/license/id.js +52 -0
- package/dist/commands/license/index.js +20 -0
- package/dist/commands/license/plugins/clean.js +98 -0
- package/dist/commands/license/plugins/index.js +20 -0
- package/dist/commands/license/plugins/list.js +50 -0
- package/dist/commands/license/plugins/shared.js +325 -0
- package/dist/commands/license/plugins/sync.js +267 -0
- package/dist/commands/license/shared.js +414 -0
- package/dist/commands/license/status.js +50 -0
- package/dist/lib/api-client.js +74 -3
- package/dist/lib/app-managed-resources.js +10 -6
- package/dist/lib/app-runtime.js +29 -11
- package/dist/lib/auth-store.js +36 -68
- package/dist/lib/build-config.js +8 -0
- package/dist/lib/builtin-db.js +86 -0
- package/dist/lib/cli-config.js +176 -0
- package/dist/lib/cli-home.js +6 -21
- package/dist/lib/db-connection-check.js +178 -0
- package/dist/lib/env-config.js +7 -0
- package/dist/lib/generated-command.js +23 -3
- package/dist/lib/plugin-storage.js +127 -0
- package/dist/lib/prompt-validators.js +4 -4
- package/dist/lib/run-npm.js +53 -0
- package/dist/lib/runtime-env-vars.js +32 -0
- package/dist/lib/runtime-generator.js +89 -10
- package/dist/lib/self-manager.js +57 -2
- package/dist/lib/skills-manager.js +2 -2
- package/dist/lib/startup-update.js +85 -7
- package/dist/locale/en-US.json +16 -13
- package/dist/locale/zh-CN.json +16 -13
- package/nocobase-ctl.config.json +82 -0
- package/package.json +16 -4
package/dist/lib/run-npm.js
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
16
16
|
*/
|
|
17
17
|
import fs from 'node:fs';
|
|
18
|
+
import fsp from 'node:fs/promises';
|
|
19
|
+
import os from 'node:os';
|
|
18
20
|
import path from 'node:path';
|
|
19
21
|
import spawn from 'cross-spawn';
|
|
20
22
|
const FORWARDED_SIGNALS = ['SIGINT', 'SIGTERM'];
|
|
@@ -178,6 +180,57 @@ export function commandOutput(name, args, options) {
|
|
|
178
180
|
});
|
|
179
181
|
});
|
|
180
182
|
}
|
|
183
|
+
async function readCommandOutputFile(filePath) {
|
|
184
|
+
try {
|
|
185
|
+
return await fsp.readFile(filePath, 'utf8');
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return '';
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
export async function commandOutputViaFile(name, args, options) {
|
|
192
|
+
const cwd = resolveCwd(options?.cwd);
|
|
193
|
+
const label = options?.errorName ?? name;
|
|
194
|
+
const command = resolveCommandName(name);
|
|
195
|
+
const captureDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'nocobase-cli-output-'));
|
|
196
|
+
const stdoutPath = path.join(captureDir, 'stdout.log');
|
|
197
|
+
const stderrPath = path.join(captureDir, 'stderr.log');
|
|
198
|
+
const stdoutHandle = await fsp.open(stdoutPath, 'w');
|
|
199
|
+
const stderrHandle = await fsp.open(stderrPath, 'w');
|
|
200
|
+
try {
|
|
201
|
+
const result = await new Promise((resolve, reject) => {
|
|
202
|
+
const child = spawn(command, [...args], {
|
|
203
|
+
cwd,
|
|
204
|
+
env: {
|
|
205
|
+
...process.env,
|
|
206
|
+
...options?.env,
|
|
207
|
+
},
|
|
208
|
+
stdio: ['ignore', stdoutHandle.fd, stderrHandle.fd],
|
|
209
|
+
windowsHide: process.platform === 'win32',
|
|
210
|
+
});
|
|
211
|
+
child.once('error', reject);
|
|
212
|
+
child.once('close', (code, signal) => {
|
|
213
|
+
resolve({ code, signal });
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
await stdoutHandle.close();
|
|
217
|
+
await stderrHandle.close();
|
|
218
|
+
const stdout = await readCommandOutputFile(stdoutPath);
|
|
219
|
+
const stderr = await readCommandOutputFile(stderrPath);
|
|
220
|
+
if (result.code === 0) {
|
|
221
|
+
return stdout.trim();
|
|
222
|
+
}
|
|
223
|
+
if (result.signal) {
|
|
224
|
+
throw new Error(`${label} exited due to signal ${result.signal}`);
|
|
225
|
+
}
|
|
226
|
+
const details = stderr.trim() || stdout.trim();
|
|
227
|
+
throw new Error(details ? `${label} exited with code ${result.code}: ${details}` : `${label} exited with code ${result.code}`);
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
await Promise.allSettled([stdoutHandle.close(), stderrHandle.close()]);
|
|
231
|
+
await fsp.rm(captureDir, { recursive: true, force: true });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
181
234
|
/** Run `yarn` with the given argument list, inheriting stdio (errors label as `npm` for compatibility). */
|
|
182
235
|
export function runNpm(args, options) {
|
|
183
236
|
return run('yarn', [...args], { ...options, errorName: 'npm' });
|
|
@@ -0,0 +1,32 @@
|
|
|
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 { resolveBuiltinDbConnection } from './builtin-db.js';
|
|
10
|
+
function put(out, key, value) {
|
|
11
|
+
if (value === undefined || value === null) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
out[key] = String(value);
|
|
15
|
+
}
|
|
16
|
+
export async function buildRuntimeEnvVars(runtime) {
|
|
17
|
+
const config = runtime.env.config ?? {};
|
|
18
|
+
const out = {
|
|
19
|
+
...runtime.env.envVars,
|
|
20
|
+
};
|
|
21
|
+
if (runtime.kind !== 'local' && runtime.kind !== 'docker') {
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
if (!config.builtinDb) {
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
const connection = await resolveBuiltinDbConnection(runtime);
|
|
28
|
+
put(out, 'DB_DIALECT', connection.dbDialect);
|
|
29
|
+
put(out, 'DB_HOST', connection.dbHost);
|
|
30
|
+
put(out, 'DB_PORT', connection.dbPort);
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
@@ -1,3 +1,11 @@
|
|
|
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
|
+
*/
|
|
1
9
|
/**
|
|
2
10
|
* This file is part of the NocoBase (R) project.
|
|
3
11
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
@@ -60,12 +68,59 @@ function toGeneratedParameter(parameter, usedFlagNames) {
|
|
|
60
68
|
required: parameter.required,
|
|
61
69
|
description: parameter.description,
|
|
62
70
|
type: inferParameterType(parameter.schema),
|
|
71
|
+
format: parameter.schema?.format,
|
|
63
72
|
isArray: parameter.schema?.type === 'array',
|
|
73
|
+
isFile: parameter.schema?.type === 'string' && parameter.schema?.format === 'binary',
|
|
64
74
|
};
|
|
65
75
|
}
|
|
66
76
|
function getJsonRequestSchema(requestBody) {
|
|
67
77
|
return requestBody?.content?.['application/json']?.schema;
|
|
68
78
|
}
|
|
79
|
+
function getMultipartRequestSchema(requestBody) {
|
|
80
|
+
return requestBody?.content?.['multipart/form-data']?.schema;
|
|
81
|
+
}
|
|
82
|
+
function getRequestContentType(requestBody) {
|
|
83
|
+
if (!requestBody || '$ref' in requestBody) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
if (requestBody.content?.['multipart/form-data']) {
|
|
87
|
+
return 'multipart/form-data';
|
|
88
|
+
}
|
|
89
|
+
if (requestBody.content?.['application/json']) {
|
|
90
|
+
return 'application/json';
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
function getRequestSchema(requestBody) {
|
|
95
|
+
return getMultipartRequestSchema(requestBody) ?? getJsonRequestSchema(requestBody);
|
|
96
|
+
}
|
|
97
|
+
function isBinarySchema(schema) {
|
|
98
|
+
if (!schema || typeof schema !== 'object') {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
if (schema.type === 'string' && schema.format === 'binary') {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
return [...(schema.oneOf ?? []), ...(schema.anyOf ?? []), ...(schema.allOf ?? [])].some(isBinarySchema);
|
|
105
|
+
}
|
|
106
|
+
function getResponseType(operation) {
|
|
107
|
+
for (const response of Object.values(operation.responses ?? {})) {
|
|
108
|
+
const content = response?.content ?? {};
|
|
109
|
+
const mediaTypes = Object.keys(content);
|
|
110
|
+
const hasJson = mediaTypes.some((mediaType) => mediaType.includes('json'));
|
|
111
|
+
if (hasJson) {
|
|
112
|
+
return 'json';
|
|
113
|
+
}
|
|
114
|
+
const hasBinary = mediaTypes.some((mediaType) => {
|
|
115
|
+
const schema = content[mediaType]?.schema;
|
|
116
|
+
return mediaType === 'application/octet-stream' || mediaType.includes('zip') || isBinarySchema(schema);
|
|
117
|
+
});
|
|
118
|
+
if (hasBinary) {
|
|
119
|
+
return 'binary';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
69
124
|
function normalizeCompositeSchema(schema) {
|
|
70
125
|
if (!schema || typeof schema !== 'object') {
|
|
71
126
|
return schema;
|
|
@@ -139,7 +194,7 @@ function describeSchemaShape(schema, options = {}) {
|
|
|
139
194
|
return type;
|
|
140
195
|
}
|
|
141
196
|
function extractBodyParameters(requestBody, usedFlagNames) {
|
|
142
|
-
const schema =
|
|
197
|
+
const schema = getRequestSchema(requestBody);
|
|
143
198
|
const properties = normalizeCompositeSchema(schema)?.properties;
|
|
144
199
|
const required = new Set(normalizeCompositeSchema(schema)?.required ?? []);
|
|
145
200
|
return Object.entries(properties ?? {}).map(([name, propertySchema]) => ({
|
|
@@ -149,7 +204,9 @@ function extractBodyParameters(requestBody, usedFlagNames) {
|
|
|
149
204
|
required: required.has(name),
|
|
150
205
|
description: propertySchema.description,
|
|
151
206
|
type: inferParameterType(propertySchema),
|
|
207
|
+
format: propertySchema.format,
|
|
152
208
|
isArray: propertySchema.type === 'array',
|
|
209
|
+
isFile: propertySchema.type === 'string' && propertySchema.format === 'binary',
|
|
153
210
|
jsonEncoded: propertySchema.type === 'object' || propertySchema.type === 'array',
|
|
154
211
|
jsonShape: describeSchemaShape(propertySchema),
|
|
155
212
|
}));
|
|
@@ -179,6 +236,9 @@ function formatFlagExample(parameter) {
|
|
|
179
236
|
if (parameter.type === 'boolean') {
|
|
180
237
|
return `--${parameter.flagName}`;
|
|
181
238
|
}
|
|
239
|
+
if (parameter.isFile) {
|
|
240
|
+
return `--${parameter.flagName} <path>`;
|
|
241
|
+
}
|
|
182
242
|
if (parameter.type === 'object' || parameter.jsonEncoded) {
|
|
183
243
|
if (parameter.type === 'array' || parameter.isArray) {
|
|
184
244
|
return `--${parameter.flagName} '[]'`;
|
|
@@ -216,12 +276,13 @@ export function buildExamples(commandId, operation) {
|
|
|
216
276
|
const requiredParameters = operation.parameters.filter((parameter) => parameter.required);
|
|
217
277
|
const requiredFlags = requiredParameters.map(formatFlagExample);
|
|
218
278
|
const requiredNonBodyFlags = requiredParameters.filter((parameter) => parameter.in !== 'body').map(formatFlagExample);
|
|
219
|
-
const
|
|
279
|
+
const outputFlag = operation.responseType === 'binary' ? ' --output <path>' : '';
|
|
280
|
+
const examples = [`nb api ${commandId}${requiredFlags.length ? ` ${requiredFlags.join(' ')}` : ''}${outputFlag}`];
|
|
220
281
|
const firstOptional = operation.parameters.find((parameter) => !parameter.required);
|
|
221
282
|
if (firstOptional) {
|
|
222
|
-
examples.push(`${examples[0]} ${formatFlagExample(firstOptional)}
|
|
283
|
+
examples.push(`${examples[0]} ${formatFlagExample(firstOptional)}`.trim());
|
|
223
284
|
}
|
|
224
|
-
if (operation.hasBody) {
|
|
285
|
+
if (operation.hasBody && operation.requestContentType !== 'multipart/form-data') {
|
|
225
286
|
const prefix = `nb api ${commandId}${requiredNonBodyFlags.length ? ` ${requiredNonBodyFlags.join(' ')}` : ''}`;
|
|
226
287
|
examples.push(`${prefix} --body '${buildSampleBody(operation.parameters)}'`);
|
|
227
288
|
}
|
|
@@ -248,9 +309,17 @@ function buildDescription(operation) {
|
|
|
248
309
|
}
|
|
249
310
|
if (operation.hasBody) {
|
|
250
311
|
const bodyFlags = operation.parameters.filter((parameter) => parameter.in === 'body').map((parameter) => `--${parameter.flagName}`);
|
|
251
|
-
|
|
252
|
-
? `Request body:
|
|
253
|
-
|
|
312
|
+
if (operation.requestContentType === 'multipart/form-data') {
|
|
313
|
+
sections.push(bodyFlags.length ? `Request body: multipart form fields (${bodyFlags.join(', ')}).` : 'Request body: multipart form data.');
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
sections.push(bodyFlags.length
|
|
317
|
+
? `Request body: use body field flags (${bodyFlags.join(', ')}) or pass raw JSON via \`--body\` / \`--body-file\`.`
|
|
318
|
+
: 'Request body: JSON via `--body` or `--body-file`.');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (operation.responseType === 'binary') {
|
|
322
|
+
sections.push('Response body: binary download written to `--output`.');
|
|
254
323
|
}
|
|
255
324
|
return sections.join('\n\n');
|
|
256
325
|
}
|
|
@@ -357,6 +426,8 @@ export async function generateRuntime(document, configFile, baseUrl) {
|
|
|
357
426
|
const bodyParameters = extractBodyParameters(operation.requestBody, usedFlagNames);
|
|
358
427
|
const allParameters = [...parameters, ...bodyParameters];
|
|
359
428
|
const hasBody = Boolean(operation.requestBody && !('$ref' in operation.requestBody));
|
|
429
|
+
const requestContentType = getRequestContentType(operation.requestBody);
|
|
430
|
+
const responseType = getResponseType(operation);
|
|
360
431
|
const moduleDisplayName = moduleConfig.name ?? moduleKey;
|
|
361
432
|
const moduleDescription = moduleConfig.description;
|
|
362
433
|
const resourceDisplayName = resourceConfig?.name ?? resourceKey;
|
|
@@ -366,9 +437,11 @@ export async function generateRuntime(document, configFile, baseUrl) {
|
|
|
366
437
|
description: operation.description,
|
|
367
438
|
});
|
|
368
439
|
const resourceSegments = toResourceSegments(pathTemplate);
|
|
369
|
-
const mappedResourceSegments = resourceSegments.length && resourceConfig?.
|
|
370
|
-
? [
|
|
371
|
-
: resourceSegments
|
|
440
|
+
const mappedResourceSegments = resourceSegments.length && resourceConfig?.segments?.length
|
|
441
|
+
? [...resourceConfig.segments.map(toKebabCase), ...resourceSegments.slice(1)]
|
|
442
|
+
: resourceSegments.length && resourceConfig?.name
|
|
443
|
+
? [toKebabCase(resourceConfig.name), ...resourceSegments.slice(1)]
|
|
444
|
+
: resourceSegments;
|
|
372
445
|
const segments = [
|
|
373
446
|
...(resourceConfig?.topLevel ? [] : [toKebabCase(moduleDisplayName)]),
|
|
374
447
|
...mappedResourceSegments,
|
|
@@ -397,15 +470,21 @@ export async function generateRuntime(document, configFile, baseUrl) {
|
|
|
397
470
|
tags: operation.tags,
|
|
398
471
|
description: operationText.description,
|
|
399
472
|
hasBody,
|
|
473
|
+
requestContentType,
|
|
474
|
+
responseType,
|
|
400
475
|
parameters: allParameters,
|
|
401
476
|
}),
|
|
402
477
|
examples: buildExamples(segments.join(' '), {
|
|
403
478
|
parameters: allParameters,
|
|
404
479
|
hasBody,
|
|
480
|
+
requestContentType,
|
|
481
|
+
responseType,
|
|
405
482
|
}),
|
|
406
483
|
parameters: allParameters,
|
|
407
484
|
hasBody,
|
|
408
485
|
bodyRequired: operation.requestBody && !('$ref' in operation.requestBody) ? operation.requestBody.required : undefined,
|
|
486
|
+
requestContentType,
|
|
487
|
+
responseType,
|
|
409
488
|
});
|
|
410
489
|
}
|
|
411
490
|
const schemaHash = createHash('sha1').update(JSON.stringify(document)).digest('hex').slice(0, 8);
|
package/dist/lib/self-manager.js
CHANGED
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
import fs from 'node:fs';
|
|
10
|
+
import fsp from 'node:fs/promises';
|
|
10
11
|
import path from 'node:path';
|
|
11
12
|
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { resolveCliHomeDir } from './cli-home.js';
|
|
12
14
|
import { commandOutput, run } from './run-npm.js';
|
|
13
15
|
const DEFAULT_PACKAGE_NAME = '@nocobase/cli';
|
|
14
16
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
17
|
+
const INSTALL_METHOD_CACHE_FILE = 'self-install-methods.json';
|
|
15
18
|
function normalizePath(value) {
|
|
16
19
|
return path.resolve(value);
|
|
17
20
|
}
|
|
@@ -115,6 +118,38 @@ function detectInstallMethod(packageRoot, globalPrefix) {
|
|
|
115
118
|
}
|
|
116
119
|
return 'unknown';
|
|
117
120
|
}
|
|
121
|
+
function getInstallMethodCacheFile() {
|
|
122
|
+
return path.join(resolveCliHomeDir('global'), INSTALL_METHOD_CACHE_FILE);
|
|
123
|
+
}
|
|
124
|
+
function getInstallMethodCacheKey(packageRoot) {
|
|
125
|
+
return normalizePath(path.join(packageRoot, 'bin', 'run.js'));
|
|
126
|
+
}
|
|
127
|
+
async function readInstallMethodCache() {
|
|
128
|
+
try {
|
|
129
|
+
const raw = await fsp.readFile(getInstallMethodCacheFile(), 'utf8');
|
|
130
|
+
return JSON.parse(raw);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return {};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async function writeInstallMethodCache(state) {
|
|
137
|
+
const filePath = getInstallMethodCacheFile();
|
|
138
|
+
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
139
|
+
await fsp.writeFile(filePath, JSON.stringify(state, null, 2));
|
|
140
|
+
}
|
|
141
|
+
async function readCachedInstallMethod(packageRoot) {
|
|
142
|
+
const state = await readInstallMethodCache();
|
|
143
|
+
return state.entries?.[getInstallMethodCacheKey(packageRoot)];
|
|
144
|
+
}
|
|
145
|
+
async function writeCachedInstallMethod(packageRoot, entry) {
|
|
146
|
+
const state = await readInstallMethodCache();
|
|
147
|
+
const entries = {
|
|
148
|
+
...(state.entries ?? {}),
|
|
149
|
+
[getInstallMethodCacheKey(packageRoot)]: entry,
|
|
150
|
+
};
|
|
151
|
+
await writeInstallMethodCache({ entries });
|
|
152
|
+
}
|
|
118
153
|
async function readGlobalPrefix(commandOutputFn) {
|
|
119
154
|
try {
|
|
120
155
|
return (await commandOutputFn('npm', ['prefix', '-g'], {
|
|
@@ -177,14 +212,34 @@ export function formatSelfUpdateUnavailableMessage(status) {
|
|
|
177
212
|
export function getSelfUpdatePackageSpec(status) {
|
|
178
213
|
return `${status.packageName}@${status.channel}`;
|
|
179
214
|
}
|
|
215
|
+
export async function inspectSelfInstall(options = {}) {
|
|
216
|
+
const packageRoot = options.packageRoot ? normalizePath(options.packageRoot) : PACKAGE_ROOT;
|
|
217
|
+
const commandOutputFn = options.commandOutputFn ?? commandOutput;
|
|
218
|
+
const cachedInstallMethod = await readCachedInstallMethod(packageRoot);
|
|
219
|
+
const globalPrefix = cachedInstallMethod?.globalPrefix ?? await readGlobalPrefix(commandOutputFn);
|
|
220
|
+
const installMethod = cachedInstallMethod?.installMethod ?? detectInstallMethod(packageRoot, globalPrefix);
|
|
221
|
+
if (!cachedInstallMethod) {
|
|
222
|
+
await writeCachedInstallMethod(packageRoot, {
|
|
223
|
+
installMethod,
|
|
224
|
+
globalPrefix,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
packageRoot,
|
|
229
|
+
installMethod,
|
|
230
|
+
globalPrefix,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
180
233
|
export async function inspectSelfStatus(options = {}) {
|
|
181
234
|
const packageRoot = options.packageRoot ? normalizePath(options.packageRoot) : PACKAGE_ROOT;
|
|
182
235
|
const packageName = options.packageName ?? DEFAULT_PACKAGE_NAME;
|
|
183
236
|
const currentVersion = options.currentVersion ?? readCurrentVersion(packageRoot);
|
|
184
237
|
const channel = options.channel && options.channel !== 'auto' ? options.channel : detectChannel(currentVersion);
|
|
185
238
|
const commandOutputFn = options.commandOutputFn ?? commandOutput;
|
|
186
|
-
const globalPrefix = await
|
|
187
|
-
|
|
239
|
+
const { installMethod, globalPrefix } = await inspectSelfInstall({
|
|
240
|
+
packageRoot,
|
|
241
|
+
commandOutputFn,
|
|
242
|
+
});
|
|
188
243
|
let latestVersion;
|
|
189
244
|
let registryError;
|
|
190
245
|
try {
|
|
@@ -10,7 +10,7 @@ import fsp from 'node:fs/promises';
|
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { resolveCliHomeDir } from './cli-home.js';
|
|
12
12
|
import { compareVersions } from './self-manager.js';
|
|
13
|
-
import { commandOutput, run } from './run-npm.js';
|
|
13
|
+
import { commandOutput, commandOutputViaFile, run } from './run-npm.js';
|
|
14
14
|
export const NOCOBASE_SKILLS_SOURCE = 'nocobase/skills';
|
|
15
15
|
export const NOCOBASE_SKILLS_PACKAGE_NAME = '@nocobase/skills';
|
|
16
16
|
const NOCOBASE_SKILLS_NAME_PREFIX = 'nocobase-';
|
|
@@ -57,7 +57,7 @@ async function writeManagedSkillsState(workspaceRoot, state) {
|
|
|
57
57
|
export async function listGlobalSkills(options = {}) {
|
|
58
58
|
const globalRoot = resolveSkillsRoot(options);
|
|
59
59
|
await ensureSkillsWorkspaceRoot(globalRoot);
|
|
60
|
-
const output = await (options.commandOutputFn ??
|
|
60
|
+
const output = await (options.commandOutputFn ?? commandOutputViaFile)('npx', ['-y', 'skills', 'list', '-g', '--json'], {
|
|
61
61
|
cwd: globalRoot,
|
|
62
62
|
errorName: 'skills list',
|
|
63
63
|
});
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import fs from 'node:fs/promises';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
11
12
|
import * as p from '@clack/prompts';
|
|
12
|
-
import { inspectSelfStatus, } from './self-manager.js';
|
|
13
|
+
import { inspectSelfInstall, inspectSelfStatus, } from './self-manager.js';
|
|
13
14
|
import { inspectSkillsStatus } from './skills-manager.js';
|
|
14
15
|
import { resolveCliHomeDir } from './cli-home.js';
|
|
15
16
|
import { isInteractiveTerminal, printWarning } from './ui.js';
|
|
@@ -19,8 +20,37 @@ const NB_SKIP_STARTUP_UPDATE_ENV = 'NB_SKIP_STARTUP_UPDATE';
|
|
|
19
20
|
function getStateFile() {
|
|
20
21
|
return path.join(resolveCliHomeDir('global'), STARTUP_UPDATE_STATE_FILE);
|
|
21
22
|
}
|
|
23
|
+
function getCurrentInstallBinPath() {
|
|
24
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'bin', 'run.js');
|
|
25
|
+
}
|
|
26
|
+
function getCurrentInstallEntry(state) {
|
|
27
|
+
return state.entries?.[getCurrentInstallBinPath()];
|
|
28
|
+
}
|
|
22
29
|
function todayStamp(now = new Date()) {
|
|
23
|
-
|
|
30
|
+
const timeZone = String(process.env.TZ ?? '').trim();
|
|
31
|
+
if (timeZone) {
|
|
32
|
+
try {
|
|
33
|
+
const parts = new Intl.DateTimeFormat('en-CA', {
|
|
34
|
+
timeZone,
|
|
35
|
+
year: 'numeric',
|
|
36
|
+
month: '2-digit',
|
|
37
|
+
day: '2-digit',
|
|
38
|
+
}).formatToParts(now);
|
|
39
|
+
const year = parts.find((part) => part.type === 'year')?.value;
|
|
40
|
+
const month = parts.find((part) => part.type === 'month')?.value;
|
|
41
|
+
const day = parts.find((part) => part.type === 'day')?.value;
|
|
42
|
+
if (year && month && day) {
|
|
43
|
+
return `${year}-${month}-${day}`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Fall back to the host local timezone.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const year = now.getFullYear();
|
|
51
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
52
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
53
|
+
return `${year}-${month}-${day}`;
|
|
24
54
|
}
|
|
25
55
|
function shouldSkipByArgv(argv) {
|
|
26
56
|
const tokens = argv.filter((token) => token && !token.startsWith('-'));
|
|
@@ -46,8 +76,46 @@ async function writeState(state) {
|
|
|
46
76
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
47
77
|
await fs.writeFile(filePath, JSON.stringify(state, null, 2));
|
|
48
78
|
}
|
|
79
|
+
function readCurrentInstallLastCheckedDate(state) {
|
|
80
|
+
return getCurrentInstallEntry(state)?.lastCheckedDate ?? state.lastCheckedDate;
|
|
81
|
+
}
|
|
82
|
+
async function writeCurrentInstallEntry(updater) {
|
|
83
|
+
const state = await readState();
|
|
84
|
+
const installBinPath = getCurrentInstallBinPath();
|
|
85
|
+
const nextEntry = updater(getCurrentInstallEntry(state), state);
|
|
86
|
+
await writeState({
|
|
87
|
+
...state,
|
|
88
|
+
entries: {
|
|
89
|
+
...(state.entries ?? {}),
|
|
90
|
+
[installBinPath]: nextEntry,
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
49
94
|
async function markChecked(now = new Date()) {
|
|
50
|
-
await
|
|
95
|
+
await writeCurrentInstallEntry((current, state) => {
|
|
96
|
+
return {
|
|
97
|
+
policy: current?.policy ?? 'daily',
|
|
98
|
+
lastCheckedDate: todayStamp(now),
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
async function disableStartupUpdateForCurrentInstall() {
|
|
103
|
+
await writeCurrentInstallEntry((current, state) => {
|
|
104
|
+
return {
|
|
105
|
+
policy: 'disabled',
|
|
106
|
+
lastCheckedDate: current?.lastCheckedDate
|
|
107
|
+
?? state.lastCheckedDate,
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
async function enableDailyStartupUpdateForCurrentInstall() {
|
|
112
|
+
await writeCurrentInstallEntry((current, state) => {
|
|
113
|
+
return {
|
|
114
|
+
policy: 'daily',
|
|
115
|
+
lastCheckedDate: current?.lastCheckedDate
|
|
116
|
+
?? state.lastCheckedDate,
|
|
117
|
+
};
|
|
118
|
+
});
|
|
51
119
|
}
|
|
52
120
|
export async function shouldRunStartupUpdateCheck(argv, now = new Date()) {
|
|
53
121
|
if (process.env[NB_SKIP_STARTUP_UPDATE_ENV] === '1') {
|
|
@@ -57,7 +125,20 @@ export async function shouldRunStartupUpdateCheck(argv, now = new Date()) {
|
|
|
57
125
|
return false;
|
|
58
126
|
}
|
|
59
127
|
const state = await readState();
|
|
60
|
-
|
|
128
|
+
const currentEntry = getCurrentInstallEntry(state);
|
|
129
|
+
if (currentEntry?.policy === 'disabled') {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
if (currentEntry?.policy === 'daily') {
|
|
133
|
+
return readCurrentInstallLastCheckedDate(state) !== todayStamp(now);
|
|
134
|
+
}
|
|
135
|
+
const selfInstall = await inspectSelfInstall();
|
|
136
|
+
if (!shouldEnableStartupUpdateForInstallMethod(selfInstall.installMethod)) {
|
|
137
|
+
await disableStartupUpdateForCurrentInstall();
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
await enableDailyStartupUpdateForCurrentInstall();
|
|
141
|
+
return readCurrentInstallLastCheckedDate(state) !== todayStamp(now);
|
|
61
142
|
}
|
|
62
143
|
export function shouldEnableStartupUpdateForInstallMethod(installMethod) {
|
|
63
144
|
return installMethod === 'npm-global';
|
|
@@ -173,9 +254,6 @@ export async function maybeRunStartupUpdatePrompt(argv) {
|
|
|
173
254
|
return { kind: 'skipped' };
|
|
174
255
|
}
|
|
175
256
|
const selfStatus = await inspectSelfStatus();
|
|
176
|
-
if (!shouldEnableStartupUpdateForInstallMethod(selfStatus.installMethod)) {
|
|
177
|
-
return { kind: 'skipped' };
|
|
178
|
-
}
|
|
179
257
|
const skillsStatus = await inspectSkillsStatus();
|
|
180
258
|
if (!hasPendingUpdates(selfStatus, skillsStatus)) {
|
|
181
259
|
await markChecked();
|
package/dist/locale/en-US.json
CHANGED
|
@@ -62,6 +62,13 @@
|
|
|
62
62
|
"allocateNotDockerPublished": "Failed to allocate an available TCP port that is not already published by Docker.",
|
|
63
63
|
"alreadyInUse": "Port {{port}} is already in use. Choose another port.",
|
|
64
64
|
"alreadyInUseByDocker": "Port {{port}} is already in use by a Docker container. Choose another port."
|
|
65
|
+
},
|
|
66
|
+
"dbConnection": {
|
|
67
|
+
"unreachable": "Can't reach the database at {{host}}:{{port}}. Check the host, port, and network connectivity. Details: {{details}}",
|
|
68
|
+
"timeout": "Timed out connecting to the database at {{host}}:{{port}} after about {{seconds}} seconds.",
|
|
69
|
+
"authenticationFailed": "Failed to sign in to database \"{{database}}\" with user \"{{user}}\". Check the username and password.",
|
|
70
|
+
"databaseNotFound": "Database \"{{database}}\" does not exist or is not accessible with the current connection settings.",
|
|
71
|
+
"connectionFailed": "Database connection check failed. Details: {{details}}"
|
|
65
72
|
}
|
|
66
73
|
},
|
|
67
74
|
"commands": {
|
|
@@ -73,10 +80,6 @@
|
|
|
73
80
|
},
|
|
74
81
|
"scope": {
|
|
75
82
|
"message": "Where should this connection be saved?",
|
|
76
|
-
"autoLabel": "Auto",
|
|
77
|
-
"autoHint": "project if this repo already has .nocobase, otherwise global",
|
|
78
|
-
"projectLabel": "Project",
|
|
79
|
-
"projectHint": ".nocobase in this repo",
|
|
80
83
|
"globalLabel": "Global",
|
|
81
84
|
"globalHint": "user-level config"
|
|
82
85
|
},
|
|
@@ -273,7 +276,7 @@
|
|
|
273
276
|
"envExists": "Env \"{{envName}}\" already exists. Choose another env name."
|
|
274
277
|
},
|
|
275
278
|
"messages": {
|
|
276
|
-
"title": "Set Up
|
|
279
|
+
"title": "Set Up NocoBase for Coding Agents",
|
|
277
280
|
"appNameRequiredWhenSkipped": "Env name is required when prompts are skipped.",
|
|
278
281
|
"appNameEnvHelp": "Use `nb init --yes --env <envName>` to continue.",
|
|
279
282
|
"resumeEnvRequired": "Env name is required when resuming setup.",
|
|
@@ -303,24 +306,24 @@
|
|
|
303
306
|
}
|
|
304
307
|
},
|
|
305
308
|
"webUi": {
|
|
306
|
-
"pageTitle": "Set Up
|
|
307
|
-
"documentHeading": "Set Up
|
|
308
|
-
"documentHint": "Connect an existing NocoBase app, or install a new one
|
|
309
|
+
"pageTitle": "Set Up NocoBase for Coding Agents",
|
|
310
|
+
"documentHeading": "Set Up NocoBase for Coding Agents",
|
|
311
|
+
"documentHint": "Connect an existing NocoBase app, or install a new one, so coding agents can access and work with NocoBase.",
|
|
309
312
|
"gettingStarted": {
|
|
310
313
|
"title": "Getting started",
|
|
311
|
-
"description": "
|
|
314
|
+
"description": "Choose whether to connect an existing app or install a new one."
|
|
312
315
|
},
|
|
313
316
|
"connectExistingApp": {
|
|
314
317
|
"title": "Connect an existing app",
|
|
315
|
-
"description": "
|
|
318
|
+
"description": "Save your app connection."
|
|
316
319
|
},
|
|
317
320
|
"createNewApp": {
|
|
318
|
-
"title": "
|
|
319
|
-
"description": "Set
|
|
321
|
+
"title": "Install a new app",
|
|
322
|
+
"description": "Set app basics and install options."
|
|
320
323
|
},
|
|
321
324
|
"downloadAppFiles": {
|
|
322
325
|
"title": "Download app files",
|
|
323
|
-
"description": "Choose
|
|
326
|
+
"description": "Choose how to get the app files."
|
|
324
327
|
},
|
|
325
328
|
"configureDatabase": {
|
|
326
329
|
"title": "Configure the database",
|
package/dist/locale/zh-CN.json
CHANGED
|
@@ -62,6 +62,13 @@
|
|
|
62
62
|
"allocateNotDockerPublished": "分配未被 Docker 占用的可用 TCP 端口失败。",
|
|
63
63
|
"alreadyInUse": "端口 {{port}} 已被占用,请更换其他端口。",
|
|
64
64
|
"alreadyInUseByDocker": "端口 {{port}} 已被 Docker 容器占用,请更换其他端口。"
|
|
65
|
+
},
|
|
66
|
+
"dbConnection": {
|
|
67
|
+
"unreachable": "无法连接到数据库 {{host}}:{{port}}。请检查主机、端口和网络连通性。详情:{{details}}",
|
|
68
|
+
"timeout": "连接数据库 {{host}}:{{port}} 超时,约 {{seconds}} 秒内未成功建立连接。",
|
|
69
|
+
"authenticationFailed": "无法使用用户 \"{{user}}\" 登录数据库 \"{{database}}\"。请检查用户名和密码。",
|
|
70
|
+
"databaseNotFound": "数据库 \"{{database}}\" 不存在,或当前连接配置无权访问该数据库。",
|
|
71
|
+
"connectionFailed": "数据库连接检查失败。详情:{{details}}"
|
|
65
72
|
}
|
|
66
73
|
},
|
|
67
74
|
"commands": {
|
|
@@ -73,10 +80,6 @@
|
|
|
73
80
|
},
|
|
74
81
|
"scope": {
|
|
75
82
|
"message": "这个连接要保存到哪里?",
|
|
76
|
-
"autoLabel": "自动",
|
|
77
|
-
"autoHint": "当前仓库已有 .nocobase 时保存到项目内,否则保存到全局",
|
|
78
|
-
"projectLabel": "项目内",
|
|
79
|
-
"projectHint": "保存在当前仓库的 .nocobase 中",
|
|
80
83
|
"globalLabel": "全局",
|
|
81
84
|
"globalHint": "保存在用户级配置中"
|
|
82
85
|
},
|
|
@@ -273,7 +276,7 @@
|
|
|
273
276
|
"envExists": "Env \"{{envName}}\" 已存在,请换一个 env name。"
|
|
274
277
|
},
|
|
275
278
|
"messages": {
|
|
276
|
-
"title": "
|
|
279
|
+
"title": "配置供 Coding Agents 使用的 NocoBase",
|
|
277
280
|
"appNameRequiredWhenSkipped": "跳过 prompts 时必须提供 Env name。",
|
|
278
281
|
"appNameEnvHelp": "请使用 `nb init --yes --env <envName>` 继续。",
|
|
279
282
|
"resumeEnvRequired": "恢复安装时必须提供 Env name。",
|
|
@@ -303,24 +306,24 @@
|
|
|
303
306
|
}
|
|
304
307
|
},
|
|
305
308
|
"webUi": {
|
|
306
|
-
"pageTitle": "
|
|
307
|
-
"documentHeading": "
|
|
308
|
-
"documentHint": "连接已有的 NocoBase
|
|
309
|
+
"pageTitle": "配置供 Coding Agents 使用的 NocoBase",
|
|
310
|
+
"documentHeading": "配置供 Coding Agents 使用的 NocoBase",
|
|
311
|
+
"documentHint": "连接已有的 NocoBase 应用,或安装一个新的应用,让 Coding Agents 可以在当前工作区中访问和操作 NocoBase。",
|
|
309
312
|
"gettingStarted": {
|
|
310
313
|
"title": "开始设置",
|
|
311
|
-
"description": "
|
|
314
|
+
"description": "选择连接已有应用,或安装一个新应用。"
|
|
312
315
|
},
|
|
313
316
|
"connectExistingApp": {
|
|
314
317
|
"title": "连接已有应用",
|
|
315
|
-
"description": "
|
|
318
|
+
"description": "保存现有应用连接。"
|
|
316
319
|
},
|
|
317
320
|
"createNewApp": {
|
|
318
|
-
"title": "
|
|
319
|
-
"description": "
|
|
321
|
+
"title": "安装新应用",
|
|
322
|
+
"description": "设置应用基础信息和安装选项。"
|
|
320
323
|
},
|
|
321
324
|
"downloadAppFiles": {
|
|
322
325
|
"title": "下载应用文件",
|
|
323
|
-
"description": "
|
|
326
|
+
"description": "选择获取应用文件的方式。"
|
|
324
327
|
},
|
|
325
328
|
"configureDatabase": {
|
|
326
329
|
"title": "配置数据库",
|