@butterbase/cli 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 +255 -0
- package/bin/butterbase.ts +137 -0
- package/dist/bin/butterbase.d.ts +3 -0
- package/dist/bin/butterbase.d.ts.map +1 -0
- package/dist/bin/butterbase.js +112 -0
- package/dist/bin/butterbase.js.map +1 -0
- package/dist/src/commands/apps.d.ts +5 -0
- package/dist/src/commands/apps.d.ts.map +1 -0
- package/dist/src/commands/apps.js +113 -0
- package/dist/src/commands/apps.js.map +1 -0
- package/dist/src/commands/config.d.ts +5 -0
- package/dist/src/commands/config.d.ts.map +1 -0
- package/dist/src/commands/config.js +43 -0
- package/dist/src/commands/config.js.map +1 -0
- package/dist/src/commands/functions.d.ts +15 -0
- package/dist/src/commands/functions.d.ts.map +1 -0
- package/dist/src/commands/functions.js +94 -0
- package/dist/src/commands/functions.js.map +1 -0
- package/dist/src/commands/init.d.ts +2 -0
- package/dist/src/commands/init.d.ts.map +1 -0
- package/dist/src/commands/init.js +118 -0
- package/dist/src/commands/init.js.map +1 -0
- package/dist/src/commands/schema.d.ts +10 -0
- package/dist/src/commands/schema.d.ts.map +1 -0
- package/dist/src/commands/schema.js +69 -0
- package/dist/src/commands/schema.js.map +1 -0
- package/dist/src/commands/storage.d.ts +10 -0
- package/dist/src/commands/storage.d.ts.map +1 -0
- package/dist/src/commands/storage.js +105 -0
- package/dist/src/commands/storage.js.map +1 -0
- package/dist/src/lib/api-client.d.ts +44 -0
- package/dist/src/lib/api-client.d.ts.map +1 -0
- package/dist/src/lib/api-client.js +129 -0
- package/dist/src/lib/api-client.js.map +1 -0
- package/dist/src/lib/config.d.ts +47 -0
- package/dist/src/lib/config.d.ts.map +1 -0
- package/dist/src/lib/config.js +87 -0
- package/dist/src/lib/config.js.map +1 -0
- package/package.json +35 -0
- package/src/commands/apps.ts +130 -0
- package/src/commands/config.ts +50 -0
- package/src/commands/functions.ts +116 -0
- package/src/commands/init.ts +151 -0
- package/src/commands/schema.ts +78 -0
- package/src/commands/storage.ts +117 -0
- package/src/lib/api-client.ts +176 -0
- package/src/lib/config.ts +112 -0
- package/templates/react-vite/.env.example +2 -0
- package/templates/react-vite/README.md +38 -0
- package/templates/react-vite/index.html +13 -0
- package/templates/react-vite/package.json +25 -0
- package/templates/react-vite/src/App.css +33 -0
- package/templates/react-vite/src/App.tsx +77 -0
- package/templates/react-vite/src/index.css +8 -0
- package/templates/react-vite/src/lib.ts +6 -0
- package/templates/react-vite/src/main.tsx +10 -0
- package/templates/react-vite/tsconfig.json +21 -0
- package/templates/react-vite/vite.config.ts +6 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import prompts from 'prompts';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { getMergedConfig } from '../lib/config.js';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
|
|
12
|
+
interface TemplateVariables {
|
|
13
|
+
PROJECT_NAME: string;
|
|
14
|
+
APP_ID: string;
|
|
15
|
+
API_URL: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function replaceVariables(content: string, variables: TemplateVariables): Promise<string> {
|
|
19
|
+
let result = content;
|
|
20
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
21
|
+
result = result.replace(new RegExp(`{{${key}}}`, 'g'), value);
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function copyTemplate(
|
|
27
|
+
templateDir: string,
|
|
28
|
+
targetDir: string,
|
|
29
|
+
variables: TemplateVariables
|
|
30
|
+
): Promise<void> {
|
|
31
|
+
const files = await fs.readdir(templateDir, { withFileTypes: true });
|
|
32
|
+
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
const sourcePath = path.join(templateDir, file.name);
|
|
35
|
+
const targetPath = path.join(targetDir, file.name);
|
|
36
|
+
|
|
37
|
+
if (file.isDirectory()) {
|
|
38
|
+
await fs.ensureDir(targetPath);
|
|
39
|
+
await copyTemplate(sourcePath, targetPath, variables);
|
|
40
|
+
} else {
|
|
41
|
+
const content = await fs.readFile(sourcePath, 'utf-8');
|
|
42
|
+
const processedContent = await replaceVariables(content, variables);
|
|
43
|
+
await fs.writeFile(targetPath, processedContent);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function initCommand(template?: string) {
|
|
49
|
+
console.log(chalk.blue('🚀 Initialize Butterbase Project\n'));
|
|
50
|
+
|
|
51
|
+
// Get project name
|
|
52
|
+
const { projectName } = await prompts({
|
|
53
|
+
type: 'text',
|
|
54
|
+
name: 'projectName',
|
|
55
|
+
message: 'Project name:',
|
|
56
|
+
initial: 'my-butterbase-app',
|
|
57
|
+
validate: (value) => value.length > 0 || 'Project name is required',
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (!projectName) {
|
|
61
|
+
console.log(chalk.yellow('Cancelled'));
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Select template
|
|
66
|
+
if (!template) {
|
|
67
|
+
const { selectedTemplate } = await prompts({
|
|
68
|
+
type: 'select',
|
|
69
|
+
name: 'selectedTemplate',
|
|
70
|
+
message: 'Select a template:',
|
|
71
|
+
choices: [
|
|
72
|
+
{ title: 'React + Vite', value: 'react-vite' },
|
|
73
|
+
{ title: 'Next.js (coming soon)', value: 'nextjs', disabled: true },
|
|
74
|
+
{ title: 'Vue + Vite (coming soon)', value: 'vue-vite', disabled: true },
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (!selectedTemplate) {
|
|
79
|
+
console.log(chalk.yellow('Cancelled'));
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
template = selectedTemplate;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Get Butterbase configuration
|
|
87
|
+
const config = await getMergedConfig();
|
|
88
|
+
|
|
89
|
+
const { appId } = await prompts({
|
|
90
|
+
type: 'text',
|
|
91
|
+
name: 'appId',
|
|
92
|
+
message: 'Butterbase App ID (leave empty to create later):',
|
|
93
|
+
initial: config.currentApp || '',
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const apiUrl = config.endpoint || 'http://localhost:4000';
|
|
97
|
+
|
|
98
|
+
// Create project directory
|
|
99
|
+
const targetDir = path.join(process.cwd(), projectName);
|
|
100
|
+
|
|
101
|
+
if (await fs.pathExists(targetDir)) {
|
|
102
|
+
console.log(chalk.red(`✗ Directory "${projectName}" already exists`));
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const spinner = ora('Creating project...').start();
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
// Get template directory
|
|
110
|
+
const templateDir = path.join(__dirname, '../../templates', template!);
|
|
111
|
+
|
|
112
|
+
if (!await fs.pathExists(templateDir)) {
|
|
113
|
+
throw new Error(`Template "${template}" not found`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Create target directory
|
|
117
|
+
await fs.ensureDir(targetDir);
|
|
118
|
+
|
|
119
|
+
// Copy template with variable replacement
|
|
120
|
+
const variables: TemplateVariables = {
|
|
121
|
+
PROJECT_NAME: projectName,
|
|
122
|
+
APP_ID: appId || 'your-app-id',
|
|
123
|
+
API_URL: apiUrl,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
await copyTemplate(templateDir, targetDir, variables);
|
|
127
|
+
|
|
128
|
+
// Create .env from .env.example
|
|
129
|
+
const envExamplePath = path.join(targetDir, '.env.example');
|
|
130
|
+
const envPath = path.join(targetDir, '.env');
|
|
131
|
+
if (await fs.pathExists(envExamplePath)) {
|
|
132
|
+
await fs.copy(envExamplePath, envPath);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
spinner.succeed('Project created!');
|
|
136
|
+
|
|
137
|
+
console.log(chalk.green('\n✓ Project initialized successfully!\n'));
|
|
138
|
+
console.log(chalk.gray('Next steps:\n'));
|
|
139
|
+
console.log(chalk.white(` cd ${projectName}`));
|
|
140
|
+
console.log(chalk.white(` npm install`));
|
|
141
|
+
console.log(chalk.white(` npm run dev`));
|
|
142
|
+
|
|
143
|
+
if (!appId) {
|
|
144
|
+
console.log(chalk.yellow('\n⚠ Remember to update .env with your Butterbase App ID'));
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
spinner.fail('Failed to create project');
|
|
148
|
+
console.error(chalk.red((error as Error).message));
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import { getSchema, applySchema } from '../lib/api-client.js';
|
|
5
|
+
import { getCurrentAppId } from '../lib/config.js';
|
|
6
|
+
|
|
7
|
+
async function requireAppId(appId?: string): Promise<string> {
|
|
8
|
+
if (appId) return appId;
|
|
9
|
+
|
|
10
|
+
const currentAppId = await getCurrentAppId();
|
|
11
|
+
if (!currentAppId) {
|
|
12
|
+
console.log(chalk.red('✗ No app specified and no current app set'));
|
|
13
|
+
console.log(chalk.gray('Use: butterbase apps use <app-id>'));
|
|
14
|
+
console.log(chalk.gray('Or: butterbase schema get --app <app-id>'));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return currentAppId;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function schemaGetCommand(options: { app?: string; output?: string }) {
|
|
22
|
+
const appId = await requireAppId(options.app);
|
|
23
|
+
const spinner = ora('Fetching schema...').start();
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const response: any = await getSchema(appId);
|
|
27
|
+
spinner.stop();
|
|
28
|
+
|
|
29
|
+
const schemaJson = JSON.stringify(response.schema, null, 2);
|
|
30
|
+
|
|
31
|
+
if (options.output) {
|
|
32
|
+
await fs.writeFile(options.output, schemaJson);
|
|
33
|
+
console.log(chalk.green(`✓ Schema saved to ${options.output}`));
|
|
34
|
+
} else {
|
|
35
|
+
console.log(schemaJson);
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
spinner.fail('Failed to fetch schema');
|
|
39
|
+
console.error(chalk.red((error as Error).message));
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function schemaApplyCommand(file: string, options: { app?: string; dryRun?: boolean; name?: string }) {
|
|
45
|
+
const appId = await requireAppId(options.app);
|
|
46
|
+
|
|
47
|
+
if (!await fs.pathExists(file)) {
|
|
48
|
+
console.log(chalk.red(`✗ File not found: ${file}`));
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const spinner = ora('Reading schema file...').start();
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const schemaContent = await fs.readFile(file, 'utf-8');
|
|
56
|
+
const schema = JSON.parse(schemaContent);
|
|
57
|
+
|
|
58
|
+
spinner.text = options.dryRun ? 'Running dry-run...' : 'Applying schema...';
|
|
59
|
+
|
|
60
|
+
const response: any = await applySchema(appId, schema, options.dryRun, options.name);
|
|
61
|
+
|
|
62
|
+
spinner.stop();
|
|
63
|
+
|
|
64
|
+
if (options.dryRun) {
|
|
65
|
+
console.log(chalk.blue('\n📋 Dry-run results:\n'));
|
|
66
|
+
console.log(JSON.stringify(response, null, 2));
|
|
67
|
+
} else {
|
|
68
|
+
console.log(chalk.green('\n✓ Schema applied successfully!'));
|
|
69
|
+
if (response.migration_id) {
|
|
70
|
+
console.log(chalk.gray(` Migration ID: ${response.migration_id}`));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
spinner.fail(options.dryRun ? 'Dry-run failed' : 'Failed to apply schema');
|
|
75
|
+
console.error(chalk.red((error as Error).message));
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import { generateUploadUrl, listStorageObjects, deleteStorageObject } from '../lib/api-client.js';
|
|
5
|
+
import { getCurrentAppId } from '../lib/config.js';
|
|
6
|
+
|
|
7
|
+
async function requireAppId(appId?: string): Promise<string> {
|
|
8
|
+
if (appId) return appId;
|
|
9
|
+
|
|
10
|
+
const currentAppId = await getCurrentAppId();
|
|
11
|
+
if (!currentAppId) {
|
|
12
|
+
console.log(chalk.red('✗ No app specified and no current app set'));
|
|
13
|
+
console.log(chalk.gray('Use: butterbase apps use <app-id>'));
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return currentAppId;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function storageListCommand(options: { app?: string }) {
|
|
21
|
+
const appId = await requireAppId(options.app);
|
|
22
|
+
const spinner = ora('Fetching storage objects...').start();
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const response: any = await listStorageObjects(appId);
|
|
26
|
+
spinner.stop();
|
|
27
|
+
|
|
28
|
+
if (!response.objects || response.objects.length === 0) {
|
|
29
|
+
console.log(chalk.yellow('No files found'));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log(chalk.blue('\nStorage objects:\n'));
|
|
34
|
+
for (const obj of response.objects) {
|
|
35
|
+
console.log(chalk.bold(obj.filename));
|
|
36
|
+
console.log(chalk.gray(` ID: ${obj.id}`));
|
|
37
|
+
console.log(chalk.gray(` Size: ${(obj.size_bytes / 1024).toFixed(2)} KB`));
|
|
38
|
+
console.log(chalk.gray(` Type: ${obj.content_type}`));
|
|
39
|
+
console.log();
|
|
40
|
+
}
|
|
41
|
+
} catch (error) {
|
|
42
|
+
spinner.fail('Failed to fetch storage objects');
|
|
43
|
+
console.error(chalk.red((error as Error).message));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function storageUploadCommand(file: string, options: { app?: string }) {
|
|
49
|
+
const appId = await requireAppId(options.app);
|
|
50
|
+
|
|
51
|
+
if (!await fs.pathExists(file)) {
|
|
52
|
+
console.log(chalk.red(`✗ File not found: ${file}`));
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const spinner = ora('Uploading file...').start();
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const stats = await fs.stat(file);
|
|
60
|
+
const filename = file.split('/').pop()!;
|
|
61
|
+
|
|
62
|
+
// Detect content type (basic implementation)
|
|
63
|
+
let contentType = 'application/octet-stream';
|
|
64
|
+
if (filename.endsWith('.png')) contentType = 'image/png';
|
|
65
|
+
else if (filename.endsWith('.jpg') || filename.endsWith('.jpeg')) contentType = 'image/jpeg';
|
|
66
|
+
else if (filename.endsWith('.pdf')) contentType = 'application/pdf';
|
|
67
|
+
else if (filename.endsWith('.txt')) contentType = 'text/plain';
|
|
68
|
+
|
|
69
|
+
// Get presigned upload URL
|
|
70
|
+
const uploadData: any = await generateUploadUrl(appId, filename, contentType, stats.size);
|
|
71
|
+
|
|
72
|
+
// Read file and upload to S3
|
|
73
|
+
const fileBuffer = await fs.readFile(file);
|
|
74
|
+
|
|
75
|
+
const uploadResponse = await fetch(uploadData.uploadUrl, {
|
|
76
|
+
method: 'PUT',
|
|
77
|
+
headers: {
|
|
78
|
+
'Content-Type': contentType,
|
|
79
|
+
},
|
|
80
|
+
body: fileBuffer,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (!uploadResponse.ok) {
|
|
84
|
+
throw new Error('Failed to upload file to storage');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
spinner.succeed('File uploaded');
|
|
88
|
+
console.log(chalk.green('\n✓ File uploaded successfully!'));
|
|
89
|
+
console.log(chalk.gray(` Object ID: ${uploadData.objectId}`));
|
|
90
|
+
console.log(chalk.gray(` Object Key: ${uploadData.objectKey}`));
|
|
91
|
+
} catch (error) {
|
|
92
|
+
spinner.fail('Failed to upload file');
|
|
93
|
+
console.error(chalk.red((error as Error).message));
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function storageDeleteCommand(objectId: string, options: { app?: string }) {
|
|
99
|
+
const appId = await requireAppId(options.app);
|
|
100
|
+
|
|
101
|
+
if (!objectId) {
|
|
102
|
+
console.log(chalk.red('✗ Object ID is required'));
|
|
103
|
+
console.log(chalk.gray('Usage: butterbase storage delete <object-id>'));
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const spinner = ora('Deleting object...').start();
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
await deleteStorageObject(appId, objectId);
|
|
111
|
+
spinner.succeed('Object deleted');
|
|
112
|
+
} catch (error) {
|
|
113
|
+
spinner.fail('Failed to delete object');
|
|
114
|
+
console.error(chalk.red((error as Error).message));
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { getMergedConfig } from './config.js';
|
|
2
|
+
|
|
3
|
+
export interface ApiError {
|
|
4
|
+
error: string;
|
|
5
|
+
details?: unknown;
|
|
6
|
+
hint?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get authorization headers
|
|
11
|
+
*/
|
|
12
|
+
async function getHeaders(): Promise<HeadersInit> {
|
|
13
|
+
const config = await getMergedConfig();
|
|
14
|
+
const headers: HeadersInit = {
|
|
15
|
+
'Content-Type': 'application/json',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
if (config.apiKey) {
|
|
19
|
+
headers['Authorization'] = `Bearer ${config.apiKey}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return headers;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get base URL from config
|
|
27
|
+
*/
|
|
28
|
+
async function getBaseUrl(): Promise<string> {
|
|
29
|
+
const config = await getMergedConfig();
|
|
30
|
+
return config.endpoint;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Make a GET request
|
|
35
|
+
*/
|
|
36
|
+
export async function apiGet<T>(path: string): Promise<T> {
|
|
37
|
+
const baseUrl = await getBaseUrl();
|
|
38
|
+
const headers = await getHeaders();
|
|
39
|
+
|
|
40
|
+
const res = await fetch(`${baseUrl}${path}`, { headers });
|
|
41
|
+
const body: any = await res.json();
|
|
42
|
+
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
throw new Error(body.error || body.message || 'Request failed');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return body as T;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Make a POST request
|
|
52
|
+
*/
|
|
53
|
+
export async function apiPost<T>(path: string, data: unknown): Promise<T> {
|
|
54
|
+
const baseUrl = await getBaseUrl();
|
|
55
|
+
const headers = await getHeaders();
|
|
56
|
+
|
|
57
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers,
|
|
60
|
+
body: JSON.stringify(data),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const body: any = await res.json();
|
|
64
|
+
|
|
65
|
+
if (!res.ok) {
|
|
66
|
+
throw new Error(body.error || body.message || 'Request failed');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return body as T;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Make a PATCH request
|
|
74
|
+
*/
|
|
75
|
+
export async function apiPatch<T>(path: string, data: unknown): Promise<T> {
|
|
76
|
+
const baseUrl = await getBaseUrl();
|
|
77
|
+
const headers = await getHeaders();
|
|
78
|
+
|
|
79
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
80
|
+
method: 'PATCH',
|
|
81
|
+
headers,
|
|
82
|
+
body: JSON.stringify(data),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const body: any = await res.json();
|
|
86
|
+
|
|
87
|
+
if (!res.ok) {
|
|
88
|
+
throw new Error(body.error || body.message || 'Request failed');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return body as T;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Make a DELETE request
|
|
96
|
+
*/
|
|
97
|
+
export async function apiDelete<T>(path: string): Promise<T> {
|
|
98
|
+
const baseUrl = await getBaseUrl();
|
|
99
|
+
const headers = await getHeaders();
|
|
100
|
+
|
|
101
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
102
|
+
method: 'DELETE',
|
|
103
|
+
headers,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Handle 204 No Content
|
|
107
|
+
if (res.status === 204) {
|
|
108
|
+
return {} as T;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const body: any = await res.json();
|
|
112
|
+
|
|
113
|
+
if (!res.ok) {
|
|
114
|
+
throw new Error(body.error || body.message || 'Request failed');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return body as T;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// MCP tool wrappers
|
|
121
|
+
|
|
122
|
+
export async function initApp(name: string) {
|
|
123
|
+
return apiPost('/init', { name });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function listApps() {
|
|
127
|
+
return apiGet('/apps');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function deleteApp(appId: string) {
|
|
131
|
+
return apiDelete(`/apps/${appId}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function getSchema(appId: string) {
|
|
135
|
+
return apiGet(`/v1/${appId}/schema`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function applySchema(appId: string, schema: any, dryRun?: boolean, name?: string) {
|
|
139
|
+
return apiPost(`/v1/${appId}/schema/apply`, { schema, dry_run: dryRun, name });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function deployFunction(appId: string, data: {
|
|
143
|
+
name: string;
|
|
144
|
+
code: string;
|
|
145
|
+
description?: string;
|
|
146
|
+
envVars?: Record<string, string>;
|
|
147
|
+
timeoutMs?: number;
|
|
148
|
+
memoryLimitMb?: number;
|
|
149
|
+
trigger: { type: string; config?: any };
|
|
150
|
+
}) {
|
|
151
|
+
return apiPost(`/v1/${appId}/functions`, data);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function listFunctions(appId: string) {
|
|
155
|
+
return apiGet(`/v1/${appId}/functions`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function getFunctionLogs(appId: string, functionName: string, level?: string, limit?: number) {
|
|
159
|
+
const params = new URLSearchParams();
|
|
160
|
+
if (level) params.set('level', level);
|
|
161
|
+
if (limit) params.set('limit', String(limit));
|
|
162
|
+
const query = params.toString();
|
|
163
|
+
return apiGet(`/v1/${appId}/functions/${functionName}/logs${query ? `?${query}` : ''}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function generateUploadUrl(appId: string, filename: string, contentType: string, sizeBytes: number) {
|
|
167
|
+
return apiPost(`/storage/${appId}/upload`, { filename, contentType, sizeBytes });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function listStorageObjects(appId: string) {
|
|
171
|
+
return apiGet(`/storage/${appId}/objects`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function deleteStorageObject(appId: string, objectId: string) {
|
|
175
|
+
return apiDelete(`/storage/${appId}/${objectId}`);
|
|
176
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
export interface ButterbaseConfig {
|
|
6
|
+
endpoint: string;
|
|
7
|
+
apiKey?: string;
|
|
8
|
+
currentApp?: string;
|
|
9
|
+
apps?: Record<string, {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
apiUrl: string;
|
|
13
|
+
}>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const CONFIG_DIR = path.join(os.homedir(), '.butterbase');
|
|
17
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
18
|
+
const PROJECT_CONFIG_FILE = '.butterbase/config.json';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get the global config file path
|
|
22
|
+
*/
|
|
23
|
+
export function getConfigPath(): string {
|
|
24
|
+
return CONFIG_FILE;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Ensure config directory exists
|
|
29
|
+
*/
|
|
30
|
+
async function ensureConfigDir(): Promise<void> {
|
|
31
|
+
await fs.ensureDir(CONFIG_DIR);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Load global configuration
|
|
36
|
+
*/
|
|
37
|
+
export async function loadConfig(): Promise<ButterbaseConfig> {
|
|
38
|
+
await ensureConfigDir();
|
|
39
|
+
|
|
40
|
+
if (await fs.pathExists(CONFIG_FILE)) {
|
|
41
|
+
return await fs.readJson(CONFIG_FILE);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Return default config
|
|
45
|
+
return {
|
|
46
|
+
endpoint: 'http://localhost:4000',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Save global configuration
|
|
52
|
+
*/
|
|
53
|
+
export async function saveConfig(config: ButterbaseConfig): Promise<void> {
|
|
54
|
+
await ensureConfigDir();
|
|
55
|
+
await fs.writeJson(CONFIG_FILE, config, { spaces: 2 });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Update a specific config value
|
|
60
|
+
*/
|
|
61
|
+
export async function updateConfig(key: keyof ButterbaseConfig, value: any): Promise<void> {
|
|
62
|
+
const config = await loadConfig();
|
|
63
|
+
config[key] = value;
|
|
64
|
+
await saveConfig(config);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Load project-level configuration (from current directory)
|
|
69
|
+
*/
|
|
70
|
+
export async function loadProjectConfig(): Promise<Partial<ButterbaseConfig> | null> {
|
|
71
|
+
if (await fs.pathExists(PROJECT_CONFIG_FILE)) {
|
|
72
|
+
return await fs.readJson(PROJECT_CONFIG_FILE);
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Save project-level configuration
|
|
79
|
+
*/
|
|
80
|
+
export async function saveProjectConfig(config: Partial<ButterbaseConfig>): Promise<void> {
|
|
81
|
+
await fs.ensureDir(path.dirname(PROJECT_CONFIG_FILE));
|
|
82
|
+
await fs.writeJson(PROJECT_CONFIG_FILE, config, { spaces: 2 });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get merged configuration (project overrides global)
|
|
87
|
+
*/
|
|
88
|
+
export async function getMergedConfig(): Promise<ButterbaseConfig> {
|
|
89
|
+
const globalConfig = await loadConfig();
|
|
90
|
+
const projectConfig = await loadProjectConfig();
|
|
91
|
+
|
|
92
|
+
if (projectConfig) {
|
|
93
|
+
return { ...globalConfig, ...projectConfig };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return globalConfig;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get the current app ID from config
|
|
101
|
+
*/
|
|
102
|
+
export async function getCurrentAppId(): Promise<string | undefined> {
|
|
103
|
+
const config = await getMergedConfig();
|
|
104
|
+
return config.currentApp;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Set the current app ID
|
|
109
|
+
*/
|
|
110
|
+
export async function setCurrentAppId(appId: string): Promise<void> {
|
|
111
|
+
await updateConfig('currentApp', appId);
|
|
112
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
A Butterbase-powered React application.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
1. Install dependencies:
|
|
8
|
+
```bash
|
|
9
|
+
npm install
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
2. Copy `.env.example` to `.env` and configure:
|
|
13
|
+
```bash
|
|
14
|
+
cp .env.example .env
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
3. Start development server:
|
|
18
|
+
```bash
|
|
19
|
+
npm run dev
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Butterbase Configuration
|
|
23
|
+
|
|
24
|
+
This project is connected to Butterbase app: `{{APP_ID}}`
|
|
25
|
+
|
|
26
|
+
- API URL: `{{API_URL}}` (all endpoints including auth)
|
|
27
|
+
|
|
28
|
+
## Available Scripts
|
|
29
|
+
|
|
30
|
+
- `npm run dev` - Start development server
|
|
31
|
+
- `npm run build` - Build for production
|
|
32
|
+
- `npm run preview` - Preview production build
|
|
33
|
+
|
|
34
|
+
## Learn More
|
|
35
|
+
|
|
36
|
+
- [Butterbase Documentation](https://docs.butterbase.com)
|
|
37
|
+
- [React Documentation](https://react.dev)
|
|
38
|
+
- [Vite Documentation](https://vitejs.dev)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>{{PROJECT_NAME}}</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|