flarecms 0.1.0
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/README.md +73 -0
- package/dist/auth/index.js +40 -0
- package/dist/cli/commands.js +389 -0
- package/dist/cli/index.js +403 -0
- package/dist/cli/mcp.js +209 -0
- package/dist/db/index.js +164 -0
- package/dist/index.js +17626 -0
- package/package.json +105 -0
- package/scripts/fix-api-paths.mjs +32 -0
- package/scripts/fix-imports.mjs +38 -0
- package/scripts/prefix-css.mjs +45 -0
- package/src/api/lib/cache.ts +45 -0
- package/src/api/lib/response.ts +40 -0
- package/src/api/middlewares/auth.ts +186 -0
- package/src/api/middlewares/cors.ts +10 -0
- package/src/api/middlewares/rbac.ts +85 -0
- package/src/api/routes/auth.ts +377 -0
- package/src/api/routes/collections.ts +205 -0
- package/src/api/routes/content.ts +175 -0
- package/src/api/routes/device.ts +160 -0
- package/src/api/routes/magic.ts +150 -0
- package/src/api/routes/mcp.ts +273 -0
- package/src/api/routes/oauth.ts +160 -0
- package/src/api/routes/settings.ts +43 -0
- package/src/api/routes/setup.ts +307 -0
- package/src/api/routes/tokens.ts +80 -0
- package/src/api/schemas/auth.ts +15 -0
- package/src/api/schemas/index.ts +51 -0
- package/src/api/schemas/tokens.ts +24 -0
- package/src/auth/index.ts +28 -0
- package/src/cli/commands.ts +217 -0
- package/src/cli/index.ts +21 -0
- package/src/cli/mcp.ts +210 -0
- package/src/cli/tests/cli.test.ts +40 -0
- package/src/cli/tests/create.test.ts +87 -0
- package/src/client/FlareAdminRouter.tsx +47 -0
- package/src/client/app.tsx +175 -0
- package/src/client/components/app-sidebar.tsx +227 -0
- package/src/client/components/collection-modal.tsx +215 -0
- package/src/client/components/content-list.tsx +247 -0
- package/src/client/components/dynamic-form.tsx +190 -0
- package/src/client/components/field-modal.tsx +221 -0
- package/src/client/components/settings/api-token-section.tsx +400 -0
- package/src/client/components/settings/general-section.tsx +224 -0
- package/src/client/components/settings/security-section.tsx +154 -0
- package/src/client/components/settings/seo-section.tsx +200 -0
- package/src/client/components/settings/signup-section.tsx +257 -0
- package/src/client/components/ui/accordion.tsx +78 -0
- package/src/client/components/ui/avatar.tsx +107 -0
- package/src/client/components/ui/badge.tsx +52 -0
- package/src/client/components/ui/button.tsx +60 -0
- package/src/client/components/ui/card.tsx +103 -0
- package/src/client/components/ui/checkbox.tsx +27 -0
- package/src/client/components/ui/collapsible.tsx +19 -0
- package/src/client/components/ui/dialog.tsx +162 -0
- package/src/client/components/ui/icon-picker.tsx +485 -0
- package/src/client/components/ui/icons-data.ts +8476 -0
- package/src/client/components/ui/input.tsx +20 -0
- package/src/client/components/ui/label.tsx +20 -0
- package/src/client/components/ui/popover.tsx +91 -0
- package/src/client/components/ui/select.tsx +204 -0
- package/src/client/components/ui/separator.tsx +23 -0
- package/src/client/components/ui/sheet.tsx +141 -0
- package/src/client/components/ui/sidebar.tsx +722 -0
- package/src/client/components/ui/skeleton.tsx +13 -0
- package/src/client/components/ui/sonner.tsx +47 -0
- package/src/client/components/ui/switch.tsx +30 -0
- package/src/client/components/ui/table.tsx +116 -0
- package/src/client/components/ui/tabs.tsx +80 -0
- package/src/client/components/ui/textarea.tsx +18 -0
- package/src/client/components/ui/tooltip.tsx +68 -0
- package/src/client/hooks/use-mobile.ts +19 -0
- package/src/client/index.css +149 -0
- package/src/client/index.ts +7 -0
- package/src/client/layouts/admin-layout.tsx +93 -0
- package/src/client/layouts/settings-layout.tsx +104 -0
- package/src/client/lib/api.ts +72 -0
- package/src/client/lib/utils.ts +6 -0
- package/src/client/main.tsx +10 -0
- package/src/client/pages/collection-detail.tsx +634 -0
- package/src/client/pages/collections.tsx +180 -0
- package/src/client/pages/dashboard.tsx +133 -0
- package/src/client/pages/device.tsx +66 -0
- package/src/client/pages/document-detail-page.tsx +139 -0
- package/src/client/pages/documents-page.tsx +103 -0
- package/src/client/pages/login.tsx +345 -0
- package/src/client/pages/settings.tsx +65 -0
- package/src/client/pages/setup.tsx +129 -0
- package/src/client/pages/signup.tsx +188 -0
- package/src/client/store/auth.ts +30 -0
- package/src/client/store/collections.ts +13 -0
- package/src/client/store/config.ts +12 -0
- package/src/client/store/fetcher.ts +30 -0
- package/src/client/store/router.ts +95 -0
- package/src/client/store/schema.ts +39 -0
- package/src/client/store/settings.ts +31 -0
- package/src/client/types.ts +34 -0
- package/src/db/dynamic.ts +70 -0
- package/src/db/index.ts +16 -0
- package/src/db/migrations/001_initial_schema.ts +57 -0
- package/src/db/migrations/002_auth_tables.ts +84 -0
- package/src/db/migrator.ts +61 -0
- package/src/db/schema.ts +142 -0
- package/src/index.ts +12 -0
- package/src/server/index.ts +66 -0
- package/src/types.ts +20 -0
- package/style.css.d.ts +8 -0
- package/tests/css.test.ts +21 -0
- package/tests/modular.test.ts +29 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import * as p from "@clack/prompts";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
import { downloadTemplate } from "giget";
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync, cpSync, mkdirSync } from "node:fs";
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
import { runMcpBridge } from "./mcp.ts";
|
|
9
|
+
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
const execAsync = promisify(exec);
|
|
13
|
+
const PROJECT_NAME_PATTERN = /^[a-z0-9-]+$/;
|
|
14
|
+
|
|
15
|
+
// Template definitions
|
|
16
|
+
const TEMPLATES = {
|
|
17
|
+
starter: {
|
|
18
|
+
name: "Starter (Hono + React)",
|
|
19
|
+
description: "Unified single-process FlareCMS starter for Cloudflare Workers",
|
|
20
|
+
dir: "templates/starter",
|
|
21
|
+
},
|
|
22
|
+
blog: {
|
|
23
|
+
name: "Blog (Coming Soon)",
|
|
24
|
+
description: "A pre-configured blog template",
|
|
25
|
+
dir: "templates/blog",
|
|
26
|
+
},
|
|
27
|
+
nextjs: {
|
|
28
|
+
name: "Next.js (App Router)",
|
|
29
|
+
description: "Modern Next.js template with FlareCMS + Hono API",
|
|
30
|
+
dir: "templates/nextjs",
|
|
31
|
+
},
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
export async function createProjectCommand() {
|
|
35
|
+
console.clear();
|
|
36
|
+
console.log(`\n ${pc.bold(pc.yellow("— F L A R E C M S —"))}\n`);
|
|
37
|
+
p.intro("Create a new FlareCMS project");
|
|
38
|
+
|
|
39
|
+
const projectName = await p.text({
|
|
40
|
+
message: "Project name?",
|
|
41
|
+
placeholder: "my-flare-site",
|
|
42
|
+
defaultValue: "my-flare-site",
|
|
43
|
+
validate: (value) => {
|
|
44
|
+
if (!value) return "Project name is required";
|
|
45
|
+
if (!PROJECT_NAME_PATTERN.test(value))
|
|
46
|
+
return "Project name can only contain lowercase letters, numbers, and hyphens";
|
|
47
|
+
return undefined;
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (p.isCancel(projectName)) {
|
|
52
|
+
p.cancel("Operation cancelled.");
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const projectDir = resolve(process.cwd(), projectName as string);
|
|
57
|
+
|
|
58
|
+
if (existsSync(projectDir)) {
|
|
59
|
+
const overwrite = await p.confirm({
|
|
60
|
+
message: `Directory ${projectName} already exists. Overwrite?`,
|
|
61
|
+
initialValue: false,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (p.isCancel(overwrite) || !overwrite) {
|
|
65
|
+
p.cancel("Operation cancelled.");
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Step 2: Template Selection
|
|
71
|
+
const templateKey = await p.select({
|
|
72
|
+
message: "Which template would you like to use?",
|
|
73
|
+
options: Object.entries(TEMPLATES).map(([key, t]) => ({
|
|
74
|
+
value: key,
|
|
75
|
+
label: t.name,
|
|
76
|
+
hint: t.description,
|
|
77
|
+
})),
|
|
78
|
+
initialValue: "starter",
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (p.isCancel(templateKey)) {
|
|
82
|
+
p.cancel("Operation cancelled.");
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const shouldInstall = await p.confirm({
|
|
87
|
+
message: "Install dependencies with bun?",
|
|
88
|
+
initialValue: true,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (p.isCancel(shouldInstall)) {
|
|
92
|
+
p.cancel("Operation cancelled.");
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const s = p.spinner();
|
|
97
|
+
s.start("Creating project...");
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
if (templateKey === 'nextjs') {
|
|
101
|
+
s.stop("Starting Next.js setup...");
|
|
102
|
+
|
|
103
|
+
// 1. Run create-next-app
|
|
104
|
+
const nextVersion = "15.1.0"; // Stable default, can be customized
|
|
105
|
+
p.log.info(`${pc.cyan("Running:")} bun create next-app@${nextVersion} ${projectName} --typescript --tailwind --eslint --app --src-dir false --import-alias "@/*" --use-bun`);
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
await execAsync(`bun create next-app@${nextVersion} ${projectName} --typescript --tailwind --eslint --app --src-dir false --import-alias "@/*" --use-bun --yes`, {
|
|
109
|
+
env: { ...process.env, NEXT_TELEMETRY_DISABLED: "1" }
|
|
110
|
+
});
|
|
111
|
+
} catch (err) {
|
|
112
|
+
// Fallback or handle error
|
|
113
|
+
p.log.warn("create-next-app failed or was already present. Continuing with injection...");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
s.start("Injecting FlareCMS configuration...");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const template = TEMPLATES[templateKey as keyof typeof TEMPLATES];
|
|
120
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
121
|
+
const cliDir = resolve(currentFilePath, "..");
|
|
122
|
+
const localTemplatesRoot = resolve(cliDir, "..", "..", "..", "..", "templates");
|
|
123
|
+
const localTemplatePath = resolve(localTemplatesRoot, template.dir.split('/').pop() || '');
|
|
124
|
+
|
|
125
|
+
if (existsSync(localTemplatePath)) {
|
|
126
|
+
if (!existsSync(projectDir)) {
|
|
127
|
+
mkdirSync(projectDir, { recursive: true });
|
|
128
|
+
}
|
|
129
|
+
cpSync(localTemplatePath, projectDir, { recursive: true });
|
|
130
|
+
} else if (templateKey !== 'nextjs') {
|
|
131
|
+
// Production flow: download from GitHub
|
|
132
|
+
const remoteSource = `github:fhorray/flarecms/${template.dir}`;
|
|
133
|
+
await downloadTemplate(remoteSource, {
|
|
134
|
+
dir: projectDir,
|
|
135
|
+
force: true,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 1. Update package.json
|
|
140
|
+
const pkgPath = resolve(projectDir, "package.json");
|
|
141
|
+
if (existsSync(pkgPath)) {
|
|
142
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
143
|
+
pkg.name = projectName;
|
|
144
|
+
pkg.version = "0.1.0";
|
|
145
|
+
// Ensure flarecms dependency is present
|
|
146
|
+
pkg.dependencies = pkg.dependencies || {};
|
|
147
|
+
pkg.dependencies["flarecms"] = "latest";
|
|
148
|
+
pkg.dependencies["hono"] = "latest";
|
|
149
|
+
|
|
150
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 2. Setup Wrangler logic
|
|
154
|
+
const wranglerPath = resolve(projectDir, "wrangler.jsonc");
|
|
155
|
+
if (existsSync(wranglerPath)) {
|
|
156
|
+
let wranglerContent = readFileSync(wranglerPath, "utf-8");
|
|
157
|
+
// Basic JSON string replacement for simplicity in CLI
|
|
158
|
+
wranglerContent = wranglerContent.replace(/"name":\s*".*"/, `"name": "${projectName}"`);
|
|
159
|
+
wranglerContent = wranglerContent.replace(/"database_name":\s*".*"/, `"database_name": "${projectName}-db"`);
|
|
160
|
+
writeFileSync(wranglerPath, wranglerContent);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
s.stop("Project created!");
|
|
164
|
+
|
|
165
|
+
if (shouldInstall) {
|
|
166
|
+
s.start(`Installing dependencies with ${pc.cyan("bun")}...`);
|
|
167
|
+
try {
|
|
168
|
+
await execAsync("bun install", { cwd: projectDir });
|
|
169
|
+
s.stop("Dependencies installed!");
|
|
170
|
+
|
|
171
|
+
s.start("Generating TypeScript bindings...");
|
|
172
|
+
try {
|
|
173
|
+
await execAsync("bun wrangler types", { cwd: projectDir });
|
|
174
|
+
s.stop("TypeScript bindings generated!");
|
|
175
|
+
} catch (err) {
|
|
176
|
+
s.stop("Failed to generate bindings (this is normal if wrangler.jsonc is incomplete)");
|
|
177
|
+
}
|
|
178
|
+
} catch (err) {
|
|
179
|
+
s.stop("Failed to install dependencies");
|
|
180
|
+
p.log.warn(`Run ${pc.cyan(`cd ${projectName} && bun install && bun cf-typegen`)} manually`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const steps = [`cd ${projectName}`, "bun run dev"];
|
|
185
|
+
p.note(steps.join("\n"), "Next steps");
|
|
186
|
+
|
|
187
|
+
p.outro(`${pc.green("Done!")} Your FlareCMS project is ready at ${pc.cyan(projectName)}`);
|
|
188
|
+
|
|
189
|
+
} catch (error) {
|
|
190
|
+
s.stop("Failed to create project");
|
|
191
|
+
p.log.error(error instanceof Error ? error.message : String(error));
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function mcpCommand(args: string[]) {
|
|
197
|
+
// Simple argument parsing for CLI
|
|
198
|
+
let url: string = (process.env.FLARE_API_URL as string) || "http://localhost:8787";
|
|
199
|
+
let token = process.env.FLARE_API_TOKEN;
|
|
200
|
+
|
|
201
|
+
for (let i = 0; i < args.length; i++) {
|
|
202
|
+
if (args[i] === "--url" && i + 1 < args.length) {
|
|
203
|
+
url = args[i + 1] as string;
|
|
204
|
+
i++;
|
|
205
|
+
} else if (args[i] === "--token" && i + 1 < args.length) {
|
|
206
|
+
token = args[i + 1];
|
|
207
|
+
i++;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!url) {
|
|
212
|
+
console.error(pc.red("Error: --url is required or must be set via FLARE_API_URL"));
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
await runMcpBridge({ url, token });
|
|
217
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { createProjectCommand, mcpCommand } from "./commands.ts";
|
|
3
|
+
|
|
4
|
+
async function main() {
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
|
|
7
|
+
// Simplistic command router
|
|
8
|
+
if (args[0] === "mcp") {
|
|
9
|
+
await mcpCommand(args.slice(1));
|
|
10
|
+
} else if (args.length === 0 || args[0] === "create") {
|
|
11
|
+
await createProjectCommand();
|
|
12
|
+
} else {
|
|
13
|
+
console.log("Unknown command. Available commands: create, mcp");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
main().catch((err) => {
|
|
19
|
+
console.error(err);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
});
|
package/src/cli/mcp.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlareCMS MCP Bridge Server (CLI Implementation)
|
|
3
|
+
* Implements MCP (Model Context Protocol) over stdio
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface McpOptions {
|
|
7
|
+
url: string;
|
|
8
|
+
token?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const TOOLS = [
|
|
12
|
+
{
|
|
13
|
+
name: "list_collections",
|
|
14
|
+
description: "Fetches all defined content schema collections available in FlareCMS.",
|
|
15
|
+
inputSchema: { type: "object", properties: {} }
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: "get_collection_schema",
|
|
19
|
+
description: "Fetches the full field structure and metadata of a specific FlareCMS collection.",
|
|
20
|
+
inputSchema: {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
collection: { type: "string", description: "The slug of the collection to inspect" }
|
|
24
|
+
},
|
|
25
|
+
required: ["collection"]
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "read_content",
|
|
30
|
+
description: "Reads the paginated content of a specific FlareCMS collection dynamically.",
|
|
31
|
+
inputSchema: {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {
|
|
34
|
+
collection: { type: "string", description: "The slug of the collection to read" },
|
|
35
|
+
limit: { type: "number", description: "Number of records to fetch (default 10)" }
|
|
36
|
+
},
|
|
37
|
+
required: ["collection"]
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "create_document",
|
|
42
|
+
description: "Creates a new document in a specified collection.",
|
|
43
|
+
inputSchema: {
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: {
|
|
46
|
+
collection: { type: "string", description: "The collection slug" },
|
|
47
|
+
data: {
|
|
48
|
+
type: "object",
|
|
49
|
+
description: "The document data (e.g. { title: 'Hello', status: 'published' })"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
required: ["collection", "data"]
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "update_document",
|
|
57
|
+
description: "Updates an existing document in a specified collection.",
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: "object",
|
|
60
|
+
properties: {
|
|
61
|
+
collection: { type: "string", description: "The collection slug" },
|
|
62
|
+
id: { type: "string", description: "The document ID to update" },
|
|
63
|
+
data: {
|
|
64
|
+
type: "object",
|
|
65
|
+
description: "The data fields to update"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
required: ["collection", "id", "data"]
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "create_collection",
|
|
73
|
+
description: "Creates a new content collection and its physical table.",
|
|
74
|
+
inputSchema: {
|
|
75
|
+
type: "object",
|
|
76
|
+
properties: {
|
|
77
|
+
slug: { type: "string", description: "Unique URL slug for the collection" },
|
|
78
|
+
label: { type: "string", description: "Display label" },
|
|
79
|
+
labelSingular: { type: "string", description: "Singular display label" },
|
|
80
|
+
description: { type: "string", description: "Optional description" },
|
|
81
|
+
isPublic: { type: "boolean", description: "Whether it is publicly readable" }
|
|
82
|
+
},
|
|
83
|
+
required: ["slug", "label"]
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "update_collection",
|
|
88
|
+
description: "Updates collection metadata.",
|
|
89
|
+
inputSchema: {
|
|
90
|
+
type: "object",
|
|
91
|
+
properties: {
|
|
92
|
+
id: { type: "string", description: "The collection ID (ULID)" },
|
|
93
|
+
data: { type: "object", description: "Metadata fields to update" }
|
|
94
|
+
},
|
|
95
|
+
required: ["id", "data"]
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "add_field",
|
|
100
|
+
description: "Adds a new field to an existing collection.",
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: "object",
|
|
103
|
+
properties: {
|
|
104
|
+
collection_id: { type: "string", description: "The collection ID (ULID)" },
|
|
105
|
+
field: {
|
|
106
|
+
type: "object",
|
|
107
|
+
properties: {
|
|
108
|
+
slug: { type: "string" },
|
|
109
|
+
label: { type: "string" },
|
|
110
|
+
type: { type: "string", enum: ["text", "number", "boolean", "date", "richtext"] },
|
|
111
|
+
required: { type: "boolean" }
|
|
112
|
+
},
|
|
113
|
+
required: ["slug", "label", "type"]
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
required: ["collection_id", "field"]
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
export async function runMcpBridge(options: McpOptions) {
|
|
122
|
+
const { url, token } = options;
|
|
123
|
+
const executeUrl = `${url.replace(/\/$/, '')}/api/mcp/execute`;
|
|
124
|
+
|
|
125
|
+
async function callFlareCMS(tool: string, args: any) {
|
|
126
|
+
if (!token) {
|
|
127
|
+
throw new Error("No API Token provided. Use --token or FLARE_API_TOKEN environment variable.");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const response = await fetch(executeUrl, {
|
|
131
|
+
method: "POST",
|
|
132
|
+
headers: {
|
|
133
|
+
"Content-Type": "application/json",
|
|
134
|
+
Authorization: `Bearer ${token}`,
|
|
135
|
+
},
|
|
136
|
+
body: JSON.stringify({ tool, arguments: args }),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
const errorText = await response.text();
|
|
141
|
+
throw new Error(`FlareCMS API Error (${response.status}): ${errorText}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return await response.json();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function handleRequest(request: any) {
|
|
148
|
+
const { method, params } = request;
|
|
149
|
+
|
|
150
|
+
switch (method) {
|
|
151
|
+
case "initialize":
|
|
152
|
+
return {
|
|
153
|
+
protocolVersion: "2024-11-05",
|
|
154
|
+
capabilities: { tools: {} },
|
|
155
|
+
serverInfo: { name: "@flarecms/cli", version: "0.1.0" }
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
case "notifications/initialized":
|
|
159
|
+
return null;
|
|
160
|
+
|
|
161
|
+
case "tools/list":
|
|
162
|
+
return { tools: TOOLS };
|
|
163
|
+
|
|
164
|
+
case "tools/call":
|
|
165
|
+
try {
|
|
166
|
+
return await callFlareCMS(params.name, params.arguments);
|
|
167
|
+
} catch (error: any) {
|
|
168
|
+
return {
|
|
169
|
+
content: [{ type: "text", text: `Error executing tool '${params.name}': ${error.message}` }],
|
|
170
|
+
isError: true
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
default:
|
|
175
|
+
return { error: { code: -32601, message: `Method not found: ${method}` } };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const decoder = new TextDecoder();
|
|
180
|
+
let buffer = "";
|
|
181
|
+
|
|
182
|
+
for await (const chunk of Bun.stdin.stream()) {
|
|
183
|
+
buffer += decoder.decode(chunk);
|
|
184
|
+
let lines = buffer.split("\n");
|
|
185
|
+
buffer = lines.pop() || "";
|
|
186
|
+
|
|
187
|
+
for (const line of lines) {
|
|
188
|
+
if (!line.trim()) continue;
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const request = JSON.parse(line);
|
|
192
|
+
const result: any = await handleRequest(request);
|
|
193
|
+
|
|
194
|
+
if (result === null) continue;
|
|
195
|
+
|
|
196
|
+
process.stdout.write(JSON.stringify({
|
|
197
|
+
jsonrpc: "2.0",
|
|
198
|
+
id: request.id,
|
|
199
|
+
result: result.error ? undefined : result,
|
|
200
|
+
error: result.error
|
|
201
|
+
}) + "\n");
|
|
202
|
+
} catch (e: any) {
|
|
203
|
+
process.stdout.write(JSON.stringify({
|
|
204
|
+
jsonrpc: "2.0",
|
|
205
|
+
error: { code: -32700, message: "Parse error" }
|
|
206
|
+
}) + "\n");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { expect, test, describe, beforeAll, afterAll } from "bun:test";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { rmSync, existsSync } from "node:fs";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
function runCLI(args: string[], cwd: string = process.cwd()) {
|
|
7
|
+
const cliPath = resolve(__dirname, "../src/index.ts");
|
|
8
|
+
return spawnSync("bun", [cliPath, ...args], { cwd, encoding: "utf-8" });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("FlareCMS CLI Scaffolder", () => {
|
|
12
|
+
const TEST_DIR = resolve(__dirname, "../../.test_output");
|
|
13
|
+
|
|
14
|
+
beforeAll(() => {
|
|
15
|
+
if (existsSync(TEST_DIR)) {
|
|
16
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
17
|
+
}
|
|
18
|
+
spawnSync("mkdir", ["-p", TEST_DIR]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterAll(() => {
|
|
22
|
+
if (existsSync(TEST_DIR)) {
|
|
23
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("CLI prints unknown command message for invalid command", () => {
|
|
28
|
+
const result = runCLI(["invalid-command"]);
|
|
29
|
+
expect(result.status).not.toBe(0); // Should exit with error code 1
|
|
30
|
+
expect(result.stdout + result.stderr).toContain("Unknown command. Available commands: create");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("CLI renders prompts successfully for create command", () => {
|
|
34
|
+
const result = runCLI(["create"]);
|
|
35
|
+
// Since we are mocking execution without a real interactive shell or pipe, it prints the prompt and waits or crashes on EOF.
|
|
36
|
+
// The test environment doesn't allow interaction so clack will just render the first prompt.
|
|
37
|
+
expect(result.stdout + result.stderr).toContain("Create a new FlareCMS project");
|
|
38
|
+
expect(result.stdout + result.stderr).toContain("Project name?");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, it, expect, mock, beforeAll, afterAll } from 'bun:test';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { createProjectCommand } from '../commands.ts';
|
|
5
|
+
|
|
6
|
+
// 1. Mock @clack/prompts
|
|
7
|
+
mock.module('@clack/prompts', () => ({
|
|
8
|
+
intro: () => { },
|
|
9
|
+
outro: () => { },
|
|
10
|
+
text: async () => 'test-project',
|
|
11
|
+
select: async () => 'starter',
|
|
12
|
+
confirm: async () => true,
|
|
13
|
+
spinner: () => ({
|
|
14
|
+
start: () => { },
|
|
15
|
+
stop: () => { }
|
|
16
|
+
}),
|
|
17
|
+
note: () => { },
|
|
18
|
+
cancel: () => { },
|
|
19
|
+
isCancel: (val: any) => val === null,
|
|
20
|
+
log: {
|
|
21
|
+
info: () => { },
|
|
22
|
+
warn: () => { },
|
|
23
|
+
error: () => { },
|
|
24
|
+
}
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
// 2. Mock giget
|
|
28
|
+
mock.module('giget', () => ({
|
|
29
|
+
downloadTemplate: async (src: string, opts: any) => {
|
|
30
|
+
// Create the destination folder and a dummy package.json to simulate download
|
|
31
|
+
if (!existsSync(opts.dir)) {
|
|
32
|
+
mkdirSync(opts.dir, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
const pkg = { name: 'template', dependencies: {} };
|
|
35
|
+
const wrangler = { name: 'template', d1_databases: [{ database_name: 'template-db' }] };
|
|
36
|
+
|
|
37
|
+
const { writeFileSync } = require('node:fs');
|
|
38
|
+
const { resolve } = require('node:path');
|
|
39
|
+
|
|
40
|
+
writeFileSync(resolve(opts.dir, 'package.json'), JSON.stringify(pkg, null, 2));
|
|
41
|
+
writeFileSync(resolve(opts.dir, 'wrangler.jsonc'), JSON.stringify(wrangler, null, 2));
|
|
42
|
+
return { dir: opts.dir };
|
|
43
|
+
}
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
// 3. Mock child_process exec
|
|
47
|
+
mock.module('node:child_process', () => ({
|
|
48
|
+
exec: (cmd: string, opts: any, cb: any) => {
|
|
49
|
+
cb(null, { stdout: '', stderr: '' });
|
|
50
|
+
}
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
describe('CLI: createProjectCommand', () => {
|
|
54
|
+
const testDir = resolve(process.cwd(), 'test-project');
|
|
55
|
+
|
|
56
|
+
afterAll(() => {
|
|
57
|
+
if (existsSync(testDir)) {
|
|
58
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should create a project and patch manifests correctly', async () => {
|
|
63
|
+
// Mock process.exit to prevent test from exiting
|
|
64
|
+
const exitSpy = mock(() => { });
|
|
65
|
+
const originalExit = process.exit;
|
|
66
|
+
// @ts-ignore
|
|
67
|
+
process.exit = exitSpy;
|
|
68
|
+
|
|
69
|
+
await createProjectCommand();
|
|
70
|
+
|
|
71
|
+
// Verify directory exists
|
|
72
|
+
expect(existsSync(testDir)).toBe(true);
|
|
73
|
+
|
|
74
|
+
// Verify package.json was patched
|
|
75
|
+
const pkg = JSON.parse(readFileSync(resolve(testDir, 'package.json'), 'utf-8'));
|
|
76
|
+
expect(pkg.name).toBe('test-project');
|
|
77
|
+
expect(pkg.dependencies.flarecms).toBe('latest');
|
|
78
|
+
|
|
79
|
+
// Verify wrangler.jsonc was patched
|
|
80
|
+
const wrangler = JSON.parse(readFileSync(resolve(testDir, 'wrangler.jsonc'), 'utf-8'));
|
|
81
|
+
expect(wrangler.name).toBe('test-project');
|
|
82
|
+
expect(wrangler.d1_databases[0].database_name).toBe('test-project-db');
|
|
83
|
+
|
|
84
|
+
// Restore process.exit
|
|
85
|
+
process.exit = originalExit;
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import App from './app';
|
|
3
|
+
import { setBase, setApiBaseUrl } from './store/config';
|
|
4
|
+
import { initRouter } from './store/router';
|
|
5
|
+
import { TooltipProvider } from './components/ui/tooltip';
|
|
6
|
+
|
|
7
|
+
export interface FlareAdminProps {
|
|
8
|
+
/**
|
|
9
|
+
* The base path where the admin UI is mounted.
|
|
10
|
+
* @default "/admin"
|
|
11
|
+
*/
|
|
12
|
+
basePath?: string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The base URL for FlareCMS API calls.
|
|
16
|
+
* @default "/api"
|
|
17
|
+
*/
|
|
18
|
+
apiBaseUrl?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* FlareAdminRouter is the main entry point for the FlareCMS Admin UI.
|
|
23
|
+
* It manages the routing and state for the entire administrative dashboard.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* <FlareAdminRouter basePath="/admin" apiBaseUrl="/api/flarecms" />
|
|
27
|
+
*/
|
|
28
|
+
export function FlareAdminRouter({
|
|
29
|
+
basePath = '/admin',
|
|
30
|
+
apiBaseUrl = '/api'
|
|
31
|
+
}: FlareAdminProps) {
|
|
32
|
+
|
|
33
|
+
// Initialize configuration stores and router
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
setBase(basePath);
|
|
36
|
+
setApiBaseUrl(apiBaseUrl);
|
|
37
|
+
initRouter(basePath);
|
|
38
|
+
}, [basePath, apiBaseUrl]);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="flare-admin">
|
|
42
|
+
<TooltipProvider>
|
|
43
|
+
<App />
|
|
44
|
+
</TooltipProvider>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|