listpage_cli 0.0.293 → 0.0.295
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/adapters/cli-interaction.js +172 -0
- package/bin/adapters/node-fs-adapter.js +109 -0
- package/bin/app/dispatch.js +15 -0
- package/bin/app/execute.js +33 -0
- package/bin/app/parse-args.js +39 -0
- package/bin/cli.js +61 -75
- package/bin/commands/build-project-command.js +9 -0
- package/bin/commands/deploy-project-command.js +9 -0
- package/bin/commands/init-command.js +9 -0
- package/bin/commands/install-skill-command.js +9 -0
- package/bin/copy.js +14 -126
- package/bin/domain/command-result.js +34 -0
- package/bin/domain/interaction-result.js +14 -0
- package/bin/domain/package-name.js +12 -0
- package/bin/ports/build-project-command.js +2 -0
- package/bin/ports/deploy-project-command.js +2 -0
- package/bin/ports/filesystem-capability.js +2 -0
- package/bin/ports/fs-port.js +22 -0
- package/bin/ports/init-command.js +2 -0
- package/bin/ports/install-skill-command.js +2 -0
- package/bin/prompts.js +105 -16
- package/bin/services/artifact-validator.js +47 -0
- package/bin/services/build-project-service.js +190 -0
- package/bin/services/command-runner.js +45 -0
- package/bin/services/config-loader.js +113 -0
- package/bin/services/deploy-project-service.js +237 -0
- package/bin/services/filesystem-capability-service.js +137 -0
- package/bin/services/init-service.js +64 -0
- package/bin/services/install-skill-service.js +34 -0
- package/bin/shared/json-with-comments.js +9 -0
- package/package.json +6 -4
- package/templates/backend-template/package.json.tmpl +1 -1
- package/templates/frontend-template/package.json.tmpl +2 -2
- package/templates/package-app-template/package.json +1 -1
- package/templates/skills-template/listpage/examples.md +565 -0
- package/skills/listpage/examples.md +0 -243
- package/templates/rush-template/docs/ListPage-AI/347/224/237/346/210/220/350/247/204/350/214/203.md +0 -305
- /package/{skills → templates/skills-template}/listpage/SKILL.md +0 -0
- /package/{skills → templates/skills-template}/listpage/api.md +0 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runBuildProjectFlow = runBuildProjectFlow;
|
|
7
|
+
const command_result_1 = require("../domain/command-result");
|
|
8
|
+
const config_loader_1 = require("./config-loader");
|
|
9
|
+
const command_runner_1 = require("./command-runner");
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const node_fs_1 = require("node:fs");
|
|
12
|
+
const CONFIG_ERROR_CODE_MAP = {
|
|
13
|
+
CONFIG_NOT_FOUND: "E_CONFIG_NOT_FOUND",
|
|
14
|
+
CONFIG_READ_FAILED: "E_CONFIG_READ_FAILED",
|
|
15
|
+
CONFIG_INVALID_JSON: "E_CONFIG_INVALID_JSON",
|
|
16
|
+
CONFIG_REQUIRED_FIELD_MISSING: "E_CONFIG_REQUIRED_FIELD_MISSING",
|
|
17
|
+
};
|
|
18
|
+
async function runBuildProjectFlow(deps) {
|
|
19
|
+
const projectRoot = deps.fs.cwd();
|
|
20
|
+
const configPath = (0, config_loader_1.getDefaultConfigPath)(projectRoot, deps.fs.join);
|
|
21
|
+
let configResult;
|
|
22
|
+
try {
|
|
23
|
+
configResult = (0, config_loader_1.loadProjectConfig)({
|
|
24
|
+
readText: deps.fs.readText,
|
|
25
|
+
}, configPath);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error instanceof config_loader_1.ConfigLoaderError) {
|
|
29
|
+
return (0, command_result_1.commandError)(error.message, CONFIG_ERROR_CODE_MAP[error.code], 1);
|
|
30
|
+
}
|
|
31
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
32
|
+
return (0, command_result_1.commandError)(message, command_result_1.COMMAND_ERROR_CODES.invalidPath, 1);
|
|
33
|
+
}
|
|
34
|
+
if (deps.executeBuild) {
|
|
35
|
+
try {
|
|
36
|
+
const result = await deps.executeBuild({
|
|
37
|
+
projectRoot,
|
|
38
|
+
configPath: configResult.configPath,
|
|
39
|
+
config: configResult.config,
|
|
40
|
+
});
|
|
41
|
+
return normalizeBuildResult(result);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
return (0, command_result_1.commandError)(formatExecuteBuildThrownError(error), command_result_1.COMMAND_ERROR_CODES.executionFailed, getNonZeroExitCode(error));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const commandRunner = (0, command_runner_1.createCommandRunner)();
|
|
48
|
+
const buildConfig = toBuildProjectConfig(configResult.config);
|
|
49
|
+
const buildTargets = getBuildTargets(projectRoot, buildConfig);
|
|
50
|
+
try {
|
|
51
|
+
for (const target of buildTargets) {
|
|
52
|
+
const output = await commandRunner({
|
|
53
|
+
command: "npm",
|
|
54
|
+
args: ["run", "build"],
|
|
55
|
+
cwd: target.projectDir,
|
|
56
|
+
});
|
|
57
|
+
if (output.exitCode !== 0) {
|
|
58
|
+
return (0, command_result_1.commandError)(`[${command_runner_1.COMMAND_RUNNER_STAGE}] 构建命令执行失败: npm run build (cwd=${target.projectDir}, exit=${output.exitCode})`, command_result_1.COMMAND_ERROR_CODES.executionFailed, output.exitCode);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
copyBuildArtifacts(projectRoot, buildTargets);
|
|
62
|
+
copyConfiguredFiles(projectRoot, buildConfig.copyFiles);
|
|
63
|
+
return (0, command_result_1.commandOk)();
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (error instanceof command_runner_1.ExecutionError) {
|
|
67
|
+
return (0, command_result_1.commandError)(error.message, command_result_1.COMMAND_ERROR_CODES.executionFailed, error.exitCode);
|
|
68
|
+
}
|
|
69
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
70
|
+
return (0, command_result_1.commandError)(`[${command_runner_1.COMMAND_RUNNER_STAGE}] 构建命令执行异常: npm run build (${message})`, command_result_1.COMMAND_ERROR_CODES.executionFailed, 1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function getBuildTargets(projectRoot, config) {
|
|
74
|
+
return [...config.frontend, config.backend].map((item) => ({
|
|
75
|
+
...item,
|
|
76
|
+
projectDir: resolveBuildTarget(projectRoot, item.projectDir),
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
function toBuildProjectConfig(config) {
|
|
80
|
+
const frontendRaw = config.frontend;
|
|
81
|
+
const backendRaw = config.backend;
|
|
82
|
+
return {
|
|
83
|
+
frontend: frontendRaw.map((item) => toBuildTarget(item)),
|
|
84
|
+
backend: toBuildTarget(backendRaw),
|
|
85
|
+
copyFiles: toCopySpecs(config.copyFiles),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function toBuildTarget(item) {
|
|
89
|
+
const projectDir = asRequiredString(item.projectDir, "projectDir");
|
|
90
|
+
const packageSubDir = asRequiredString(item.packageSubDir, "packageSubDir");
|
|
91
|
+
const buildArtifactDirName = asOptionalString(item.buildArtifactDirName, "dist");
|
|
92
|
+
const artifactAlias = asOptionalString(item.artifactAlias, "dist");
|
|
93
|
+
return {
|
|
94
|
+
projectDir,
|
|
95
|
+
packageSubDir,
|
|
96
|
+
buildArtifactDirName,
|
|
97
|
+
artifactAlias,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function asRequiredString(value, fieldName) {
|
|
101
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
102
|
+
return value.trim();
|
|
103
|
+
}
|
|
104
|
+
throw new Error(`配置字段无效: ${fieldName}`);
|
|
105
|
+
}
|
|
106
|
+
function asOptionalString(value, fallback) {
|
|
107
|
+
if (typeof value !== "string") {
|
|
108
|
+
return fallback;
|
|
109
|
+
}
|
|
110
|
+
const normalized = value.trim();
|
|
111
|
+
return normalized === "" ? fallback : normalized;
|
|
112
|
+
}
|
|
113
|
+
function resolveBuildTarget(projectRoot, projectDir) {
|
|
114
|
+
return node_path_1.default.isAbsolute(projectDir)
|
|
115
|
+
? projectDir
|
|
116
|
+
: node_path_1.default.resolve(projectRoot, projectDir);
|
|
117
|
+
}
|
|
118
|
+
function toCopySpecs(value) {
|
|
119
|
+
if (!Array.isArray(value)) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
return value.map((item, index) => toCopySpec(item, index));
|
|
123
|
+
}
|
|
124
|
+
function toCopySpec(item, index) {
|
|
125
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
126
|
+
throw new Error(`配置字段无效: copyFiles[${index}]`);
|
|
127
|
+
}
|
|
128
|
+
const source = asRequiredString(item.source, `copyFiles[${index}].source`);
|
|
129
|
+
const dest = asRequiredString(item.dest, `copyFiles[${index}].dest`);
|
|
130
|
+
return { source, dest };
|
|
131
|
+
}
|
|
132
|
+
function copyBuildArtifacts(projectRoot, targets) {
|
|
133
|
+
for (const target of targets) {
|
|
134
|
+
const sourceDir = node_path_1.default.resolve(target.projectDir, target.buildArtifactDirName);
|
|
135
|
+
if (!(0, node_fs_1.existsSync)(sourceDir)) {
|
|
136
|
+
throw new Error(`[${command_runner_1.COMMAND_RUNNER_STAGE}] 构建产物不存在: ${sourceDir}`);
|
|
137
|
+
}
|
|
138
|
+
const targetDir = node_path_1.default.resolve(projectRoot, ".listpage", "output", target.packageSubDir, target.artifactAlias);
|
|
139
|
+
if ((0, node_fs_1.existsSync)(targetDir)) {
|
|
140
|
+
(0, node_fs_1.rmSync)(targetDir, { recursive: true, force: true });
|
|
141
|
+
}
|
|
142
|
+
(0, node_fs_1.mkdirSync)(node_path_1.default.dirname(targetDir), { recursive: true });
|
|
143
|
+
(0, node_fs_1.cpSync)(sourceDir, targetDir, { recursive: true });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function copyConfiguredFiles(projectRoot, copySpecs) {
|
|
147
|
+
for (const spec of copySpecs) {
|
|
148
|
+
const sourcePath = node_path_1.default.resolve(projectRoot, spec.source);
|
|
149
|
+
if (!(0, node_fs_1.existsSync)(sourcePath)) {
|
|
150
|
+
throw new Error(`[${command_runner_1.COMMAND_RUNNER_STAGE}] 复制源不存在: ${sourcePath}`);
|
|
151
|
+
}
|
|
152
|
+
const targetPath = node_path_1.default.resolve(projectRoot, ".listpage", "output", spec.dest);
|
|
153
|
+
if ((0, node_fs_1.existsSync)(targetPath)) {
|
|
154
|
+
(0, node_fs_1.rmSync)(targetPath, { recursive: true, force: true });
|
|
155
|
+
}
|
|
156
|
+
(0, node_fs_1.mkdirSync)(node_path_1.default.dirname(targetPath), { recursive: true });
|
|
157
|
+
const sourceIsDirectory = (0, node_fs_1.statSync)(sourcePath).isDirectory();
|
|
158
|
+
(0, node_fs_1.cpSync)(sourcePath, targetPath, { recursive: sourceIsDirectory });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function normalizeBuildResult(result) {
|
|
162
|
+
if (result.ok) {
|
|
163
|
+
if (result.exitCode === 0) {
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
return (0, command_result_1.commandError)(`[${command_runner_1.COMMAND_RUNNER_STAGE}] 构建命令执行失败: 执行器返回成功但退出码非零 (exit=${result.exitCode})`, command_result_1.COMMAND_ERROR_CODES.executionFailed, result.exitCode);
|
|
167
|
+
}
|
|
168
|
+
if (result.exitCode !== 0) {
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
return (0, command_result_1.commandError)(result.message ??
|
|
172
|
+
`[${command_runner_1.COMMAND_RUNNER_STAGE}] 构建命令执行失败: 执行器返回失败但退出码为 0`, result.errorCode ?? command_result_1.COMMAND_ERROR_CODES.executionFailed, 1);
|
|
173
|
+
}
|
|
174
|
+
function formatExecuteBuildThrownError(error) {
|
|
175
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
176
|
+
return `[${command_runner_1.COMMAND_RUNNER_STAGE}] 构建命令执行异常: ${message}`;
|
|
177
|
+
}
|
|
178
|
+
function getNonZeroExitCode(error) {
|
|
179
|
+
if (!error || typeof error !== "object") {
|
|
180
|
+
return 1;
|
|
181
|
+
}
|
|
182
|
+
const candidate = error;
|
|
183
|
+
if (typeof candidate.exitCode === "number" && candidate.exitCode !== 0) {
|
|
184
|
+
return candidate.exitCode;
|
|
185
|
+
}
|
|
186
|
+
if (typeof candidate.status === "number" && candidate.status !== 0) {
|
|
187
|
+
return candidate.status;
|
|
188
|
+
}
|
|
189
|
+
return 1;
|
|
190
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ExecutionError = exports.COMMAND_RUNNER_STAGE = void 0;
|
|
4
|
+
exports.createCommandRunner = createCommandRunner;
|
|
5
|
+
const node_child_process_1 = require("node:child_process");
|
|
6
|
+
exports.COMMAND_RUNNER_STAGE = "build";
|
|
7
|
+
class ExecutionError extends Error {
|
|
8
|
+
constructor(code, stage, command, exitCode, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "ExecutionError";
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.stage = stage;
|
|
13
|
+
this.command = command;
|
|
14
|
+
this.exitCode = exitCode;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.ExecutionError = ExecutionError;
|
|
18
|
+
function createCommandRunner(deps) {
|
|
19
|
+
const spawnSyncImpl = deps?.spawnSyncImpl ?? node_child_process_1.spawnSync;
|
|
20
|
+
const stage = deps?.stage ?? exports.COMMAND_RUNNER_STAGE;
|
|
21
|
+
return async (input) => {
|
|
22
|
+
const args = input.args ?? [];
|
|
23
|
+
const output = spawnSyncImpl(input.command, args, {
|
|
24
|
+
cwd: input.cwd,
|
|
25
|
+
env: {
|
|
26
|
+
...process.env,
|
|
27
|
+
...input.env,
|
|
28
|
+
},
|
|
29
|
+
shell: true,
|
|
30
|
+
stdio: "inherit",
|
|
31
|
+
});
|
|
32
|
+
if (output.error) {
|
|
33
|
+
const maybeNodeError = output.error;
|
|
34
|
+
const isNotFound = maybeNodeError.code === "ENOENT";
|
|
35
|
+
const code = isNotFound
|
|
36
|
+
? "COMMAND_NOT_FOUND"
|
|
37
|
+
: "COMMAND_EXECUTION_FAILED";
|
|
38
|
+
const messagePrefix = isNotFound ? "命令不存在" : "命令执行异常";
|
|
39
|
+
throw new ExecutionError(code, stage, input.command, 1, `[${stage}][${code}] ${messagePrefix}: ${input.command} (${output.error.message})`);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
exitCode: output.status ?? 1,
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConfigLoaderError = exports.CONFIG_LOADER_STAGE = void 0;
|
|
4
|
+
exports.getDefaultConfigPath = getDefaultConfigPath;
|
|
5
|
+
exports.loadProjectConfig = loadProjectConfig;
|
|
6
|
+
exports.CONFIG_LOADER_STAGE = "config";
|
|
7
|
+
const DEFAULT_CONFIG_NAME = "listpage.config.json";
|
|
8
|
+
const REQUIRED_CONFIG_FIELDS = ["backend.projectDir", "backend.packageSubDir"];
|
|
9
|
+
class ConfigLoaderError extends Error {
|
|
10
|
+
constructor(code, configPath, message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.stage = exports.CONFIG_LOADER_STAGE;
|
|
13
|
+
this.name = "ConfigLoaderError";
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.configPath = configPath;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.ConfigLoaderError = ConfigLoaderError;
|
|
19
|
+
function getDefaultConfigPath(projectRoot, join) {
|
|
20
|
+
return join(projectRoot, DEFAULT_CONFIG_NAME);
|
|
21
|
+
}
|
|
22
|
+
function loadProjectConfig(reader, configPath) {
|
|
23
|
+
const raw = readConfigText(reader, configPath);
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = JSON.parse(raw);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
30
|
+
throw new ConfigLoaderError("CONFIG_INVALID_JSON", configPath, `[${exports.CONFIG_LOADER_STAGE}][CONFIG_INVALID_JSON] 无法解析配置文件: ${configPath} (${reason})`);
|
|
31
|
+
}
|
|
32
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
33
|
+
throw new ConfigLoaderError("CONFIG_INVALID_JSON", configPath, `[${exports.CONFIG_LOADER_STAGE}][CONFIG_INVALID_JSON] 配置文件顶层必须是对象: ${configPath}`);
|
|
34
|
+
}
|
|
35
|
+
const config = parsed;
|
|
36
|
+
validateRequiredFields(config, configPath);
|
|
37
|
+
return {
|
|
38
|
+
configPath,
|
|
39
|
+
config,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function readConfigText(reader, configPath) {
|
|
43
|
+
try {
|
|
44
|
+
return reader.readText(configPath);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
48
|
+
const maybeNodeError = error;
|
|
49
|
+
const isNotFound = maybeNodeError?.code === "ENOENT";
|
|
50
|
+
const errorCode = isNotFound
|
|
51
|
+
? "CONFIG_NOT_FOUND"
|
|
52
|
+
: "CONFIG_READ_FAILED";
|
|
53
|
+
const errorLabel = isNotFound ? "找不到配置文件" : "读取配置文件失败";
|
|
54
|
+
throw new ConfigLoaderError(errorCode, configPath, `[${exports.CONFIG_LOADER_STAGE}][${errorCode}] 无效项目上下文,${errorLabel}: ${configPath} (${reason})`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function validateRequiredFields(config, configPath) {
|
|
58
|
+
const frontend = config.frontend;
|
|
59
|
+
if (!Array.isArray(frontend) || frontend.length === 0) {
|
|
60
|
+
throw new ConfigLoaderError("CONFIG_REQUIRED_FIELD_MISSING", configPath, `[${exports.CONFIG_LOADER_STAGE}][CONFIG_REQUIRED_FIELD_MISSING] 配置缺少必填字段: frontend (至少包含一个构建项)`);
|
|
61
|
+
}
|
|
62
|
+
frontend.forEach((item, index) => {
|
|
63
|
+
const projectDir = getNestedString({ frontend: item }, "frontend.projectDir");
|
|
64
|
+
if (!projectDir) {
|
|
65
|
+
throw new ConfigLoaderError("CONFIG_REQUIRED_FIELD_MISSING", configPath, `[${exports.CONFIG_LOADER_STAGE}][CONFIG_REQUIRED_FIELD_MISSING] 配置缺少必填字段: frontend[${index}].projectDir`);
|
|
66
|
+
}
|
|
67
|
+
const packageSubDir = getNestedString({ frontend: item }, "frontend.packageSubDir");
|
|
68
|
+
if (!packageSubDir) {
|
|
69
|
+
throw new ConfigLoaderError("CONFIG_REQUIRED_FIELD_MISSING", configPath, `[${exports.CONFIG_LOADER_STAGE}][CONFIG_REQUIRED_FIELD_MISSING] 配置缺少必填字段: frontend[${index}].packageSubDir`);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
for (const field of REQUIRED_CONFIG_FIELDS) {
|
|
73
|
+
const value = getNestedString(config, field);
|
|
74
|
+
if (!value) {
|
|
75
|
+
throw new ConfigLoaderError("CONFIG_REQUIRED_FIELD_MISSING", configPath, `[${exports.CONFIG_LOADER_STAGE}][CONFIG_REQUIRED_FIELD_MISSING] 配置缺少必填字段: ${field}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
validateCopyFiles(config, configPath);
|
|
79
|
+
}
|
|
80
|
+
function validateCopyFiles(config, configPath) {
|
|
81
|
+
const copyFiles = config.copyFiles;
|
|
82
|
+
if (copyFiles === undefined) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (!Array.isArray(copyFiles)) {
|
|
86
|
+
throw new ConfigLoaderError("CONFIG_REQUIRED_FIELD_MISSING", configPath, `[${exports.CONFIG_LOADER_STAGE}][CONFIG_REQUIRED_FIELD_MISSING] 配置缺少必填字段: copyFiles (必须为数组)`);
|
|
87
|
+
}
|
|
88
|
+
copyFiles.forEach((item, index) => {
|
|
89
|
+
const source = getNestedString({ item }, "item.source");
|
|
90
|
+
if (!source) {
|
|
91
|
+
throw new ConfigLoaderError("CONFIG_REQUIRED_FIELD_MISSING", configPath, `[${exports.CONFIG_LOADER_STAGE}][CONFIG_REQUIRED_FIELD_MISSING] 配置缺少必填字段: copyFiles[${index}].source`);
|
|
92
|
+
}
|
|
93
|
+
const dest = getNestedString({ item }, "item.dest");
|
|
94
|
+
if (!dest) {
|
|
95
|
+
throw new ConfigLoaderError("CONFIG_REQUIRED_FIELD_MISSING", configPath, `[${exports.CONFIG_LOADER_STAGE}][CONFIG_REQUIRED_FIELD_MISSING] 配置缺少必填字段: copyFiles[${index}].dest`);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function getNestedString(source, dottedPath) {
|
|
100
|
+
const nodes = dottedPath.split(".");
|
|
101
|
+
let current = source;
|
|
102
|
+
for (const node of nodes) {
|
|
103
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
current = current[node];
|
|
107
|
+
}
|
|
108
|
+
if (typeof current !== "string") {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
const normalized = current.trim();
|
|
112
|
+
return normalized === "" ? undefined : normalized;
|
|
113
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runDeployProjectFlow = runDeployProjectFlow;
|
|
4
|
+
const command_result_1 = require("../domain/command-result");
|
|
5
|
+
const config_loader_1 = require("./config-loader");
|
|
6
|
+
const command_runner_1 = require("./command-runner");
|
|
7
|
+
const artifact_validator_1 = require("./artifact-validator");
|
|
8
|
+
const DEPLOY_STAGE = "deploy";
|
|
9
|
+
const ARTIFACT_STAGE = "artifact";
|
|
10
|
+
const CONFIG_ERROR_CODE_MAP = {
|
|
11
|
+
CONFIG_NOT_FOUND: "E_CONFIG_NOT_FOUND",
|
|
12
|
+
CONFIG_READ_FAILED: "E_CONFIG_READ_FAILED",
|
|
13
|
+
CONFIG_INVALID_JSON: "E_CONFIG_INVALID_JSON",
|
|
14
|
+
CONFIG_REQUIRED_FIELD_MISSING: "E_CONFIG_REQUIRED_FIELD_MISSING",
|
|
15
|
+
};
|
|
16
|
+
async function runDeployProjectFlow(deps) {
|
|
17
|
+
const projectRoot = deps.fs.cwd();
|
|
18
|
+
const configPath = (0, config_loader_1.getDefaultConfigPath)(projectRoot, deps.fs.join);
|
|
19
|
+
let configResult;
|
|
20
|
+
try {
|
|
21
|
+
configResult = (0, config_loader_1.loadProjectConfig)({
|
|
22
|
+
readText: deps.fs.readText,
|
|
23
|
+
}, configPath);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error instanceof config_loader_1.ConfigLoaderError) {
|
|
27
|
+
return (0, command_result_1.commandError)(error.message, CONFIG_ERROR_CODE_MAP[error.code], 1);
|
|
28
|
+
}
|
|
29
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
30
|
+
return (0, command_result_1.commandError)(message, command_result_1.COMMAND_ERROR_CODES.invalidPath, 1);
|
|
31
|
+
}
|
|
32
|
+
let executionInput;
|
|
33
|
+
try {
|
|
34
|
+
executionInput = parseDeployInput(projectRoot, configResult.config, deps.fs.resolve);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
38
|
+
return (0, command_result_1.commandError)(`[${DEPLOY_STAGE}] 部署配置无效: ${message}`, "E_CONFIG_REQUIRED_FIELD_MISSING", 1);
|
|
39
|
+
}
|
|
40
|
+
const validator = (0, artifact_validator_1.createArtifactValidator)({
|
|
41
|
+
resolve: deps.fs.resolve,
|
|
42
|
+
exists: deps.fs.exists,
|
|
43
|
+
isDirectory: deps.fs.isDirectory,
|
|
44
|
+
readDir: deps.fs.readDir,
|
|
45
|
+
});
|
|
46
|
+
const validationResult = validator({
|
|
47
|
+
outputDir: executionInput.outputDir,
|
|
48
|
+
requiredRelativePaths: collectRequiredArtifacts(configResult.config),
|
|
49
|
+
});
|
|
50
|
+
if (!validationResult.ok) {
|
|
51
|
+
const message = validationResult.issues.map((issue) => issue.message).join("; ");
|
|
52
|
+
return (0, command_result_1.commandError)(message, command_result_1.COMMAND_ERROR_CODES.executionFailed, 1);
|
|
53
|
+
}
|
|
54
|
+
if (deps.executeDeploy) {
|
|
55
|
+
try {
|
|
56
|
+
const result = await deps.executeDeploy({
|
|
57
|
+
projectRoot,
|
|
58
|
+
configPath: configResult.configPath,
|
|
59
|
+
config: configResult.config,
|
|
60
|
+
outputDir: executionInput.outputDir,
|
|
61
|
+
remoteImage: executionInput.remoteImage,
|
|
62
|
+
docker: executionInput.docker,
|
|
63
|
+
});
|
|
64
|
+
return normalizeDeployResult(result);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
return (0, command_result_1.commandError)(formatDeployThrownError(error), command_result_1.COMMAND_ERROR_CODES.executionFailed, getNonZeroExitCode(error));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const commandRunner = deps.createRunner
|
|
71
|
+
? deps.createRunner()
|
|
72
|
+
: (0, command_runner_1.createCommandRunner)({ stage: DEPLOY_STAGE });
|
|
73
|
+
return runDockerDeploy(commandRunner, executionInput);
|
|
74
|
+
}
|
|
75
|
+
function parseDeployInput(projectRoot, config, resolve) {
|
|
76
|
+
const dockerRaw = asObject(config.docker, "docker");
|
|
77
|
+
const registry = asObject(dockerRaw.registry, "docker.registry");
|
|
78
|
+
const build = asOptionalObject(dockerRaw.build);
|
|
79
|
+
const imageName = asRequiredString(dockerRaw.imageName ?? dockerRaw.appName, "docker.imageName/appName");
|
|
80
|
+
const imageTag = asRequiredString(dockerRaw.imageTag, "docker.imageTag");
|
|
81
|
+
const registryUrl = asRequiredString(registry.url, "docker.registry.url");
|
|
82
|
+
const registryNamespace = asRequiredString(registry.namespace, "docker.registry.namespace");
|
|
83
|
+
const username = asRequiredString(registry.username, "docker.registry.username");
|
|
84
|
+
const password = asOptionalString(registry.password);
|
|
85
|
+
const platform = asOptionalString(build?.platform);
|
|
86
|
+
const outputDir = resolve(projectRoot, ".listpage", "output");
|
|
87
|
+
return {
|
|
88
|
+
outputDir,
|
|
89
|
+
remoteImage: `${registryUrl}/${registryNamespace}/${imageName}:${imageTag}`,
|
|
90
|
+
docker: {
|
|
91
|
+
imageName,
|
|
92
|
+
imageTag,
|
|
93
|
+
registryUrl,
|
|
94
|
+
registryNamespace,
|
|
95
|
+
username,
|
|
96
|
+
password,
|
|
97
|
+
platform,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function collectRequiredArtifacts(config) {
|
|
102
|
+
const required = new Set(["Dockerfile"]);
|
|
103
|
+
const frontend = config.frontend;
|
|
104
|
+
if (Array.isArray(frontend)) {
|
|
105
|
+
for (const item of frontend) {
|
|
106
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const record = item;
|
|
110
|
+
const packageSubDir = asOptionalString(record.packageSubDir) ?? "public";
|
|
111
|
+
const artifactAlias = asOptionalString(record.artifactAlias) ?? "dist";
|
|
112
|
+
required.add(`${packageSubDir}/${artifactAlias}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (config.backend && typeof config.backend === "object" && !Array.isArray(config.backend)) {
|
|
116
|
+
const backend = config.backend;
|
|
117
|
+
const packageSubDir = asOptionalString(backend.packageSubDir) ?? "server";
|
|
118
|
+
const artifactAlias = asOptionalString(backend.artifactAlias) ?? "dist";
|
|
119
|
+
required.add(`${packageSubDir}/${artifactAlias}`);
|
|
120
|
+
}
|
|
121
|
+
return [...required];
|
|
122
|
+
}
|
|
123
|
+
async function runDockerDeploy(runner, input) {
|
|
124
|
+
const { docker } = input;
|
|
125
|
+
const localImage = `${docker.imageName}:${docker.imageTag}`;
|
|
126
|
+
const loginArgs = docker.password
|
|
127
|
+
? [
|
|
128
|
+
"login",
|
|
129
|
+
`--username=${docker.username}`,
|
|
130
|
+
`--password=${docker.password}`,
|
|
131
|
+
docker.registryUrl,
|
|
132
|
+
]
|
|
133
|
+
: [
|
|
134
|
+
"login",
|
|
135
|
+
`--username=${docker.username}`,
|
|
136
|
+
"--password-stdin",
|
|
137
|
+
docker.registryUrl,
|
|
138
|
+
];
|
|
139
|
+
const buildArgs = [
|
|
140
|
+
"buildx",
|
|
141
|
+
"build",
|
|
142
|
+
...(docker.platform ? ["--platform", docker.platform] : []),
|
|
143
|
+
"-t",
|
|
144
|
+
localImage,
|
|
145
|
+
".",
|
|
146
|
+
];
|
|
147
|
+
const stageCommands = [
|
|
148
|
+
{ stage: "login", args: loginArgs, cwd: input.outputDir },
|
|
149
|
+
{ stage: "build", args: buildArgs, cwd: input.outputDir },
|
|
150
|
+
{
|
|
151
|
+
stage: "tag",
|
|
152
|
+
args: ["tag", localImage, input.remoteImage],
|
|
153
|
+
cwd: input.outputDir,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
stage: "push",
|
|
157
|
+
args: ["push", input.remoteImage],
|
|
158
|
+
cwd: input.outputDir,
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
try {
|
|
162
|
+
for (const command of stageCommands) {
|
|
163
|
+
const result = await runner({
|
|
164
|
+
command: "docker",
|
|
165
|
+
args: command.args,
|
|
166
|
+
cwd: command.cwd,
|
|
167
|
+
});
|
|
168
|
+
if (result.exitCode !== 0) {
|
|
169
|
+
return (0, command_result_1.commandError)(`[${DEPLOY_STAGE}][${command.stage}] Docker 命令执行失败: docker ${command.args.join(" ")} (exit=${result.exitCode})`, command_result_1.COMMAND_ERROR_CODES.executionFailed, result.exitCode);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
if (error instanceof command_runner_1.ExecutionError) {
|
|
175
|
+
return (0, command_result_1.commandError)(`[${DEPLOY_STAGE}] ${error.message}`, command_result_1.COMMAND_ERROR_CODES.executionFailed, error.exitCode);
|
|
176
|
+
}
|
|
177
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
178
|
+
return (0, command_result_1.commandError)(`[${DEPLOY_STAGE}] 部署命令执行异常: ${message}`, command_result_1.COMMAND_ERROR_CODES.executionFailed, 1);
|
|
179
|
+
}
|
|
180
|
+
return (0, command_result_1.commandOk)(`[${DEPLOY_STAGE}] 部署完成: ${input.remoteImage}\n` +
|
|
181
|
+
`[${DEPLOY_STAGE}] 运行示例: docker run -d -p 3000:3000 --name ${docker.imageName}-container ${input.remoteImage}`);
|
|
182
|
+
}
|
|
183
|
+
function normalizeDeployResult(result) {
|
|
184
|
+
if (result.ok) {
|
|
185
|
+
if (result.exitCode === 0) {
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
return (0, command_result_1.commandError)(`[${DEPLOY_STAGE}] 部署执行器返回成功但退出码非零 (exit=${result.exitCode})`, command_result_1.COMMAND_ERROR_CODES.executionFailed, result.exitCode);
|
|
189
|
+
}
|
|
190
|
+
if (result.exitCode !== 0) {
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
return (0, command_result_1.commandError)(result.message ??
|
|
194
|
+
`[${DEPLOY_STAGE}] 部署执行器返回失败但退出码为 0`, result.errorCode ?? command_result_1.COMMAND_ERROR_CODES.executionFailed, 1);
|
|
195
|
+
}
|
|
196
|
+
function asObject(value, fieldName) {
|
|
197
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
198
|
+
throw new Error(`缺少对象字段: ${fieldName}`);
|
|
199
|
+
}
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
function asOptionalObject(value) {
|
|
203
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
return value;
|
|
207
|
+
}
|
|
208
|
+
function asRequiredString(value, fieldName) {
|
|
209
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
210
|
+
return value.trim();
|
|
211
|
+
}
|
|
212
|
+
throw new Error(`缺少字段: ${fieldName}`);
|
|
213
|
+
}
|
|
214
|
+
function asOptionalString(value) {
|
|
215
|
+
if (typeof value !== "string") {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
const normalized = value.trim();
|
|
219
|
+
return normalized === "" ? undefined : normalized;
|
|
220
|
+
}
|
|
221
|
+
function formatDeployThrownError(error) {
|
|
222
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
223
|
+
return `[${DEPLOY_STAGE}] 部署执行异常: ${message}`;
|
|
224
|
+
}
|
|
225
|
+
function getNonZeroExitCode(error) {
|
|
226
|
+
if (!error || typeof error !== "object") {
|
|
227
|
+
return 1;
|
|
228
|
+
}
|
|
229
|
+
const candidate = error;
|
|
230
|
+
if (typeof candidate.exitCode === "number" && candidate.exitCode !== 0) {
|
|
231
|
+
return candidate.exitCode;
|
|
232
|
+
}
|
|
233
|
+
if (typeof candidate.status === "number" && candidate.status !== 0) {
|
|
234
|
+
return candidate.status;
|
|
235
|
+
}
|
|
236
|
+
return 1;
|
|
237
|
+
}
|