@cloudbase/cloudbase-mcp 2.25.5 → 2.25.6
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/dist/cli.cjs +3 -3
- package/dist/index.cjs +1169 -183
- package/dist/index.js +914 -69
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -205,13 +205,13 @@ function buildEnvDomainManagementResult(params) {
|
|
|
205
205
|
operation: action,
|
|
206
206
|
targetDomains: domains,
|
|
207
207
|
asyncState: "PENDING",
|
|
208
|
-
message: '
|
|
208
|
+
message: '安全域名已提交添加请求。该变更通常需要数分钟传播(平台侧);请每 10 秒轮询 queryEnv(action="domains") 直到 Status 为 ENABLE,勿一次 sleep 满 10 分钟。',
|
|
209
209
|
propagation: {
|
|
210
210
|
requiresPolling: true,
|
|
211
211
|
pollTool: "queryEnv",
|
|
212
212
|
pollAction: "domains",
|
|
213
213
|
pollIntervalSuggestionSeconds: 10,
|
|
214
|
-
timeoutSuggestionSeconds:
|
|
214
|
+
timeoutSuggestionSeconds: 300,
|
|
215
215
|
successCondition: '目标域名出现在 queryEnv(action="domains") 返回中,且 Status 为 ENABLE。',
|
|
216
216
|
},
|
|
217
217
|
next_step: {
|
|
@@ -230,13 +230,13 @@ function buildEnvDomainManagementResult(params) {
|
|
|
230
230
|
operation: action,
|
|
231
231
|
targetDomains: domains,
|
|
232
232
|
asyncState: "PENDING",
|
|
233
|
-
message: '
|
|
233
|
+
message: '安全域名已提交删除请求。该变更通常需要数分钟传播;请每 10 秒轮询 queryEnv(action="domains") 直到目标域名不再出现,勿一次 sleep 满数分钟。',
|
|
234
234
|
propagation: {
|
|
235
235
|
requiresPolling: true,
|
|
236
236
|
pollTool: "queryEnv",
|
|
237
237
|
pollAction: "domains",
|
|
238
238
|
pollIntervalSuggestionSeconds: 10,
|
|
239
|
-
timeoutSuggestionSeconds:
|
|
239
|
+
timeoutSuggestionSeconds: 300,
|
|
240
240
|
successCondition: '目标域名不再出现在 queryEnv(action="domains") 返回中。',
|
|
241
241
|
},
|
|
242
242
|
next_step: {
|
|
@@ -1944,7 +1944,7 @@ function registerEnvTools(server) {
|
|
|
1944
1944
|
if (server.ide !== 'wxide') {
|
|
1945
1945
|
server.registerTool?.("envDomainManagement", {
|
|
1946
1946
|
title: "CloudBase 环境域名管理(安全域名 / CORS 白名单)",
|
|
1947
|
-
description: "管理 CloudBase 环境的安全域名(安全域名 / CORS 白名单),支持添加和删除操作。(原工具名:createEnvDomain/deleteEnvDomain,为兼容旧AI规则可继续使用这些名称)当浏览器 Web 应用需要从本地 Vite / dev server 直接访问 CloudBase 资源时,应先用 queryEnv(action=domains) 检查当前实际浏览器 origin 对应的 host:port
|
|
1947
|
+
description: "管理 CloudBase 环境的安全域名(安全域名 / CORS 白名单),支持添加和删除操作。(原工具名:createEnvDomain/deleteEnvDomain,为兼容旧AI规则可继续使用这些名称)当浏览器 Web 应用需要从本地 Vite / dev server 直接访问 CloudBase 资源时,应先用 queryEnv(action=domains) 检查当前实际浏览器 origin 对应的 host:port 是否已在白名单中,再按该实际值添加。新增或删除后请每约 10 秒轮询 queryEnv(action=domains) 确认状态收敛,勿一次 sleep 满 10 分钟;多数环境数分钟内可收敛。⚠️ 重要:此工具仅用于 CORS/请求来源验证,不涉及 SSL 证书。自定义域名公网 HTTPS:先 queryGateway(listCustomDomains);已有域名则 manageGateway(createRoute) 显式传 domain(无需证书);仅首次绑定新域名才用 bindCustomDomain(需 certificateId)。",
|
|
1948
1948
|
inputSchema: {
|
|
1949
1949
|
action: zod_1.z
|
|
1950
1950
|
.enum(["create", "delete"])
|
|
@@ -2772,7 +2772,7 @@ ${envIdSection}
|
|
|
2772
2772
|
## 环境信息
|
|
2773
2773
|
- 操作系统: ${os_1.default.type()} ${os_1.default.release()}
|
|
2774
2774
|
- Node.js版本: ${process.version}
|
|
2775
|
-
- MCP 版本:${process.env.npm_package_version || "2.25.
|
|
2775
|
+
- MCP 版本:${process.env.npm_package_version || "2.25.6" || 0}
|
|
2776
2776
|
- 系统架构: ${os_1.default.arch()}
|
|
2777
2777
|
- 时间: ${new Date().toISOString()}
|
|
2778
2778
|
- 请求ID: ${requestId}
|
|
@@ -2853,7 +2853,7 @@ function createWrappedHandler(name, handler, server) {
|
|
|
2853
2853
|
requestId: (typeof error === 'object' && error && 'requestId' in error) ? error.requestId : '',
|
|
2854
2854
|
ide: server.ide || process.env.INTEGRATION_IDE || ''
|
|
2855
2855
|
});
|
|
2856
|
-
const mcpVersion = true ? "2.25.
|
|
2856
|
+
const mcpVersion = true ? "2.25.6" : 0;
|
|
2857
2857
|
const requestIdSuffix = requestId ? `\n🆔 RequestId: ${requestId}` : '';
|
|
2858
2858
|
const enhancedErrorMessage = `${errorMessage}${requestIdSuffix}\n\n📦 CloudBase MCP v${mcpVersion}\n🔗 遇到问题?请复制以下链接到浏览器打开\n即可自动携带错误详情快速创建 GitHub Issue:\n${issueLink}`;
|
|
2859
2859
|
// 创建新的错误对象,保持原有的错误类型但更新消息
|
|
@@ -7165,7 +7165,7 @@ const KnowledgeBaseIdMap = {
|
|
|
7165
7165
|
const CACHE_BASE_DIR = path.join(os.homedir(), ".cloudbase-mcp");
|
|
7166
7166
|
const CACHE_META_FILE = path.join(CACHE_BASE_DIR, "cache-meta.json");
|
|
7167
7167
|
const LOCK_FILE = path.join(CACHE_BASE_DIR, ".download.lock");
|
|
7168
|
-
const DEFAULT_CACHE_TTL_MS =
|
|
7168
|
+
const DEFAULT_CACHE_TTL_MS = 60 * 60 * 1000; // default 1 hour (was 24h; templates/docs refresh more often)
|
|
7169
7169
|
// Promise wrapper for lockfile methods
|
|
7170
7170
|
function acquireLock(lockPath, options) {
|
|
7171
7171
|
return new Promise((resolve, reject) => {
|
|
@@ -8056,7 +8056,7 @@ function registerGatewayTools(server) {
|
|
|
8056
8056
|
{
|
|
8057
8057
|
tool: "queryGateway",
|
|
8058
8058
|
action: "getRoute",
|
|
8059
|
-
reason: "
|
|
8059
|
+
reason: "创建后通常数秒到约 30 秒内生效;请立刻轮询 getRoute 或探测 accessUrl,勿盲等 60 秒以上",
|
|
8060
8060
|
},
|
|
8061
8061
|
{
|
|
8062
8062
|
tool: "queryPermissions",
|
|
@@ -8204,7 +8204,7 @@ function registerGatewayTools(server) {
|
|
|
8204
8204
|
const privilege = await getGatewayPrivilege();
|
|
8205
8205
|
if (privilege.EnableService !== true) {
|
|
8206
8206
|
privilegeHint =
|
|
8207
|
-
"⚠️ HTTP 网关总开关未开启,路由创建成功后访问仍将返回 HTTPSERVICE_NONACTIVATED(403);请先调用 manageGateway(action=\"enableService\", enable=true)
|
|
8207
|
+
"⚠️ HTTP 网关总开关未开启,路由创建成功后访问仍将返回 HTTPSERVICE_NONACTIVATED(403);请先调用 manageGateway(action=\"enableService\", enable=true) 开启,再立刻探测 accessUrl(通常数秒到约 30 秒内生效,勿盲等 60 秒以上)。";
|
|
8208
8208
|
privilegeNextActions = [
|
|
8209
8209
|
{
|
|
8210
8210
|
tool: "manageGateway",
|
|
@@ -8240,7 +8240,7 @@ function registerGatewayTools(server) {
|
|
|
8240
8240
|
: payload.resolved.enablePathTransmission === false
|
|
8241
8241
|
? ";路径透传关闭(网关会剥掉触发路径前缀后再转发给后端)"
|
|
8242
8242
|
: ";路径透传未显式设置(平台默认 false,会剥掉触发路径前缀)") +
|
|
8243
|
-
|
|
8243
|
+
`。注意:路由配置传播通常数秒到约 30 秒;请立刻用 queryGateway(getRoute) 或探测 accessUrl 确认,勿盲等 60 秒以上。该操作只创建网关入口,不会自动放开上游权限;若上游是云函数且需要匿名或浏览器直接访问,请继续检查函数资源权限。` +
|
|
8244
8244
|
(privilegeHint ? ` ${privilegeHint}` : ""), [
|
|
8245
8245
|
...privilegeNextActions,
|
|
8246
8246
|
...(routeMutationNextActions(payload.resolved.upstreamResourceName) ??
|
|
@@ -11208,7 +11208,7 @@ class TelemetryReporter {
|
|
|
11208
11208
|
const nodeVersion = process.version; // Node.js版本
|
|
11209
11209
|
const arch = os_1.default.arch(); // 系统架构
|
|
11210
11210
|
// 从构建时注入的版本号获取MCP版本信息
|
|
11211
|
-
const mcpVersion = process.env.npm_package_version || "2.25.
|
|
11211
|
+
const mcpVersion = process.env.npm_package_version || "2.25.6" || 0;
|
|
11212
11212
|
return {
|
|
11213
11213
|
userAgent: `${osType} ${osRelease} ${arch} ${nodeVersion} CloudBase-MCP/${mcpVersion}`,
|
|
11214
11214
|
deviceId: this.deviceId,
|
|
@@ -11545,6 +11545,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
|
11545
11545
|
exports.CUSTOM_IMAGE_RUNTIME = exports.MANAGE_FUNCTION_ACTIONS = exports.QUERY_FUNCTION_ACTIONS = exports.TRIGGER_CONFIG_EXAMPLES = exports.SUPPORTED_TRIGGER_TYPES = exports.DEFAULT_NODEJS_RUNTIME = exports.SUPPORTED_NODEJS_RUNTIMES = exports.RECOMMENDED_RUNTIMES = exports.DEFAULT_RUNTIME = exports.ALL_SUPPORTED_RUNTIMES = exports.SUPPORTED_RUNTIMES = void 0;
|
|
11546
11546
|
exports.formatRuntimeList = formatRuntimeList;
|
|
11547
11547
|
exports.validateTimerCron = validateTimerCron;
|
|
11548
|
+
exports.pickManageFunctionName = pickManageFunctionName;
|
|
11548
11549
|
exports.shouldInstallDependencyForFunction = shouldInstallDependencyForFunction;
|
|
11549
11550
|
exports.resolveEventFunctionRuntime = resolveEventFunctionRuntime;
|
|
11550
11551
|
exports.buildFunctionOperationErrorMessage = buildFunctionOperationErrorMessage;
|
|
@@ -11773,6 +11774,19 @@ const MANAGE_LAYER_SCHEMA = zod_1.z.object({
|
|
|
11773
11774
|
layerName: zod_1.z.string().describe("层名称"),
|
|
11774
11775
|
layerVersion: zod_1.z.number().describe("层版本号"),
|
|
11775
11776
|
});
|
|
11777
|
+
/**
|
|
11778
|
+
* Prefer top-level functionName; fall back to func.name for the common
|
|
11779
|
+
* mistake of nesting the target name under func (createFunction shape).
|
|
11780
|
+
*/
|
|
11781
|
+
function pickManageFunctionName(input) {
|
|
11782
|
+
if (typeof input.functionName === "string" && input.functionName.trim()) {
|
|
11783
|
+
return input.functionName.trim();
|
|
11784
|
+
}
|
|
11785
|
+
if (typeof input.func?.name === "string" && input.func.name.trim()) {
|
|
11786
|
+
return input.func.name.trim();
|
|
11787
|
+
}
|
|
11788
|
+
return undefined;
|
|
11789
|
+
}
|
|
11776
11790
|
function normalizeFunctionLayers(layers) {
|
|
11777
11791
|
if (!Array.isArray(layers)) {
|
|
11778
11792
|
return [];
|
|
@@ -12239,7 +12253,12 @@ function registerFunctionTools(server) {
|
|
|
12239
12253
|
throw new Error(`不支持的操作类型: ${input.action}`);
|
|
12240
12254
|
}
|
|
12241
12255
|
};
|
|
12242
|
-
const handleManageFunctions = async (
|
|
12256
|
+
const handleManageFunctions = async (rawInput) => {
|
|
12257
|
+
// Accept func.name as a fallback for top-level functionName (common agent mistake).
|
|
12258
|
+
const pickedName = pickManageFunctionName(rawInput);
|
|
12259
|
+
const input = pickedName && rawInput.functionName !== pickedName
|
|
12260
|
+
? { ...rawInput, functionName: pickedName }
|
|
12261
|
+
: rawInput;
|
|
12243
12262
|
ensureActionAllowedInCloudMode(input);
|
|
12244
12263
|
switch (input.action) {
|
|
12245
12264
|
case "createFunction": {
|
|
@@ -12945,7 +12964,12 @@ function registerFunctionTools(server) {
|
|
|
12945
12964
|
"此参数传 cloudfunctions 或 functions 目录的绝对路径。" +
|
|
12946
12965
|
"SDK 会自动拼接函数名子目录,无需预先压缩 zip 或 base64 编码。"),
|
|
12947
12966
|
force: zod_1.z.boolean().optional().describe("createFunction 时是否覆盖"),
|
|
12948
|
-
functionName: zod_1.z
|
|
12967
|
+
functionName: zod_1.z
|
|
12968
|
+
.string()
|
|
12969
|
+
.optional()
|
|
12970
|
+
.describe("目标函数名称(顶层)。updateFunctionCode / updateFunctionConfig / invokeFunction 等 action 使用此字段。" +
|
|
12971
|
+
"不要只写在 func.name:createFunction 用 func.name,其它 action 用顶层 functionName。" +
|
|
12972
|
+
"若误传 func.name,也会被识别为 functionName。"),
|
|
12949
12973
|
zipFile: zod_1.z.string().optional().describe("仅兼容特殊场景:预先准备好的代码包 base64 编码。普通 createFunction/updateFunctionCode 默认不要先压缩 zip,优先使用 functionRootPath。"),
|
|
12950
12974
|
handler: zod_1.z.string().optional().describe("函数入口"),
|
|
12951
12975
|
timeout: zod_1.z.number().optional().describe("配置更新时的超时时间"),
|
|
@@ -13000,12 +13024,47 @@ module.exports = __WEBPACK_EXTERNAL_MODULE_ws__;
|
|
|
13000
13024
|
/***/ }),
|
|
13001
13025
|
|
|
13002
13026
|
/***/ 6473:
|
|
13003
|
-
/***/ ((__unused_webpack_module, exports, __webpack_require__)
|
|
13027
|
+
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {
|
|
13004
13028
|
|
|
13005
13029
|
|
|
13030
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13031
|
+
if (k2 === undefined) k2 = k;
|
|
13032
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13033
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13034
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13035
|
+
}
|
|
13036
|
+
Object.defineProperty(o, k2, desc);
|
|
13037
|
+
}) : (function(o, m, k, k2) {
|
|
13038
|
+
if (k2 === undefined) k2 = k;
|
|
13039
|
+
o[k2] = m[k];
|
|
13040
|
+
}));
|
|
13041
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
13042
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
13043
|
+
}) : function(o, v) {
|
|
13044
|
+
o["default"] = v;
|
|
13045
|
+
});
|
|
13046
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
13047
|
+
var ownKeys = function(o) {
|
|
13048
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
13049
|
+
var ar = [];
|
|
13050
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
13051
|
+
return ar;
|
|
13052
|
+
};
|
|
13053
|
+
return ownKeys(o);
|
|
13054
|
+
};
|
|
13055
|
+
return function (mod) {
|
|
13056
|
+
if (mod && mod.__esModule) return mod;
|
|
13057
|
+
var result = {};
|
|
13058
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
13059
|
+
__setModuleDefault(result, mod);
|
|
13060
|
+
return result;
|
|
13061
|
+
};
|
|
13062
|
+
})();
|
|
13006
13063
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
13007
13064
|
exports.__resetPgReadyCache = __resetPgReadyCache;
|
|
13008
13065
|
exports.registerPGDatabaseTools = registerPGDatabaseTools;
|
|
13066
|
+
const fs = __importStar(__webpack_require__(4421));
|
|
13067
|
+
const path = __importStar(__webpack_require__(2521));
|
|
13009
13068
|
const zod_1 = __webpack_require__(2971);
|
|
13010
13069
|
const cloudbase_manager_js_1 = __webpack_require__(3431);
|
|
13011
13070
|
const tool_result_js_1 = __webpack_require__(9835);
|
|
@@ -13026,6 +13085,8 @@ const MANAGE_ACTIONS = [
|
|
|
13026
13085
|
"applyMigration",
|
|
13027
13086
|
"listMigrations",
|
|
13028
13087
|
"migrationDetail",
|
|
13088
|
+
"describeMigrationTask",
|
|
13089
|
+
"fetchMigration",
|
|
13029
13090
|
"rollbackMigration",
|
|
13030
13091
|
"repairMigration",
|
|
13031
13092
|
];
|
|
@@ -13186,8 +13247,170 @@ function isSchemaDdlRisk(risk, sql) {
|
|
|
13186
13247
|
const verb = getSqlVerb(stripLeadingSqlComments(sql));
|
|
13187
13248
|
return ["DROP", "TRUNCATE", "ALTER"].includes(verb);
|
|
13188
13249
|
}
|
|
13250
|
+
/**
|
|
13251
|
+
* Canonical local migration directory — must match CloudBase CLI
|
|
13252
|
+
* `MIGRATIONS_DIR` (`cloudbase/migrations`) so MCP+CLI mixed workflows share files.
|
|
13253
|
+
* Legacy bare `migrations/` is accepted only when an existing matching file is found.
|
|
13254
|
+
*/
|
|
13255
|
+
const LOCAL_MIGRATIONS_DIR = "cloudbase/migrations";
|
|
13256
|
+
const LOCAL_MIGRATIONS_DIR_LEGACY = "migrations";
|
|
13257
|
+
/** Same constraint as applyMigration.migrationName (broader than CLI /^[a-z_]+$/ — digits allowed). */
|
|
13258
|
+
const MIGRATION_NAME_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
13259
|
+
function getMigrationProjectRoot() {
|
|
13260
|
+
const fromEnv = process.env.WORKSPACE_FOLDER_PATHS ||
|
|
13261
|
+
process.env.PROJECT_ROOT ||
|
|
13262
|
+
process.env.GITHUB_WORKSPACE ||
|
|
13263
|
+
process.env.CI_PROJECT_DIR ||
|
|
13264
|
+
process.env.BUILD_SOURCESDIRECTORY;
|
|
13265
|
+
if (fromEnv && fromEnv.trim()) {
|
|
13266
|
+
// IDEs may inject multiple folders separated by path.delimiter.
|
|
13267
|
+
const first = fromEnv.split(path.delimiter).map((p) => p.trim()).find(Boolean);
|
|
13268
|
+
if (first) {
|
|
13269
|
+
return first;
|
|
13270
|
+
}
|
|
13271
|
+
}
|
|
13272
|
+
return process.cwd();
|
|
13273
|
+
}
|
|
13274
|
+
function buildLocalMigrationFileName(version, name) {
|
|
13275
|
+
return `${version}_${name}.sql`;
|
|
13276
|
+
}
|
|
13189
13277
|
function buildLocalMigrationFileHint(version, name) {
|
|
13190
|
-
return
|
|
13278
|
+
return `${LOCAL_MIGRATIONS_DIR}/${buildLocalMigrationFileName(version, name)}`;
|
|
13279
|
+
}
|
|
13280
|
+
/** Normalize SQL for local file compare/write (LF + single trailing newline). */
|
|
13281
|
+
function normalizeMigrationSqlForLocalFile(sql) {
|
|
13282
|
+
const normalized = sql.replace(/\r\n/g, "\n").replace(/\s+$/u, "");
|
|
13283
|
+
return `${normalized}\n`;
|
|
13284
|
+
}
|
|
13285
|
+
/**
|
|
13286
|
+
* Ensure the workspace has a Git-truth SQL file for this migration before Push.
|
|
13287
|
+
* Prefer write-to-workspace when missing; fail closed on mismatch or write failure
|
|
13288
|
+
* (Supabase MCP#241 class: remote apply without local truth source).
|
|
13289
|
+
*/
|
|
13290
|
+
function ensureLocalMigrationSqlFile(version, name, sql) {
|
|
13291
|
+
const fileName = buildLocalMigrationFileName(version, name);
|
|
13292
|
+
const expected = normalizeMigrationSqlForLocalFile(sql);
|
|
13293
|
+
const projectRoot = getMigrationProjectRoot();
|
|
13294
|
+
const candidates = [
|
|
13295
|
+
{
|
|
13296
|
+
relativePath: `${LOCAL_MIGRATIONS_DIR}/${fileName}`,
|
|
13297
|
+
absolutePath: path.join(projectRoot, LOCAL_MIGRATIONS_DIR, fileName),
|
|
13298
|
+
},
|
|
13299
|
+
{
|
|
13300
|
+
relativePath: `${LOCAL_MIGRATIONS_DIR_LEGACY}/${fileName}`,
|
|
13301
|
+
absolutePath: path.join(projectRoot, LOCAL_MIGRATIONS_DIR_LEGACY, fileName),
|
|
13302
|
+
},
|
|
13303
|
+
];
|
|
13304
|
+
for (const candidate of candidates) {
|
|
13305
|
+
if (!fs.existsSync(candidate.absolutePath)) {
|
|
13306
|
+
continue;
|
|
13307
|
+
}
|
|
13308
|
+
let existing;
|
|
13309
|
+
try {
|
|
13310
|
+
existing = fs.readFileSync(candidate.absolutePath, "utf8");
|
|
13311
|
+
}
|
|
13312
|
+
catch (error) {
|
|
13313
|
+
return {
|
|
13314
|
+
ok: false,
|
|
13315
|
+
errorCode: "LOCAL_MIGRATION_FILE_WRITE_FAILED",
|
|
13316
|
+
message: `Failed to read existing local migration file ${candidate.relativePath}: ` +
|
|
13317
|
+
`${error instanceof Error ? error.message : String(error)}`,
|
|
13318
|
+
relativePath: candidate.relativePath,
|
|
13319
|
+
absolutePath: candidate.absolutePath,
|
|
13320
|
+
};
|
|
13321
|
+
}
|
|
13322
|
+
if (normalizeMigrationSqlForLocalFile(existing) === expected) {
|
|
13323
|
+
return {
|
|
13324
|
+
ok: true,
|
|
13325
|
+
relativePath: candidate.relativePath,
|
|
13326
|
+
absolutePath: candidate.absolutePath,
|
|
13327
|
+
action: "matched",
|
|
13328
|
+
};
|
|
13329
|
+
}
|
|
13330
|
+
return {
|
|
13331
|
+
ok: false,
|
|
13332
|
+
errorCode: "LOCAL_MIGRATION_FILE_MISMATCH",
|
|
13333
|
+
message: `Local migration file ${candidate.relativePath} already exists but its content does not match the sql argument. ` +
|
|
13334
|
+
"Refuse to overwrite (fail closed). Update the local file to match, or use a new migrationVersion / migrationName. " +
|
|
13335
|
+
"Push was NOT submitted.",
|
|
13336
|
+
relativePath: candidate.relativePath,
|
|
13337
|
+
absolutePath: candidate.absolutePath,
|
|
13338
|
+
};
|
|
13339
|
+
}
|
|
13340
|
+
const target = candidates[0];
|
|
13341
|
+
try {
|
|
13342
|
+
fs.mkdirSync(path.dirname(target.absolutePath), { recursive: true });
|
|
13343
|
+
fs.writeFileSync(target.absolutePath, expected, "utf8");
|
|
13344
|
+
}
|
|
13345
|
+
catch (error) {
|
|
13346
|
+
return {
|
|
13347
|
+
ok: false,
|
|
13348
|
+
errorCode: "LOCAL_MIGRATION_FILE_WRITE_FAILED",
|
|
13349
|
+
message: `Failed to write local migration file ${target.relativePath} under workspace ${projectRoot}: ` +
|
|
13350
|
+
`${error instanceof Error ? error.message : String(error)}. ` +
|
|
13351
|
+
"Push was NOT submitted. Fix workspace permissions or write the file manually, then retry applyMigration.",
|
|
13352
|
+
relativePath: target.relativePath,
|
|
13353
|
+
absolutePath: target.absolutePath,
|
|
13354
|
+
};
|
|
13355
|
+
}
|
|
13356
|
+
return {
|
|
13357
|
+
ok: true,
|
|
13358
|
+
relativePath: target.relativePath,
|
|
13359
|
+
absolutePath: target.absolutePath,
|
|
13360
|
+
action: "created",
|
|
13361
|
+
};
|
|
13362
|
+
}
|
|
13363
|
+
/**
|
|
13364
|
+
* Sync remote migration Query bodies into cloudbase/migrations/ (CLI fetch parity).
|
|
13365
|
+
* Without force, existing files are skipped; with force, overwritten.
|
|
13366
|
+
*/
|
|
13367
|
+
function syncFetchedMigrationsToLocalFiles(items, force) {
|
|
13368
|
+
const projectRoot = getMigrationProjectRoot();
|
|
13369
|
+
const migrationsDir = path.join(projectRoot, LOCAL_MIGRATIONS_DIR);
|
|
13370
|
+
const files = [];
|
|
13371
|
+
for (const item of items) {
|
|
13372
|
+
const version = item.Version.trim();
|
|
13373
|
+
const name = item.Name.trim();
|
|
13374
|
+
if (!MIGRATION_NAME_PATTERN.test(name)) {
|
|
13375
|
+
return {
|
|
13376
|
+
ok: false,
|
|
13377
|
+
errorCode: "LOCAL_MIGRATION_FETCH_INVALID_NAME",
|
|
13378
|
+
message: `Remote migration name "${name}" (version=${version}) is invalid. ` +
|
|
13379
|
+
"Names must match /^[a-z][a-z0-9_]*$/ (same as applyMigration.migrationName). " +
|
|
13380
|
+
"Repair the remote history record, then retry fetchMigration.",
|
|
13381
|
+
};
|
|
13382
|
+
}
|
|
13383
|
+
if (!item.Query.trim()) {
|
|
13384
|
+
return {
|
|
13385
|
+
ok: false,
|
|
13386
|
+
errorCode: "LOCAL_MIGRATION_FETCH_EMPTY_QUERY",
|
|
13387
|
+
message: `DescribePGUserMigration returned empty Query for version=${version} (${name}). ` +
|
|
13388
|
+
"Refuse to write an empty local migration file (would poison Git checksums).",
|
|
13389
|
+
};
|
|
13390
|
+
}
|
|
13391
|
+
const relativePath = buildLocalMigrationFileHint(version, name);
|
|
13392
|
+
const absolutePath = path.join(migrationsDir, buildLocalMigrationFileName(version, name));
|
|
13393
|
+
const exists = fs.existsSync(absolutePath);
|
|
13394
|
+
if (exists && !force) {
|
|
13395
|
+
files.push({ version, name, relativePath, absolutePath, action: "skipped" });
|
|
13396
|
+
continue;
|
|
13397
|
+
}
|
|
13398
|
+
try {
|
|
13399
|
+
fs.mkdirSync(migrationsDir, { recursive: true });
|
|
13400
|
+
fs.writeFileSync(absolutePath, normalizeMigrationSqlForLocalFile(item.Query), "utf8");
|
|
13401
|
+
}
|
|
13402
|
+
catch (error) {
|
|
13403
|
+
return {
|
|
13404
|
+
ok: false,
|
|
13405
|
+
errorCode: "LOCAL_MIGRATION_FILE_WRITE_FAILED",
|
|
13406
|
+
message: `Failed to write local migration file ${relativePath} under workspace ${projectRoot}: ` +
|
|
13407
|
+
`${error instanceof Error ? error.message : String(error)}`,
|
|
13408
|
+
relativePath,
|
|
13409
|
+
};
|
|
13410
|
+
}
|
|
13411
|
+
files.push({ version, name, relativePath, absolutePath, action: "written" });
|
|
13412
|
+
}
|
|
13413
|
+
return { ok: true, files };
|
|
13191
13414
|
}
|
|
13192
13415
|
/** How many recent migration records to scan when verifying a freshly pushed version. */
|
|
13193
13416
|
const MIGRATION_VERIFY_LIMIT = 100;
|
|
@@ -13222,10 +13445,27 @@ const MIGRATION_VERIFY_DELAY_MS = 1500;
|
|
|
13222
13445
|
const MIGRATION_LIST_PAGE_SIZE = 100;
|
|
13223
13446
|
/**
|
|
13224
13447
|
* PushPGUserMigrations is async: poll DescribeTaskResult until Succeed/Failed.
|
|
13225
|
-
*
|
|
13448
|
+
* Default wait aligns with CLI `tcb db pg migration up` (10 minutes).
|
|
13449
|
+
* Override per call via applyMigration.taskPollTimeoutMs.
|
|
13226
13450
|
*/
|
|
13227
13451
|
const MIGRATION_TASK_POLL_INTERVAL_MS = 1500;
|
|
13228
|
-
|
|
13452
|
+
/** CLI parity: `TASK_MAX_WAIT_MS = 10 * 60 * 1000` in cloudbase-cli migration up. */
|
|
13453
|
+
const MIGRATION_TASK_DEFAULT_WAIT_MS = 10 * 60 * 1000;
|
|
13454
|
+
const MIGRATION_TASK_MIN_WAIT_MS = 5_000;
|
|
13455
|
+
const MIGRATION_TASK_MAX_WAIT_MS = MIGRATION_TASK_DEFAULT_WAIT_MS;
|
|
13456
|
+
function resolveMigrationTaskPollTimeoutMs(requested) {
|
|
13457
|
+
if (requested === undefined || !Number.isFinite(requested)) {
|
|
13458
|
+
return MIGRATION_TASK_DEFAULT_WAIT_MS;
|
|
13459
|
+
}
|
|
13460
|
+
const rounded = Math.floor(requested);
|
|
13461
|
+
if (rounded < MIGRATION_TASK_MIN_WAIT_MS) {
|
|
13462
|
+
return MIGRATION_TASK_MIN_WAIT_MS;
|
|
13463
|
+
}
|
|
13464
|
+
if (rounded > MIGRATION_TASK_MAX_WAIT_MS) {
|
|
13465
|
+
return MIGRATION_TASK_MAX_WAIT_MS;
|
|
13466
|
+
}
|
|
13467
|
+
return rounded;
|
|
13468
|
+
}
|
|
13229
13469
|
/**
|
|
13230
13470
|
* Confirm a pushed migration version is present in the remote migration history.
|
|
13231
13471
|
*
|
|
@@ -13309,12 +13549,114 @@ async function listAllRemoteMigrationSummaries(context, cloudBaseOptions) {
|
|
|
13309
13549
|
}
|
|
13310
13550
|
return { summaries, latestVersion, total };
|
|
13311
13551
|
}
|
|
13552
|
+
/** `<version>_<name>.sql` — version is the identity key for completeness checks. */
|
|
13553
|
+
const LOCAL_MIGRATION_FILENAME_RE = /^(\d{14})_(.+)\.sql$/;
|
|
13554
|
+
function listLocalMigrationFilesInDir(dirAbsolute, dirRelative) {
|
|
13555
|
+
const map = new Map();
|
|
13556
|
+
if (!fs.existsSync(dirAbsolute)) {
|
|
13557
|
+
return map;
|
|
13558
|
+
}
|
|
13559
|
+
let stat;
|
|
13560
|
+
try {
|
|
13561
|
+
stat = fs.statSync(dirAbsolute);
|
|
13562
|
+
}
|
|
13563
|
+
catch {
|
|
13564
|
+
return map;
|
|
13565
|
+
}
|
|
13566
|
+
if (!stat.isDirectory()) {
|
|
13567
|
+
return map;
|
|
13568
|
+
}
|
|
13569
|
+
let fileNames;
|
|
13570
|
+
try {
|
|
13571
|
+
fileNames = fs.readdirSync(dirAbsolute);
|
|
13572
|
+
}
|
|
13573
|
+
catch {
|
|
13574
|
+
return map;
|
|
13575
|
+
}
|
|
13576
|
+
for (const fileName of fileNames) {
|
|
13577
|
+
const match = LOCAL_MIGRATION_FILENAME_RE.exec(fileName);
|
|
13578
|
+
if (!match) {
|
|
13579
|
+
continue;
|
|
13580
|
+
}
|
|
13581
|
+
const version = match[1];
|
|
13582
|
+
const name = match[2];
|
|
13583
|
+
const absolutePath = path.join(dirAbsolute, fileName);
|
|
13584
|
+
let query;
|
|
13585
|
+
try {
|
|
13586
|
+
query = fs.readFileSync(absolutePath, "utf8");
|
|
13587
|
+
}
|
|
13588
|
+
catch {
|
|
13589
|
+
continue;
|
|
13590
|
+
}
|
|
13591
|
+
if (!query.trim()) {
|
|
13592
|
+
continue;
|
|
13593
|
+
}
|
|
13594
|
+
map.set(version, {
|
|
13595
|
+
version,
|
|
13596
|
+
name,
|
|
13597
|
+
query,
|
|
13598
|
+
relativePath: `${dirRelative}/${fileName}`,
|
|
13599
|
+
});
|
|
13600
|
+
}
|
|
13601
|
+
return map;
|
|
13602
|
+
}
|
|
13603
|
+
/**
|
|
13604
|
+
* Index local migration SQL by version. Canonical `cloudbase/migrations/` wins over legacy `migrations/`.
|
|
13605
|
+
*/
|
|
13606
|
+
function listLocalMigrationFilesByVersion() {
|
|
13607
|
+
const projectRoot = getMigrationProjectRoot();
|
|
13608
|
+
const legacy = listLocalMigrationFilesInDir(path.join(projectRoot, LOCAL_MIGRATIONS_DIR_LEGACY), LOCAL_MIGRATIONS_DIR_LEGACY);
|
|
13609
|
+
const canonical = listLocalMigrationFilesInDir(path.join(projectRoot, LOCAL_MIGRATIONS_DIR), LOCAL_MIGRATIONS_DIR);
|
|
13610
|
+
const merged = new Map(legacy);
|
|
13611
|
+
for (const [version, entry] of canonical) {
|
|
13612
|
+
merged.set(version, entry);
|
|
13613
|
+
}
|
|
13614
|
+
return merged;
|
|
13615
|
+
}
|
|
13616
|
+
function remoteHistoryCoveredByLocalFiles(summaries, localByVersion, pendingVersion) {
|
|
13617
|
+
for (const summary of summaries) {
|
|
13618
|
+
if (summary.Version === pendingVersion) {
|
|
13619
|
+
// Pending SQL comes from the tool args / ensureLocal write.
|
|
13620
|
+
continue;
|
|
13621
|
+
}
|
|
13622
|
+
const local = localByVersion.get(summary.Version);
|
|
13623
|
+
if (!local || !local.query.trim()) {
|
|
13624
|
+
return false;
|
|
13625
|
+
}
|
|
13626
|
+
}
|
|
13627
|
+
return true;
|
|
13628
|
+
}
|
|
13312
13629
|
/**
|
|
13313
|
-
* Build Push/Preview Migrations =
|
|
13314
|
-
*
|
|
13630
|
+
* Build Push/Preview Migrations = full candidate set (server skips already-applied).
|
|
13631
|
+
*
|
|
13632
|
+
* Prefer CLI parity when the workspace already has SQL for every remote version:
|
|
13633
|
+
* read local files (no per-version DescribePGUserMigration). Otherwise hydrate
|
|
13634
|
+
* remote Queries via List + N×Describe (agent/remote-first when the local tree is incomplete).
|
|
13315
13635
|
*/
|
|
13316
13636
|
async function buildMigrationsPayloadWithRemoteHistory(context, pending, cloudBaseOptions) {
|
|
13317
13637
|
const { summaries, latestVersion } = await listAllRemoteMigrationSummaries(context, cloudBaseOptions);
|
|
13638
|
+
const localByVersion = listLocalMigrationFilesByVersion();
|
|
13639
|
+
if (remoteHistoryCoveredByLocalFiles(summaries, localByVersion, pending.Version)) {
|
|
13640
|
+
const migrations = [];
|
|
13641
|
+
for (const summary of summaries) {
|
|
13642
|
+
if (summary.Version === pending.Version) {
|
|
13643
|
+
continue;
|
|
13644
|
+
}
|
|
13645
|
+
const local = localByVersion.get(summary.Version);
|
|
13646
|
+
migrations.push({
|
|
13647
|
+
Version: local.version,
|
|
13648
|
+
Name: local.name || summary.Name,
|
|
13649
|
+
Query: local.query,
|
|
13650
|
+
});
|
|
13651
|
+
}
|
|
13652
|
+
migrations.push(pending);
|
|
13653
|
+
return {
|
|
13654
|
+
migrations,
|
|
13655
|
+
remoteCount: summaries.length,
|
|
13656
|
+
latestVersion,
|
|
13657
|
+
payloadSource: "local",
|
|
13658
|
+
};
|
|
13659
|
+
}
|
|
13318
13660
|
const migrations = [];
|
|
13319
13661
|
for (const summary of summaries) {
|
|
13320
13662
|
if (summary.Version === pending.Version) {
|
|
@@ -13334,10 +13676,23 @@ async function buildMigrationsPayloadWithRemoteHistory(context, pending, cloudBa
|
|
|
13334
13676
|
});
|
|
13335
13677
|
}
|
|
13336
13678
|
migrations.push(pending);
|
|
13337
|
-
return {
|
|
13679
|
+
return {
|
|
13680
|
+
migrations,
|
|
13681
|
+
remoteCount: summaries.length,
|
|
13682
|
+
latestVersion,
|
|
13683
|
+
payloadSource: "hydrate",
|
|
13684
|
+
};
|
|
13338
13685
|
}
|
|
13339
|
-
|
|
13340
|
-
|
|
13686
|
+
function formatMigrationsPayloadSourceLabel(source, remoteCount) {
|
|
13687
|
+
if (remoteCount <= 0) {
|
|
13688
|
+
return "no remote history";
|
|
13689
|
+
}
|
|
13690
|
+
return source === "local"
|
|
13691
|
+
? `local tree covered ${remoteCount} remote migration(s) (CLI parity, skipped Describe hydrate)`
|
|
13692
|
+
: `hydrated ${remoteCount} remote migration(s) via Describe (local tree incomplete)`;
|
|
13693
|
+
}
|
|
13694
|
+
async function waitPgMigrationTask(context, taskId, cloudBaseOptions, maxWaitMs = MIGRATION_TASK_DEFAULT_WAIT_MS) {
|
|
13695
|
+
const deadline = Date.now() + maxWaitMs;
|
|
13341
13696
|
let last = { TaskId: taskId };
|
|
13342
13697
|
while (Date.now() <= deadline) {
|
|
13343
13698
|
last = (await callPgMigrationApi(context, "DescribeTaskResult", { TaskId: taskId }, cloudBaseOptions));
|
|
@@ -13347,13 +13702,21 @@ async function waitPgMigrationTask(context, taskId, cloudBaseOptions) {
|
|
|
13347
13702
|
}
|
|
13348
13703
|
await sleep(MIGRATION_TASK_POLL_INTERVAL_MS);
|
|
13349
13704
|
}
|
|
13350
|
-
const error = new Error(`DescribeTaskResult timed out after ${Math.floor(
|
|
13705
|
+
const error = new Error(`DescribeTaskResult timed out after ${Math.floor(maxWaitMs / 1000)}s ` +
|
|
13351
13706
|
`for TaskId=${taskId} (last Status=${last.Status || "-"}, Phase=${last.Phase || "-"}).`);
|
|
13352
13707
|
error.lastTask = last;
|
|
13353
13708
|
throw error;
|
|
13354
13709
|
}
|
|
13710
|
+
function buildMigrationTaskPendingNextActions(version, taskId) {
|
|
13711
|
+
const actions = [];
|
|
13712
|
+
if (taskId) {
|
|
13713
|
+
actions.push(buildNextAction(MANAGE_PG_DATABASE, "describeMigrationTask", "FIRST: poll DescribeTaskResult for Status/Phase/Reason. listMigrations alone cannot explain a Failed task (see issue #857).", { action: "describeMigrationTask", taskId }));
|
|
13714
|
+
}
|
|
13715
|
+
actions.push(buildNextAction(MANAGE_PG_DATABASE, "listMigrations", "Check whether migrationVersion landed in remote history. Do NOT re-push the same version while the task may still be running.", { action: "listMigrations", limit: 20 }), buildNextAction(MANAGE_PG_DATABASE, "migrationDetail", "If listMigrations already shows this version, inspect the applied record. If missing after several minutes and the task is Failed/terminal, inspect Conflicts/SQL before considering a NEW migrationVersion.", { action: "migrationDetail", migrationVersion: version }));
|
|
13716
|
+
return actions;
|
|
13717
|
+
}
|
|
13355
13718
|
const MIGRATION_VERSION_REQUIRED_MESSAGE = "migrationVersion is required (14-digit UTC timestamp YYYYMMDDHHMMSS). " +
|
|
13356
|
-
"Decide the version first, write local file migrations/<version>_<migrationName>.sql, " +
|
|
13719
|
+
"Decide the version first, write local file cloudbase/migrations/<version>_<migrationName>.sql, " +
|
|
13357
13720
|
"then call planMigration/applyMigration with the same migrationVersion and migrationName.";
|
|
13358
13721
|
function requireExplicitMigrationVersion(args, actionLabel) {
|
|
13359
13722
|
const version = args.migrationVersion?.trim();
|
|
@@ -13690,6 +14053,30 @@ function createDefaultDependencies(server) {
|
|
|
13690
14053
|
createClient: async (context) => createManagerPgClient(context, server?.cloudBaseOptions),
|
|
13691
14054
|
};
|
|
13692
14055
|
}
|
|
14056
|
+
function isPgMigrationApiDatabase(value) {
|
|
14057
|
+
return Boolean(value &&
|
|
14058
|
+
typeof value === "object" &&
|
|
14059
|
+
"previewPGUserMigrations" in value &&
|
|
14060
|
+
typeof value.previewPGUserMigrations === "function" &&
|
|
14061
|
+
"pushPGUserMigrations" in value &&
|
|
14062
|
+
typeof value.pushPGUserMigrations === "function" &&
|
|
14063
|
+
"listPGUserMigrations" in value &&
|
|
14064
|
+
typeof value.listPGUserMigrations === "function" &&
|
|
14065
|
+
"describeTaskResult" in value &&
|
|
14066
|
+
typeof value.describeTaskResult === "function");
|
|
14067
|
+
}
|
|
14068
|
+
/**
|
|
14069
|
+
* Actions without a native wrapper in manager-node (e.g. RollbackPGUserMigrations)
|
|
14070
|
+
* are intentionally absent and keep using the commonService fallback.
|
|
14071
|
+
*/
|
|
14072
|
+
const PgMigrationNativeActionMethods = {
|
|
14073
|
+
PreviewPGUserMigrations: "previewPGUserMigrations",
|
|
14074
|
+
PushPGUserMigrations: "pushPGUserMigrations",
|
|
14075
|
+
RepairPGUserMigrationHistory: "repairPGUserMigrationHistory",
|
|
14076
|
+
ListPGUserMigrations: "listPGUserMigrations",
|
|
14077
|
+
DescribePGUserMigration: "describePGUserMigration",
|
|
14078
|
+
DescribeTaskResult: "describeTaskResult",
|
|
14079
|
+
};
|
|
13693
14080
|
function isExecutePGSqlDatabase(value) {
|
|
13694
14081
|
return Boolean(value &&
|
|
13695
14082
|
typeof value === "object" &&
|
|
@@ -13730,13 +14117,22 @@ async function executeManagerPGSql(context, sql, cloudBaseOptions) {
|
|
|
13730
14117
|
}
|
|
13731
14118
|
throw new Error("Current @cloudbase/manager-node runtime does not expose database.executePGSql or commonService fallback. Upgrade to @cloudbase/manager-node >= 5.4.0.");
|
|
13732
14119
|
}
|
|
13733
|
-
/**
|
|
14120
|
+
/**
|
|
14121
|
+
* Call a CloudBase PG migration API, preferring native manager-node methods
|
|
14122
|
+
* (>= 5.6.5) and falling back to the commonService channel for older runtimes
|
|
14123
|
+
* and actions without a native wrapper (RollbackPGUserMigrations).
|
|
14124
|
+
*/
|
|
13734
14125
|
async function callPgMigrationApi(context, action, params, cloudBaseOptions) {
|
|
13735
14126
|
const manager = await (0, cloudbase_manager_js_1.getCloudBaseManager)({
|
|
13736
14127
|
cloudBaseOptions: cloudBaseOptions
|
|
13737
14128
|
? { ...cloudBaseOptions, envId: context.envId }
|
|
13738
14129
|
: { envId: context.envId },
|
|
13739
14130
|
});
|
|
14131
|
+
const nativeMethod = PgMigrationNativeActionMethods[action];
|
|
14132
|
+
if (nativeMethod && isPgMigrationApiDatabase(manager.database)) {
|
|
14133
|
+
const invoke = manager.database[nativeMethod];
|
|
14134
|
+
return invoke.call(manager.database, { EnvId: context.envId, ...params });
|
|
14135
|
+
}
|
|
13740
14136
|
if (!isCloudBaseWithCommonService(manager)) {
|
|
13741
14137
|
throw new Error("Current @cloudbase/manager-node runtime does not support migration APIs. Upgrade to @cloudbase/manager-node >= 5.4.0.");
|
|
13742
14138
|
}
|
|
@@ -13977,11 +14373,11 @@ async function handleExecuteSql(args, context, deps) {
|
|
|
13977
14373
|
success: false,
|
|
13978
14374
|
errorCode: "DDL_USE_APPLY_MIGRATION",
|
|
13979
14375
|
message: "Schema DDL (CREATE/ALTER/DROP/TRUNCATE/...) must use applyMigration with an explicit migrationVersion, " +
|
|
13980
|
-
"not execute. Write local migrations/<version>_<name>.sql first, then call applyMigration. " +
|
|
14376
|
+
"not execute. Write local cloudbase/migrations/<version>_<name>.sql first, then call applyMigration. " +
|
|
13981
14377
|
"Set allowDdlViaExecute=true only for exceptional one-off ops that intentionally bypass migration history.",
|
|
13982
14378
|
data: {
|
|
13983
14379
|
classification,
|
|
13984
|
-
localFileHintPattern: "migrations/<migrationVersion>_<migrationName>.sql",
|
|
14380
|
+
localFileHintPattern: "cloudbase/migrations/<migrationVersion>_<migrationName>.sql",
|
|
13985
14381
|
},
|
|
13986
14382
|
nextActions: [
|
|
13987
14383
|
buildNextAction(MANAGE_PG_DATABASE, "planMigration", "Preview the schema change as a versioned migration (provide migrationVersion + migrationName).", {
|
|
@@ -14090,7 +14486,7 @@ async function handleDryRun(args) {
|
|
|
14090
14486
|
wouldExecute: false,
|
|
14091
14487
|
sqlPreview: args.sql.trim().slice(0, 500),
|
|
14092
14488
|
preferredAction: "applyMigration",
|
|
14093
|
-
localFileHintPattern: "migrations/<migrationVersion>_<migrationName>.sql",
|
|
14489
|
+
localFileHintPattern: "cloudbase/migrations/<migrationVersion>_<migrationName>.sql",
|
|
14094
14490
|
},
|
|
14095
14491
|
message: "SQL dry run completed. Schema DDL should use applyMigration with an explicit migrationVersion, not execute.",
|
|
14096
14492
|
nextActions: [
|
|
@@ -14148,10 +14544,13 @@ async function handlePlanMigration(args, context, deps, cloudBaseOptions) {
|
|
|
14148
14544
|
}
|
|
14149
14545
|
try {
|
|
14150
14546
|
const hydrated = await buildMigrationsPayloadWithRemoteHistory(context, pending, cloudBaseOptions);
|
|
14547
|
+
const includeAll = Boolean(args.includeAll);
|
|
14151
14548
|
const result = await callPgMigrationApi(context, "PreviewPGUserMigrations", {
|
|
14152
14549
|
Migrations: hydrated.migrations,
|
|
14550
|
+
IncludeAll: includeAll,
|
|
14153
14551
|
}, cloudBaseOptions);
|
|
14154
14552
|
const executable = result.Executable === true;
|
|
14553
|
+
const payloadSourceLabel = formatMigrationsPayloadSourceLabel(hydrated.payloadSource, hydrated.remoteCount);
|
|
14155
14554
|
return buildPgToolResult({
|
|
14156
14555
|
success: true,
|
|
14157
14556
|
data: {
|
|
@@ -14160,22 +14559,25 @@ async function handlePlanMigration(args, context, deps, cloudBaseOptions) {
|
|
|
14160
14559
|
localFileHint,
|
|
14161
14560
|
hydratedRemoteCount: hydrated.remoteCount,
|
|
14162
14561
|
latestRemoteVersion: hydrated.latestVersion || null,
|
|
14562
|
+
payloadSource: hydrated.payloadSource,
|
|
14563
|
+
includeAll,
|
|
14163
14564
|
executable,
|
|
14164
14565
|
apiResult: result,
|
|
14165
14566
|
},
|
|
14166
14567
|
message: executable
|
|
14167
|
-
? `Migration plan generated via PreviewPGUserMigrations (
|
|
14168
|
-
: `Migration plan is NOT executable (PreviewPGUserMigrations.Executable=false after
|
|
14568
|
+
? `Migration plan generated via PreviewPGUserMigrations (${payloadSourceLabel}${includeAll ? ", includeAll=true" : ""}). Reuse migrationVersion=${version} on applyMigration. Ensure local file ${localFileHint} exists and matches.`
|
|
14569
|
+
: `Migration plan is NOT executable (PreviewPGUserMigrations.Executable=false after ${payloadSourceLabel}${includeAll ? ", includeAll=true" : ""}). Inspect apiResult.Conflicts before applyMigration. Common causes: version older than latest remote (${hydrated.latestVersion || "unknown"}) — retry with includeAll=true (CLI --include-all) if intentionally out-of-order, or pick a newer version; checksum mismatch.`,
|
|
14169
14570
|
nextActions: [
|
|
14170
14571
|
buildNextAction(MANAGE_PG_DATABASE, "applyMigration", executable
|
|
14171
14572
|
? "Review the plan above. If it looks correct, call applyMigration with the same migrationVersion."
|
|
14172
|
-
: "Resolve Conflicts (often pick a migrationVersion newer than LatestVersion), then retry planMigration/applyMigration.", {
|
|
14573
|
+
: "Resolve Conflicts (often pick a migrationVersion newer than LatestVersion, or set includeAll=true for intentional out-of-order), then retry planMigration/applyMigration.", {
|
|
14173
14574
|
action: "applyMigration",
|
|
14174
14575
|
migrationName: args.migrationName,
|
|
14175
14576
|
migrationVersion: version,
|
|
14176
14577
|
sql: args.sql,
|
|
14177
14578
|
confirm: true,
|
|
14178
14579
|
...(args.rollbackSql ? { rollbackSql: args.rollbackSql } : {}),
|
|
14580
|
+
...(includeAll ? { includeAll: true } : {}),
|
|
14179
14581
|
}),
|
|
14180
14582
|
],
|
|
14181
14583
|
});
|
|
@@ -14216,6 +14618,33 @@ async function handleApplyMigration(args, context, deps, cloudBaseOptions) {
|
|
|
14216
14618
|
}
|
|
14217
14619
|
const version = versionOrError;
|
|
14218
14620
|
const localFileHint = buildLocalMigrationFileHint(version, args.migrationName);
|
|
14621
|
+
// Prefer write-to-workspace (or fail closed) so remote history cannot advance without a Git truth source.
|
|
14622
|
+
const localFile = ensureLocalMigrationSqlFile(version, args.migrationName, args.sql);
|
|
14623
|
+
if (!localFile.ok) {
|
|
14624
|
+
return buildPgToolResult({
|
|
14625
|
+
success: false,
|
|
14626
|
+
errorCode: localFile.errorCode,
|
|
14627
|
+
data: {
|
|
14628
|
+
migrationVersion: version,
|
|
14629
|
+
migrationName: args.migrationName,
|
|
14630
|
+
localFileHint,
|
|
14631
|
+
localFilePath: localFile.relativePath ?? localFileHint,
|
|
14632
|
+
localFileAbsolutePath: localFile.absolutePath ?? null,
|
|
14633
|
+
verified: false,
|
|
14634
|
+
},
|
|
14635
|
+
message: localFile.message,
|
|
14636
|
+
nextActions: [
|
|
14637
|
+
buildNextAction(MANAGE_PG_DATABASE, "planMigration", "After aligning the local SQL file with the sql argument (or choosing a new migrationVersion), preview again.", {
|
|
14638
|
+
action: "planMigration",
|
|
14639
|
+
migrationName: args.migrationName,
|
|
14640
|
+
migrationVersion: version,
|
|
14641
|
+
sql: args.sql,
|
|
14642
|
+
...(args.rollbackSql ? { rollbackSql: args.rollbackSql } : {}),
|
|
14643
|
+
...(args.includeAll ? { includeAll: true } : {}),
|
|
14644
|
+
}),
|
|
14645
|
+
],
|
|
14646
|
+
});
|
|
14647
|
+
}
|
|
14219
14648
|
const pending = {
|
|
14220
14649
|
Version: version,
|
|
14221
14650
|
Name: args.migrationName,
|
|
@@ -14224,12 +14653,14 @@ async function handleApplyMigration(args, context, deps, cloudBaseOptions) {
|
|
|
14224
14653
|
if (args.rollbackSql?.trim()) {
|
|
14225
14654
|
pending.Rollback = args.rollbackSql;
|
|
14226
14655
|
}
|
|
14656
|
+
const includeAll = Boolean(args.includeAll);
|
|
14227
14657
|
try {
|
|
14228
14658
|
// Pushing only the pending migration while remote history exists makes the plan
|
|
14229
|
-
// non-executable (remote_history_not_found_locally).
|
|
14230
|
-
//
|
|
14659
|
+
// non-executable (remote_history_not_found_locally). Prefer local tree when complete
|
|
14660
|
+
// (CLI parity); otherwise hydrate remote Queries (List + N×Describe).
|
|
14231
14661
|
const hydrated = await buildMigrationsPayloadWithRemoteHistory(context, pending, cloudBaseOptions);
|
|
14232
|
-
const
|
|
14662
|
+
const payloadSourceLabel = formatMigrationsPayloadSourceLabel(hydrated.payloadSource, hydrated.remoteCount);
|
|
14663
|
+
const preview = await callPgMigrationApi(context, "PreviewPGUserMigrations", { Migrations: hydrated.migrations, IncludeAll: includeAll }, cloudBaseOptions);
|
|
14233
14664
|
if (preview.Executable !== true) {
|
|
14234
14665
|
return buildPgToolResult({
|
|
14235
14666
|
success: false,
|
|
@@ -14240,27 +14671,33 @@ async function handleApplyMigration(args, context, deps, cloudBaseOptions) {
|
|
|
14240
14671
|
localFileHint,
|
|
14241
14672
|
hydratedRemoteCount: hydrated.remoteCount,
|
|
14242
14673
|
latestRemoteVersion: hydrated.latestVersion || null,
|
|
14674
|
+
payloadSource: hydrated.payloadSource,
|
|
14675
|
+
includeAll,
|
|
14243
14676
|
previewResult: preview,
|
|
14244
14677
|
verified: false,
|
|
14245
14678
|
},
|
|
14246
14679
|
message: `PreviewPGUserMigrations reported Executable=false for migrationVersion=${version} ` +
|
|
14247
|
-
`(latest remote=${hydrated.latestVersion || "unknown"},
|
|
14680
|
+
`(latest remote=${hydrated.latestVersion || "unknown"}, ${payloadSourceLabel}, includeAll=${includeAll}). ` +
|
|
14248
14681
|
"Push was NOT submitted. Inspect previewResult.Conflicts — common reasons: " +
|
|
14249
|
-
"local_migration_before_latest_remote (pick a newer 14-digit version) or checksum_mismatch. " +
|
|
14682
|
+
"local_migration_before_latest_remote (pick a newer 14-digit version, or set includeAll=true / CLI --include-all for intentional out-of-order) or checksum_mismatch. " +
|
|
14250
14683
|
"If the schema change is urgent you may fall back to action=execute with allowDdlViaExecute=true, but that bypasses migration history.",
|
|
14251
14684
|
nextActions: [
|
|
14252
14685
|
buildNextAction(MANAGE_PG_DATABASE, "listMigrations", "Check LatestVersion and pick a migrationVersion strictly newer than it.", { action: "listMigrations", limit: 20 }),
|
|
14253
|
-
buildNextAction(MANAGE_PG_DATABASE, "planMigration", "Re-run planMigration after adjusting migrationVersion/SQL.", {
|
|
14686
|
+
buildNextAction(MANAGE_PG_DATABASE, "planMigration", "Re-run planMigration after adjusting migrationVersion/SQL (or includeAll=true for out-of-order).", {
|
|
14254
14687
|
action: "planMigration",
|
|
14255
14688
|
migrationName: args.migrationName,
|
|
14256
14689
|
migrationVersion: version,
|
|
14257
14690
|
sql: args.sql,
|
|
14258
14691
|
...(args.rollbackSql ? { rollbackSql: args.rollbackSql } : {}),
|
|
14692
|
+
...(includeAll ? { includeAll: true } : {}),
|
|
14259
14693
|
}),
|
|
14260
14694
|
],
|
|
14261
14695
|
});
|
|
14262
14696
|
}
|
|
14263
|
-
const params = {
|
|
14697
|
+
const params = {
|
|
14698
|
+
Migrations: hydrated.migrations,
|
|
14699
|
+
IncludeAll: includeAll,
|
|
14700
|
+
};
|
|
14264
14701
|
if (args.lockTimeoutMs !== undefined) {
|
|
14265
14702
|
params.LockTimeoutMs = args.lockTimeoutMs;
|
|
14266
14703
|
}
|
|
@@ -14269,10 +14706,35 @@ async function handleApplyMigration(args, context, deps, cloudBaseOptions) {
|
|
|
14269
14706
|
}
|
|
14270
14707
|
const result = await callPgMigrationApi(context, "PushPGUserMigrations", params, cloudBaseOptions);
|
|
14271
14708
|
const taskId = typeof result.TaskId === "string" ? result.TaskId.trim() : "";
|
|
14709
|
+
const pollTimeoutMs = resolveMigrationTaskPollTimeoutMs(args.taskPollTimeoutMs);
|
|
14710
|
+
const shouldWaitForTask = args.waitForTask !== false;
|
|
14272
14711
|
let taskResult = null;
|
|
14712
|
+
if (taskId && !shouldWaitForTask) {
|
|
14713
|
+
return buildPgToolResult({
|
|
14714
|
+
success: false,
|
|
14715
|
+
errorCode: "MIGRATION_TASK_PENDING",
|
|
14716
|
+
data: {
|
|
14717
|
+
migrationVersion: version,
|
|
14718
|
+
migrationName: args.migrationName,
|
|
14719
|
+
localFileHint,
|
|
14720
|
+
hydratedRemoteCount: hydrated.remoteCount,
|
|
14721
|
+
payloadSource: hydrated.payloadSource,
|
|
14722
|
+
apiResult: result,
|
|
14723
|
+
taskResult: { TaskId: taskId },
|
|
14724
|
+
taskPollTimeoutMs: pollTimeoutMs,
|
|
14725
|
+
waitForTask: false,
|
|
14726
|
+
verified: null,
|
|
14727
|
+
},
|
|
14728
|
+
message: `PushPGUserMigrations accepted TaskId=${taskId} (waitForTask=false). ` +
|
|
14729
|
+
"TaskId means accepted, not applied. Call action=describeMigrationTask FIRST for Status/Phase/Reason, " +
|
|
14730
|
+
`then listMigrations to see whether migrationVersion=${version} landed. ` +
|
|
14731
|
+
"Do NOT re-push the same version while the task may still be running.",
|
|
14732
|
+
nextActions: buildMigrationTaskPendingNextActions(version, taskId),
|
|
14733
|
+
});
|
|
14734
|
+
}
|
|
14273
14735
|
if (taskId) {
|
|
14274
14736
|
try {
|
|
14275
|
-
taskResult = await waitPgMigrationTask(context, taskId, cloudBaseOptions);
|
|
14737
|
+
taskResult = await waitPgMigrationTask(context, taskId, cloudBaseOptions, pollTimeoutMs);
|
|
14276
14738
|
}
|
|
14277
14739
|
catch (error) {
|
|
14278
14740
|
const lastTask = error.lastTask;
|
|
@@ -14286,14 +14748,16 @@ async function handleApplyMigration(args, context, deps, cloudBaseOptions) {
|
|
|
14286
14748
|
hydratedRemoteCount: hydrated.remoteCount,
|
|
14287
14749
|
apiResult: result,
|
|
14288
14750
|
taskResult: lastTask ?? { TaskId: taskId },
|
|
14751
|
+
taskPollTimeoutMs: pollTimeoutMs,
|
|
14289
14752
|
verified: null,
|
|
14290
14753
|
},
|
|
14291
|
-
message: `PushPGUserMigrations returned TaskId=${taskId}, but waiting for DescribeTaskResult timed out
|
|
14292
|
-
|
|
14293
|
-
"The migration may
|
|
14294
|
-
|
|
14295
|
-
|
|
14296
|
-
|
|
14754
|
+
message: `PushPGUserMigrations returned TaskId=${taskId}, but waiting for DescribeTaskResult timed out ` +
|
|
14755
|
+
`after ${Math.floor(pollTimeoutMs / 1000)}s: ${error instanceof Error ? error.message : String(error)}. ` +
|
|
14756
|
+
"The migration may STILL be running in the background (large DDL / lock waits). " +
|
|
14757
|
+
"Call action=describeMigrationTask FIRST (Status/Phase/Reason), then listMigrations. " +
|
|
14758
|
+
"Do NOT re-push the same migrationVersion, and do NOT fall back to execute until the task is terminal " +
|
|
14759
|
+
"and listMigrations confirms the version is missing.",
|
|
14760
|
+
nextActions: buildMigrationTaskPendingNextActions(version, taskId),
|
|
14297
14761
|
});
|
|
14298
14762
|
}
|
|
14299
14763
|
const status = String(taskResult.Status || "").toLowerCase();
|
|
@@ -14321,6 +14785,7 @@ async function handleApplyMigration(args, context, deps, cloudBaseOptions) {
|
|
|
14321
14785
|
migrationVersion: version,
|
|
14322
14786
|
sql: args.sql,
|
|
14323
14787
|
...(args.rollbackSql ? { rollbackSql: args.rollbackSql } : {}),
|
|
14788
|
+
...(includeAll ? { includeAll: true } : {}),
|
|
14324
14789
|
}),
|
|
14325
14790
|
buildNextAction(MANAGE_PG_DATABASE, "listMigrations", "Confirm the version is still absent from remote history.", { action: "listMigrations", limit: 20 }),
|
|
14326
14791
|
],
|
|
@@ -14338,6 +14803,7 @@ async function handleApplyMigration(args, context, deps, cloudBaseOptions) {
|
|
|
14338
14803
|
migrationName: args.migrationName,
|
|
14339
14804
|
localFileHint,
|
|
14340
14805
|
hydratedRemoteCount: hydrated.remoteCount,
|
|
14806
|
+
includeAll,
|
|
14341
14807
|
apiResult: result,
|
|
14342
14808
|
taskResult: taskResult,
|
|
14343
14809
|
verified: false,
|
|
@@ -14378,12 +14844,18 @@ async function handleApplyMigration(args, context, deps, cloudBaseOptions) {
|
|
|
14378
14844
|
migrationVersion: version,
|
|
14379
14845
|
migrationName: args.migrationName,
|
|
14380
14846
|
localFileHint,
|
|
14847
|
+
localFilePath: localFile.relativePath,
|
|
14848
|
+
localFileAbsolutePath: localFile.absolutePath,
|
|
14849
|
+
localFileAction: localFile.action,
|
|
14381
14850
|
hydratedRemoteCount: hydrated.remoteCount,
|
|
14851
|
+
payloadSource: hydrated.payloadSource,
|
|
14852
|
+
includeAll,
|
|
14382
14853
|
apiResult: result,
|
|
14383
14854
|
taskResult: taskResult,
|
|
14384
14855
|
verified: true,
|
|
14385
14856
|
},
|
|
14386
|
-
message: `Migrations applied via PushPGUserMigrations (
|
|
14857
|
+
message: `Migrations applied via PushPGUserMigrations (${payloadSourceLabel}${includeAll ? ", includeAll=true" : ""}), task ${taskId ? `TaskId=${taskId} ` : ""}verified present in the remote migration history. ` +
|
|
14858
|
+
`Local SQL ${localFile.action === "created" ? "written" : "matched"} at ${localFile.relativePath}.`,
|
|
14387
14859
|
});
|
|
14388
14860
|
}
|
|
14389
14861
|
catch (error) {
|
|
@@ -14444,6 +14916,156 @@ async function handleMigrationDetail(args, context, deps, cloudBaseOptions) {
|
|
|
14444
14916
|
});
|
|
14445
14917
|
}
|
|
14446
14918
|
}
|
|
14919
|
+
/**
|
|
14920
|
+
* One-shot DescribeTaskResult for a PushPGUserMigrations TaskId.
|
|
14921
|
+
* Complements listMigrations: history shows whether a version landed; task result shows
|
|
14922
|
+
* Status/Phase/Reason while the job is running or after it Failed (issue #857 evidence path).
|
|
14923
|
+
*/
|
|
14924
|
+
async function handleDescribeMigrationTask(args, context, _deps, cloudBaseOptions) {
|
|
14925
|
+
const taskId = args.taskId?.trim();
|
|
14926
|
+
if (!taskId) {
|
|
14927
|
+
return buildPgToolResult({
|
|
14928
|
+
success: false,
|
|
14929
|
+
errorCode: "TASK_ID_REQUIRED",
|
|
14930
|
+
message: "Provide taskId when action=describeMigrationTask " +
|
|
14931
|
+
"(from applyMigration Push / MIGRATION_TASK_PENDING / MIGRATION_TASK_TIMEOUT data.taskResult.TaskId).",
|
|
14932
|
+
});
|
|
14933
|
+
}
|
|
14934
|
+
try {
|
|
14935
|
+
const taskResult = (await callPgMigrationApi(context, "DescribeTaskResult", { TaskId: taskId }, cloudBaseOptions));
|
|
14936
|
+
const status = String(taskResult.Status || "").toLowerCase();
|
|
14937
|
+
const versionHint = args.migrationVersion?.trim();
|
|
14938
|
+
const nextActions = [];
|
|
14939
|
+
if (status === "succeed") {
|
|
14940
|
+
nextActions.push(buildNextAction(MANAGE_PG_DATABASE, "listMigrations", "Task Succeed — confirm migrationVersion is present in remote history before dependent SQL.", { action: "listMigrations", limit: 20 }));
|
|
14941
|
+
if (versionHint) {
|
|
14942
|
+
nextActions.push(buildNextAction(MANAGE_PG_DATABASE, "migrationDetail", "Inspect the applied migration record.", { action: "migrationDetail", migrationVersion: versionHint }));
|
|
14943
|
+
}
|
|
14944
|
+
}
|
|
14945
|
+
else if (status === "failed") {
|
|
14946
|
+
nextActions.push(buildNextAction(MANAGE_PG_DATABASE, "listMigrations", "Confirm the version is absent from history, then fix Conflicts/SQL and retry with a NEW migrationVersion (do not re-push the failed version blindly).", { action: "listMigrations", limit: 20 }));
|
|
14947
|
+
}
|
|
14948
|
+
else {
|
|
14949
|
+
nextActions.push(buildNextAction(MANAGE_PG_DATABASE, "describeMigrationTask", "Task still running — poll again shortly. Do NOT re-push the same migrationVersion.", { action: "describeMigrationTask", taskId }), buildNextAction(MANAGE_PG_DATABASE, "listMigrations", "Optionally check whether the version already landed while Status is still non-terminal.", { action: "listMigrations", limit: 20 }));
|
|
14950
|
+
}
|
|
14951
|
+
const statusLabel = taskResult.Status || "unknown";
|
|
14952
|
+
return buildPgToolResult({
|
|
14953
|
+
success: true,
|
|
14954
|
+
data: {
|
|
14955
|
+
taskId,
|
|
14956
|
+
taskResult: taskResult,
|
|
14957
|
+
terminal: status === "succeed" || status === "failed",
|
|
14958
|
+
...(versionHint ? { migrationVersion: versionHint } : {}),
|
|
14959
|
+
},
|
|
14960
|
+
message: status === "succeed"
|
|
14961
|
+
? `DescribeTaskResult TaskId=${taskId} Status=Succeed (Phase=${taskResult.Phase || "-"}). Verify with listMigrations before treating schema as applied.`
|
|
14962
|
+
: status === "failed"
|
|
14963
|
+
? `DescribeTaskResult TaskId=${taskId} Status=Failed at phase=${taskResult.Phase || "-"}: ${taskResult.Reason || "unknown reason"}. Do NOT re-push the same migrationVersion until Conflicts/SQL are fixed.`
|
|
14964
|
+
: `DescribeTaskResult TaskId=${taskId} Status=${statusLabel} Phase=${taskResult.Phase || "-"}. Task is not terminal yet — poll again; do not re-push.`,
|
|
14965
|
+
nextActions,
|
|
14966
|
+
});
|
|
14967
|
+
}
|
|
14968
|
+
catch (error) {
|
|
14969
|
+
return buildPgToolResult({
|
|
14970
|
+
success: false,
|
|
14971
|
+
errorCode: "MIGRATION_API_ERROR",
|
|
14972
|
+
message: `DescribeTaskResult failed for TaskId=${taskId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
14973
|
+
});
|
|
14974
|
+
}
|
|
14975
|
+
}
|
|
14976
|
+
/**
|
|
14977
|
+
* Pull remote migration history SQL into cloudbase/migrations/ (CLI `tcb db pg migration fetch`).
|
|
14978
|
+
* Optional migrationVersion fetches one record; omit for full history. force overwrites existing files.
|
|
14979
|
+
*/
|
|
14980
|
+
async function handleFetchMigration(args, context, _deps, cloudBaseOptions) {
|
|
14981
|
+
const force = args.force === true;
|
|
14982
|
+
const singleVersion = args.migrationVersion?.trim();
|
|
14983
|
+
try {
|
|
14984
|
+
const remoteItems = [];
|
|
14985
|
+
if (singleVersion) {
|
|
14986
|
+
const detail = await callPgMigrationApi(context, "DescribePGUserMigration", { MigrationVersion: singleVersion }, cloudBaseOptions);
|
|
14987
|
+
const version = typeof detail.Version === "string" && detail.Version.trim()
|
|
14988
|
+
? detail.Version.trim()
|
|
14989
|
+
: singleVersion;
|
|
14990
|
+
const name = typeof detail.Name === "string" && detail.Name.trim()
|
|
14991
|
+
? detail.Name.trim()
|
|
14992
|
+
: "";
|
|
14993
|
+
const query = typeof detail.Query === "string" ? detail.Query : "";
|
|
14994
|
+
remoteItems.push({ Version: version, Name: name || version, Query: query });
|
|
14995
|
+
}
|
|
14996
|
+
else {
|
|
14997
|
+
const { summaries } = await listAllRemoteMigrationSummaries(context, cloudBaseOptions);
|
|
14998
|
+
for (const summary of summaries) {
|
|
14999
|
+
const detail = await callPgMigrationApi(context, "DescribePGUserMigration", { MigrationVersion: summary.Version }, cloudBaseOptions);
|
|
15000
|
+
const version = typeof detail.Version === "string" && detail.Version.trim()
|
|
15001
|
+
? detail.Version.trim()
|
|
15002
|
+
: summary.Version;
|
|
15003
|
+
const name = typeof detail.Name === "string" && detail.Name.trim()
|
|
15004
|
+
? detail.Name.trim()
|
|
15005
|
+
: summary.Name;
|
|
15006
|
+
const query = typeof detail.Query === "string" ? detail.Query : "";
|
|
15007
|
+
remoteItems.push({ Version: version, Name: name, Query: query });
|
|
15008
|
+
}
|
|
15009
|
+
}
|
|
15010
|
+
const sync = syncFetchedMigrationsToLocalFiles(remoteItems, force);
|
|
15011
|
+
if (!sync.ok) {
|
|
15012
|
+
return buildPgToolResult({
|
|
15013
|
+
success: false,
|
|
15014
|
+
errorCode: sync.errorCode,
|
|
15015
|
+
message: sync.message,
|
|
15016
|
+
data: {
|
|
15017
|
+
migrationsDir: LOCAL_MIGRATIONS_DIR,
|
|
15018
|
+
force,
|
|
15019
|
+
localFilePath: sync.relativePath,
|
|
15020
|
+
},
|
|
15021
|
+
});
|
|
15022
|
+
}
|
|
15023
|
+
const written = sync.files.filter((f) => f.action === "written");
|
|
15024
|
+
const skipped = sync.files.filter((f) => f.action === "skipped");
|
|
15025
|
+
return buildPgToolResult({
|
|
15026
|
+
success: true,
|
|
15027
|
+
data: {
|
|
15028
|
+
migrationsDir: LOCAL_MIGRATIONS_DIR,
|
|
15029
|
+
force,
|
|
15030
|
+
migrationVersion: singleVersion || null,
|
|
15031
|
+
total: remoteItems.length,
|
|
15032
|
+
writtenCount: written.length,
|
|
15033
|
+
skippedCount: skipped.length,
|
|
15034
|
+
written: written.map((f) => f.relativePath),
|
|
15035
|
+
skipped: skipped.map((f) => f.relativePath),
|
|
15036
|
+
files: sync.files.map(({ version, name, relativePath, action }) => ({
|
|
15037
|
+
version,
|
|
15038
|
+
name,
|
|
15039
|
+
relativePath,
|
|
15040
|
+
action,
|
|
15041
|
+
})),
|
|
15042
|
+
},
|
|
15043
|
+
message: remoteItems.length === 0
|
|
15044
|
+
? "No remote migrations to fetch. Local cloudbase/migrations/ unchanged."
|
|
15045
|
+
: `Fetched ${remoteItems.length} remote migration(s): wrote ${written.length}, skipped ${skipped.length}` +
|
|
15046
|
+
(skipped.length > 0 && !force
|
|
15047
|
+
? " (existing files left untouched; pass force=true to overwrite, matching CLI --force)."
|
|
15048
|
+
: ".") +
|
|
15049
|
+
` Directory: ${LOCAL_MIGRATIONS_DIR}/.`,
|
|
15050
|
+
nextActions: skipped.length > 0 && !force
|
|
15051
|
+
? [
|
|
15052
|
+
buildNextAction(MANAGE_PG_DATABASE, "fetchMigration", "Re-run with force=true to overwrite skipped local files from remote history (checksum realign).", {
|
|
15053
|
+
action: "fetchMigration",
|
|
15054
|
+
force: true,
|
|
15055
|
+
...(singleVersion ? { migrationVersion: singleVersion } : {}),
|
|
15056
|
+
}),
|
|
15057
|
+
]
|
|
15058
|
+
: undefined,
|
|
15059
|
+
});
|
|
15060
|
+
}
|
|
15061
|
+
catch (error) {
|
|
15062
|
+
return buildPgToolResult({
|
|
15063
|
+
success: false,
|
|
15064
|
+
errorCode: "MIGRATION_API_ERROR",
|
|
15065
|
+
message: `fetchMigration failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
15066
|
+
});
|
|
15067
|
+
}
|
|
15068
|
+
}
|
|
14447
15069
|
async function handleRollbackMigration(args, context, deps, cloudBaseOptions) {
|
|
14448
15070
|
if (args.lastN === undefined || args.lastN < 1 || !Number.isInteger(args.lastN)) {
|
|
14449
15071
|
return buildPgToolResult({
|
|
@@ -14658,11 +15280,11 @@ function registerPGDatabaseTools(server, providedDeps) {
|
|
|
14658
15280
|
});
|
|
14659
15281
|
server.registerTool?.(MANAGE_PG_DATABASE, {
|
|
14660
15282
|
title: "管理 PostgreSQL 上下文或执行写入 SQL",
|
|
14661
|
-
description: "管理 CloudBase PostgreSQL:执行已确认的写入 SQL、SQL 风险预检、迁移管理。建表/ALTER/DROP 等 schema 变更必须使用 applyMigration(显式 migrationVersion
|
|
15283
|
+
description: "管理 CloudBase PostgreSQL:执行已确认的写入 SQL、SQL 风险预检、迁移管理。建表/ALTER/DROP 等 schema 变更必须使用 applyMigration(显式 migrationVersion;成功前自动写入或校验本地 cloudbase/migrations/<version>_<name>.sql,与 CLI tcb db pg migration 一致),不要默认用 execute。execute 主要用于 DML 与 GRANT/RLS 等运维 SQL。",
|
|
14662
15284
|
inputSchema: {
|
|
14663
15285
|
action: zod_1.z
|
|
14664
15286
|
.enum(MANAGE_ACTIONS)
|
|
14665
|
-
.describe("操作类型:execute=执行已确认的写入 SQL(DML/GRANT/RLS;schema DDL 默认拒绝,需 allowDdlViaExecute=true);dryRun=只分析 SQL 风险不执行;planMigration=预览迁移计划(需 migrationName + migrationVersion + sql);applyMigration=应用迁移,建表/改 schema 首选(需 migrationName + migrationVersion + sql + confirm=true
|
|
15287
|
+
.describe("操作类型:execute=执行已确认的写入 SQL(DML/GRANT/RLS;schema DDL 默认拒绝,需 allowDdlViaExecute=true);dryRun=只分析 SQL 风险不执行;planMigration=预览迁移计划(需 migrationName + migrationVersion + sql;可选 includeAll=true 允许乱序,对齐 CLI --include-all);applyMigration=应用迁移,建表/改 schema 首选(需 migrationName + migrationVersion + sql + confirm=true;可选 includeAll;本地 SQL 缺失则自动写入 cloudbase/migrations/,内容不一致则 LOCAL_MIGRATION_FILE_MISMATCH fail-closed;成功返回前会轮询 DescribeTaskResult(默认最长 10 分钟,可用 taskPollTimeoutMs / waitForTask 调整)并校验 migrationVersion 已落入远端历史;超时返回 MIGRATION_TASK_TIMEOUT,必须先 describeMigrationTask 再 listMigrations,禁止立刻重推同 version;未落库时返回 success=false 且 errorCode=MIGRATION_NOT_APPLIED);listMigrations=查询已应用的 Migration 列表(可传 limit/offset 分页);migrationDetail=查看单条 Migration 详情(需 migrationVersion);describeMigrationTask=按 TaskId 查询 Push 异步任务状态(DescribeTaskResult:Status/Phase/Reason;需 taskId;用于 waitForTask=false / MIGRATION_TASK_TIMEOUT / 失败诊断,listMigrations 看不到 Reason);fetchMigration=从远端 history 拉取 SQL 写入本地 cloudbase/migrations/(对齐 CLI tcb db pg migration fetch;可选 migrationVersion 拉单条,省略则全量;force=true 覆盖已存在文件,默认跳过);rollbackMigration=回滚最近 N 条 Migration(需 lastN + confirm=true);repairMigration=修复 Migration 历史记录(需 migrationVersion + migrationName + repairStatus + repairReason)"),
|
|
14666
15288
|
sql: zod_1.z
|
|
14667
15289
|
.string()
|
|
14668
15290
|
.optional()
|
|
@@ -14700,7 +15322,7 @@ function registerPGDatabaseTools(server, providedDeps) {
|
|
|
14700
15322
|
.string()
|
|
14701
15323
|
.regex(/^\d{14}$/)
|
|
14702
15324
|
.optional()
|
|
14703
|
-
.describe("14 位时间戳 YYYYMMDDHHMMSS。plan/apply/detail/repair
|
|
15325
|
+
.describe("14 位时间戳 YYYYMMDDHHMMSS。plan/apply/detail/repair 必填;fetchMigration 可选(传入则只拉该条,省略则拉全量远端 history);禁止由服务端静默生成,避免与本地 cloudbase/migrations/<version>_<name>.sql 分叉。"),
|
|
14704
15326
|
rollbackSql: zod_1.z
|
|
14705
15327
|
.string()
|
|
14706
15328
|
.optional()
|
|
@@ -14734,6 +15356,21 @@ function registerPGDatabaseTools(server, providedDeps) {
|
|
|
14734
15356
|
.int()
|
|
14735
15357
|
.optional()
|
|
14736
15358
|
.describe("apply 可选:单条 SQL 执行最长时间(毫秒),默认 300000。"),
|
|
15359
|
+
taskPollTimeoutMs: zod_1.z
|
|
15360
|
+
.number()
|
|
15361
|
+
.int()
|
|
15362
|
+
.min(MIGRATION_TASK_MIN_WAIT_MS)
|
|
15363
|
+
.max(MIGRATION_TASK_MAX_WAIT_MS)
|
|
15364
|
+
.optional()
|
|
15365
|
+
.describe(`apply 可选:轮询 DescribeTaskResult 的最长等待(毫秒)。默认 ${MIGRATION_TASK_DEFAULT_WAIT_MS}(与 CLI tcb db pg migration up 的 10 分钟对齐)。范围 ${MIGRATION_TASK_MIN_WAIT_MS}-${MIGRATION_TASK_MAX_WAIT_MS}。超时后务必先 describeMigrationTask(taskId) 再 listMigrations,禁止立刻重推同 version。`),
|
|
15366
|
+
waitForTask: zod_1.z
|
|
15367
|
+
.boolean()
|
|
15368
|
+
.optional()
|
|
15369
|
+
.describe("apply 可选,默认 true。设为 false 时 Push 后立即返回 TaskId(errorCode=MIGRATION_TASK_PENDING),由调用方用 describeMigrationTask 轮询任务终态,再用 listMigrations 确认是否落库;适合 MCP host 工具调用超时较短的场景。默认 true 会同步等到任务终态。"),
|
|
15370
|
+
taskId: zod_1.z
|
|
15371
|
+
.string()
|
|
15372
|
+
.optional()
|
|
15373
|
+
.describe("describeMigrationTask 必填:PushPGUserMigrations / applyMigration 返回的 TaskId。用于一次性查询 DescribeTaskResult(Status/Phase/Reason),不轮询等待。"),
|
|
14737
15374
|
repairStatus: zod_1.z
|
|
14738
15375
|
.enum(["applied", "reverted"])
|
|
14739
15376
|
.optional()
|
|
@@ -14742,6 +15379,14 @@ function registerPGDatabaseTools(server, providedDeps) {
|
|
|
14742
15379
|
.string()
|
|
14743
15380
|
.optional()
|
|
14744
15381
|
.describe("repair 必填:修复原因。"),
|
|
15382
|
+
force: zod_1.z
|
|
15383
|
+
.boolean()
|
|
15384
|
+
.optional()
|
|
15385
|
+
.describe("fetchMigration 可选,默认 false。true=覆盖本地已存在的同名 SQL 文件(对齐 CLI tcb db pg migration fetch --force);false=跳过已存在文件。用于从远端 history 重新对齐 Git checksum。"),
|
|
15386
|
+
includeAll: zod_1.z
|
|
15387
|
+
.boolean()
|
|
15388
|
+
.optional()
|
|
15389
|
+
.describe("planMigration / applyMigration 可选,默认 false。true=允许 out-of-order(version 小于远端 LatestVersion)仍可 Preview/Push,对齐 CLI tcb db pg migration up --include-all;仅在确认要补历史/乱序迁移时使用,日常应选更大的 migrationVersion。"),
|
|
14745
15390
|
allowDdlViaExecute: zod_1.z
|
|
14746
15391
|
.boolean()
|
|
14747
15392
|
.optional()
|
|
@@ -14789,6 +15434,8 @@ function registerPGDatabaseTools(server, providedDeps) {
|
|
|
14789
15434
|
case "applyMigration":
|
|
14790
15435
|
case "listMigrations":
|
|
14791
15436
|
case "migrationDetail":
|
|
15437
|
+
case "describeMigrationTask":
|
|
15438
|
+
case "fetchMigration":
|
|
14792
15439
|
case "rollbackMigration":
|
|
14793
15440
|
case "repairMigration": {
|
|
14794
15441
|
switch (args.action) {
|
|
@@ -14796,6 +15443,8 @@ function registerPGDatabaseTools(server, providedDeps) {
|
|
|
14796
15443
|
case "applyMigration": return handleApplyMigration(args, context, deps, cbOpts);
|
|
14797
15444
|
case "listMigrations": return handleListMigrations(args, context, deps, cbOpts);
|
|
14798
15445
|
case "migrationDetail": return handleMigrationDetail(args, context, deps, cbOpts);
|
|
15446
|
+
case "describeMigrationTask": return handleDescribeMigrationTask(args, context, deps, cbOpts);
|
|
15447
|
+
case "fetchMigration": return handleFetchMigration(args, context, deps, cbOpts);
|
|
14799
15448
|
case "rollbackMigration": return handleRollbackMigration(args, context, deps, cbOpts);
|
|
14800
15449
|
case "repairMigration": return handleRepairMigration(args, context, deps, cbOpts);
|
|
14801
15450
|
default: return buildPgToolResult({ success: false, errorCode: "UNSUPPORTED_ACTION", message: `Unsupported action: ${args.action}` });
|
|
@@ -14848,7 +15497,7 @@ function buildWriteVerificationHint(resourceId) {
|
|
|
14848
15497
|
return `对于 ${resourceId} 这类有后端权限控制的集合,前端调用 .doc(id).update() / .doc(id).remove() 后,不能只看是否没有抛异常。请显式检查返回结果中的 updated / deleted 是否大于 0;如果 result.code、result.message 存在,或 updated / deleted 为 0,要把它当作真实失败并向上抛错。`;
|
|
14849
15498
|
}
|
|
14850
15499
|
function buildPermissionPropagationHint(resourceId) {
|
|
14851
|
-
return `刚更新完 ${resourceId}
|
|
15500
|
+
return `刚更新完 ${resourceId} 的安全规则时,后端权限通常在数秒到约 30 秒内生效。若紧接着的真实写操作仍返回 DATABASE_PERMISSION_DENIED,请先间隔数秒用同一登录态重试同一条 .doc(id).update() / .doc(id).remove();不要盲等数分钟,也不要立刻连续重写规则,更不要在短暂传播窗口里把旧拒绝直接当成规则表达式仍然错误。`;
|
|
14852
15501
|
}
|
|
14853
15502
|
function buildEnvelope(data, message) {
|
|
14854
15503
|
return {
|
|
@@ -14873,6 +15522,171 @@ function mapResourceType(resourceType) {
|
|
|
14873
15522
|
};
|
|
14874
15523
|
return resourceTypeMap[resourceType];
|
|
14875
15524
|
}
|
|
15525
|
+
/**
|
|
15526
|
+
* Platform ModifyResourcePermission / DescribeResourcePermission reject PG envs.
|
|
15527
|
+
* CLI migrated `tcb permission` → `tcb policy` (OPA Rego via
|
|
15528
|
+
* permission.modifyEnvAuthzConfig / describeEnvAuthzConfig).
|
|
15529
|
+
*/
|
|
15530
|
+
function isPostgresqlPermissionApiUnsupported(error) {
|
|
15531
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
15532
|
+
return /does not support PostgreSQL type environments/i.test(message);
|
|
15533
|
+
}
|
|
15534
|
+
const AUTHZ_USER_REGO_KEY = "authz.user.rego";
|
|
15535
|
+
function looksLikeUserRego(value) {
|
|
15536
|
+
return /^\s*package\s+authz\.user\b/m.test(value);
|
|
15537
|
+
}
|
|
15538
|
+
/** Detect legacy function securityRule shapes that mean "public invoke". */
|
|
15539
|
+
function isPublicFunctionInvokeRule(securityRule) {
|
|
15540
|
+
if (!securityRule || securityRule.trim() === "") {
|
|
15541
|
+
return false;
|
|
15542
|
+
}
|
|
15543
|
+
const trimmed = securityRule.trim();
|
|
15544
|
+
if (trimmed === "true") {
|
|
15545
|
+
return true;
|
|
15546
|
+
}
|
|
15547
|
+
try {
|
|
15548
|
+
const parsed = JSON.parse(trimmed);
|
|
15549
|
+
if (parsed === true || parsed === "true") {
|
|
15550
|
+
return true;
|
|
15551
|
+
}
|
|
15552
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
15553
|
+
return false;
|
|
15554
|
+
}
|
|
15555
|
+
const obj = parsed;
|
|
15556
|
+
if (obj.invoke === true || obj.invoke === "true") {
|
|
15557
|
+
return true;
|
|
15558
|
+
}
|
|
15559
|
+
for (const value of Object.values(obj)) {
|
|
15560
|
+
if (value &&
|
|
15561
|
+
typeof value === "object" &&
|
|
15562
|
+
!Array.isArray(value) &&
|
|
15563
|
+
(value.invoke === true ||
|
|
15564
|
+
value.invoke === "true")) {
|
|
15565
|
+
return true;
|
|
15566
|
+
}
|
|
15567
|
+
}
|
|
15568
|
+
}
|
|
15569
|
+
catch {
|
|
15570
|
+
return false;
|
|
15571
|
+
}
|
|
15572
|
+
return false;
|
|
15573
|
+
}
|
|
15574
|
+
/**
|
|
15575
|
+
* Build a CLI-aligned user Rego that opens cloud-function access for
|
|
15576
|
+
* anonymous + unauthenticated callers (tcb policy set example + HTTP no-token).
|
|
15577
|
+
*/
|
|
15578
|
+
function buildPublicFunctionsUserRego(resourceId) {
|
|
15579
|
+
const comment = resourceId && resourceId !== "*"
|
|
15580
|
+
? `# Public HTTP/API access for function ${resourceId} (aligned with tcb policy set)`
|
|
15581
|
+
: `# Public HTTP/API access for cloud functions (aligned with tcb policy set)`;
|
|
15582
|
+
return [
|
|
15583
|
+
"package authz.user",
|
|
15584
|
+
"",
|
|
15585
|
+
"default allow := false",
|
|
15586
|
+
"",
|
|
15587
|
+
comment,
|
|
15588
|
+
"allow if {",
|
|
15589
|
+
' input.cloudbase.resource_type == "functions"',
|
|
15590
|
+
' input.subject.auth_type in {"anonymous", "unauthenticated"}',
|
|
15591
|
+
"}",
|
|
15592
|
+
"",
|
|
15593
|
+
].join("\n");
|
|
15594
|
+
}
|
|
15595
|
+
function resolveFunctionAuthzRegoInput(securityRule, resourceId) {
|
|
15596
|
+
if (securityRule && looksLikeUserRego(securityRule)) {
|
|
15597
|
+
return securityRule;
|
|
15598
|
+
}
|
|
15599
|
+
if (isPublicFunctionInvokeRule(securityRule)) {
|
|
15600
|
+
return buildPublicFunctionsUserRego(resourceId);
|
|
15601
|
+
}
|
|
15602
|
+
throw new Error(`PostgreSQL environments manage HTTP/function gateway auth via OPA Rego ` +
|
|
15603
|
+
`(same as CLI \`tcb policy set\`), not ModifyResourcePermission / function security-rule JSON. ` +
|
|
15604
|
+
`Pass either:\n` +
|
|
15605
|
+
`1) permission="CUSTOM" with securityRule as a full Rego document starting with \`package authz.user\`, or\n` +
|
|
15606
|
+
`2) permission="CUSTOM" with securityRule='{"invoke":true}' to generate a public-functions allow policy.\n` +
|
|
15607
|
+
`See https://docs.cloudbase.net/cli-v1/policy/management`);
|
|
15608
|
+
}
|
|
15609
|
+
async function describeEnvAuthzUserRego(cloudbase) {
|
|
15610
|
+
if (!cloudbase?.permission?.describeEnvAuthzConfig) {
|
|
15611
|
+
throw new Error("Current @cloudbase/manager-node does not expose permission.describeEnvAuthzConfig. Upgrade manager-node (>= 5.5.5) to align with CLI tcb policy get.");
|
|
15612
|
+
}
|
|
15613
|
+
const result = await cloudbase.permission.describeEnvAuthzConfig({
|
|
15614
|
+
key: AUTHZ_USER_REGO_KEY,
|
|
15615
|
+
});
|
|
15616
|
+
const value = typeof result?.Item?.Value === "string"
|
|
15617
|
+
? result.Item.Value
|
|
15618
|
+
: typeof result?.Value === "string"
|
|
15619
|
+
? result.Value
|
|
15620
|
+
: "";
|
|
15621
|
+
return { value, raw: result };
|
|
15622
|
+
}
|
|
15623
|
+
async function modifyEnvAuthzUserRego(cloudbase, value) {
|
|
15624
|
+
if (!cloudbase?.permission?.modifyEnvAuthzConfig) {
|
|
15625
|
+
throw new Error("Current @cloudbase/manager-node does not expose permission.modifyEnvAuthzConfig. Upgrade manager-node (>= 5.5.5) to align with CLI tcb policy set.");
|
|
15626
|
+
}
|
|
15627
|
+
return cloudbase.permission.modifyEnvAuthzConfig({
|
|
15628
|
+
key: AUTHZ_USER_REGO_KEY,
|
|
15629
|
+
value,
|
|
15630
|
+
});
|
|
15631
|
+
}
|
|
15632
|
+
async function describeResourcePermissionWithFunctionPgFallback(options) {
|
|
15633
|
+
const { cloudbase, envId, resourceType, resources } = options;
|
|
15634
|
+
try {
|
|
15635
|
+
const result = await cloudbase.permission.describeResourcePermission({
|
|
15636
|
+
resourceType: mapResourceType(resourceType),
|
|
15637
|
+
resources,
|
|
15638
|
+
});
|
|
15639
|
+
return result;
|
|
15640
|
+
}
|
|
15641
|
+
catch (error) {
|
|
15642
|
+
if (resourceType !== "function" || !isPostgresqlPermissionApiUnsupported(error)) {
|
|
15643
|
+
throw error;
|
|
15644
|
+
}
|
|
15645
|
+
const fallback = await describeEnvAuthzUserRego(cloudbase);
|
|
15646
|
+
const targets = resources && resources.length > 0 ? resources : ["*"];
|
|
15647
|
+
const permissionList = targets.map((resourceId) => ({
|
|
15648
|
+
ResourceType: "function",
|
|
15649
|
+
Resource: resourceId,
|
|
15650
|
+
Permission: "CUSTOM",
|
|
15651
|
+
SecurityRule: fallback.value || "",
|
|
15652
|
+
}));
|
|
15653
|
+
return {
|
|
15654
|
+
Data: {
|
|
15655
|
+
TotalCount: permissionList.length,
|
|
15656
|
+
PermissionList: permissionList,
|
|
15657
|
+
},
|
|
15658
|
+
RequestId: fallback.raw?.RequestId,
|
|
15659
|
+
fallback: "describeEnvAuthzConfig",
|
|
15660
|
+
raw: fallback.raw,
|
|
15661
|
+
};
|
|
15662
|
+
}
|
|
15663
|
+
}
|
|
15664
|
+
async function modifyFunctionPermissionWithPgFallback(options) {
|
|
15665
|
+
const { cloudbase, resourceId, permission, securityRule } = options;
|
|
15666
|
+
try {
|
|
15667
|
+
const result = await cloudbase.permission.modifyResourcePermission({
|
|
15668
|
+
resourceType: "function",
|
|
15669
|
+
resource: resourceId,
|
|
15670
|
+
permission,
|
|
15671
|
+
securityRule,
|
|
15672
|
+
});
|
|
15673
|
+
return { result };
|
|
15674
|
+
}
|
|
15675
|
+
catch (error) {
|
|
15676
|
+
if (!isPostgresqlPermissionApiUnsupported(error)) {
|
|
15677
|
+
throw error;
|
|
15678
|
+
}
|
|
15679
|
+
if (permission !== "CUSTOM") {
|
|
15680
|
+
throw new Error(`PostgreSQL environments do not support ModifyResourcePermission. ` +
|
|
15681
|
+
`Align with CLI \`tcb policy set\`: use permission="CUSTOM" and pass OPA Rego ` +
|
|
15682
|
+
`(package authz.user) or securityRule='{"invoke":true}' for public functions. ` +
|
|
15683
|
+
`Underlying error: ${error instanceof Error ? error.message : String(error)}`);
|
|
15684
|
+
}
|
|
15685
|
+
const rego = resolveFunctionAuthzRegoInput(securityRule, resourceId);
|
|
15686
|
+
const result = await modifyEnvAuthzUserRego(cloudbase, rego);
|
|
15687
|
+
return { result, fallback: "modifyEnvAuthzConfig", rego };
|
|
15688
|
+
}
|
|
15689
|
+
}
|
|
14876
15690
|
function normalizeRecordArray(value, label) {
|
|
14877
15691
|
if (value === undefined) {
|
|
14878
15692
|
return undefined;
|
|
@@ -15048,7 +15862,7 @@ function registerPermissionTools(server) {
|
|
|
15048
15862
|
};
|
|
15049
15863
|
server.registerTool?.("queryPermissions", {
|
|
15050
15864
|
title: "查询 CloudBase 权限与用户配置",
|
|
15051
|
-
description: "查询 CloudBase 权限与用户配置,支持查询资源权限(数据库/云函数/存储桶等)、角色列表/详情、应用用户列表/详情。\n\n示例:\n- 查询存储桶权限:`action=\"getResourcePermission\", resourceType=\"storage\", resourceId=\"bucket-name\"`\n\n📌 跨后端边界提示:调用前先用 `envQuery(action=\"info\", envId=...)` 看 `EnvInfo.RuntimeBackends`。`resourceType=\"noSqlDatabase\"` 查询的是 CloudBase NoSQL 集合规则,与 CloudBase PostgreSQL(PG)表的行级安全(RLS)是两套独立机制——同一个 PG 环境里 NoSQL 集合若仍在使用,对那些集合查询本工具结果**仍然有效**。要查 PG 表 RLS,请改用 `queryPgDatabase(action=\"sql\", sql=\"SELECT * FROM pg_policies WHERE tablename=...\")`。本工具不涉及 MySQL
|
|
15865
|
+
description: "查询 CloudBase 权限与用户配置,支持查询资源权限(数据库/云函数/存储桶等)、角色列表/详情、应用用户列表/详情。\n\n示例:\n- 查询存储桶权限:`action=\"getResourcePermission\", resourceType=\"storage\", resourceId=\"bucket-name\"`\n\n📌 跨后端边界提示:调用前先用 `envQuery(action=\"info\", envId=...)` 看 `EnvInfo.RuntimeBackends`。`resourceType=\"noSqlDatabase\"` 查询的是 CloudBase NoSQL 集合规则,与 CloudBase PostgreSQL(PG)表的行级安全(RLS)是两套独立机制——同一个 PG 环境里 NoSQL 集合若仍在使用,对那些集合查询本工具结果**仍然有效**。要查 PG 表 RLS,请改用 `queryPgDatabase(action=\"sql\", sql=\"SELECT * FROM pg_policies WHERE tablename=...\")`。本工具不涉及 MySQL 权限。\n\n⚠️ PostgreSQL 环境:平台 `DescribeResourcePermission` 对 PG 环境会直接拒绝。当 `resourceType=\"function\"` 时,本工具会自动回退到 Manager SDK `describeEnvAuthzConfig`(与 CLI `tcb policy get` 一致,读取 `authz.user.rego`)。",
|
|
15052
15866
|
inputSchema: {
|
|
15053
15867
|
action: zod_1.z.enum(QUERY_PERMISSION_ACTIONS),
|
|
15054
15868
|
resourceType: zod_1.z
|
|
@@ -15080,8 +15894,10 @@ function registerPermissionTools(server) {
|
|
|
15080
15894
|
if (resourceType === "storage") {
|
|
15081
15895
|
await ensureStorageBucketsExist(cloudbase, [resourceId]);
|
|
15082
15896
|
}
|
|
15083
|
-
const result = await
|
|
15084
|
-
|
|
15897
|
+
const result = await describeResourcePermissionWithFunctionPgFallback({
|
|
15898
|
+
cloudbase,
|
|
15899
|
+
envId,
|
|
15900
|
+
resourceType,
|
|
15085
15901
|
resources: [resourceId],
|
|
15086
15902
|
});
|
|
15087
15903
|
(0, cloudbase_manager_js_1.logCloudBaseResult)(server.logger, result);
|
|
@@ -15097,8 +15913,11 @@ function registerPermissionTools(server) {
|
|
|
15097
15913
|
aclTag: matchedPermission?.Permission,
|
|
15098
15914
|
permissions,
|
|
15099
15915
|
hints,
|
|
15100
|
-
|
|
15101
|
-
|
|
15916
|
+
...(result.fallback ? { fallback: result.fallback } : {}),
|
|
15917
|
+
raw: result.raw ?? result,
|
|
15918
|
+
}, result.fallback
|
|
15919
|
+
? "资源权限查询成功(PostgreSQL 环境已回退到 describeEnvAuthzConfig / tcb policy get)"
|
|
15920
|
+
: "资源权限查询成功");
|
|
15102
15921
|
}
|
|
15103
15922
|
case "listResourcePermissions": {
|
|
15104
15923
|
if (!resourceType) {
|
|
@@ -15107,8 +15926,10 @@ function registerPermissionTools(server) {
|
|
|
15107
15926
|
if (resourceType === "storage" && resourceIds?.length) {
|
|
15108
15927
|
await ensureStorageBucketsExist(cloudbase, resourceIds);
|
|
15109
15928
|
}
|
|
15110
|
-
const result = await
|
|
15111
|
-
|
|
15929
|
+
const result = await describeResourcePermissionWithFunctionPgFallback({
|
|
15930
|
+
cloudbase,
|
|
15931
|
+
envId,
|
|
15932
|
+
resourceType,
|
|
15112
15933
|
resources: resourceIds,
|
|
15113
15934
|
});
|
|
15114
15935
|
(0, cloudbase_manager_js_1.logCloudBaseResult)(server.logger, result);
|
|
@@ -15129,8 +15950,11 @@ function registerPermissionTools(server) {
|
|
|
15129
15950
|
permissions,
|
|
15130
15951
|
resourceHints,
|
|
15131
15952
|
total: result.Data.TotalCount ?? 0,
|
|
15132
|
-
|
|
15133
|
-
|
|
15953
|
+
...(result.fallback ? { fallback: result.fallback } : {}),
|
|
15954
|
+
raw: result.raw ?? result,
|
|
15955
|
+
}, result.fallback
|
|
15956
|
+
? "资源权限列表查询成功(PostgreSQL 环境已回退到 describeEnvAuthzConfig / tcb policy get)"
|
|
15957
|
+
: "资源权限列表查询成功");
|
|
15134
15958
|
}
|
|
15135
15959
|
case "listRoles": {
|
|
15136
15960
|
const result = await cloudbase.permission.describeRoleList({
|
|
@@ -15209,7 +16033,7 @@ function registerPermissionTools(server) {
|
|
|
15209
16033
|
}));
|
|
15210
16034
|
server.registerTool?.("managePermissions", {
|
|
15211
16035
|
title: "管理 CloudBase 权限与用户配置",
|
|
15212
|
-
description: "管理 CloudBase 权限与用户配置,支持修改资源权限(数据库/云函数/存储桶等)、角色管理、成员与策略增删、应用用户 CRUD。\n\n示例:\n- 设置存储桶为私有:`action=\"updateResourcePermission\", resourceType=\"storage\", resourceId=\"bucket-name\", permission=\"PRIVATE\"`\n- 创建角色:`action=\"createRole\", roleName=\"admin\", roleIdentity=\"admin\"`\n\n注意:`createUser` / `updateUser` 是环境侧应用用户管理能力,适合测试账号、管理员或预置用户,不应替代浏览器里的 Web SDK 注册表单;前端用户名密码注册应使用 `auth.signUp({ username, password })`,登录应使用 `auth.signInWithPassword({ username, password })`。直接在浏览器里用 `auth.signUp` 创建用户名密码用户取决于 SDK/provider 支持,使用前必须验证;不支持时应走后端或管理端边界,不能在浏览器暴露密钥。`securityRule` 的详细语义取决于 `resourceType`:`doc._openid`、`auth.openid`、查询条件子集校验,以及 `create` / `update` / `delete` JSON 模板仅适用于 `resourceType=\"noSqlDatabase\"` 的文档数据库安全规则;配置 `function` 或 `storage` 时,请参考各自官方安全规则文档,而不是复用 NoSQL 模板。\n\n📌 跨后端边界提示:调用前先用 `envQuery(action=\"info\", envId=...)` 看 `EnvInfo.RuntimeBackends`:\n- `resourceType=\"noSqlDatabase\"` 仅作用于 CloudBase NoSQL 文档数据库的集合;CloudBase PostgreSQL(PG)表的行级权限**不**受它控制——PG 表请改用 RLS:`managePgDatabase(action=\"execute\", confirm=true)` 跑 `ALTER TABLE ... ENABLE ROW LEVEL SECURITY` 与 `CREATE POLICY ...`。同一个 PG 环境里如果还有 NoSQL 集合在用,对那些**集合**继续使用 `noSqlDatabase` 规则是正确的——不是\"PG 环境就禁用本工具\"。\n- `resourceType=\"storage\"` 控制的是 NoSQL/COS 存储桶 ACL;PG 的 `pgstore` bucket 不在此 `resourceType` 覆盖范围内。\n- 本工具不涉及 MySQL;MySQL 数据库权限请走 MySQL 自身的 GRANT/REVOKE 语句(通过 `manageMysqlDatabase
|
|
16036
|
+
description: "管理 CloudBase 权限与用户配置,支持修改资源权限(数据库/云函数/存储桶等)、角色管理、成员与策略增删、应用用户 CRUD。\n\n示例:\n- 设置存储桶为私有:`action=\"updateResourcePermission\", resourceType=\"storage\", resourceId=\"bucket-name\", permission=\"PRIVATE\"`\n- 创建角色:`action=\"createRole\", roleName=\"admin\", roleIdentity=\"admin\"`\n- 放开云函数匿名/未登录访问(PG 会走 OPA,对齐 CLI `tcb policy set`):`action=\"updateResourcePermission\", resourceType=\"function\", resourceId=\"myFn\", permission=\"CUSTOM\", securityRule='{\"invoke\":true}'`\n\n注意:`createUser` / `updateUser` 是环境侧应用用户管理能力,适合测试账号、管理员或预置用户,不应替代浏览器里的 Web SDK 注册表单;前端用户名密码注册应使用 `auth.signUp({ username, password })`,登录应使用 `auth.signInWithPassword({ username, password })`。直接在浏览器里用 `auth.signUp` 创建用户名密码用户取决于 SDK/provider 支持,使用前必须验证;不支持时应走后端或管理端边界,不能在浏览器暴露密钥。`securityRule` 的详细语义取决于 `resourceType`:`doc._openid`、`auth.openid`、查询条件子集校验,以及 `create` / `update` / `delete` JSON 模板仅适用于 `resourceType=\"noSqlDatabase\"` 的文档数据库安全规则;配置 `function` 或 `storage` 时,请参考各自官方安全规则文档,而不是复用 NoSQL 模板。\n\n📌 跨后端边界提示:调用前先用 `envQuery(action=\"info\", envId=...)` 看 `EnvInfo.RuntimeBackends`:\n- `resourceType=\"noSqlDatabase\"` 仅作用于 CloudBase NoSQL 文档数据库的集合;CloudBase PostgreSQL(PG)表的行级权限**不**受它控制——PG 表请改用 RLS:`managePgDatabase(action=\"execute\", confirm=true)` 跑 `ALTER TABLE ... ENABLE ROW LEVEL SECURITY` 与 `CREATE POLICY ...`。同一个 PG 环境里如果还有 NoSQL 集合在用,对那些**集合**继续使用 `noSqlDatabase` 规则是正确的——不是\"PG 环境就禁用本工具\"。\n- `resourceType=\"storage\"` 控制的是 NoSQL/COS 存储桶 ACL;PG 的 `pgstore` bucket 不在此 `resourceType` 覆盖范围内。\n- 本工具不涉及 MySQL;MySQL 数据库权限请走 MySQL 自身的 GRANT/REVOKE 语句(通过 `manageMysqlDatabase`)。\n\n⚠️ PostgreSQL 环境:平台 `ModifyResourcePermission` 对 PG 环境会直接拒绝。当 `resourceType=\"function\"` 时,本工具会自动回退到 Manager SDK `modifyEnvAuthzConfig`(与 CLI `tcb policy set` 一致,写入 `authz.user.rego`)。`securityRule` 可传完整 Rego(`package authz.user`)或 `'{\"invoke\":true}'`(自动生成放通 anonymous/unauthenticated 调 functions 的策略)。设置 Rego 后旧网关鉴权会失效,行为与 CLI 相同。",
|
|
15213
16037
|
inputSchema: {
|
|
15214
16038
|
action: zod_1.z.enum(MANAGE_PERMISSION_ACTIONS),
|
|
15215
16039
|
resourceType: zod_1.z
|
|
@@ -15257,12 +16081,29 @@ function registerPermissionTools(server) {
|
|
|
15257
16081
|
if (!resourceType || !resourceId || !permission) {
|
|
15258
16082
|
throw new Error("action=updateResourcePermission 时必须提供 resourceType、resourceId 和 permission");
|
|
15259
16083
|
}
|
|
15260
|
-
|
|
15261
|
-
|
|
15262
|
-
|
|
15263
|
-
|
|
15264
|
-
|
|
15265
|
-
|
|
16084
|
+
let result;
|
|
16085
|
+
let fallback;
|
|
16086
|
+
let appliedRego;
|
|
16087
|
+
if (resourceType === "function") {
|
|
16088
|
+
const updated = await modifyFunctionPermissionWithPgFallback({
|
|
16089
|
+
cloudbase,
|
|
16090
|
+
envId,
|
|
16091
|
+
resourceId,
|
|
16092
|
+
permission,
|
|
16093
|
+
securityRule,
|
|
16094
|
+
});
|
|
16095
|
+
result = updated.result;
|
|
16096
|
+
fallback = updated.fallback;
|
|
16097
|
+
appliedRego = updated.rego;
|
|
16098
|
+
}
|
|
16099
|
+
else {
|
|
16100
|
+
result = await cloudbase.permission.modifyResourcePermission({
|
|
16101
|
+
resourceType: mapResourceType(resourceType),
|
|
16102
|
+
resource: resourceId,
|
|
16103
|
+
permission,
|
|
16104
|
+
securityRule,
|
|
16105
|
+
});
|
|
16106
|
+
}
|
|
15266
16107
|
(0, cloudbase_manager_js_1.logCloudBaseResult)(server.logger, result);
|
|
15267
16108
|
const hints = permission === "CUSTOM" ? buildPermissionHints(securityRule, resourceId) : [];
|
|
15268
16109
|
return buildEnvelope({
|
|
@@ -15272,6 +16113,8 @@ function registerPermissionTools(server) {
|
|
|
15272
16113
|
resourceId,
|
|
15273
16114
|
permission,
|
|
15274
16115
|
hints,
|
|
16116
|
+
...(fallback ? { fallback } : {}),
|
|
16117
|
+
...(appliedRego ? { rego: appliedRego } : {}),
|
|
15275
16118
|
verificationHint: resourceType === "noSqlDatabase" && permission === "CUSTOM"
|
|
15276
16119
|
? buildWriteVerificationHint(resourceId)
|
|
15277
16120
|
: undefined,
|
|
@@ -15279,7 +16122,9 @@ function registerPermissionTools(server) {
|
|
|
15279
16122
|
? buildPermissionPropagationHint(resourceId)
|
|
15280
16123
|
: undefined,
|
|
15281
16124
|
raw: result,
|
|
15282
|
-
},
|
|
16125
|
+
}, fallback
|
|
16126
|
+
? "资源权限更新成功(PostgreSQL 环境已回退到 modifyEnvAuthzConfig / tcb policy set)"
|
|
16127
|
+
: "资源权限更新成功");
|
|
15283
16128
|
}
|
|
15284
16129
|
case "createRole": {
|
|
15285
16130
|
if (!roleName || !roleIdentity) {
|
|
@@ -15898,7 +16743,7 @@ function registerSetupTools(server) {
|
|
|
15898
16743
|
server.registerTool("downloadTemplate", {
|
|
15899
16744
|
title: "下载项目模板",
|
|
15900
16745
|
description: `自动下载并部署CloudBase项目模板。
|
|
15901
|
-
**Note**: Call this tool when the user requests to create a new project using a CloudBase template.\n\n支持的模板:\n- react: React + CloudBase 全栈应用模板\n- vue: Vue + CloudBase 全栈应用模板\n- miniprogram: 微信小程序 + 云开发模板 \n- uniapp: UniApp + CloudBase 跨端应用模板\n- rules: 只包含AI编辑器配置文件(包含Cursor、WindSurf、CodeBuddy等所有主流编辑器配置),适合在已有项目中补充AI编辑器配置\n\n支持的IDE类型:\n- all: 下载所有IDE配置\n- cursor: Cursor AI编辑器\n- 其他IDE类型见下方列表\n\n注意:如果未传入 ide 参数且无法从环境变量检测到 IDE,将提示错误并要求传入 ide 参数\n- windsurf: WindSurf AI编辑器\n- codebuddy: CodeBuddy AI编辑器\n- claude-code: Claude Code AI编辑器\n- cline: Cline AI编辑器\n- gemini-cli: Gemini CLI\n- opencode: OpenCode AI编辑器\n- qwen-code: 通义灵码\n- baidu-comate: 百度Comate\n- openai-codex-cli: OpenAI Codex CLI\n- augment-code: Augment Code\n- github-copilot: GitHub Copilot\n- roocode: RooCode AI编辑器\n- tongyi-lingma: 通义灵码\n- trae: Trae AI编辑器\n- qoder: Qoder AI编辑器\n- antigravity: Google Antigravity AI编辑器\n- vscode: Visual Studio Code\n- kiro: Kiro AI编辑器\n- aider: Aider AI编辑器\n\n特别说明:\n- rules 模板会自动包含当前 mcp 版本号信息(版本号:${ true ? "2.25.
|
|
16746
|
+
**Note**: Call this tool when the user requests to create a new project using a CloudBase template.\n\n支持的模板:\n- react: React + CloudBase 全栈应用模板\n- vue: Vue + CloudBase 全栈应用模板\n- miniprogram: 微信小程序 + 云开发模板 \n- uniapp: UniApp + CloudBase 跨端应用模板\n- rules: 只包含AI编辑器配置文件(包含Cursor、WindSurf、CodeBuddy等所有主流编辑器配置),适合在已有项目中补充AI编辑器配置\n\n支持的IDE类型:\n- all: 下载所有IDE配置\n- cursor: Cursor AI编辑器\n- 其他IDE类型见下方列表\n\n注意:如果未传入 ide 参数且无法从环境变量检测到 IDE,将提示错误并要求传入 ide 参数\n- windsurf: WindSurf AI编辑器\n- codebuddy: CodeBuddy AI编辑器\n- claude-code: Claude Code AI编辑器\n- cline: Cline AI编辑器\n- gemini-cli: Gemini CLI\n- opencode: OpenCode AI编辑器\n- qwen-code: 通义灵码\n- baidu-comate: 百度Comate\n- openai-codex-cli: OpenAI Codex CLI\n- augment-code: Augment Code\n- github-copilot: GitHub Copilot\n- roocode: RooCode AI编辑器\n- tongyi-lingma: 通义灵码\n- trae: Trae AI编辑器\n- qoder: Qoder AI编辑器\n- antigravity: Google Antigravity AI编辑器\n- vscode: Visual Studio Code\n- kiro: Kiro AI编辑器\n- aider: Aider AI编辑器\n\n特别说明:\n- rules 模板会自动包含当前 mcp 版本号信息(版本号:${ true ? "2.25.6" : 0}),便于后续维护和版本追踪\n- 下载 rules 模板时,如果项目中已存在 README.md 文件,系统会自动保护该文件不被覆盖(除非设置 overwrite=true)`,
|
|
15902
16747
|
inputSchema: {
|
|
15903
16748
|
template: zod_1.z
|
|
15904
16749
|
.enum(["react", "vue", "miniprogram", "uniapp", "rules"])
|