mirror of
https://github.com/base44/skills.git
synced 2026-09-14 16:22:01 +08:00
1463369a86
* feat(base44-sandbox): add cloud-sandbox remote-dev skill flavor Add a focused, separately-installable "sandbox flavor" for developing a Base44 app inside Base44's cloud sandbox (PR base44-dev/apper#11608), where the implementation is remote and writing a backend-function file is what ships it — no local checkout and no Base44 CLI. - skills/base44-sandbox: new hand-authored skill. Backend functions and frontend code are supported; entities, connectors, and agents are marked not-supported-yet; no automations. Concise inline guidance with a pointer to base44-cli's functions-create reference (no duplicated copies). - skills/base44-remote-dev: vendored connection skill (MCP / HTTP bridge, tools, edit→preview→verify loop) from the upstream PR. - New base44-sandbox plugin across channels: second entry in the Claude and Codex marketplaces + plugins/base44-sandbox/ bundle (its own .claude/.codex/ .cursor manifests + skills/assets symlinks). Bundles base44-remote-dev, base44-sandbox, base44-sdk, base44-troubleshooter; excludes deploy-oriented base44-cli. npx skills --skill documented in the README. - validate-template.mjs now discovers and validates all .cursor-plugin manifests (repo root + plugins/*/). - sync-cli-skill flags base44-sandbox for manual review when function authoring conventions change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(base44-sandbox): support connectors via MCP tools and projectless CLI Connectors are now usable in remote-dev (apper#11608 MCP connector tools + cli#547 projectless connectors), so flip them from "not supported" to a documented connect flow: - list_connectors (apps:read) / initiate_connector_connection (apps:write) over MCP, and base44 connectors list-available/initiate/pull over the CLI with --app-id (no local project / config.jsonc). - Document the declarative/replace scope semantics (read current scopes first, pass the complete set) and the human-consent authorization-URL step. - Note the connector CLI is the one allowed CLI use in remote-dev; it targets a remote app by id with no deploy step (not a contradiction of the no-CLI rule, which is about local-project/deploy commands). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(base44-remote-dev): document the connector MCP tools Upstream's base44-remote-dev SKILL.md doesn't yet list the connector tools shipped in apper#11608, so add a "Connectors (OAuth integrations)" section covering list_connectors (apps:read) and initiate_connector_connection (apps:write) — declarative/replace scope semantics and the human-consent redirect-URL step — plus the projectless base44 connectors CLI equivalent. Marked as a local addition (HTML comment) to reconcile when upstream documents these tools. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(base44-remote-dev): drop local-addition note from connectors section The connectors section will align with upstream by merge time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(docs): use entry.ts as the backend-function entry file The function entry file (and the function.jsonc "entry" value) is entry.ts, not index.ts. Update all function examples across base44-sandbox, base44-sdk, and base44-cli docs (functions-create, automations, project-structure trees). Invocation already uses base44.functions.invoke('name', data) everywhere; no change needed there. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(base44-sandbox): connector token usage, complete README, reference-first - Add "Using a connected connector in code": fetch the OAuth token via base44.asServiceRole.connectors.getConnection(type) inside a backend function, with a link to base44-sdk's connectors reference. - Add a "Reference order & the complete README" section: prefer this skill and its siblings over web search, and point to the public app-specific onboarding README endpoint. - Surface the reference-first note near the top too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(base44-sdk): clarify functions.invoke() returns the raw axios response invoke() resolves to the raw axios response, so the function's JSON is on .data (not the top-level object), and it throws on non-2xx with the error body at err.response.data. Update the invoke method doc and every frontend example to read res.data, and add the warning in SKILL.md + QUICK_REFERENCE. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(base44-sandbox): note invoke() returns raw axios response (.data) Add a small frontend-calling clarification so agents read the function's JSON off res.data and handle the non-2xx throw; link to base44-sdk for detail. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(base44-sandbox): support entities and agents (filesystem-only) Entities and agents are now authorable in the sandbox — writing the .jsonc file auto-syncs, no entities/agents push or deploy. Flip both from "not supported" to supported, broaden the mental model to "writing a resource file is the deploy", and add Entities and Agents authoring sections (file naming, schema, links to base44-cli references with their push/deploy sections flagged as not applicable). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(base44-sandbox): add base44-cli reference to the Agents section Match the functions/entities pattern: link the Agents section to the base44-cli Agent Schema and make clear to ignore agents push/pull/deploy commands (sandbox auto-syncs the file). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(sandbox): drop HTTP bridge surface, document base44 sandbox CLI names Remove the HTTP REST sandbox-bridge option (POST /api/apps/<id>/sandbox-bridge/<tool>) from base44-remote-dev and base44-sandbox; local agents use MCP or the `base44 sandbox` CLI instead. Clarify CLI naming throughout: add the MCP-tool -> CLI-command mapping (list_directory->sandbox ls, read_file->sandbox read, write_file->sandbox write, edit_file->sandbox edit, run_command->sandbox run; grep/release unchanged) and annotate inline tool references with their CLI command names. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01842h6eP9hLWHnoRrERmbLo * docs(sandbox): drop the sandbox release command/tool The release command is no longer supported, so remove it from the base44 sandbox CLI mapping table. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01842h6eP9hLWHnoRrERmbLo * docs(sandbox): document checkpoint command and create_checkpoint tool Add the `base44 sandbox checkpoint` CLI command (base44/cli#547) and the `create_checkpoint` MCP tool (base44-dev/apper#11608) to the base44-remote-dev and base44-sandbox skills, including the sandbox:write scope, the CLI/MCP name mapping, and the COMMIT_FLUSH_PENDING error code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(sandbox): functions need only entry.ts, no function.jsonc In the sandbox the function is inferred from the directory, so authors only create entry.ts under base44/functions/<name>/; the function.jsonc config is not required and is ignored in this mode. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(sandbox): note function.jsonc guidance in cli ref doesn't apply Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
317 lines
8.5 KiB
JavaScript
317 lines
8.5 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { promises as fs } from "node:fs";
|
|
import path from "node:path";
|
|
import process from "node:process";
|
|
|
|
const repoRoot = process.cwd();
|
|
const errors = [];
|
|
const warnings = [];
|
|
|
|
const pluginNamePattern = /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/;
|
|
|
|
function addError(message) {
|
|
errors.push(message);
|
|
}
|
|
|
|
function addWarning(message) {
|
|
warnings.push(message);
|
|
}
|
|
|
|
async function pathExists(targetPath) {
|
|
try {
|
|
await fs.access(targetPath);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function readJsonFile(filePath, context) {
|
|
let raw;
|
|
try {
|
|
raw = await fs.readFile(filePath, "utf8");
|
|
} catch {
|
|
addError(`${context} is missing: ${filePath}`);
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(raw);
|
|
} catch (error) {
|
|
addError(`${context} contains invalid JSON (${filePath}): ${error.message}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function normalizeNewlines(content) {
|
|
return content.replace(/\r\n/g, "\n");
|
|
}
|
|
|
|
function parseFrontmatter(content) {
|
|
const normalized = normalizeNewlines(content);
|
|
if (!normalized.startsWith("---\n")) {
|
|
return null;
|
|
}
|
|
|
|
const closingIndex = normalized.indexOf("\n---\n", 4);
|
|
if (closingIndex === -1) {
|
|
return null;
|
|
}
|
|
|
|
const frontmatterBlock = normalized.slice(4, closingIndex);
|
|
const fields = {};
|
|
|
|
for (const line of frontmatterBlock.split("\n")) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || trimmed.startsWith("#")) {
|
|
continue;
|
|
}
|
|
const separator = line.indexOf(":");
|
|
if (separator === -1) {
|
|
continue;
|
|
}
|
|
const key = line.slice(0, separator).trim();
|
|
const value = line.slice(separator + 1).trim();
|
|
fields[key] = value;
|
|
}
|
|
|
|
return fields;
|
|
}
|
|
|
|
async function walkFiles(dirPath) {
|
|
const files = [];
|
|
const stack = [dirPath];
|
|
|
|
while (stack.length > 0) {
|
|
const current = stack.pop();
|
|
const entries = await fs.readdir(current, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
const entryPath = path.join(current, entry.name);
|
|
if (entry.isDirectory()) {
|
|
stack.push(entryPath);
|
|
} else if (entry.isFile()) {
|
|
files.push(entryPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
return files;
|
|
}
|
|
|
|
function isSafeRelativePath(value) {
|
|
if (typeof value !== "string" || value.length === 0) {
|
|
return false;
|
|
}
|
|
if (value.startsWith("http://") || value.startsWith("https://")) {
|
|
return true;
|
|
}
|
|
if (path.isAbsolute(value)) {
|
|
return false;
|
|
}
|
|
const normalized = path.posix.normalize(value.replace(/\\/g, "/"));
|
|
return !normalized.startsWith("../") && normalized !== "..";
|
|
}
|
|
|
|
function extractPathValues(value) {
|
|
if (typeof value === "string") {
|
|
return [value];
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
return value.flatMap((entry) => extractPathValues(entry));
|
|
}
|
|
|
|
if (value && typeof value === "object") {
|
|
const candidates = [];
|
|
if (typeof value.path === "string") {
|
|
candidates.push(value.path);
|
|
}
|
|
if (typeof value.file === "string") {
|
|
candidates.push(value.file);
|
|
}
|
|
return candidates;
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
async function validateReferencedPath(pluginDir, fieldName, pathValue, pluginName) {
|
|
if (pathValue.startsWith("http://") || pathValue.startsWith("https://")) {
|
|
return;
|
|
}
|
|
|
|
if (!isSafeRelativePath(pathValue)) {
|
|
addError(
|
|
`${pluginName}: field "${fieldName}" has invalid path "${pathValue}". Use a relative path without ".." or absolute prefixes.`
|
|
);
|
|
return;
|
|
}
|
|
|
|
const resolved = path.resolve(pluginDir, pathValue);
|
|
const exists = await pathExists(resolved);
|
|
if (!exists) {
|
|
addError(`${pluginName}: field "${fieldName}" references missing path "${pathValue}".`);
|
|
}
|
|
}
|
|
|
|
async function validateFrontmatterFile(filePath, componentName, requiredKeys, pluginName) {
|
|
const content = await fs.readFile(filePath, "utf8");
|
|
const parsed = parseFrontmatter(content);
|
|
const relativeFile = path.relative(repoRoot, filePath);
|
|
|
|
if (!parsed) {
|
|
addError(`${pluginName}: ${componentName} file missing YAML frontmatter: ${relativeFile}`);
|
|
return;
|
|
}
|
|
|
|
for (const key of requiredKeys) {
|
|
if (!parsed[key] || parsed[key].length === 0) {
|
|
addError(`${pluginName}: ${componentName} file missing "${key}" in frontmatter: ${relativeFile}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function validateComponentFrontmatter(pluginDir, pluginName) {
|
|
const rulesDir = path.join(pluginDir, "rules");
|
|
if (await pathExists(rulesDir)) {
|
|
const files = await walkFiles(rulesDir);
|
|
for (const file of files) {
|
|
const ext = path.extname(file).toLowerCase();
|
|
if (ext === ".md" || ext === ".mdc" || ext === ".markdown") {
|
|
await validateFrontmatterFile(file, "rule", ["description"], pluginName);
|
|
}
|
|
}
|
|
}
|
|
|
|
const skillsDir = path.join(pluginDir, "skills");
|
|
if (await pathExists(skillsDir)) {
|
|
const files = await walkFiles(skillsDir);
|
|
for (const file of files) {
|
|
if (path.basename(file) === "SKILL.md") {
|
|
await validateFrontmatterFile(file, "skill", ["name", "description"], pluginName);
|
|
}
|
|
}
|
|
}
|
|
|
|
const agentsDir = path.join(pluginDir, "agents");
|
|
if (await pathExists(agentsDir)) {
|
|
const files = await walkFiles(agentsDir);
|
|
for (const file of files) {
|
|
const ext = path.extname(file).toLowerCase();
|
|
if (ext === ".md" || ext === ".mdc" || ext === ".markdown") {
|
|
await validateFrontmatterFile(file, "agent", ["name", "description"], pluginName);
|
|
}
|
|
}
|
|
}
|
|
|
|
const commandsDir = path.join(pluginDir, "commands");
|
|
if (await pathExists(commandsDir)) {
|
|
const files = await walkFiles(commandsDir);
|
|
for (const file of files) {
|
|
const ext = path.extname(file).toLowerCase();
|
|
if (ext === ".md" || ext === ".mdc" || ext === ".markdown" || ext === ".txt") {
|
|
await validateFrontmatterFile(file, "command", ["name", "description"], pluginName);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async function discoverPluginDirs() {
|
|
// Every directory containing a .cursor-plugin/plugin.json is a plugin to validate:
|
|
// the repo root plus any bundle under plugins/*/.
|
|
const dirs = [];
|
|
if (await pathExists(path.join(repoRoot, ".cursor-plugin", "plugin.json"))) {
|
|
dirs.push(repoRoot);
|
|
}
|
|
|
|
const pluginsRoot = path.join(repoRoot, "plugins");
|
|
if (await pathExists(pluginsRoot)) {
|
|
const entries = await fs.readdir(pluginsRoot, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
if (!entry.isDirectory()) {
|
|
continue;
|
|
}
|
|
const candidate = path.join(pluginsRoot, entry.name);
|
|
if (await pathExists(path.join(candidate, ".cursor-plugin", "plugin.json"))) {
|
|
dirs.push(candidate);
|
|
}
|
|
}
|
|
}
|
|
|
|
return dirs;
|
|
}
|
|
|
|
async function validatePlugin(pluginDir) {
|
|
const manifestPath = path.join(pluginDir, ".cursor-plugin", "plugin.json");
|
|
const pluginManifest = await readJsonFile(manifestPath, "Plugin manifest");
|
|
if (!pluginManifest) {
|
|
return;
|
|
}
|
|
|
|
const pluginName = pluginManifest.name ?? "<unknown>";
|
|
|
|
if (typeof pluginManifest.name !== "string" || !pluginNamePattern.test(pluginManifest.name)) {
|
|
addError(
|
|
`${pluginName}: "name" in plugin.json must be lowercase and use only alphanumerics, hyphens, and periods.`
|
|
);
|
|
}
|
|
|
|
const manifestFields = ["logo", "rules", "skills", "agents", "commands", "hooks", "mcpServers"];
|
|
for (const field of manifestFields) {
|
|
const values = extractPathValues(pluginManifest[field]);
|
|
for (const value of values) {
|
|
await validateReferencedPath(pluginDir, field, value, pluginName);
|
|
}
|
|
}
|
|
|
|
await validateComponentFrontmatter(pluginDir, pluginName);
|
|
}
|
|
|
|
async function main() {
|
|
const pluginDirs = await discoverPluginDirs();
|
|
if (pluginDirs.length === 0) {
|
|
addError("No .cursor-plugin/plugin.json manifest found at the repo root or under plugins/*/.");
|
|
summarizeAndExit();
|
|
return;
|
|
}
|
|
|
|
for (const pluginDir of pluginDirs) {
|
|
await validatePlugin(pluginDir);
|
|
}
|
|
|
|
// hooks/mcp checks apply to the repo root only (shared plugin assets).
|
|
const hooksPath = path.join(repoRoot, "hooks", "hooks.json");
|
|
if (!(await pathExists(hooksPath))) {
|
|
addWarning("no hooks/hooks.json file found (only needed when using hooks).");
|
|
}
|
|
|
|
const mcpPath = path.join(repoRoot, "mcp.json");
|
|
if (!(await pathExists(mcpPath))) {
|
|
addWarning("no mcp.json file found (only needed when using MCP servers).");
|
|
}
|
|
|
|
summarizeAndExit();
|
|
}
|
|
|
|
function summarizeAndExit() {
|
|
if (warnings.length > 0) {
|
|
console.log("Warnings:");
|
|
for (const warning of warnings) {
|
|
console.log(`- ${warning}`);
|
|
}
|
|
console.log("");
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
console.error("Validation failed:");
|
|
for (const error of errors) {
|
|
console.error(`- ${error}`);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log("Validation passed.");
|
|
}
|
|
|
|
await main(); |