cloudcc-cli 2.2.5 → 2.2.7
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/.cloudcc-cache.json +38 -0
- package/README.md +1435 -522
- package/bin/cc.js +7 -2
- package/bin/index.js +4 -0
- package/java/com/cloudcc/core/BaseException.java +100 -0
- package/java/com/cloudcc/core/BusiException.java +43 -0
- package/java/com/cloudcc/core/CCService.java +3 -1
- package/java/com/cloudcc/core/StringUtils.java +7 -0
- package/java/com/cloudcc/core/TimeUtil.java +33 -0
- package/java/com/cloudcc/core/UserInfo.java +9 -0
- package/package.json +7 -1
- package/pom.xml +1 -1
- package/skill/BACKEND_CODE.md +114 -0
- package/skill/CLI_CHEATSHEET.md +90 -0
- package/skill/INSTALL_AND_BOOTSTRAP.md +59 -0
- package/skill/OBJECTS_AND_FIELDS.md +120 -0
- package/skill/REQUIREMENTS_BREAKDOWN.md +98 -0
- package/skill/SKILL.md +33 -0
- package/skill/VUE_CUSTOM_COMPONENT.md +50 -0
- package/src/api/backend-sdk-java.md +427 -0
- package/src/api/ccdk-sdk.md +1039 -0
- package/src/application/create.js +114 -0
- package/src/application/get.js +13 -0
- package/src/application/index.js +8 -0
- package/src/classes/doc.js +486 -0
- package/src/classes/index.js +1 -0
- package/src/mcp/cliRunner.js +61 -0
- package/src/mcp/index.js +84 -12
- package/src/mcp/readme.md +6 -3
- package/src/mcp/tools/Application Creator/handler.js +78 -0
- package/src/mcp/tools/Approval/handler.js +34 -151
- package/src/mcp/tools/Class Creator/handler.js +18 -15
- package/src/mcp/tools/Class Detail Retriever/handler.js +8 -9
- package/src/mcp/tools/Class Editor Guide/handler.js +5 -19
- package/src/mcp/tools/Class List Retriever/handler.js +8 -3
- package/src/mcp/tools/Class Publisher/handler.js +7 -9
- package/src/mcp/tools/Class Puller/handler.js +6 -65
- package/src/mcp/tools/Client Script Detail Retriever/handler.js +12 -18
- package/src/mcp/tools/Client Script Editor Guide/handler.js +9 -605
- package/src/mcp/tools/Client Script List Retriever/handler.js +30 -33
- package/src/mcp/tools/Client Script Publisher/handler.js +12 -11
- package/src/mcp/tools/Client Script Puller/handler.js +23 -30
- package/src/mcp/tools/CloudCC Development Overview/handler.js +11 -5
- package/src/mcp/tools/Component Creator/handler.js +12 -11
- package/src/mcp/tools/Component Detail Retriever/handler.js +12 -9
- package/src/mcp/tools/Component Editor Guide/handler.js +5 -22
- package/src/mcp/tools/Component List Retriever/handler.js +21 -18
- package/src/mcp/tools/Component Publisher/handler.js +25 -3
- package/src/mcp/tools/Component Puller/handler.js +13 -16
- package/src/mcp/tools/Dev Environment Creator/handler.js +5 -72
- package/src/mcp/tools/Dev Environment Validator/handler.js +5 -66
- package/src/mcp/tools/Developer Key Setup Guide/handler.js +11 -20
- package/src/mcp/tools/JSP Migrator/handler.js +842 -0
- package/src/mcp/tools/Menu Creator/handler.js +86 -0
- package/src/mcp/tools/Object Creator/handler.js +14 -6
- package/src/mcp/tools/Object Fields Creator/handler.js +9 -10
- package/src/mcp/tools/Object Fields Retriever/handler.js +6 -3
- package/src/mcp/tools/Object List Retriever/handler.js +10 -7
- package/src/mcp/tools/Scheduled Class Creator/handler.js +12 -16
- package/src/mcp/tools/Scheduled Class Detail Retriever/handler.js +7 -9
- package/src/mcp/tools/Scheduled Class List Retriever/handler.js +21 -23
- package/src/mcp/tools/Scheduled Class Publisher/handler.js +7 -9
- package/src/mcp/tools/Scheduled Class Puller/handler.js +6 -70
- package/src/mcp/tools/Trigger Creator/handler.js +12 -20
- package/src/mcp/tools/Trigger Detail Retriever/handler.js +7 -9
- package/src/mcp/tools/Trigger Editor Guide/handler.js +10 -35
- package/src/mcp/tools/Trigger List Retriever/handler.js +12 -4
- package/src/mcp/tools/Trigger Publisher/handler.js +8 -11
- package/src/mcp/tools/Trigger Puller/handler.js +12 -17
- package/src/menu/common.js +16 -0
- package/src/menu/create-object.js +94 -0
- package/src/menu/create-page.js +108 -0
- package/src/menu/create-script.js +108 -0
- package/src/menu/create-site.js +108 -0
- package/src/menu/create.js +54 -0
- package/src/menu/index.js +7 -0
- package/src/plugin/doc.js +801 -0
- package/src/plugin/index.js +1 -0
- package/src/plugin/pull.js +3 -0
- package/src/project/doc.js +378 -0
- package/src/project/index.js +1 -0
- package/src/script/doc.js +259 -0
- package/src/script/index.js +1 -0
- package/src/timer/index.js +1 -0
- package/src/triggers/doc.js +342 -0
- package/src/triggers/index.js +5 -0
- package/target/classes/com/cloudcc/core/BaseException.class +0 -0
- package/target/classes/com/cloudcc/core/BusiException.class +0 -0
- package/target/classes/com/cloudcc/core/CCService.class +0 -0
- package/target/classes/com/cloudcc/core/StringUtils.class +0 -0
- package/target/classes/com/cloudcc/core/TimeUtil.class +0 -0
- package/target/classes/com/cloudcc/core/UserInfo.class +0 -0
- package/template/lib/ccopenapi-0.0.4.jar +0 -0
- package/test/application.cli.test.js +30 -0
- package/test/classes.cli.test.js +121 -0
- package/test/fields.cli.test.js +69 -0
- package/test/mcp.cli.test.js +21 -0
- package/test/menu.cli.test.js +41 -0
- package/test/object.cli.test.js +64 -0
- package/test/plugin.cli.test.js +109 -0
- package/test/script.cli.test.js +101 -0
- package/test/timer.cli.test.js +107 -0
- package/test/trigger.cli.test.js +146 -0
- package/.vscode/settings.json +0 -3
- package/bin/mcp-svc.js +0 -13
- package/src/mcp/MCP/345/234/272/346/231/257/346/250/241/346/213/237.md +0 -8
- package/src/mcp/index-sse-svc.js +0 -126
- package/src/mcp/index-streamable-svc.js +0 -180
- package/src/mcp/tools/Class Detail Retriever/prompt.js +0 -37
- package/src/mcp/tools/Class Editor Guide/prompt.js +0 -468
- package/src/mcp/tools/Class Publisher/prompt.js +0 -40
- package/src/mcp/tools/Class Puller/prompt.js +0 -49
- package/src/mcp/tools/Client Script Creator/handler.js +0 -179
- package/src/mcp/tools/CloudCC Development Overview/prompt.js +0 -870
- package/src/mcp/tools/Component Editor Guide/prompt.js +0 -519
- package/src/mcp/tools/Component Publisher/prompt.js +0 -659
- package/src/mcp/tools/Dev Environment Creator/prompt.js +0 -273
- package/src/mcp/tools/Dev Environment Validator/prompt.js +0 -193
- package/src/mcp/tools/Developer Key Setup Guide/prompt.js +0 -71
- package/src/mcp/tools/Object Fields Retriever/prompt.js +0 -10
- package/src/mcp/tools/Object List Retriever/prompt.js +0 -10
- package/src/mcp/tools/ccdk/fetcher.js +0 -18
- package/src/mcp/tools/ccdk/handler.js +0 -98
- package/src/mcp/tools/ccdk/prompt.js +0 -453
- package/target/ccopenapi-0.0.3-classes.jar +0 -0
- package/target/ccopenapi-0.0.3.jar +0 -0
- package/target/maven-archiver/pom.properties +0 -3
- package/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst +0 -18
- package/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +0 -19
- package/template/lib/ccopenapi-0.0.3.jar +0 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const test = require("node:test");
|
|
2
|
+
const assert = require("node:assert/strict");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const { exec } = require("node:child_process");
|
|
5
|
+
const { promisify } = require("node:util");
|
|
6
|
+
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
const repoRoot = path.resolve(__dirname, "..");
|
|
9
|
+
|
|
10
|
+
function quoteArg(value) {
|
|
11
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function runCc(args) {
|
|
15
|
+
const cmd = `npx --prefix "${repoRoot}" cc ${args.map(quoteArg).join(" ")}`;
|
|
16
|
+
return execAsync(cmd, { cwd: repoRoot });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test("对象管理流程:get 对象列表→create 对象→get 列表校验", async (t) => {
|
|
20
|
+
const objectLabel = `TestObj${Date.now()}`;
|
|
21
|
+
const labelNeedle = "testobj";
|
|
22
|
+
let createdSlugHit = false;
|
|
23
|
+
|
|
24
|
+
t.test("1) create 创建自定义对象", async () => {
|
|
25
|
+
await assert.doesNotReject(() => runCc(["create", "object", repoRoot, objectLabel]));
|
|
26
|
+
console.error(`\n[object-test] 已创建自定义对象 label: ${objectLabel}`);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
t.test("2) get 获取标准对象列表(standard)", async () => {
|
|
30
|
+
const { stdout } = await runCc(["get", "object", repoRoot, "standard"]);
|
|
31
|
+
const list = JSON.parse(stdout.trim());
|
|
32
|
+
assert.ok(Array.isArray(list), "get object standard 应返回数组");
|
|
33
|
+
assert.ok(list.length > 0, "标准对象列表应非空");
|
|
34
|
+
// 标准对象列表不要求包含新建自定义对象
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
t.test("3) get 获取自定义对象列表(custom)并校验包含新对象", async () => {
|
|
38
|
+
const { stdout } = await runCc(["get", "object", repoRoot, "custom"]);
|
|
39
|
+
const list = JSON.parse(stdout.trim());
|
|
40
|
+
assert.ok(Array.isArray(list), "get object custom 应返回数组");
|
|
41
|
+
assert.ok(list.length > 0, "自定义对象列表应非空");
|
|
42
|
+
|
|
43
|
+
createdSlugHit = list.some((o) => {
|
|
44
|
+
const s = `${o.schemetableName || ""}`.toLowerCase();
|
|
45
|
+
const l = `${o.label || o.objname || ""}`.toLowerCase();
|
|
46
|
+
return s.includes(labelNeedle) || l.includes(labelNeedle);
|
|
47
|
+
});
|
|
48
|
+
assert.ok(createdSlugHit, "custom 列表应包含刚创建的自定义对象(按 testobj 关键字匹配)");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
t.test("4) get 获取全部对象列表(both / 默认)并校验包含新对象", async () => {
|
|
52
|
+
const { stdout } = await runCc(["get", "object", repoRoot]);
|
|
53
|
+
const list = JSON.parse(stdout.trim());
|
|
54
|
+
assert.ok(Array.isArray(list), "get object(默认)应返回数组");
|
|
55
|
+
assert.ok(list.length > 0, "全部对象列表应非空");
|
|
56
|
+
|
|
57
|
+
const found = list.some((o) => {
|
|
58
|
+
const s = `${o.schemetableName || ""}`.toLowerCase();
|
|
59
|
+
const l = `${o.label || o.objname || ""}`.toLowerCase();
|
|
60
|
+
return s.includes(labelNeedle) || l.includes(labelNeedle);
|
|
61
|
+
});
|
|
62
|
+
assert.ok(found || createdSlugHit, "both 列表应包含刚创建的自定义对象");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const test = require("node:test");
|
|
2
|
+
const assert = require("node:assert/strict");
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { exec } = require("node:child_process");
|
|
6
|
+
const { promisify } = require("node:util");
|
|
7
|
+
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
const repoRoot = path.resolve(__dirname, "..");
|
|
10
|
+
const pluginsRoot = path.join(repoRoot, "plugins");
|
|
11
|
+
|
|
12
|
+
function quoteArg(value) {
|
|
13
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function runCc(args) {
|
|
17
|
+
const cmd = `npx --prefix "${repoRoot}" cc ${args.map(quoteArg).join(" ")}`;
|
|
18
|
+
return execAsync(cmd, { cwd: repoRoot });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test("组件管理流程:创建→doc→发布→拉取→详情→列表→清空 plugins 文件夹", async (t) => {
|
|
22
|
+
const pluginName = `CCPlugin${Date.now()}`;
|
|
23
|
+
const pluginDir = path.join(pluginsRoot, pluginName);
|
|
24
|
+
const configPath = path.join(pluginDir, "config.json");
|
|
25
|
+
const vuePath = path.join(pluginDir, `${pluginName}.vue`);
|
|
26
|
+
let publishedId = null;
|
|
27
|
+
let publishOk = false;
|
|
28
|
+
let pulledPluginName = null;
|
|
29
|
+
let pulledPluginId = null;
|
|
30
|
+
|
|
31
|
+
t.test("1) 创建组件", async () => {
|
|
32
|
+
await runCc(["create", "plugin", pluginName]);
|
|
33
|
+
|
|
34
|
+
assert.equal(fs.existsSync(pluginDir), true);
|
|
35
|
+
assert.equal(fs.existsSync(configPath), true);
|
|
36
|
+
assert.equal(fs.existsSync(vuePath), true);
|
|
37
|
+
|
|
38
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
39
|
+
assert.ok(config.component && config.component.includes(pluginName));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
t.test("2) 获取 doc(plugin overview)", async () => {
|
|
43
|
+
const { stdout } = await runCc(["doc", "plugin", "overview"]);
|
|
44
|
+
assert.ok(stdout.length > 0, "doc plugin 应有输出");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
t.test("3) 发布组件(publish:以输出判断成功)", async () => {
|
|
48
|
+
const { stdout, stderr } = await runCc(["publish", "plugin", pluginName]);
|
|
49
|
+
const out = `${stdout}\n${stderr}`.trim();
|
|
50
|
+
// 注意:src/plugin/publish1.js 使用 child_process.exec 的回调触发上传,
|
|
51
|
+
// 在 CLI 进程退出前不一定能等到 build/upload 完成,因此这里用“编译阶段成功”作为发布成功的判定。
|
|
52
|
+
publishOk = /Compilation Successful|Compilation Successful!|Success|Successful|成功|returnCode\s*[:=]\s*200|200/i.test(out);
|
|
53
|
+
assert.ok(publishOk, "publish 应至少输出编译成功或发布成功信息");
|
|
54
|
+
|
|
55
|
+
// 兼容:部分环境不会写回 config.id,因此这里不再强依赖 publishedId
|
|
56
|
+
const configAfterPublish = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
57
|
+
publishedId = configAfterPublish.id || null;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
t.test("4) 拉取组件(先 get 列表,再按 id pull)", async () => {
|
|
61
|
+
// 先拿线上列表,再选择第一个可用的 id 拉取(不依赖 publish 是否写回 id)
|
|
62
|
+
const { stdout } = await runCc(["get", "plugin", repoRoot]);
|
|
63
|
+
const list = JSON.parse(stdout.trim());
|
|
64
|
+
assert.ok(Array.isArray(list), "get plugin 应返回数组");
|
|
65
|
+
assert.ok(list.length > 0, "线上组件列表应非空");
|
|
66
|
+
|
|
67
|
+
const first = list.find((x) => x && x.id) || null;
|
|
68
|
+
assert.ok(first && first.id, "应能从列表中取到组件 id");
|
|
69
|
+
pulledPluginId = first.id;
|
|
70
|
+
|
|
71
|
+
// pull 支持通过 id 拉取并自动创建本地目录
|
|
72
|
+
await runCc(["pull", "plugin", pulledPluginId, repoRoot]);
|
|
73
|
+
|
|
74
|
+
// 通过本地目录判断拉取是否成功:pull.js 会归一化名称(去掉 component- 前缀)
|
|
75
|
+
const guessName =
|
|
76
|
+
(first.name && String(first.name)) ||
|
|
77
|
+
(first.component && String(first.component)) ||
|
|
78
|
+
null;
|
|
79
|
+
pulledPluginName = guessName ? guessName.replace(/^component-/, "") : null;
|
|
80
|
+
assert.ok(pulledPluginName, "应能从列表推断出拉取后的本地插件目录名");
|
|
81
|
+
|
|
82
|
+
const pulledDir = path.join(pluginsRoot, pulledPluginName);
|
|
83
|
+
assert.ok(fs.existsSync(pulledDir), "pull 后应生成本地插件目录");
|
|
84
|
+
assert.ok(fs.existsSync(path.join(pulledDir, "config.json")), "pull 后应生成 config.json");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
t.test("5) 查询组件详情(detail:本地 name)", async () => {
|
|
88
|
+
// 优先对拉取到的插件做 detail(确保链路:get -> pull -> detail)
|
|
89
|
+
const pulledDir = path.join(pluginsRoot, pulledPluginName);
|
|
90
|
+
assert.ok(pulledPluginName, "需要 pulledPluginName");
|
|
91
|
+
assert.ok(fs.existsSync(pulledDir), "pull 后本地目录应存在");
|
|
92
|
+
await assert.doesNotReject(() => runCc(["detail", "plugin", pulledPluginName, "", repoRoot]));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
t.test("6) get 获取线上组件列表", async () => {
|
|
96
|
+
const { stdout } = await runCc(["get", "plugin", repoRoot]);
|
|
97
|
+
const list = JSON.parse(stdout.trim());
|
|
98
|
+
assert.ok(Array.isArray(list), "get plugin 应返回数组");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
t.test("7) 清空 plugins 文件夹", async () => {
|
|
102
|
+
if (fs.existsSync(pluginsRoot)) {
|
|
103
|
+
fs.rmSync(pluginsRoot, { recursive: true, force: true });
|
|
104
|
+
}
|
|
105
|
+
fs.mkdirSync(pluginsRoot, { recursive: true });
|
|
106
|
+
const remain = fs.readdirSync(pluginsRoot);
|
|
107
|
+
assert.equal(remain.length, 0);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const test = require("node:test");
|
|
2
|
+
const assert = require("node:assert/strict");
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { exec } = require("node:child_process");
|
|
6
|
+
const { promisify } = require("node:util");
|
|
7
|
+
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
const repoRoot = path.resolve(__dirname, "..");
|
|
10
|
+
const scriptRoot = path.join(repoRoot, "script");
|
|
11
|
+
|
|
12
|
+
function quoteArg(value) {
|
|
13
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function runCc(args) {
|
|
17
|
+
const cmd = `npx --prefix "${repoRoot}" cc ${args.map(quoteArg).join(" ")}`;
|
|
18
|
+
return execAsync(cmd, { cwd: repoRoot });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test("客户端脚本管理流程:创建→doc→发布→拉取→线上详情→列表→批量拉取前2个→清空 script 文件夹", async (t) => {
|
|
22
|
+
const objName = "Account";
|
|
23
|
+
const scriptName = `TestScript${Date.now()}`;
|
|
24
|
+
const namePath = `${objName}/${scriptName}`;
|
|
25
|
+
const scriptDir = path.join(scriptRoot, objName, scriptName);
|
|
26
|
+
const configPath = path.join(scriptDir, "config.json");
|
|
27
|
+
const jsPath = path.join(scriptDir, `${scriptName}.js`);
|
|
28
|
+
let topIds = [];
|
|
29
|
+
|
|
30
|
+
t.test("1) 创建客户端脚本", async () => {
|
|
31
|
+
const body = encodeURI(JSON.stringify({ objName, scriptName }));
|
|
32
|
+
await runCc(["create", "script", body]);
|
|
33
|
+
|
|
34
|
+
assert.equal(fs.existsSync(scriptDir), true);
|
|
35
|
+
assert.equal(fs.existsSync(configPath), true);
|
|
36
|
+
assert.equal(fs.existsSync(jsPath), true);
|
|
37
|
+
|
|
38
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
39
|
+
assert.equal(config.scriptName, scriptName);
|
|
40
|
+
assert.equal(config.objName, objName);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
t.test("2) 获取 doc(script overview)", async () => {
|
|
44
|
+
const { stdout } = await runCc(["doc", "script", "overview"]);
|
|
45
|
+
assert.match(stdout, /客户端脚本|CCDK|编辑指南/i);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
t.test("3) 发布脚本(publish,写回 config.id)", async () => {
|
|
49
|
+
await runCc(["publish", "script", namePath]);
|
|
50
|
+
const configAfterPublish = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
51
|
+
assert.ok(configAfterPublish.id, "publish 后应写入 config.id");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
t.test("4) 拉取脚本(pull:覆盖本地)", async () => {
|
|
55
|
+
const localJs = fs.readFileSync(jsPath, "utf8");
|
|
56
|
+
const modifiedJs = localJs + "\n// LOCAL_ONLY";
|
|
57
|
+
fs.writeFileSync(jsPath, modifiedJs, "utf8");
|
|
58
|
+
|
|
59
|
+
await runCc(["pull", "script", namePath, repoRoot]);
|
|
60
|
+
const jsAfterPull = fs.readFileSync(jsPath, "utf8");
|
|
61
|
+
assert.ok(!jsAfterPull.includes("LOCAL_ONLY"), "pull 后应使用远端内容覆盖本地");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
t.test("5) 查询线上脚本详情(detail:仅使用 id 或 namePath)", async () => {
|
|
65
|
+
await assert.doesNotReject(() => runCc(["detail", "script", namePath, "", repoRoot]));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
t.test("6) get 获取线上脚本列表(取前2个 id)", async () => {
|
|
69
|
+
const listQuery = encodeURI(JSON.stringify({ pageNo: 1, pageSize: 20, condition: {} }));
|
|
70
|
+
const { stdout } = await runCc(["get", "script", listQuery, repoRoot]);
|
|
71
|
+
const list = JSON.parse(stdout.trim());
|
|
72
|
+
assert.ok(Array.isArray(list), "get script 应返回数组");
|
|
73
|
+
topIds = list.slice(0, 2).map((x) => x.id).filter(Boolean);
|
|
74
|
+
assert.ok(topIds.length >= 1, "线上脚本列表至少应包含 1 个 id");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
t.test("7) 批量拉取前2个脚本(pullList)", async () => {
|
|
78
|
+
assert.ok(topIds.length >= 1, "pullList 需要至少 1 个线上脚本 id");
|
|
79
|
+
const beforeCount = fs.existsSync(scriptRoot)
|
|
80
|
+
? fs.readdirSync(scriptRoot).filter((n) => fs.statSync(path.join(scriptRoot, n)).isDirectory()).length
|
|
81
|
+
: 0;
|
|
82
|
+
|
|
83
|
+
for (const id of topIds) {
|
|
84
|
+
await runCc(["pullList", "script", id, repoRoot]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const afterCount = fs.existsSync(scriptRoot)
|
|
88
|
+
? fs.readdirSync(scriptRoot).filter((n) => fs.statSync(path.join(scriptRoot, n)).isDirectory()).length
|
|
89
|
+
: 0;
|
|
90
|
+
assert.ok(afterCount >= beforeCount, "pullList 之后 script 目录应存在/可用");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
t.test("8) 清空 script 文件夹", async () => {
|
|
94
|
+
if (fs.existsSync(scriptRoot)) {
|
|
95
|
+
fs.rmSync(scriptRoot, { recursive: true, force: true });
|
|
96
|
+
}
|
|
97
|
+
fs.mkdirSync(scriptRoot, { recursive: true });
|
|
98
|
+
const remain = fs.readdirSync(scriptRoot);
|
|
99
|
+
assert.equal(remain.length, 0);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const test = require("node:test");
|
|
2
|
+
const assert = require("node:assert/strict");
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { exec } = require("node:child_process");
|
|
6
|
+
const { promisify } = require("node:util");
|
|
7
|
+
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
const repoRoot = path.resolve(__dirname, "..");
|
|
10
|
+
const scheduleRoot = path.join(repoRoot, "schedule");
|
|
11
|
+
|
|
12
|
+
function quoteArg(value) {
|
|
13
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function runCc(args) {
|
|
17
|
+
const cmd = `npx --prefix "${repoRoot}" cc ${args.map(quoteArg).join(" ")}`;
|
|
18
|
+
return execAsync(cmd, { cwd: repoRoot });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test("定时任务管理流程:创建→发布→拉取→线上详情→列表→批量拉取前2个→清空 schedule 文件夹", async (t) => {
|
|
22
|
+
const timerName = `CCTimer${Date.now()}`;
|
|
23
|
+
const timerDir = path.join(scheduleRoot, timerName);
|
|
24
|
+
const configPath = path.join(timerDir, "config.json");
|
|
25
|
+
const javaPath = path.join(timerDir, `${timerName}.java`);
|
|
26
|
+
let publishedId = null;
|
|
27
|
+
let topIds = [];
|
|
28
|
+
|
|
29
|
+
t.test("1) 创建定时任务", async () => {
|
|
30
|
+
await runCc(["create", "timer", timerName]);
|
|
31
|
+
|
|
32
|
+
assert.equal(fs.existsSync(timerDir), true);
|
|
33
|
+
assert.equal(fs.existsSync(configPath), true);
|
|
34
|
+
assert.equal(fs.existsSync(javaPath), true);
|
|
35
|
+
|
|
36
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
37
|
+
assert.equal(config.name, timerName);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
t.test("2) 发布定时任务(publish,写回 config.id)", async () => {
|
|
41
|
+
await runCc(["publish", "timer", timerName]);
|
|
42
|
+
const configAfterPublish = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
43
|
+
publishedId = configAfterPublish.id || null;
|
|
44
|
+
assert.ok(publishedId, "publish 后应写入 config.id");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
t.test("3) 拉取定时任务(pull:覆盖 SOURCE 区域)", async () => {
|
|
48
|
+
const localJava = fs.readFileSync(javaPath, "utf8");
|
|
49
|
+
const modifiedJava = localJava.replace(
|
|
50
|
+
/\/\/ @SOURCE_CONTENT_START[\s\S]*?\/\/ @SOURCE_CONTENT_END/,
|
|
51
|
+
"// @SOURCE_CONTENT_START\nSystem.out.print(\"LOCAL_ONLY\");\n// @SOURCE_CONTENT_END"
|
|
52
|
+
);
|
|
53
|
+
fs.writeFileSync(javaPath, modifiedJava, "utf8");
|
|
54
|
+
|
|
55
|
+
await runCc(["pull", "timer", timerName]);
|
|
56
|
+
const javaAfterPull = fs.readFileSync(javaPath, "utf8");
|
|
57
|
+
assert.ok(!javaAfterPull.includes("LOCAL_ONLY"), "pull 后应使用远端源码替换本地标记内容");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
t.test("4) 查询线上定时任务详情(detail:仅使用 id)", async () => {
|
|
61
|
+
assert.ok(publishedId, "发布后应有 publishedId");
|
|
62
|
+
await assert.doesNotReject(() => runCc(["detail", "timer", "", publishedId]));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
t.test("5) get 获取线上定时任务列表(取前2个 id)", async () => {
|
|
66
|
+
const listQuery = encodeURI(
|
|
67
|
+
JSON.stringify({
|
|
68
|
+
shownum: "2000",
|
|
69
|
+
showpage: "1",
|
|
70
|
+
fid: "",
|
|
71
|
+
sname: "",
|
|
72
|
+
rptcond: "",
|
|
73
|
+
rptorder: "",
|
|
74
|
+
})
|
|
75
|
+
);
|
|
76
|
+
const { stdout } = await runCc(["get", "timer", listQuery, repoRoot]);
|
|
77
|
+
const list = JSON.parse(stdout.trim());
|
|
78
|
+
assert.ok(Array.isArray(list), "get timer 应返回数组");
|
|
79
|
+
topIds = list.slice(0, 2).map((x) => x.id).filter(Boolean);
|
|
80
|
+
assert.ok(topIds.length >= 1, "线上定时任务列表至少应包含 1 个 id");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
t.test("6) 批量拉取前2个定时任务(pullList)", async () => {
|
|
84
|
+
assert.ok(topIds.length >= 1, "pullList 需要至少 1 个线上定时任务 id");
|
|
85
|
+
const beforeCount = fs.existsSync(scheduleRoot)
|
|
86
|
+
? fs.readdirSync(scheduleRoot).filter((n) => fs.statSync(path.join(scheduleRoot, n)).isDirectory()).length
|
|
87
|
+
: 0;
|
|
88
|
+
|
|
89
|
+
for (const id of topIds) {
|
|
90
|
+
await runCc(["pullList", "timer", id, repoRoot]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const afterCount = fs.readdirSync(scheduleRoot).filter((n) =>
|
|
94
|
+
fs.statSync(path.join(scheduleRoot, n)).isDirectory()
|
|
95
|
+
).length;
|
|
96
|
+
assert.ok(afterCount >= beforeCount, "pullList 之后 schedule 目录应存在/可用");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
t.test("7) 清空 schedule 文件夹", async () => {
|
|
100
|
+
if (fs.existsSync(scheduleRoot)) {
|
|
101
|
+
fs.rmSync(scheduleRoot, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
fs.mkdirSync(scheduleRoot, { recursive: true });
|
|
104
|
+
const remain = fs.readdirSync(scheduleRoot);
|
|
105
|
+
assert.equal(remain.length, 0);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
const test = require("node:test");
|
|
2
|
+
const assert = require("node:assert/strict");
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { exec } = require("node:child_process");
|
|
6
|
+
const { promisify } = require("node:util");
|
|
7
|
+
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
const repoRoot = path.resolve(__dirname, "..");
|
|
10
|
+
const triggersRoot = path.join(repoRoot, "triggers");
|
|
11
|
+
|
|
12
|
+
function quoteArg(value) {
|
|
13
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function runCc(args) {
|
|
17
|
+
const cmd = `npx --prefix "${repoRoot}" cc ${args.map(quoteArg).join(" ")}`;
|
|
18
|
+
return execAsync(cmd, { cwd: repoRoot });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test("触发器管理流程:获取对象→创建→doc→发布→拉取→线上详情→列表→批量拉取前2个→清空 triggers 文件夹", async (t) => {
|
|
22
|
+
let schemetableName = "Account";
|
|
23
|
+
let targetObjectId = null;
|
|
24
|
+
let apiname = null;
|
|
25
|
+
const triggerTime = "beforeInsert";
|
|
26
|
+
const triggerName = `TestTrigger${Date.now()}`;
|
|
27
|
+
const namePath = `${schemetableName.toLowerCase()}/${triggerName}`;
|
|
28
|
+
const triggerDir = path.join(triggersRoot, schemetableName.toLowerCase(), triggerName);
|
|
29
|
+
const configPath = path.join(triggerDir, "config.json");
|
|
30
|
+
const javaPath = path.join(triggerDir, `${triggerName}.java`);
|
|
31
|
+
let topIds = [];
|
|
32
|
+
|
|
33
|
+
t.test("1) 获取自定义对象列表(取 schemetableName 用于创建触发器)", async () => {
|
|
34
|
+
const { stdout } = await runCc(["get", "object", repoRoot, "custom"]);
|
|
35
|
+
const list = JSON.parse(stdout.trim());
|
|
36
|
+
assert.ok(Array.isArray(list) && list.length > 0, "get object custom 应返回非空数组");
|
|
37
|
+
const first = list[0];
|
|
38
|
+
schemetableName = first.schemetableName || first.label || "Account";
|
|
39
|
+
targetObjectId = first.id || null;
|
|
40
|
+
apiname = `NTrigger${Date.now()}`;
|
|
41
|
+
assert.ok(targetObjectId, "应能拿到自定义对象 id 作为 targetObjectId");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
t.test("2) 创建触发器", async () => {
|
|
45
|
+
const body = encodeURI(
|
|
46
|
+
JSON.stringify({
|
|
47
|
+
schemetableName,
|
|
48
|
+
targetObjectId,
|
|
49
|
+
triggerTime,
|
|
50
|
+
name: triggerName,
|
|
51
|
+
apiname,
|
|
52
|
+
})
|
|
53
|
+
);
|
|
54
|
+
await runCc(["create", "triggers", body]);
|
|
55
|
+
|
|
56
|
+
const expectedDir = path.join(triggersRoot, schemetableName.toLowerCase(), triggerName);
|
|
57
|
+
assert.equal(fs.existsSync(expectedDir), true);
|
|
58
|
+
assert.equal(fs.existsSync(path.join(expectedDir, "config.json")), true);
|
|
59
|
+
assert.equal(fs.existsSync(path.join(expectedDir, `${triggerName}.java`)), true);
|
|
60
|
+
|
|
61
|
+
const config = JSON.parse(fs.readFileSync(path.join(expectedDir, "config.json"), "utf8"));
|
|
62
|
+
assert.equal(config.name, triggerName);
|
|
63
|
+
assert.equal(config.apiname, apiname);
|
|
64
|
+
assert.equal(config.targetObjectId, targetObjectId);
|
|
65
|
+
assert.equal(config.triggerTime, triggerTime);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
t.test("3) 获取 doc(triggers overview)", async () => {
|
|
69
|
+
const { stdout } = await runCc(["doc", "triggers", "overview"]);
|
|
70
|
+
assert.match(stdout, /触发器|CCTrigger|编辑指南/i);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
t.test("4) 发布触发器(publish,写回 config.id)", async () => {
|
|
74
|
+
const namePathForPublish = `${schemetableName.toLowerCase()}/${triggerName}`;
|
|
75
|
+
await runCc(["publish", "triggers", namePathForPublish]);
|
|
76
|
+
const configAfterPublish = JSON.parse(
|
|
77
|
+
fs.readFileSync(path.join(triggersRoot, schemetableName.toLowerCase(), triggerName, "config.json"), "utf8")
|
|
78
|
+
);
|
|
79
|
+
assert.ok(configAfterPublish.id, "publish 后应写入 config.id");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
t.test("5) 拉取触发器(pull:覆盖 SOURCE 区域)", async () => {
|
|
83
|
+
const triggerPath = path.join(triggersRoot, schemetableName.toLowerCase(), triggerName);
|
|
84
|
+
const javaPathLocal = path.join(triggerPath, `${triggerName}.java`);
|
|
85
|
+
const localJava = fs.readFileSync(javaPathLocal, "utf8");
|
|
86
|
+
const modifiedJava = localJava.replace(
|
|
87
|
+
/\/\/ @SOURCE_CONTENT_START[\s\S]*?\/\/ @SOURCE_CONTENT_END/,
|
|
88
|
+
"// @SOURCE_CONTENT_START\nSystem.out.print(\"LOCAL_ONLY\");\n// @SOURCE_CONTENT_END"
|
|
89
|
+
);
|
|
90
|
+
fs.writeFileSync(javaPathLocal, modifiedJava, "utf8");
|
|
91
|
+
|
|
92
|
+
await runCc(["pull", "triggers", `${schemetableName.toLowerCase()}/${triggerName}`]);
|
|
93
|
+
const javaAfterPull = fs.readFileSync(javaPathLocal, "utf8");
|
|
94
|
+
assert.ok(!javaAfterPull.includes("LOCAL_ONLY"), "pull 后应使用远端源码替换本地标记内容");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
t.test("6) 查询线上触发器详情(detail:仅使用 id)", async () => {
|
|
98
|
+
const configPathLocal = path.join(triggersRoot, schemetableName.toLowerCase(), triggerName, "config.json");
|
|
99
|
+
const publishedId = JSON.parse(fs.readFileSync(configPathLocal, "utf8")).id;
|
|
100
|
+
assert.ok(publishedId, "发布后应有 id");
|
|
101
|
+
await assert.doesNotReject(() => runCc(["detail", "triggers", "", publishedId]));
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
t.test("7) get 获取线上触发器列表(取前2个 id)", async () => {
|
|
105
|
+
const listQuery = encodeURI(
|
|
106
|
+
JSON.stringify({
|
|
107
|
+
shownum: "2000",
|
|
108
|
+
showpage: "1",
|
|
109
|
+
fid: "",
|
|
110
|
+
sname: "",
|
|
111
|
+
rptcond: "",
|
|
112
|
+
rptorder: "",
|
|
113
|
+
})
|
|
114
|
+
);
|
|
115
|
+
const { stdout } = await runCc(["get", "triggers", listQuery, repoRoot]);
|
|
116
|
+
const list = JSON.parse(stdout.trim());
|
|
117
|
+
assert.ok(Array.isArray(list), "get triggers 应返回数组");
|
|
118
|
+
topIds = list.slice(0, 2).map((x) => x.id).filter(Boolean);
|
|
119
|
+
assert.ok(topIds.length >= 1, "线上触发器列表至少应包含 1 个 id");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
t.test("8) 批量拉取前2个触发器(pullList)", async () => {
|
|
123
|
+
assert.ok(topIds.length >= 1, "pullList 需要至少 1 个线上触发器 id");
|
|
124
|
+
const beforeCount = fs.existsSync(triggersRoot)
|
|
125
|
+
? fs.readdirSync(triggersRoot).filter((n) => fs.statSync(path.join(triggersRoot, n)).isDirectory()).length
|
|
126
|
+
: 0;
|
|
127
|
+
|
|
128
|
+
for (const id of topIds) {
|
|
129
|
+
await runCc(["pullList", "triggers", id, repoRoot]);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const afterCount = fs.existsSync(triggersRoot)
|
|
133
|
+
? fs.readdirSync(triggersRoot).filter((n) => fs.statSync(path.join(triggersRoot, n)).isDirectory()).length
|
|
134
|
+
: 0;
|
|
135
|
+
assert.ok(afterCount >= beforeCount, "pullList 之后 triggers 目录应存在/可用");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
t.test("9) 清空 triggers 文件夹", async () => {
|
|
139
|
+
if (fs.existsSync(triggersRoot)) {
|
|
140
|
+
fs.rmSync(triggersRoot, { recursive: true, force: true });
|
|
141
|
+
}
|
|
142
|
+
fs.mkdirSync(triggersRoot, { recursive: true });
|
|
143
|
+
const remain = fs.readdirSync(triggersRoot);
|
|
144
|
+
assert.equal(remain.length, 0);
|
|
145
|
+
});
|
|
146
|
+
});
|
package/.vscode/settings.json
DELETED
package/bin/mcp-svc.js
DELETED