diff --git a/.gitignore b/.gitignore index 09cb76c1..9a996e72 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ **/dist **/node_modules **/.env +**/.cloudbase-sites/ **/*.dxt # Environment variables .env.local diff --git a/doc/mcp-tools.md b/doc/mcp-tools.md index 87c2ea8a..c4ac6f24 100644 --- a/doc/mcp-tools.md +++ b/doc/mcp-tools.md @@ -1065,7 +1065,7 @@ CloudBase 云函数统一写入口。支持创建函数、更新代码、更新 --- ### `manageHosting` -管理 CloudBase 静态托管的变更操作。action=upload 上传本地构建产物;action=delete 删除托管文件或目录(必须 confirm=true);action=setWebsiteDocument 设置首页/错误页/路由规则;action=enableService 开通静态托管;action=bindDomain / unbindDomain / updateDomain 管理自定义域名;action=downloadFile / downloadDirectory 下载托管内容到本地。若任务只是查看配置、文件或域名状态,请改用 queryHosting。 +管理 CloudBase 静态托管的变更操作。action=upload 上传本地构建产物到共享域名(域名格式:-.tcloudbaseapp.com/);action=delete 删除托管文件或目录(必须 confirm=true);action=setWebsiteDocument 设置首页/错误页/路由规则;action=enableService 开通静态托管;action=bindDomain / unbindDomain / updateDomain 管理自定义域名;action=downloadFile / downloadDirectory 下载托管内容到本地。⚠️ 新项目部署优先使用 manageApps(部署到独立子域名),本工具适合已有老项目继续使用或作为 manageApps 的 fallback。manageApps 与 manageHosting 域名不同,切换会导致老链接失效。若任务只是查看配置、文件或域名状态,请改用 queryHosting。 #### 参数 diff --git a/mcp/src/tools/apps.test.ts b/mcp/src/tools/apps.test.ts index 03f06aca..70c2ac07 100644 --- a/mcp/src/tools/apps.test.ts +++ b/mcp/src/tools/apps.test.ts @@ -62,6 +62,7 @@ describe("app tools", () => { mockDescribeAppInfo.mockResolvedValue({ ServiceName: "demo-app", DeployType: "static-hosting", + Domain: "demo-app-env-test.webapps.tcloudbase.com", RequestId: "req-app-info", }); mockDescribeAppVersionList.mockResolvedValue({ @@ -153,15 +154,43 @@ describe("app tools", () => { buildType: "ZIP", }), ); + expect(mockDescribeAppInfo).toHaveBeenCalledWith({ + deployType: "static-hosting", + serviceName: "demo-app", + }); expect(payload).toMatchObject({ success: true, data: { action: "deployApp", serviceName: "demo-app", + domain: "demo-app-env-test.webapps.tcloudbase.com", + accessUrl: "https://demo-app-env-test.webapps.tcloudbase.com", }, }); }); + it("manageApps(action=deployApp) should normalize access URL from app details", async () => { + mockDescribeAppInfo.mockResolvedValueOnce({ + ServiceName: "demo-app", + DeployType: "static-hosting", + Domain: "https://demo-app-env-test.webapps.tcloudbase.com/", + RequestId: "req-app-info", + }); + + const result = await tools.manageApps.handler({ + action: "deployApp", + serviceName: "demo-app", + filePath: "/tmp/demo-app", + buildPath: "dist", + }); + const payload = JSON.parse(result.content[0].text); + + expect(payload.data.domain).toBe("demo-app-env-test.webapps.tcloudbase.com"); + expect(payload.data.accessUrl).toBe("https://demo-app-env-test.webapps.tcloudbase.com"); + expect(payload.data.accessUrlSource).toBe("describeAppInfo.Domain"); + expect(payload.data.nextStep.hint).toContain("accessUrl"); + }); + it("manageApps(action=deployApp) with cosTimestamp should skip uploadCode", async () => { const result = await tools.manageApps.handler({ action: "deployApp", diff --git a/mcp/src/tools/apps.ts b/mcp/src/tools/apps.ts index ffbac8a7..f0042cc4 100644 --- a/mcp/src/tools/apps.ts +++ b/mcp/src/tools/apps.ts @@ -37,6 +37,24 @@ function getCloudAppService(cloudbase: any) { return cloudbase.cloudAppService ?? cloudbase.getCloudAppService?.(); } +function normalizeAccessUrlFromDomain(domain: unknown): { domain?: string; accessUrl?: string } { + if (typeof domain !== "string" || !domain.trim()) return {}; + const trimmed = domain.trim(); + const withProtocol = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`; + try { + const url = new URL(withProtocol); + url.hash = ""; + url.search = ""; + url.pathname = url.pathname === "/" ? "" : url.pathname.replace(/\/+$/, ""); + return { + domain: url.host, + accessUrl: url.toString().replace(/\/$/, ""), + }; + } catch { + return {}; + } +} + export function registerAppTools(server: ExtendedMcpServer) { const cloudBaseOptions = server.cloudBaseOptions; const getManager = () => getCloudBaseManager({ cloudBaseOptions }); @@ -288,7 +306,7 @@ export function registerAppTools(server: ExtendedMcpServer) { appPath: z .string() .optional() - .describe("应用线上访问路径(hosting mount path),例如 /my-web-app。不是本地目录路径;省略时默认为 /serviceName。"), + .describe("应用线上访问路径(hosting mount path),例如 /my-web-app。不是本地目录路径;CloudApp 已有独立子域名,省略时默认为 /(根路径)。"), buildPath: z .string() .optional() @@ -476,6 +494,21 @@ export function registerAppTools(server: ExtendedMcpServer) { logCloudBaseResult(server.logger, result); const { BuildId, VersionName } = result; + let appInfo: Record | undefined; + let domain: string | undefined; + let accessUrl: string | undefined; + let accessUrlLookupWarning: string | undefined; + try { + appInfo = await appService.describeAppInfo({ + deployType: "static-hosting", + serviceName, + }); + logCloudBaseResult(server.logger, appInfo); + ({ domain, accessUrl } = normalizeAccessUrlFromDomain(appInfo?.Domain)); + } catch (error) { + accessUrlLookupWarning = error instanceof Error ? error.message : String(error); + } + return jsonContent( buildEnvelope( { @@ -483,6 +516,11 @@ export function registerAppTools(server: ExtendedMcpServer) { serviceName, versionName: VersionName, buildId: BuildId, + domain, + accessUrl, + accessUrlSource: accessUrl ? "describeAppInfo.Domain" : undefined, + accessUrlLookupWarning, + app: appInfo, upload: { cosTimestamp: cosTs }, deployment: result, buildConfig: { @@ -498,10 +536,14 @@ export function registerAppTools(server: ExtendedMcpServer) { serviceName, buildId: BuildId, }, - hint: `调用 queryApps(action="getAppVersion", serviceName="${serviceName}", buildId="${BuildId}") 轮询构建状态,直到 status 变为 SUCCESS 或 FAILED。构建通常需要 3~5 分钟。若状态为 FAILED,可继续调用 queryApps(action="getBuildLog", serviceName="${serviceName}", buildId="${BuildId}") 查看构建日志诊断失败原因。`, + hint: accessUrl + ? `调用 queryApps(action="getAppVersion", serviceName="${serviceName}", buildId="${BuildId}") 轮询构建状态,直到 status 变为 SUCCESS 或 FAILED。构建成功后,后续记录部署时必须使用本结果的 accessUrl=${accessUrl},不要自行拼接域名。若状态为 FAILED,可继续调用 queryApps(action="getBuildLog", serviceName="${serviceName}", buildId="${BuildId}") 查看构建日志诊断失败原因。` + : `调用 queryApps(action="getAppVersion", serviceName="${serviceName}", buildId="${BuildId}") 轮询构建状态,直到 status 变为 SUCCESS 或 FAILED;再调用 queryApps(action="getApp", serviceName="${serviceName}") 读取 app.Domain 作为 accessUrl,不能自行拼接域名。若状态为 FAILED,可继续调用 queryApps(action="getBuildLog", serviceName="${serviceName}", buildId="${BuildId}") 查看构建日志诊断失败原因。`, }, }, - "CloudBase 应用构建已触发,请通过 queryApps 轮询构建状态。", + accessUrl + ? "CloudBase 应用构建已触发,已返回真实 accessUrl;请通过 queryApps 轮询构建状态。" + : "CloudBase 应用构建已触发,请通过 queryApps 轮询构建状态,并用 getApp 读取真实域名。", ), ); } diff --git a/plugin/cloudbase-sites/.claude-plugin/plugin.json b/plugin/cloudbase-sites/.claude-plugin/plugin.json new file mode 100644 index 00000000..6bd1fc27 --- /dev/null +++ b/plugin/cloudbase-sites/.claude-plugin/plugin.json @@ -0,0 +1,22 @@ +{ + "name": "cloudbase-sites", + "description": "CloudBase Sites — create, save, deploy, and inspect React/Vite web apps hosted on Tencent CloudBase. Inspired by Codex Sites' two-stage save→deploy pattern; powered by cloudbase-mcp.", + "version": "0.1.0", + "author": { + "name": "Tencent CloudBase", + "url": "https://cloudbase.net" + }, + "homepage": "https://github.com/TencentCloudBase/cloudbase-mcp", + "license": "MIT", + "keywords": [ + "cloudbase", + "sites", + "vibe-coding", + "vite", + "react", + "vue", + "claude-code", + "codebuddy", + "openclaw" + ] +} diff --git a/plugin/cloudbase-sites/.codex-plugin/plugin.json b/plugin/cloudbase-sites/.codex-plugin/plugin.json new file mode 100644 index 00000000..6cd310d7 --- /dev/null +++ b/plugin/cloudbase-sites/.codex-plugin/plugin.json @@ -0,0 +1,44 @@ +{ + "name": "cloudbase-sites", + "version": "0.1.0", + "description": "CloudBase Sites lets Codex create, preview, save, deploy, inspect, and roll back Vite web apps hosted on Tencent CloudBase.", + "author": { + "name": "Tencent CloudBase", + "url": "https://cloudbase.net" + }, + "homepage": "https://github.com/TencentCloudBase/cloudbase-mcp", + "repository": "https://github.com/TencentCloudBase/cloudbase-mcp", + "license": "MIT", + "keywords": [ + "cloudbase", + "sites", + "codex", + "vibe-coding", + "vite", + "react", + "vue", + "deployment" + ], + "skills": "./skills/", + "mcpServers": "./.mcp.json", + "interface": { + "displayName": "CloudBase Sites", + "shortDescription": "Build, preview, version, and deploy sites to Tencent CloudBase.", + "longDescription": "Create Vite-based React or Vue web apps, preview them locally, save versions, deploy to CloudBase CloudApp independent domains, and add CloudBase database, storage, auth, functions, or CloudRun backend capabilities through cloudbase-mcp.", + "developerName": "Tencent CloudBase", + "category": "Developer Tools", + "capabilities": [ + "Interactive", + "Write" + ], + "websiteURL": "https://cloudbase.net", + "privacyPolicyURL": "https://cloud.tencent.com/document/product/301/11470", + "termsOfServiceURL": "https://cloud.tencent.com/document/product/301/1967", + "defaultPrompt": [ + "Build a CloudBase site and preview it locally.", + "Save this version, then deploy it to CloudBase.", + "Add database-backed state to this CloudBase site." + ], + "brandColor": "#006EFF" + } +} diff --git a/plugin/cloudbase-sites/.mcp.json b/plugin/cloudbase-sites/.mcp.json new file mode 100644 index 00000000..3551de7c --- /dev/null +++ b/plugin/cloudbase-sites/.mcp.json @@ -0,0 +1,9 @@ +{ + "mcpServers": { + "cloudbase-mcp": { + "command": "npx", + "args": ["-y", "@cloudbase/cloudbase-mcp@latest"], + "env": {} + } + } +} diff --git a/plugin/cloudbase-sites/README.md b/plugin/cloudbase-sites/README.md new file mode 100644 index 00000000..4391b4f6 --- /dev/null +++ b/plugin/cloudbase-sites/README.md @@ -0,0 +1,26 @@ +# CloudBase Sites Plugin + +CloudBase Sites packages the shared CloudBase site runtime for Claude Code, +CodeBuddy, and Codex. + +## Codex local validation + +Validate the Codex plugin manifest from the repository root: + +```bash +python3 /Users/bookerzhao/.codex/skills/.system/plugin-creator/scripts/validate_plugin.py plugin/cloudbase-sites +``` + +Run the focused regression tests: + +```bash +cd mcp +node_modules/.bin/vitest run --config vitest.config.js ../tests/cloudbase-sites-plugin.test.js +``` + +## Runtime state + +Project-local runtime state is stored in `/.cloudbase-sites/`. +Machine-level supervisor state is stored in `~/.cloudbase-sites/`. + +These paths are runtime-only and should not be committed. diff --git a/plugin/cloudbase-sites/bin/cloudbase-sites b/plugin/cloudbase-sites/bin/cloudbase-sites new file mode 100755 index 00000000..ac5e2a60 --- /dev/null +++ b/plugin/cloudbase-sites/bin/cloudbase-sites @@ -0,0 +1,202 @@ +#!/usr/bin/env node +/** + * cloudbase-sites — single CLI for the CloudBase Sites plugin. + * + * Subcommands: + * init bootstrap CloudBase + React/Vue + Vite project from zero + * preview daemonize / inspect / stop / restart the Vite dev server + * save create a saved version (git commit + tag + app.json entry) + * deploy deploy a saved version to CloudApp via cloudbase-mcp manageApps + * rollback revert working tree to a saved version + * versions list saved versions and deployment status + * supervisor global watchdog daemon (start/stop/status/list/heal/reload) + * + * Run `cloudbase-sites --help` for verb-specific options. + * + * Every invocation also calls ensureSupervisorRunning() so the global + * watchdog stays alive across plugin operations (best-effort). + */ + +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const ctl = await import(join(__dirname, "..", "lib", "supervisor-ctl.mjs")); +const { ensureSupervisorRunning } = ctl; + +// Best-effort supervisor ensure on every bin invocation. Skipped for the +// daemon-loop entry to avoid recursion. +const argv0 = process.argv[2]; +const argv1 = process.argv[3]; +const isDaemonLoop = argv0 === "supervisor" && argv1 === "--daemon-loop"; +if (!isDaemonLoop) { + try { ensureSupervisorRunning({ silent: true }); } catch { /* ignore */ } +} + +const args = process.argv.slice(2); +const verb = args[0]; + +if (!verb || verb === "--help" || verb === "-h") { + printHelp(); + process.exit(0); +} + +const verbArgs = args.slice(1); + +main().catch(async (err) => { + const { emitErr } = await import(join(__dirname, "..", "lib", "emit.mjs")); + emitErr(err.message || String(err), err.code || 1); + process.exit(err.code || 1); +}); + +async function main() { + switch (verb) { + case "init": { + const { runInit, initHelp } = await import(join(__dirname, "..", "lib", "verbs", "init.mjs")); + const opts = parseInit(verbArgs); + if (opts.help) { console.log(initHelp); return; } + return runInit(opts); + } + case "preview": { + const { runPreview, previewHelp } = await import(join(__dirname, "..", "lib", "verbs", "preview.mjs")); + const opts = parsePreview(verbArgs); + if (opts.help) { console.log(previewHelp); return; } + return runPreview(opts); + } + case "save": { + const { runSave, saveHelp } = await import(join(__dirname, "..", "lib", "verbs", "save.mjs")); + const opts = parseSave(verbArgs); + if (opts.help) { console.log(saveHelp); return; } + return runSave(opts); + } + case "deploy": { + const { runDeploy, deployHelp } = await import(join(__dirname, "..", "lib", "verbs", "deploy.mjs")); + const opts = parseDeploy(verbArgs); + if (opts.help) { console.log(deployHelp); return; } + return runDeploy(opts); + } + case "rollback": { + const { runRollback, rollbackHelp } = await import(join(__dirname, "..", "lib", "verbs", "rollback.mjs")); + const opts = parseRollback(verbArgs); + if (opts.help) { console.log(rollbackHelp); return; } + return runRollback(opts); + } + case "versions": { + const { runVersions, versionsHelp } = await import(join(__dirname, "..", "lib", "verbs", "versions.mjs")); + if (verbArgs.includes("--help") || verbArgs.includes("-h")) { console.log(versionsHelp); return; } + return runVersions({}); + } + case "supervisor": { + const { runSupervisor, supervisorHelp } = await import(join(__dirname, "..", "lib", "verbs", "supervisor.mjs")); + const sub = verbArgs[0]; + if (!sub || sub === "--help" || sub === "-h") { console.log(supervisorHelp); return; } + return runSupervisor(sub, verbArgs.slice(1)); + } + case "status": + // Convenience alias for `preview --status` + { + const { runPreview } = await import(join(__dirname, "..", "lib", "verbs", "preview.mjs")); + const opts = parsePreview(["--status", ...verbArgs]); + return runPreview(opts); + } + default: { + const { emitErr } = await import(join(__dirname, "..", "lib", "emit.mjs")); + emitErr(`Unknown verb: ${verb}. Run \`cloudbase-sites --help\` for the list.`, 1); + process.exit(1); + } + } +} + +// --------------------------------------------------------------------------- +// Per-verb arg parsers +// --------------------------------------------------------------------------- + +function parseInit(argv) { + const out = { template: null, start: false, skipInstall: false, help: false }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === "--template") out.template = argv[++i]; + else if (a === "--start") out.start = true; + else if (a === "--skip-install") out.skipInstall = true; + else if (a === "--help" || a === "-h") out.help = true; + } + return out; +} + +function parsePreview(argv) { + const out = {}; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === "--port") out.port = argv[++i]; + else if (a === "--base") out.base = argv[++i]; + else if (a === "--force" || a === "-f") out.force = true; + else if (a === "--restart") out.restart = true; + else if (a === "--stop") out.stop = true; + else if (a === "--status") out.status = true; + else if (a === "--quiet" || a === "-q") out.quiet = true; + else if (a === "--help" || a === "-h") out.help = true; + } + return out; +} + +function parseSave(argv) { + const out = {}; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === "--message" || a === "-m") out.message = argv[++i]; + else if (a === "--help" || a === "-h") out.help = true; + } + return out; +} + +function parseDeploy(argv) { + const out = {}; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === "--version") out.version = argv[++i]; + else if (a === "--skip-build") out.skipBuild = true; + else if (a === "--post" || a === "--post-deploy") out.post = true; + else if (a === "--access-url") out.accessUrl = argv[++i]; + else if (a === "--build-id") out.buildId = argv[++i]; + else if (a === "--version-name") out.versionName = argv[++i]; + else if (a === "--build-status") out.buildStatus = argv[++i]; + else if (a === "--help" || a === "-h") out.help = true; + } + return out; +} + +function parseRollback(argv) { + const out = {}; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === "--to-version") out.toVersion = argv[++i]; + else if (a === "--no-restart") out.noRestart = true; + else if (a === "--help" || a === "-h") out.help = true; + } + return out; +} + +function printHelp() { + process.stdout.write([ + "cloudbase-sites — CloudBase Sites plugin CLI", + "", + "Subcommands:", + " init scaffold CloudBase + React/Vue + Vite from empty cwd", + " preview daemonize / inspect / stop / restart Vite dev server", + " save create a saved version (git checkpoint + app.json entry)", + " deploy deploy a saved version to CloudApp (manageApps)", + " rollback revert working tree to a saved version", + " versions list saved versions + deployment status", + " supervisor global watchdog daemon", + "", + "Run `cloudbase-sites --help` for verb-specific options.", + "", + "State files:", + " /.cloudbase-sites/preview.json current dev server state", + " /.cloudbase-sites/app.json siteName + versions[] + deployments[]", + " ~/.cloudbase-sites/registry.json all registered cwds (supervisor)", + " ~/.cloudbase-sites/supervisor.json global daemon state", + "", + ].join("\n")); +} diff --git a/plugin/cloudbase-sites/hooks/hooks.json b/plugin/cloudbase-sites/hooks/hooks.json new file mode 100644 index 00000000..b2dbf1b4 --- /dev/null +++ b/plugin/cloudbase-sites/hooks/hooks.json @@ -0,0 +1,25 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "bash ${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}/hooks/on-session-start.sh" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Edit|Write|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "bash ${CODEX_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}/hooks/on-file-change.sh" + } + ] + } + ] + } +} diff --git a/plugin/cloudbase-sites/hooks/on-file-change.sh b/plugin/cloudbase-sites/hooks/on-file-change.sh new file mode 100755 index 00000000..e4208173 --- /dev/null +++ b/plugin/cloudbase-sites/hooks/on-file-change.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# on-file-change.sh — Claude Code / OpenClaw / CodeBuddy PostToolUse hook +# +# Reads the hook event payload from stdin (JSON). Restarts the daemonized +# Vite dev server iff the edited file is one Vite HMR cannot handle (config +# files, tsconfig, .env, package.json). +# +# Constraints: +# - exit fast (≤ ~50ms) +# - debounce: 1.5s lockfile so a rapid burst of edits restart only once +# - background restart so the hook returns immediately +# - exit code is always 0 — never block the user's tool call + +set -u + +# Read hook payload from stdin. +PAYLOAD="$(cat)" + +# Without jq we cannot parse — silently skip. (Don't block the user.) +if ! command -v jq >/dev/null 2>&1; then + exit 0 +fi + +FILE_PATH="$(printf '%s' "$PAYLOAD" | jq -r '.tool_input.file_path // empty')" +CWD="$(printf '%s' "$PAYLOAD" | jq -r '.cwd // empty')" + +[ -z "$FILE_PATH" ] && exit 0 + +# --- Decide if file warrants a dev-server restart --------------------------- +# Vite HMR handles src/*.tsx / *.css / public/* etc. We only restart for +# changes Vite cannot hot-swap. +restart=0 +case "$(basename "$FILE_PATH")" in + package.json | tsconfig.json | tsconfig.*.json) restart=1 ;; + vite.config.* | tailwind.config.* | postcss.config.*) restart=1 ;; + .env | .env.* ) restart=1 ;; +esac +if [ "$restart" = "0" ]; then + case "$FILE_PATH" in + */vite.config.* | */tailwind.config.* | */postcss.config.*) restart=1 ;; + */.env | */.env.*) restart=1 ;; + esac +fi +[ "$restart" = "1" ] || exit 0 + +# --- Resolve target cwd (where preview.json lives) -------------------------- +TARGET_CWD="${CWD:-$(dirname "$FILE_PATH")}" + +find_preview_root() { + local d="$1" + while [ "$d" != "/" ] && [ -n "$d" ]; do + if [ -f "$d/.cloudbase-sites/preview.json" ]; then + printf '%s' "$d" + return 0 + fi + d="$(dirname "$d")" + done + return 1 +} + +PROJECT_ROOT="$(find_preview_root "$TARGET_CWD" || true)" +[ -z "$PROJECT_ROOT" ] && exit 0 + +# --- Debounce: 1.5s lockfile (Node.js Date.now() — cross-platform) ---------- +LOCK="$PROJECT_ROOT/.cloudbase-sites/restart.lock" +NOW="$(node -e 'console.log(Date.now())' 2>/dev/null || python3 -c 'import time;print(int(time.time()*1000))' 2>/dev/null || echo 0)" +if [ -f "$LOCK" ]; then + LAST="$(cat "$LOCK" 2>/dev/null || echo 0)" + case "$NOW$LAST" in *[!0-9]*) ;; *) + DELTA=$(( NOW - LAST )) + if [ "$DELTA" -lt 1500 ]; then + exit 0 + fi + ;; esac +fi +mkdir -p "$(dirname "$LOCK")" +printf '%s' "$NOW" > "$LOCK" + +# --- Async restart ----------------------------------------------------------- +SITES_BIN="$(command -v cloudbase-sites || true)" +if [ -z "$SITES_BIN" ]; then + HOOK_DIR="$(cd "$(dirname "$0")" && pwd)" + SITES_BIN="$HOOK_DIR/../bin/cloudbase-sites" + [ -x "$SITES_BIN" ] || exit 0 # give up silently — never block the user +fi + +HOOK_LOG="$PROJECT_ROOT/.cloudbase-sites/logs/hook-restart.log" +mkdir -p "$(dirname "$HOOK_LOG")" +( + cd "$PROJECT_ROOT" || exit 0 + printf '\n[%s] restart trigger: %s\n' "$(date -Iseconds 2>/dev/null || date)" "$FILE_PATH" >> "$HOOK_LOG" + "$SITES_BIN" preview --restart >> "$HOOK_LOG" 2>&1 || true +) /dev/null 2>&1 & +disown 2>/dev/null || true + +exit 0 diff --git a/plugin/cloudbase-vibe-coding/hooks/on-session-start.sh b/plugin/cloudbase-sites/hooks/on-session-start.sh similarity index 55% rename from plugin/cloudbase-vibe-coding/hooks/on-session-start.sh rename to plugin/cloudbase-sites/hooks/on-session-start.sh index e551f2c8..bb638bf5 100755 --- a/plugin/cloudbase-vibe-coding/hooks/on-session-start.sh +++ b/plugin/cloudbase-sites/hooks/on-session-start.sh @@ -15,16 +15,16 @@ # } # } # -# Hook log: /.cloudbase-agent/logs/hook-session-start.log +# Hook log: /.cloudbase-sites/logs/hook-session-start.log set -u CWD="$(pwd)" HOOK_DIR="$(cd "$(dirname "$0")" && pwd)" PLUGIN_ROOT="$(cd "$HOOK_DIR/.." && pwd)" -LOG_FALLBACK="/tmp/cloudbase-vibe-session-start.log" +LOG_FALLBACK="/tmp/cloudbase-sites-session-start.log" log() { - local target="$CWD/.cloudbase-agent/logs/hook-session-start.log" + local target="$CWD/.cloudbase-sites/logs/hook-session-start.log" mkdir -p "$(dirname "$target")" 2>/dev/null || target="$LOG_FALLBACK" printf '[%s] %s\n' "$(date -Iseconds 2>/dev/null || date)" "$*" >> "$target" 2>/dev/null || true } @@ -50,19 +50,20 @@ emit_context() { # If node is missing, we silently skip — the hook still ran its decision tree. } -# Build the static rules block used in every active branch. ~370 tokens. -RULES_BLOCK='## CloudBase Vibe Coding (plugin-injected, MUST follow) +# Build the static rules block used in every active branch. +RULES_BLOCK='## CloudBase Sites (plugin-injected, MUST follow) -You are in a session managed by the cloudbase-vibe-coding plugin. The dev server -lifecycle is owned by hooks; you must not bypass them. +You are in a session managed by the cloudbase-sites plugin. The dev server +lifecycle and version control are owned by hooks + the `cloudbase-sites` CLI; +you must not bypass them. ### Hard rules 1. **Never guess the preview URL.** It is NOT 5173/5174/5175 — the plugin uses the 17173..17272 range AND each cwd may have its own port. Always run - `cloudbase-vibe-status-preview` (or read `/.cloudbase-agent/preview.json`) - to obtain `internalUrl`. If status reports NO_PREVIEW (exit 5), wait ~5s - and retry once — the SessionStart hook may still be installing/starting. + `cloudbase-sites preview --status` (or read `/.cloudbase-sites/preview.json`) + to obtain `internalUrl`. If it reports NO_PREVIEW (exit 5), wait ~5s and + retry once — the SessionStart hook may still be installing/starting. 2. **"Make me a X app" = X IS the homepage.** When the user uses whole-house language ("build me a todo app", "make a chat app"), the request means @@ -77,32 +78,37 @@ lifecycle is owned by hooks; you must not bypass them. 3. **UI work for NEW features requires a design specification first.** When the user asks you to build a new app/feature/page from scratch (e.g. - "make a todo app", "build a chat UI", "add a dashboard"), you MUST invoke - the `ui-design` skill BEFORE writing any `.tsx`/`.css`/`.html` and produce - its design spec (Aesthetic Direction, Color Palette, Typography, Layout - Strategy). Do NOT improvise generic AI-default styling. + "make a todo app", "build a chat UI", "add a dashboard"), you MUST first + fetch the CloudBase ui-design skill via + `searchKnowledgeBase(mode=skill, skillName="ui-design")`, + produce its 4-part design spec (Aesthetic Direction, Color Palette, + Typography, Layout Strategy), output the spec to the user, THEN write + any `.tsx`/`.css`/`.html`. Do NOT improvise generic AI-default styling. Note: the CloudBase template'\''s `CLAUDE.md` "Existing Implementation First" exemption applies only to bug fixes / completing TODO markers in existing - code — it does NOT exempt you from `ui-design` when creating a new app + code — it does NOT exempt you from ui-design when creating a new app on top of the template. 4. **Never spawn `npm run dev` / `vite` / `vite build` yourself.** Dev-server lifecycle is the SessionStart + PostToolUse hooks. Build/deploy is - `cloudbase-vibe-deploy`. Bypassing them loses host=0.0.0.0, port allocation, - daemonization, snapshot, and deploy history. + `cloudbase-sites deploy`. Bypassing them loses host=0.0.0.0, port allocation, + daemonization, version metadata, and deploy history. 5. **Data persistence: BaaS-first via Web SDK + MCP-managed schema.** When the user'\''s feature needs to store / query / update data: - **Schema:** call cloudbase-mcp `writeNoSqlDatabaseStructure(action="createCollection", ...)` to create the collection (and `updateCollection` for indexes). Do NOT - ask the user to create collections manually in the console. The - `no-sql-web-sdk` skill describes the canonical patterns. + ask the user to create collections manually in the console. For canonical + patterns, fetch the no-sql-web-sdk skill via + `searchKnowledgeBase(mode=skill, skillName="no-sql-web-sdk")`. - **Reads/writes:** use `@cloudbase/js-sdk` directly from the React/Vue code (`db.collection(...).where(...).get()`, `.add()`, `.update()`, `db.collection(...).watch(...)` for realtime). The template already ships an initialized SDK at `src/utils/cloudbase.ts` — use it. - - **Auth:** if the feature needs user accounts, follow the `auth-tool` - skill first to verify provider config, then `auth-web` for client code. + - **Auth:** if the feature needs user accounts, fetch + `searchKnowledgeBase(mode=skill, skillName="auth-tool")` first to + verify provider config (the management-side configuration), then + `searchKnowledgeBase(mode=skill, skillName="auth-web")` for client code. - **Reach for cloud functions only when ALL of these are true:** (a) the logic absolutely cannot be expressed as database security rules, (b) it needs server-side secrets / third-party API keys, OR @@ -112,48 +118,77 @@ lifecycle is owned by hooks; you must not bypass them. function "just for safety" is over-engineering. 6. **Do not run browser tests / playwright / agent-browser by default.** - The user wants to SEE the running app, not read 5 minutes of test - reports. After you finish a feature: + The user wants to SEE the running app, not read 5 minutes of test reports. + After you finish a feature: - Spend zero turns on writing/running automated UI tests unless the user explicitly says "write tests" or "test it for me". - - Verify reasonably (preview is healthy, no compile error in `cloudbase-vibe-status-preview` - log) — that'\''s enough for the first version. - - Then ASK the user (similar to the deploy question): "要不要我用内置浏览器 - 打开 帮你点一遍验证一下?" — only run browser-based verification - after explicit yes. - -### Tool quick-reference (only when user explicitly asks) - -- "stop the dev server" → `cloudbase-vibe-stop-preview` -- "is it running" / "the URL" → `cloudbase-vibe-status-preview` -- "deploy this" / "publish" → `cloudbase-vibe-deploy` (then call `manageApps` - per its `nextAction` — it will pass - `framework=static, installCmd="", buildCmd=""` - to skip remote build, then - `cloudbase-vibe-deploy --post-deploy ...`). - **Uses `manageApps` (CloudApp, independent - per-session domain), NOT `manageHosting`.** - After deploy succeeds, ask the user: - "要我用 UI 设计能力进一步优化样式和体验吗?" - -### Deploy contract - -- **Each cwd has a stable `serviceName`** like `-<6hex>` written to - `/.cloudbase-agent/app.json` on first invocation. Re-deploying the same - cwd reuses this serviceName so the public URL stays stable for the user. -- **`cloudbase-vibe-deploy` emits `nextAction` with `framework=static, installCmd="", buildCmd=""`** - — local build already produces dist/, so remote npm install/build are skipped. - Only `tcb hosting deploy` runs. See `skills/cloudbase-agent-runtime/SKILL.md` for full detail. -- **Pre-flight you may need:** if no envId is bound yet (the deploy may fail - with an env error), call MCP `envQuery({ action: "info" })` once. If the - user has multiple envs, ask them to pick. After binding, retry the deploy. -- **Active suggestion (D):** after you finish a user-requested feature - (i.e. you just completed "make/build/add a X" and tests / preview look good), - ASK the user at the end of your reply: "现在这版要部署看一下吗?" — do NOT - deploy unsolicited. Only deploy after explicit user consent. After deploy - succeeds, also ask: "要我用 UI 设计能力进一步优化样式和体验吗?" - -For full contract see `skills/cloudbase-agent-runtime/SKILL.md`.' + - Verify reasonably (preview is healthy, no compile error in + `cloudbase-sites preview --status` log) — that'\''s enough. + - Then ASK the user: "要不要我用内置浏览器打开 帮你点一遍验证一下?" + — only run browser-based verification after explicit yes. + +7. **Two-stage save→deploy workflow (Codex-Sites-style).** Versions are + labeled git checkpoints; deploys are publishes of saved versions. + - When a user-visible feature is complete and the preview looks good, + ASK: "要保存这一版吗?(下次还能再调出来)" then run `cloudbase-sites save -m "