listpage_cli 0.0.293 → 0.0.294

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/bin/adapters/node-fs-adapter.js +109 -0
  2. package/bin/app/dispatch.js +15 -0
  3. package/bin/app/execute.js +33 -0
  4. package/bin/app/parse-args.js +34 -0
  5. package/bin/cli.js +55 -74
  6. package/bin/commands/build-project-command.js +9 -0
  7. package/bin/commands/init-command.js +9 -0
  8. package/bin/commands/install-skill-command.js +9 -0
  9. package/bin/copy.js +14 -126
  10. package/bin/domain/command-result.js +34 -0
  11. package/bin/domain/interaction-result.js +14 -0
  12. package/bin/ports/build-project-command.js +2 -0
  13. package/bin/ports/filesystem-capability.js +2 -0
  14. package/bin/ports/fs-port.js +22 -0
  15. package/bin/ports/init-command.js +2 -0
  16. package/bin/ports/install-skill-command.js +2 -0
  17. package/bin/prompts.js +105 -16
  18. package/bin/services/artifact-validator.js +2 -0
  19. package/bin/services/build-project-service.js +190 -0
  20. package/bin/services/command-runner.js +44 -0
  21. package/bin/services/config-loader.js +113 -0
  22. package/bin/services/filesystem-capability-service.js +136 -0
  23. package/bin/services/init-service.js +64 -0
  24. package/bin/services/install-skill-service.js +34 -0
  25. package/package.json +6 -4
  26. package/templates/backend-template/package.json.tmpl +1 -1
  27. package/templates/frontend-template/package.json.tmpl +2 -2
  28. package/templates/package-app-template/package.json +1 -1
  29. package/templates/skills-template/listpage/examples.md +565 -0
  30. package/skills/listpage/examples.md +0 -243
  31. package/templates/rush-template/docs/ListPage-AI/347/224/237/346/210/220/350/247/204/350/214/203.md +0 -305
  32. /package/{skills → templates/skills-template}/listpage/SKILL.md +0 -0
  33. /package/{skills → templates/skills-template}/listpage/api.md +0 -0
package/bin/prompts.js CHANGED
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runPrompt = runPrompt;
6
7
  exports.askRushQuestions = askRushQuestions;
7
8
  exports.askProjectPath = askProjectPath;
8
9
  exports.askOverwrite = askOverwrite;
@@ -12,60 +13,148 @@ exports.printVersion = printVersion;
12
13
  const enquirer_1 = require("enquirer");
13
14
  const fs_1 = require("fs");
14
15
  const path_1 = __importDefault(require("path"));
16
+ const interaction_result_1 = require("./domain/interaction-result");
17
+ let promptTestScript = null;
18
+ let promptTestScriptIndex = 0;
19
+ function getTestPromptMode() {
20
+ const mode = process.env.LISTPAGE_CLI_PROMPT_TEST_MODE;
21
+ if (mode === "cancel" || mode === "invalid") {
22
+ return mode;
23
+ }
24
+ return undefined;
25
+ }
26
+ function getNextScriptedPromptResult() {
27
+ if (promptTestScript === null) {
28
+ const raw = process.env.LISTPAGE_CLI_PROMPT_TEST_SCRIPT;
29
+ if (!raw) {
30
+ promptTestScript = [];
31
+ return undefined;
32
+ }
33
+ try {
34
+ const parsed = JSON.parse(raw);
35
+ if (Array.isArray(parsed)) {
36
+ promptTestScript = parsed;
37
+ }
38
+ else {
39
+ promptTestScript = [];
40
+ }
41
+ }
42
+ catch {
43
+ promptTestScript = [];
44
+ }
45
+ }
46
+ const next = promptTestScript[promptTestScriptIndex];
47
+ promptTestScriptIndex += 1;
48
+ return next;
49
+ }
50
+ function isCancelError(error) {
51
+ if (!(error instanceof Error)) {
52
+ return false;
53
+ }
54
+ const normalized = error.message.toLowerCase();
55
+ return (normalized.includes("cancel") ||
56
+ normalized.includes("aborted") ||
57
+ normalized.includes("interrupted"));
58
+ }
59
+ async function runPrompt(options, runner = enquirer_1.prompt) {
60
+ const testMode = getTestPromptMode();
61
+ if (testMode === "cancel") {
62
+ return (0, interaction_result_1.interactionCancelled)();
63
+ }
64
+ if (testMode === "invalid") {
65
+ return (0, interaction_result_1.interactionInvalid)("测试模式: 模拟无效输入");
66
+ }
67
+ const scripted = getNextScriptedPromptResult();
68
+ if (scripted !== undefined) {
69
+ if (scripted === "cancel") {
70
+ return (0, interaction_result_1.interactionCancelled)();
71
+ }
72
+ if (scripted === "invalid") {
73
+ return (0, interaction_result_1.interactionInvalid)("测试脚本: 模拟无效输入");
74
+ }
75
+ return (0, interaction_result_1.interactionValue)(scripted);
76
+ }
77
+ try {
78
+ const value = (await runner(options));
79
+ return (0, interaction_result_1.interactionValue)(value);
80
+ }
81
+ catch (error) {
82
+ if (isCancelError(error)) {
83
+ return (0, interaction_result_1.interactionCancelled)();
84
+ }
85
+ const reason = error instanceof Error ? error.message : String(error);
86
+ return (0, interaction_result_1.interactionInvalid)(reason);
87
+ }
88
+ }
15
89
  async function askRushQuestions() {
16
- const { name: frontendName } = await (0, enquirer_1.prompt)({
90
+ const frontend = await runPrompt({
17
91
  type: "input",
18
92
  name: "name",
19
93
  message: "请输入前端项目名称,为空表示不创建前端项目",
20
94
  initial: "",
21
95
  });
22
- const { name: backendName } = await (0, enquirer_1.prompt)({
96
+ if (frontend.status !== "value") {
97
+ return frontend;
98
+ }
99
+ const backend = await runPrompt({
23
100
  type: "input",
24
101
  name: "name",
25
102
  message: "请输入后端项目名称,为空表示不创建后端项目",
26
103
  initial: "",
27
104
  });
28
- return { frontendName, backendName };
105
+ if (backend.status !== "value") {
106
+ return backend;
107
+ }
108
+ return (0, interaction_result_1.interactionValue)({
109
+ frontendName: frontend.value.name,
110
+ backendName: backend.value.name,
111
+ });
29
112
  }
30
113
  async function askProjectPath() {
31
- const ans = await (0, enquirer_1.prompt)({
114
+ const result = await runPrompt({
32
115
  type: "input",
33
116
  name: "path",
34
117
  message: "请填写项目名称或路径,如果填.表示直接把项目放到当前目录下",
35
118
  initial: ".",
36
119
  });
37
- const p = (ans.path || ".").trim();
38
- return p || ".";
120
+ if (result.status !== "value") {
121
+ return result;
122
+ }
123
+ const p = (result.value.path || ".").trim();
124
+ return (0, interaction_result_1.interactionValue)(p || ".");
39
125
  }
40
126
  async function askOverwrite() {
41
- const { ok } = await (0, enquirer_1.prompt)({
127
+ const result = await runPrompt({
42
128
  type: "confirm",
43
129
  name: "ok",
44
130
  message: "目标目录非空,是否覆盖?",
45
131
  initial: false,
46
132
  });
47
- if (!ok) {
48
- console.error("已取消");
49
- process.exit(1);
133
+ if (result.status !== "value") {
134
+ return result;
50
135
  }
136
+ return (0, interaction_result_1.interactionValue)(result.value.ok);
51
137
  }
52
138
  async function askInstallDeployScript() {
53
- const { ok } = await (0, enquirer_1.prompt)({
139
+ const result = await runPrompt({
54
140
  type: "confirm",
55
141
  name: "ok",
56
142
  message: "是否添加部署脚本?这个脚本允许你可以帮你快速构建docker镜像,并发布到阿里云等环境,但是需要本机有docker环境。",
57
143
  initial: false,
58
144
  });
59
- return ok;
145
+ if (result.status !== "value") {
146
+ return result;
147
+ }
148
+ return (0, interaction_result_1.interactionValue)(result.value.ok);
60
149
  }
61
150
  function printHelp() {
62
151
  const h = [
63
152
  "用法: listpage_cli init",
64
153
  "说明: 进入中文引导式交互,按提示填写即可",
65
- "用法: listpage_cli sync-docs",
66
- "说明: 将模板中的 .trae docs 同步到执行目录的同级目录",
67
- "用法: listpage_cli install-skill [listpage] [--project]",
68
- "说明: 安装 listpage 技能到 Cursor。默认安装到 ~/.cursor/skills/;加 --project 则安装到当前项目 .cursor/skills/",
154
+ "用法: listpage_cli install-skill [skillName] [--project]",
155
+ "说明: 安装技能到 Cursor。默认 skillName test,安装到当前命令执行目录的 .cursor/skills/",
156
+ "用法: listpage_cli build-project",
157
+ "说明: 非交互校验当前目录是否为有效项目根(需存在 listpage.config.json)",
69
158
  ].join("\n");
70
159
  console.log(h);
71
160
  }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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,44 @@
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, command, exitCode, message) {
9
+ super(message);
10
+ this.stage = exports.COMMAND_RUNNER_STAGE;
11
+ this.name = "ExecutionError";
12
+ this.code = code;
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
+ return async (input) => {
21
+ const args = input.args ?? [];
22
+ const output = spawnSyncImpl(input.command, args, {
23
+ cwd: input.cwd,
24
+ env: {
25
+ ...process.env,
26
+ ...input.env,
27
+ },
28
+ shell: true,
29
+ stdio: "inherit",
30
+ });
31
+ if (output.error) {
32
+ const maybeNodeError = output.error;
33
+ const isNotFound = maybeNodeError.code === "ENOENT";
34
+ const code = isNotFound
35
+ ? "COMMAND_NOT_FOUND"
36
+ : "COMMAND_EXECUTION_FAILED";
37
+ const messagePrefix = isNotFound ? "命令不存在" : "命令执行异常";
38
+ throw new ExecutionError(code, input.command, 1, `[${exports.COMMAND_RUNNER_STAGE}][${code}] ${messagePrefix}: ${input.command} (${output.error.message})`);
39
+ }
40
+ return {
41
+ exitCode: output.status ?? 1,
42
+ };
43
+ };
44
+ }
@@ -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,136 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createFilesystemCapabilityService = createFilesystemCapabilityService;
4
+ const utils_1 = require("../utils");
5
+ function createFilesystemCapabilityService(fs, options) {
6
+ return {
7
+ isDirEmpty: (dir) => {
8
+ return !fs.exists(dir) || fs.readDir(dir).length === 0;
9
+ },
10
+ copyRushTemplate: (targetDir, vars) => {
11
+ const source = getTemplateDir(fs, options.templateRootDir, "rush");
12
+ copyDir(fs, source, targetDir, vars);
13
+ },
14
+ updateRushJsonProjects: (filepath, feAppName, beAppName) => {
15
+ const projects = [];
16
+ if (feAppName) {
17
+ projects.push({
18
+ packageName: (0, utils_1.composePkgName)(feAppName),
19
+ projectFolder: `apps/${feAppName}`,
20
+ reviewCategory: "production",
21
+ });
22
+ }
23
+ if (beAppName) {
24
+ projects.push({
25
+ packageName: (0, utils_1.composePkgName)(beAppName),
26
+ projectFolder: `servers/${beAppName}`,
27
+ reviewCategory: "production",
28
+ });
29
+ }
30
+ const content = fs.readText(filepath);
31
+ const json = (0, utils_1.readJsonWithComments)(content);
32
+ json.projects = projects;
33
+ fs.writeText(filepath, JSON.stringify(json, null, 2));
34
+ },
35
+ copyFrontendTemplate: (targetDir, vars) => {
36
+ const appName = (0, utils_1.composePkgName)(vars.FRONTEND_NAME);
37
+ const resolvedTargetDir = fs.join(targetDir, `apps/${appName}`);
38
+ const source = getTemplateDir(fs, options.templateRootDir, "frontend");
39
+ copyDir(fs, source, resolvedTargetDir, vars);
40
+ },
41
+ copyBackendTemplate: (targetDir, vars) => {
42
+ const appName = (0, utils_1.composePkgName)(vars.BACKEND_NAME);
43
+ const resolvedTargetDir = fs.join(targetDir, `servers/${appName}`);
44
+ const source = getTemplateDir(fs, options.templateRootDir, "backend");
45
+ copyDir(fs, source, resolvedTargetDir, vars);
46
+ },
47
+ copyDeployScriptTemplate: (targetDir, vars) => {
48
+ const resolvedTargetDir = fs.join(targetDir, "common", "scripts", "package-app");
49
+ const source = getTemplateDir(fs, options.templateRootDir, "package-app");
50
+ copyDir(fs, source, resolvedTargetDir, vars);
51
+ },
52
+ copySkillDir: (sourceDir, targetDir) => {
53
+ syncDirWithRename(fs, sourceDir, targetDir);
54
+ },
55
+ };
56
+ }
57
+ function getTemplateDir(fs, templateRootDir, type) {
58
+ return fs.join(templateRootDir, `${type}-template`);
59
+ }
60
+ function copyDir(fs, source, destination, vars) {
61
+ fs.ensureDir(destination);
62
+ const files = fs.readDir(source);
63
+ files.forEach((file) => {
64
+ const sourcePath = fs.join(source, file);
65
+ const destinationPath = fs.join(destination, file);
66
+ if (fs.isDirectory(sourcePath)) {
67
+ copyDir(fs, sourcePath, destinationPath, vars);
68
+ return;
69
+ }
70
+ fs.ensureDir(fs.dirname(destinationPath));
71
+ const isTemplate = file.endsWith(".tmpl");
72
+ if (!isTemplate) {
73
+ fs.copyFile(sourcePath, destinationPath);
74
+ return;
75
+ }
76
+ const template = fs.readText(sourcePath);
77
+ const content = compileTemplateContent(template, vars);
78
+ fs.writeText(destinationPath.slice(0, -5), content);
79
+ });
80
+ }
81
+ function compileTemplateContent(source, vars) {
82
+ let output = source;
83
+ for (const [key, value] of Object.entries(vars)) {
84
+ const re = new RegExp(`__${key}__`, "g");
85
+ output = output.replace(re, value);
86
+ }
87
+ return output;
88
+ }
89
+ function syncDirWithRename(fs, source, destination) {
90
+ fs.ensureDir(destination);
91
+ const items = fs.readDir(source);
92
+ items.forEach((name) => {
93
+ const sourcePath = fs.join(source, name);
94
+ const destinationPath = fs.join(destination, name);
95
+ if (fs.isDirectory(sourcePath)) {
96
+ if (fs.exists(destinationPath) && !fs.isDirectory(destinationPath)) {
97
+ fs.rename(destinationPath, getCopyPath(fs, destinationPath));
98
+ }
99
+ fs.ensureDir(destinationPath);
100
+ syncDirWithRename(fs, sourcePath, destinationPath);
101
+ return;
102
+ }
103
+ fs.ensureDir(fs.dirname(destinationPath));
104
+ if (fs.exists(destinationPath) && !fs.isDirectory(destinationPath)) {
105
+ fs.rename(destinationPath, getCopyPath(fs, destinationPath));
106
+ }
107
+ fs.copyFile(sourcePath, destinationPath);
108
+ });
109
+ }
110
+ function getCopyPath(fs, filePath) {
111
+ const dir = fs.dirname(filePath);
112
+ const { base, ext } = splitFileName(filePath);
113
+ let candidate = fs.join(dir, `${base} copy${ext}`);
114
+ if (!fs.exists(candidate)) {
115
+ return candidate;
116
+ }
117
+ let index = 2;
118
+ while (true) {
119
+ candidate = fs.join(dir, `${base} copy (${index})${ext}`);
120
+ if (!fs.exists(candidate)) {
121
+ return candidate;
122
+ }
123
+ index += 1;
124
+ }
125
+ }
126
+ function splitFileName(filePath) {
127
+ const fileName = filePath.split(/[\\/]/).pop() ?? filePath;
128
+ const extIndex = fileName.lastIndexOf(".");
129
+ if (extIndex <= 0) {
130
+ return { base: fileName, ext: "" };
131
+ }
132
+ return {
133
+ base: fileName.slice(0, extIndex),
134
+ ext: fileName.slice(extIndex),
135
+ };
136
+ }