@nocobase/cli 2.1.0-beta.33 → 2.1.0-beta.35
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/bin/run.js +2 -1
- package/bin/session-env.js +12 -0
- package/dist/commands/app/down.js +10 -13
- package/dist/commands/app/logs.js +0 -1
- package/dist/commands/app/restart.js +63 -2
- package/dist/commands/app/start.js +53 -18
- package/dist/commands/app/stop.js +0 -1
- package/dist/commands/app/upgrade.js +16 -4
- package/dist/commands/env/add.js +3 -4
- package/dist/commands/env/auth.js +3 -2
- package/dist/commands/env/remove.js +38 -13
- package/dist/commands/env/update.js +9 -2
- package/dist/commands/examples/prompts-stages.js +4 -4
- package/dist/commands/examples/prompts-test.js +4 -4
- package/dist/commands/init.js +38 -31
- package/dist/commands/install.js +100 -63
- package/dist/commands/license/activate.js +66 -64
- package/dist/commands/license/id.js +0 -1
- package/dist/commands/license/plugins/clean.js +0 -1
- package/dist/commands/license/plugins/list.js +0 -1
- package/dist/commands/license/plugins/sync.js +0 -1
- package/dist/commands/license/shared.js +3 -3
- package/dist/commands/license/status.js +0 -1
- package/dist/commands/plugin/disable.js +0 -1
- package/dist/commands/plugin/enable.js +0 -1
- package/dist/commands/plugin/list.js +0 -1
- package/dist/commands/self/update.js +12 -3
- package/dist/commands/skills/install.js +12 -3
- package/dist/commands/skills/remove.js +12 -3
- package/dist/commands/skills/update.js +12 -3
- package/dist/commands/source/dev.js +8 -2
- package/dist/commands/source/download.js +29 -17
- package/dist/commands/source/publish.js +92 -0
- package/dist/commands/source/registry/logs.js +70 -0
- package/dist/commands/source/registry/start.js +57 -0
- package/dist/commands/source/registry/status.js +33 -0
- package/dist/commands/source/registry/stop.js +48 -0
- package/dist/lib/app-managed-resources.js +30 -3
- package/dist/lib/bootstrap.js +12 -3
- package/dist/lib/db-connection-check.js +3 -23
- package/dist/lib/docker-env-file.js +52 -0
- package/dist/lib/env-auth.js +4 -3
- package/dist/lib/env-config.js +1 -0
- package/dist/lib/env-guard.js +8 -7
- package/dist/lib/generated-command.js +0 -1
- package/dist/lib/inquirer-theme.js +17 -0
- package/dist/lib/inquirer.js +244 -0
- package/dist/lib/object-utils.js +76 -0
- package/dist/lib/prompt-catalog-core.js +185 -0
- package/dist/lib/prompt-catalog-terminal.js +375 -0
- package/dist/lib/prompt-catalog.js +2 -573
- package/dist/lib/prompt-validators.js +56 -1
- package/dist/lib/resource-command.js +0 -1
- package/dist/lib/run-npm.js +8 -9
- package/dist/lib/skills-manager.js +75 -11
- package/dist/lib/source-publish.js +287 -0
- package/dist/lib/source-registry.js +188 -0
- package/dist/lib/startup-update.js +12 -8
- package/dist/lib/ui.js +28 -51
- package/dist/locale/en-US.json +8 -3
- package/dist/locale/zh-CN.json +8 -3
- package/package.json +7 -5
|
@@ -6,8 +6,11 @@
|
|
|
6
6
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
|
+
import fs from 'node:fs';
|
|
9
10
|
import fsp from 'node:fs/promises';
|
|
10
11
|
import path from 'node:path';
|
|
12
|
+
import { createGunzip } from 'node:zlib';
|
|
13
|
+
import * as tar from 'tar';
|
|
11
14
|
import { resolveCliHomeDir } from './cli-home.js';
|
|
12
15
|
import { compareVersions } from './self-manager.js';
|
|
13
16
|
import { commandOutput, commandOutputViaFile, run } from './run-npm.js';
|
|
@@ -33,6 +36,15 @@ function resolveSkillsRoot(options = {}) {
|
|
|
33
36
|
function getSkillsCacheRoot(globalRoot) {
|
|
34
37
|
return path.join(globalRoot, 'cache', 'skills');
|
|
35
38
|
}
|
|
39
|
+
function getCachedSkillsPackageDir(cacheRoot) {
|
|
40
|
+
return path.join(cacheRoot, 'node_modules', '@nocobase', 'skills');
|
|
41
|
+
}
|
|
42
|
+
function getCachedSkillsPackRoot(cacheRoot) {
|
|
43
|
+
return path.join(cacheRoot, 'pack');
|
|
44
|
+
}
|
|
45
|
+
function getCachedSkillsExtractRoot(cacheRoot) {
|
|
46
|
+
return path.join(cacheRoot, 'extract');
|
|
47
|
+
}
|
|
36
48
|
export function getManagedSkillsStateFile(workspaceRoot) {
|
|
37
49
|
return path.join(workspaceRoot, 'skills.json');
|
|
38
50
|
}
|
|
@@ -95,7 +107,7 @@ async function readPublishedSkillsVersion(options = {}) {
|
|
|
95
107
|
}
|
|
96
108
|
}
|
|
97
109
|
async function readCachedSkillsVersion(cacheRoot) {
|
|
98
|
-
const packageJsonPath = path.join(cacheRoot, '
|
|
110
|
+
const packageJsonPath = path.join(getCachedSkillsPackageDir(cacheRoot), 'package.json');
|
|
99
111
|
try {
|
|
100
112
|
const content = await fsp.readFile(packageJsonPath, 'utf8');
|
|
101
113
|
const parsed = JSON.parse(content);
|
|
@@ -106,9 +118,59 @@ async function readCachedSkillsVersion(cacheRoot) {
|
|
|
106
118
|
return undefined;
|
|
107
119
|
}
|
|
108
120
|
}
|
|
121
|
+
async function resolvePackedSkillsTarball(packRoot) {
|
|
122
|
+
const entries = await fsp.readdir(packRoot, { withFileTypes: true });
|
|
123
|
+
const tarballs = entries
|
|
124
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.tgz'))
|
|
125
|
+
.map((entry) => path.join(packRoot, entry.name))
|
|
126
|
+
.sort();
|
|
127
|
+
if (tarballs.length === 1) {
|
|
128
|
+
return tarballs[0];
|
|
129
|
+
}
|
|
130
|
+
if (tarballs.length === 0) {
|
|
131
|
+
throw new Error(`npm pack did not produce a local tarball for ${NOCOBASE_SKILLS_PACKAGE_NAME}.`);
|
|
132
|
+
}
|
|
133
|
+
throw new Error(`npm pack produced multiple tarballs for ${NOCOBASE_SKILLS_PACKAGE_NAME}.`);
|
|
134
|
+
}
|
|
135
|
+
async function extractPackedSkillsTarball(tarballPath, cacheRoot, targetVersion) {
|
|
136
|
+
const packageDir = getCachedSkillsPackageDir(cacheRoot);
|
|
137
|
+
const extractRoot = getCachedSkillsExtractRoot(cacheRoot);
|
|
138
|
+
await fsp.rm(extractRoot, { recursive: true, force: true });
|
|
139
|
+
await fsp.mkdir(extractRoot, { recursive: true });
|
|
140
|
+
try {
|
|
141
|
+
await new Promise((resolve, reject) => {
|
|
142
|
+
fs.createReadStream(tarballPath)
|
|
143
|
+
.pipe(createGunzip())
|
|
144
|
+
.pipe(tar.extract({ cwd: extractRoot, strip: 1 }))
|
|
145
|
+
.on('finish', () => resolve())
|
|
146
|
+
.on('error', reject);
|
|
147
|
+
});
|
|
148
|
+
const packageJsonPath = path.join(extractRoot, 'package.json');
|
|
149
|
+
const packageJsonRaw = await fsp.readFile(packageJsonPath, 'utf8');
|
|
150
|
+
const manifest = JSON.parse(packageJsonRaw);
|
|
151
|
+
const packageName = String(manifest.name ?? '').trim();
|
|
152
|
+
const packageVersion = String(manifest.version ?? '').trim();
|
|
153
|
+
if (packageName !== NOCOBASE_SKILLS_PACKAGE_NAME) {
|
|
154
|
+
throw new Error(`packed tarball resolved to ${packageName || '(missing package name)'} instead of ${NOCOBASE_SKILLS_PACKAGE_NAME}.`);
|
|
155
|
+
}
|
|
156
|
+
if (targetVersion && packageVersion !== targetVersion) {
|
|
157
|
+
throw new Error(`packed tarball resolved to version ${packageVersion || '(missing version)'} instead of ${targetVersion}.`);
|
|
158
|
+
}
|
|
159
|
+
await fsp.rm(packageDir, { recursive: true, force: true });
|
|
160
|
+
await fsp.mkdir(path.dirname(packageDir), { recursive: true });
|
|
161
|
+
await fsp.rename(extractRoot, packageDir);
|
|
162
|
+
return packageDir;
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
await fsp.rm(extractRoot, { recursive: true, force: true });
|
|
166
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
167
|
+
throw new Error(`failed to extract ${NOCOBASE_SKILLS_PACKAGE_NAME} tarball: ${message}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
109
170
|
async function prepareLocalSkillsPackage(globalRoot, options = {}, targetVersion) {
|
|
110
171
|
const cacheRoot = getSkillsCacheRoot(globalRoot);
|
|
111
|
-
const packageDir =
|
|
172
|
+
const packageDir = getCachedSkillsPackageDir(cacheRoot);
|
|
173
|
+
const packRoot = getCachedSkillsPackRoot(cacheRoot);
|
|
112
174
|
const packageSpec = targetVersion ? `${NOCOBASE_SKILLS_PACKAGE_NAME}@${targetVersion}` : NOCOBASE_SKILLS_PACKAGE_NAME;
|
|
113
175
|
const cachedVersion = await readCachedSkillsVersion(cacheRoot);
|
|
114
176
|
await fsp.mkdir(cacheRoot, { recursive: true });
|
|
@@ -118,17 +180,19 @@ async function prepareLocalSkillsPackage(globalRoot, options = {}, targetVersion
|
|
|
118
180
|
cleanup: async () => undefined,
|
|
119
181
|
};
|
|
120
182
|
}
|
|
121
|
-
await fsp.rm(
|
|
122
|
-
await
|
|
123
|
-
cwd: cacheRoot,
|
|
124
|
-
stdio: options.verbose ? 'inherit' : 'ignore',
|
|
125
|
-
errorName: 'npm install',
|
|
126
|
-
});
|
|
183
|
+
await fsp.rm(packRoot, { recursive: true, force: true });
|
|
184
|
+
await fsp.mkdir(packRoot, { recursive: true });
|
|
127
185
|
try {
|
|
128
|
-
await
|
|
186
|
+
await (options.runFn ?? run)('npm', ['pack', '--silent', packageSpec], {
|
|
187
|
+
cwd: packRoot,
|
|
188
|
+
stdio: options.verbose ? 'inherit' : 'ignore',
|
|
189
|
+
errorName: 'npm pack',
|
|
190
|
+
});
|
|
191
|
+
const tarballPath = await resolvePackedSkillsTarball(packRoot);
|
|
192
|
+
await extractPackedSkillsTarball(tarballPath, cacheRoot, targetVersion);
|
|
129
193
|
}
|
|
130
|
-
|
|
131
|
-
|
|
194
|
+
finally {
|
|
195
|
+
await fsp.rm(packRoot, { recursive: true, force: true });
|
|
132
196
|
}
|
|
133
197
|
return {
|
|
134
198
|
packageDir,
|
|
@@ -0,0 +1,287 @@
|
|
|
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 path from 'node:path';
|
|
10
|
+
import { commandOutput, resolveProjectCwd, run } from './run-npm.js';
|
|
11
|
+
import { DEFAULT_SOURCE_REGISTRY_PORT, parseSourceRegistryUrl, resolveSourceRegistryInfo } from './source-registry.js';
|
|
12
|
+
function trimValue(value) {
|
|
13
|
+
return String(value ?? '').trim();
|
|
14
|
+
}
|
|
15
|
+
function sanitizeEnvSegment(value) {
|
|
16
|
+
return trimValue(value).replace(/[^A-Za-z0-9]+/g, '').slice(0, 16);
|
|
17
|
+
}
|
|
18
|
+
export async function resolveSourcePublishRegistry(explicitRegistry) {
|
|
19
|
+
const normalized = trimValue(explicitRegistry);
|
|
20
|
+
if (normalized) {
|
|
21
|
+
return normalized;
|
|
22
|
+
}
|
|
23
|
+
const info = await resolveSourceRegistryInfo();
|
|
24
|
+
if (info.status === 'running') {
|
|
25
|
+
return info.url;
|
|
26
|
+
}
|
|
27
|
+
throw new Error([
|
|
28
|
+
'No npm registry was provided for source publish.',
|
|
29
|
+
'Start the local source registry with `nb source registry start`, or pass `--npm-registry <url>` explicitly.',
|
|
30
|
+
].join('\n'));
|
|
31
|
+
}
|
|
32
|
+
export async function resolveGitSha(cwd) {
|
|
33
|
+
return await commandOutput('git', ['rev-parse', '--short', 'HEAD'], {
|
|
34
|
+
cwd,
|
|
35
|
+
errorName: 'git rev-parse',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export async function readRootVersion(cwd) {
|
|
39
|
+
const root = resolveProjectCwd(cwd);
|
|
40
|
+
const { readFile } = await import('node:fs/promises');
|
|
41
|
+
const content = await readFile(path.join(root, 'lerna.json'), 'utf8');
|
|
42
|
+
const parsed = JSON.parse(content);
|
|
43
|
+
const version = trimValue(parsed.version);
|
|
44
|
+
if (!version) {
|
|
45
|
+
throw new Error(`Couldn't read a version from ${path.join(root, 'lerna.json')}.`);
|
|
46
|
+
}
|
|
47
|
+
return version;
|
|
48
|
+
}
|
|
49
|
+
export function buildSnapshotVersion(baseVersion, gitSha, now = new Date()) {
|
|
50
|
+
const yyyy = String(now.getFullYear()).padStart(4, '0');
|
|
51
|
+
const mm = String(now.getMonth() + 1).padStart(2, '0');
|
|
52
|
+
const dd = String(now.getDate()).padStart(2, '0');
|
|
53
|
+
return `${baseVersion}-snapshot.${yyyy}${mm}${dd}.${gitSha}`;
|
|
54
|
+
}
|
|
55
|
+
export async function resolveGitBranch(cwd) {
|
|
56
|
+
const branch = trimValue(await commandOutput('git', ['branch', '--show-current'], {
|
|
57
|
+
cwd,
|
|
58
|
+
errorName: 'git branch --show-current',
|
|
59
|
+
}));
|
|
60
|
+
if (!branch) {
|
|
61
|
+
throw new Error('`nb source publish --snapshot` requires a named Git branch. Detached HEAD is not supported.');
|
|
62
|
+
}
|
|
63
|
+
return branch;
|
|
64
|
+
}
|
|
65
|
+
export async function hasLocalGitChanges(cwd) {
|
|
66
|
+
const output = await commandOutput('git', ['status', '--short', '--untracked-files=all'], {
|
|
67
|
+
cwd,
|
|
68
|
+
errorName: 'git status',
|
|
69
|
+
});
|
|
70
|
+
return trimValue(output).length > 0;
|
|
71
|
+
}
|
|
72
|
+
export function buildSourcePublishBranchName(gitSha, now = new Date()) {
|
|
73
|
+
const yyyy = String(now.getFullYear()).padStart(4, '0');
|
|
74
|
+
const mm = String(now.getMonth() + 1).padStart(2, '0');
|
|
75
|
+
const dd = String(now.getDate()).padStart(2, '0');
|
|
76
|
+
const hh = String(now.getHours()).padStart(2, '0');
|
|
77
|
+
const mi = String(now.getMinutes()).padStart(2, '0');
|
|
78
|
+
const ss = String(now.getSeconds()).padStart(2, '0');
|
|
79
|
+
return `nb/source-publish-${yyyy}${mm}${dd}${hh}${mi}${ss}-${gitSha}`;
|
|
80
|
+
}
|
|
81
|
+
async function runGit(args, options) {
|
|
82
|
+
await run('git', args, {
|
|
83
|
+
cwd: options?.cwd,
|
|
84
|
+
stdio: options?.stdio,
|
|
85
|
+
env: options?.env,
|
|
86
|
+
errorName: options?.errorName ?? `git ${args.join(' ')}`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
async function commitSourceSnapshotVersion(params) {
|
|
90
|
+
await runGit(['add', '-A'], {
|
|
91
|
+
cwd: params.cwd,
|
|
92
|
+
stdio: params.stdio,
|
|
93
|
+
errorName: 'git add',
|
|
94
|
+
});
|
|
95
|
+
await runGit(['commit', '--no-verify', '-m', `chore(source-publish): ${params.version}`], {
|
|
96
|
+
cwd: params.cwd,
|
|
97
|
+
stdio: params.stdio,
|
|
98
|
+
errorName: 'git commit',
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async function createSourcePublishStash(params) {
|
|
102
|
+
if (!(await hasLocalGitChanges(params.cwd))) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
await runGit(['stash', 'push', '-u', '-m', params.label], {
|
|
106
|
+
cwd: params.cwd,
|
|
107
|
+
stdio: params.stdio,
|
|
108
|
+
errorName: 'git stash push',
|
|
109
|
+
});
|
|
110
|
+
return {
|
|
111
|
+
commit: trimValue(await commandOutput('git', ['rev-parse', '--verify', 'refs/stash'], {
|
|
112
|
+
cwd: params.cwd,
|
|
113
|
+
errorName: 'git rev-parse refs/stash',
|
|
114
|
+
})),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
async function resolveSourcePublishStashReference(params) {
|
|
118
|
+
const output = trimValue(await commandOutput('git', ['stash', 'list', '--format=%gd%x00%H'], {
|
|
119
|
+
cwd: params.cwd,
|
|
120
|
+
errorName: 'git stash list',
|
|
121
|
+
}));
|
|
122
|
+
for (const line of output.split('\n')) {
|
|
123
|
+
const [reference, commit] = line.split('\x00');
|
|
124
|
+
if (trimValue(commit) === params.stash.commit) {
|
|
125
|
+
return trimValue(reference);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
throw new Error(`Couldn't locate the saved stash for source publish: ${params.stash.commit}`);
|
|
129
|
+
}
|
|
130
|
+
function buildSourcePublishRecoveryError(params) {
|
|
131
|
+
const originalMessage = params.originalError instanceof Error
|
|
132
|
+
? params.originalError.message
|
|
133
|
+
: String(params.originalError);
|
|
134
|
+
const cleanupMessage = params.cleanupError instanceof Error
|
|
135
|
+
? params.cleanupError.message
|
|
136
|
+
: String(params.cleanupError);
|
|
137
|
+
const recoveryHints = [
|
|
138
|
+
`Project root: ${params.projectRoot}`,
|
|
139
|
+
`Temporary branch: ${params.temporaryBranch}`,
|
|
140
|
+
];
|
|
141
|
+
if (params.stash) {
|
|
142
|
+
recoveryHints.push(`Saved stash commit: ${params.stash.commit}`);
|
|
143
|
+
}
|
|
144
|
+
return new Error([
|
|
145
|
+
originalMessage,
|
|
146
|
+
'',
|
|
147
|
+
'Cleanup also failed after the publish attempt.',
|
|
148
|
+
`Cleanup error: ${cleanupMessage}`,
|
|
149
|
+
...recoveryHints,
|
|
150
|
+
].join('\n'));
|
|
151
|
+
}
|
|
152
|
+
export async function publishSourceSnapshot(params) {
|
|
153
|
+
const projectRoot = resolveProjectCwd(params.cwd);
|
|
154
|
+
const npmRegistry = await resolveSourcePublishRegistry(params.npmRegistry);
|
|
155
|
+
const originalBranch = await resolveGitBranch(projectRoot);
|
|
156
|
+
const gitSha = trimValue(await resolveGitSha(projectRoot));
|
|
157
|
+
const baseVersion = await readRootVersion(projectRoot);
|
|
158
|
+
const version = buildSnapshotVersion(baseVersion, gitSha, params.now);
|
|
159
|
+
const temporaryBranch = buildSourcePublishBranchName(gitSha, params.now);
|
|
160
|
+
const stdio = params.verbose ? 'inherit' : 'ignore';
|
|
161
|
+
let stash;
|
|
162
|
+
let onTemporaryBranch = false;
|
|
163
|
+
let branchCreated = false;
|
|
164
|
+
let publishError;
|
|
165
|
+
let result;
|
|
166
|
+
try {
|
|
167
|
+
stash = await createSourcePublishStash({
|
|
168
|
+
cwd: projectRoot,
|
|
169
|
+
label: temporaryBranch,
|
|
170
|
+
stdio,
|
|
171
|
+
});
|
|
172
|
+
await runGit(['switch', '-c', temporaryBranch], {
|
|
173
|
+
cwd: projectRoot,
|
|
174
|
+
stdio,
|
|
175
|
+
errorName: 'git switch',
|
|
176
|
+
});
|
|
177
|
+
branchCreated = true;
|
|
178
|
+
onTemporaryBranch = true;
|
|
179
|
+
if (stash) {
|
|
180
|
+
await runGit(['stash', 'apply', '--index', await resolveSourcePublishStashReference({
|
|
181
|
+
cwd: projectRoot,
|
|
182
|
+
stash,
|
|
183
|
+
})], {
|
|
184
|
+
cwd: projectRoot,
|
|
185
|
+
stdio,
|
|
186
|
+
errorName: 'git stash apply',
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
await run('yarn', ['lerna', 'version', version, '--force-publish=*', '--no-git-tag-version', '-y'], {
|
|
190
|
+
cwd: projectRoot,
|
|
191
|
+
errorName: 'lerna version',
|
|
192
|
+
stdio,
|
|
193
|
+
});
|
|
194
|
+
await commitSourceSnapshotVersion({
|
|
195
|
+
cwd: projectRoot,
|
|
196
|
+
version,
|
|
197
|
+
stdio,
|
|
198
|
+
});
|
|
199
|
+
await run('yarn', ['lerna', 'publish', 'from-package', '--registry', npmRegistry, '--dist-tag', 'local', '--yes', '--no-verify-access', '--git-head', gitSha], {
|
|
200
|
+
cwd: projectRoot,
|
|
201
|
+
errorName: 'lerna publish',
|
|
202
|
+
stdio,
|
|
203
|
+
env: {
|
|
204
|
+
npm_config_registry: npmRegistry,
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
result = {
|
|
208
|
+
version,
|
|
209
|
+
npmRegistry,
|
|
210
|
+
gitSha,
|
|
211
|
+
projectRoot,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
publishError = error;
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
if (onTemporaryBranch) {
|
|
219
|
+
await runGit(['reset', '--hard', 'HEAD'], {
|
|
220
|
+
cwd: projectRoot,
|
|
221
|
+
stdio,
|
|
222
|
+
errorName: 'git reset --hard',
|
|
223
|
+
});
|
|
224
|
+
await runGit(['clean', '-fd'], {
|
|
225
|
+
cwd: projectRoot,
|
|
226
|
+
stdio,
|
|
227
|
+
errorName: 'git clean -fd',
|
|
228
|
+
});
|
|
229
|
+
await runGit(['switch', originalBranch], {
|
|
230
|
+
cwd: projectRoot,
|
|
231
|
+
stdio,
|
|
232
|
+
errorName: 'git switch',
|
|
233
|
+
});
|
|
234
|
+
onTemporaryBranch = false;
|
|
235
|
+
}
|
|
236
|
+
if (stash) {
|
|
237
|
+
await runGit(['stash', 'pop', '--index', await resolveSourcePublishStashReference({
|
|
238
|
+
cwd: projectRoot,
|
|
239
|
+
stash,
|
|
240
|
+
})], {
|
|
241
|
+
cwd: projectRoot,
|
|
242
|
+
stdio,
|
|
243
|
+
errorName: 'git stash pop',
|
|
244
|
+
});
|
|
245
|
+
stash = undefined;
|
|
246
|
+
}
|
|
247
|
+
if (branchCreated) {
|
|
248
|
+
await runGit(['branch', '-D', temporaryBranch], {
|
|
249
|
+
cwd: projectRoot,
|
|
250
|
+
stdio,
|
|
251
|
+
errorName: 'git branch -D',
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
catch (cleanupError) {
|
|
256
|
+
if (publishError) {
|
|
257
|
+
throw buildSourcePublishRecoveryError({
|
|
258
|
+
originalError: publishError,
|
|
259
|
+
cleanupError,
|
|
260
|
+
stash,
|
|
261
|
+
temporaryBranch,
|
|
262
|
+
projectRoot,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
throw new Error([
|
|
266
|
+
'The source snapshot was published, but local Git cleanup failed afterwards.',
|
|
267
|
+
`Cleanup error: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`,
|
|
268
|
+
`Project root: ${projectRoot}`,
|
|
269
|
+
`Temporary branch: ${temporaryBranch}`,
|
|
270
|
+
...(stash ? [`Saved stash commit: ${stash.commit}`] : []),
|
|
271
|
+
].join('\n'));
|
|
272
|
+
}
|
|
273
|
+
if (publishError) {
|
|
274
|
+
throw publishError;
|
|
275
|
+
}
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
export function buildSuggestedInitCommand(result) {
|
|
279
|
+
const { host, port } = parseSourceRegistryUrl(result.npmRegistry);
|
|
280
|
+
const normalizedRegistry = result.npmRegistry || `http://${host}:${port || DEFAULT_SOURCE_REGISTRY_PORT}`;
|
|
281
|
+
const suggestedEnv = ['snapshot', sanitizeEnvSegment(result.gitSha)].filter(Boolean).join('');
|
|
282
|
+
return [
|
|
283
|
+
`nb init --env ${suggestedEnv} --yes --source npm`,
|
|
284
|
+
`--version ${result.version}`,
|
|
285
|
+
`--npm-registry=${normalizedRegistry}`,
|
|
286
|
+
].join(' ');
|
|
287
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
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 fsp from 'node:fs/promises';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { commandOutput, commandSucceeds, resolveCwd, resolveProjectCwd, run } from './run-npm.js';
|
|
12
|
+
import { resolveCliHomeRoot } from './cli-home.js';
|
|
13
|
+
export const DEFAULT_SOURCE_REGISTRY_HOST = '127.0.0.1';
|
|
14
|
+
export const DEFAULT_SOURCE_REGISTRY_PORT = 4873;
|
|
15
|
+
export const DEFAULT_SOURCE_REGISTRY_CONTAINER_NAME = 'nb-source-registry';
|
|
16
|
+
export const DEFAULT_SOURCE_REGISTRY_IMAGE = 'verdaccio/verdaccio';
|
|
17
|
+
export function parseSourceRegistryUrl(url) {
|
|
18
|
+
const parsed = new URL(url);
|
|
19
|
+
const host = trimValue(parsed.hostname) || DEFAULT_SOURCE_REGISTRY_HOST;
|
|
20
|
+
const portText = trimValue(parsed.port);
|
|
21
|
+
const port = portText ? Number(portText) : DEFAULT_SOURCE_REGISTRY_PORT;
|
|
22
|
+
return {
|
|
23
|
+
host,
|
|
24
|
+
port: Number.isFinite(port) && port > 0 ? port : DEFAULT_SOURCE_REGISTRY_PORT,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function trimValue(value) {
|
|
28
|
+
return String(value ?? '').trim();
|
|
29
|
+
}
|
|
30
|
+
function asPosixPathForDockerMount(value) {
|
|
31
|
+
return resolveCwd(value).replace(/\\/g, '/');
|
|
32
|
+
}
|
|
33
|
+
export function resolveSourceRegistryRootDir() {
|
|
34
|
+
return path.join(resolveCliHomeRoot(), 'verdaccio');
|
|
35
|
+
}
|
|
36
|
+
export function resolveSourceRegistryConfigPath() {
|
|
37
|
+
return path.join(resolveSourceRegistryRootDir(), 'config.yaml');
|
|
38
|
+
}
|
|
39
|
+
export function resolveSourceRegistryStorageDir() {
|
|
40
|
+
return path.join(resolveSourceRegistryRootDir(), 'storage');
|
|
41
|
+
}
|
|
42
|
+
export function resolveSourceRegistryUrl(host = DEFAULT_SOURCE_REGISTRY_HOST, port = DEFAULT_SOURCE_REGISTRY_PORT) {
|
|
43
|
+
return `http://${host}:${port}`;
|
|
44
|
+
}
|
|
45
|
+
export function getSourceRegistryInfo() {
|
|
46
|
+
const host = DEFAULT_SOURCE_REGISTRY_HOST;
|
|
47
|
+
const port = DEFAULT_SOURCE_REGISTRY_PORT;
|
|
48
|
+
const rootDir = resolveSourceRegistryRootDir();
|
|
49
|
+
return {
|
|
50
|
+
containerName: DEFAULT_SOURCE_REGISTRY_CONTAINER_NAME,
|
|
51
|
+
image: DEFAULT_SOURCE_REGISTRY_IMAGE,
|
|
52
|
+
host,
|
|
53
|
+
port,
|
|
54
|
+
url: resolveSourceRegistryUrl(host, port),
|
|
55
|
+
rootDir,
|
|
56
|
+
configPath: resolveSourceRegistryConfigPath(),
|
|
57
|
+
storageDir: resolveSourceRegistryStorageDir(),
|
|
58
|
+
status: 'missing',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function resolveSourceRegistryTemplatePath(cwd) {
|
|
62
|
+
return path.join(resolveProjectCwd(cwd), 'config.yaml');
|
|
63
|
+
}
|
|
64
|
+
function applySourceRegistryTemplateOverrides(template) {
|
|
65
|
+
return template
|
|
66
|
+
.replace(/\r\n/g, '\n')
|
|
67
|
+
.replace(/^storage:\s*.+$/m, 'storage: /verdaccio/storage')
|
|
68
|
+
.replace(/^(\s*)file:\s*\.\/*htpasswd\s*$/m, '$1file: /verdaccio/storage/htpasswd')
|
|
69
|
+
.replace(/^(\s*)publish:\s+\$authenticated\s*$/gm, '$1publish: $all')
|
|
70
|
+
.replace(/^(\s*)unpublish:\s+\$authenticated\s*$/gm, '$1unpublish: $all');
|
|
71
|
+
}
|
|
72
|
+
async function buildFallbackSourceRegistryConfigTemplate() {
|
|
73
|
+
return [
|
|
74
|
+
'storage: ./storage',
|
|
75
|
+
'auth:',
|
|
76
|
+
' htpasswd:',
|
|
77
|
+
' file: ./htpasswd',
|
|
78
|
+
'uplinks:',
|
|
79
|
+
' npmjs:',
|
|
80
|
+
' url: https://registry.npmmirror.com/',
|
|
81
|
+
'packages:',
|
|
82
|
+
" '@*/*':",
|
|
83
|
+
' access: $all',
|
|
84
|
+
' publish: $authenticated',
|
|
85
|
+
' unpublish: $authenticated',
|
|
86
|
+
' proxy: npmjs',
|
|
87
|
+
" '**':",
|
|
88
|
+
' access: $all',
|
|
89
|
+
' publish: $authenticated',
|
|
90
|
+
' unpublish: $authenticated',
|
|
91
|
+
' proxy: npmjs',
|
|
92
|
+
'server:',
|
|
93
|
+
' keepAliveTimeout: 60',
|
|
94
|
+
' dotfiles: ignore',
|
|
95
|
+
'max_body_size: 100mb',
|
|
96
|
+
'middlewares:',
|
|
97
|
+
' audit:',
|
|
98
|
+
' enabled: true',
|
|
99
|
+
'',
|
|
100
|
+
].join('\n');
|
|
101
|
+
}
|
|
102
|
+
export async function buildSourceRegistryConfig(cwd) {
|
|
103
|
+
const templatePath = resolveSourceRegistryTemplatePath(cwd);
|
|
104
|
+
try {
|
|
105
|
+
const template = await fsp.readFile(templatePath, 'utf8');
|
|
106
|
+
return applySourceRegistryTemplateOverrides(template);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
const fallback = await buildFallbackSourceRegistryConfigTemplate();
|
|
110
|
+
return applySourceRegistryTemplateOverrides(fallback);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export async function ensureSourceRegistryFiles(cwd) {
|
|
114
|
+
const info = getSourceRegistryInfo();
|
|
115
|
+
await fsp.mkdir(info.storageDir, { recursive: true });
|
|
116
|
+
await fsp.mkdir(info.rootDir, { recursive: true });
|
|
117
|
+
await fsp.writeFile(info.configPath, await buildSourceRegistryConfig(cwd), 'utf8');
|
|
118
|
+
return info;
|
|
119
|
+
}
|
|
120
|
+
export async function sourceRegistryContainerExists(containerName = DEFAULT_SOURCE_REGISTRY_CONTAINER_NAME) {
|
|
121
|
+
return await commandSucceeds('docker', ['container', 'inspect', containerName]);
|
|
122
|
+
}
|
|
123
|
+
export async function sourceRegistryContainerIsRunning(containerName = DEFAULT_SOURCE_REGISTRY_CONTAINER_NAME) {
|
|
124
|
+
try {
|
|
125
|
+
const output = await commandOutput('docker', ['inspect', '--format', '{{.State.Running}}', containerName], { errorName: 'docker inspect' });
|
|
126
|
+
return trimValue(output) === 'true';
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export async function resolveSourceRegistryInfo() {
|
|
133
|
+
const base = getSourceRegistryInfo();
|
|
134
|
+
if (!(await sourceRegistryContainerExists(base.containerName))) {
|
|
135
|
+
return base;
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
...base,
|
|
139
|
+
status: (await sourceRegistryContainerIsRunning(base.containerName)) ? 'running' : 'stopped',
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
export async function startSourceRegistry(options) {
|
|
143
|
+
const info = await ensureSourceRegistryFiles(options?.cwd);
|
|
144
|
+
const exists = await sourceRegistryContainerExists(info.containerName);
|
|
145
|
+
if (exists) {
|
|
146
|
+
if (await sourceRegistryContainerIsRunning(info.containerName)) {
|
|
147
|
+
return 'already-running';
|
|
148
|
+
}
|
|
149
|
+
await run('docker', ['start', info.containerName], {
|
|
150
|
+
errorName: 'docker start',
|
|
151
|
+
stdio: options?.stdio,
|
|
152
|
+
});
|
|
153
|
+
return 'started';
|
|
154
|
+
}
|
|
155
|
+
const configMount = `${asPosixPathForDockerMount(info.configPath)}:/verdaccio/conf/config.yaml`;
|
|
156
|
+
const storageMount = `${asPosixPathForDockerMount(info.storageDir)}:/verdaccio/storage`;
|
|
157
|
+
await run('docker', [
|
|
158
|
+
'run',
|
|
159
|
+
'-d',
|
|
160
|
+
'--name',
|
|
161
|
+
info.containerName,
|
|
162
|
+
'-p',
|
|
163
|
+
`${info.port}:4873`,
|
|
164
|
+
'-v',
|
|
165
|
+
configMount,
|
|
166
|
+
'-v',
|
|
167
|
+
storageMount,
|
|
168
|
+
info.image,
|
|
169
|
+
], {
|
|
170
|
+
errorName: 'docker run',
|
|
171
|
+
stdio: options?.stdio,
|
|
172
|
+
});
|
|
173
|
+
return 'started';
|
|
174
|
+
}
|
|
175
|
+
export async function stopSourceRegistry(options) {
|
|
176
|
+
const info = getSourceRegistryInfo();
|
|
177
|
+
if (!(await sourceRegistryContainerExists(info.containerName))) {
|
|
178
|
+
return 'already-stopped';
|
|
179
|
+
}
|
|
180
|
+
if (!(await sourceRegistryContainerIsRunning(info.containerName))) {
|
|
181
|
+
return 'already-stopped';
|
|
182
|
+
}
|
|
183
|
+
await run('docker', ['stop', info.containerName], {
|
|
184
|
+
errorName: 'docker stop',
|
|
185
|
+
stdio: options?.stdio,
|
|
186
|
+
});
|
|
187
|
+
return 'stopped';
|
|
188
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import fs from 'node:fs/promises';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
|
-
import
|
|
12
|
+
import { confirm } from "./inquirer.js";
|
|
13
13
|
import { inspectSelfInstall, inspectSelfStatus, } from './self-manager.js';
|
|
14
14
|
import { inspectSkillsStatus } from './skills-manager.js';
|
|
15
15
|
import { resolveCliHomeDir } from './cli-home.js';
|
|
@@ -264,13 +264,17 @@ export async function maybeRunStartupUpdatePrompt(argv) {
|
|
|
264
264
|
await markChecked();
|
|
265
265
|
return { kind: 'warned' };
|
|
266
266
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
267
|
+
let answer = false;
|
|
268
|
+
try {
|
|
269
|
+
answer = await confirm({
|
|
270
|
+
message: buildPromptMessage(selfStatus, skillsStatus),
|
|
271
|
+
default: true,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
answer = false;
|
|
276
|
+
}
|
|
277
|
+
if (!answer) {
|
|
274
278
|
printWarning(buildDeclinedWarning(selfStatus, skillsStatus));
|
|
275
279
|
await markChecked();
|
|
276
280
|
return { kind: 'declined' };
|