create-brika 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 ADDED
@@ -0,0 +1,81 @@
1
+ # create-brika
2
+
3
+ Scaffold a new BRIKA plugin with a single command.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ bun create brika my-plugin
9
+ ```
10
+
11
+ This launches an interactive wizard that:
12
+
13
+ 1. Asks for plugin details (name, description, category, author)
14
+ 2. Fetches the latest SDK version from npm
15
+ 3. Creates the complete plugin structure
16
+ 4. Installs dependencies
17
+ 5. Initializes a git repository
18
+
19
+ ## Options
20
+
21
+ ```bash
22
+ # Interactive mode (prompts for all options)
23
+ bun create brika
24
+
25
+ # With plugin name
26
+ bun create brika my-plugin
27
+
28
+ # Skip git initialization
29
+ bun create brika my-plugin --no-git
30
+
31
+ # Skip dependency installation
32
+ bun create brika my-plugin --no-install
33
+
34
+ # Show help
35
+ bun create brika --help
36
+ ```
37
+
38
+ ## Generated Structure
39
+
40
+ ```
41
+ my-plugin/
42
+ ├── package.json # Plugin manifest with blocks
43
+ ├── tsconfig.json # TypeScript configuration
44
+ ├── README.md # Documentation
45
+ ├── .gitignore
46
+ ├── src/
47
+ │ └── index.ts # Block definitions
48
+ └── locales/
49
+ └── en/
50
+ └── plugin.json # i18n translations
51
+ ```
52
+
53
+ ## Categories
54
+
55
+ When prompted for category, choose based on your plugin's purpose:
56
+
57
+ | Category | Description | Examples |
58
+ |----------|-------------|----------|
59
+ | `trigger` | Starts workflows | Timers, sensors, webhooks |
60
+ | `action` | Performs operations | Send notification, control device |
61
+ | `transform` | Processes data | Map, filter, format |
62
+ | `flow` | Controls execution | Condition, delay, split |
63
+
64
+ ## After Creating
65
+
66
+ ```bash
67
+ cd my-plugin
68
+ bun link # Link for local development
69
+ bun run tsc # Type check
70
+ ```
71
+
72
+ Add to your `brika.yml`:
73
+
74
+ ```yaml
75
+ plugins:
76
+ - path: ./my-plugin
77
+ ```
78
+
79
+ ## License
80
+
81
+ MIT
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "create-brika",
3
+ "version": "0.1.0",
4
+ "description": "Create a new BRIKA plugin with a single command",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "BRIKA Team",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/maxscharwath/brika.git",
11
+ "directory": "packages/create-brika"
12
+ },
13
+ "keywords": ["brika", "plugin", "scaffold", "cli", "create"],
14
+ "bin": {
15
+ "create-brika": "./src/index.ts"
16
+ },
17
+ "files": ["src", "template"],
18
+ "scripts": {
19
+ "dev": "bun run src/index.ts",
20
+ "tsc": "bunx --bun tsc --noEmit"
21
+ },
22
+ "dependencies": {
23
+ "@clack/prompts": "^0.11.0",
24
+ "picocolors": "^1.1.1"
25
+ },
26
+ "devDependencies": {
27
+ "@types/bun": "^1.3.5"
28
+ }
29
+ }
package/src/index.ts ADDED
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * create-brika CLI
5
+ *
6
+ * Scaffold a new BRIKA plugin with a single command.
7
+ *
8
+ * Usage:
9
+ * bun create brika my-plugin
10
+ * bunx create-brika my-plugin
11
+ */
12
+
13
+ import * as p from '@clack/prompts';
14
+ import pc from 'picocolors';
15
+ import { parseArgs } from 'util';
16
+ import { promptForConfig } from './prompts';
17
+ import { scaffold } from './scaffold';
18
+
19
+ const HELP = `
20
+ ${pc.bold('create-brika')} - Create a new BRIKA plugin
21
+
22
+ ${pc.bold('Usage:')}
23
+ ${pc.cyan('bun create brika')} ${pc.dim('[plugin-name]')} ${pc.dim('[options]')}
24
+
25
+ ${pc.bold('Options:')}
26
+ ${pc.cyan('-h, --help')} Show this help message
27
+ ${pc.cyan('--no-git')} Skip git initialization
28
+ ${pc.cyan('--no-install')} Skip dependency installation
29
+
30
+ ${pc.bold('Examples:')}
31
+ ${pc.dim('# Interactive mode')}
32
+ bun create brika
33
+
34
+ ${pc.dim('# With plugin name')}
35
+ bun create brika my-plugin
36
+
37
+ ${pc.dim('# Skip git and install')}
38
+ bun create brika my-plugin --no-git --no-install
39
+ `;
40
+
41
+ async function main(): Promise<void> {
42
+ const { positionals, values } = parseArgs({
43
+ args: Bun.argv.slice(2),
44
+ allowPositionals: true,
45
+ strict: false,
46
+ options: {
47
+ help: { type: 'boolean', short: 'h', default: false },
48
+ git: { type: 'boolean', default: true },
49
+ install: { type: 'boolean', default: true },
50
+ },
51
+ });
52
+
53
+ if (values.help) {
54
+ console.log(HELP);
55
+ return;
56
+ }
57
+
58
+ try {
59
+ const config = await promptForConfig(positionals[0]);
60
+
61
+ await scaffold({
62
+ ...config,
63
+ git: values.git !== false,
64
+ install: values.install !== false,
65
+ });
66
+
67
+ p.outro(`${pc.green('Success!')} Your plugin is ready at ${pc.cyan(`./${config.name}`)}`);
68
+
69
+ console.log();
70
+ console.log(pc.bold('Next steps:'));
71
+ console.log(` ${pc.cyan('cd')} ${config.name}`);
72
+ console.log(` ${pc.cyan('bun')} link`);
73
+ console.log();
74
+ } catch (error) {
75
+ if (error instanceof Error && error.message === 'cancelled') {
76
+ return;
77
+ }
78
+ p.cancel('An error occurred');
79
+ console.error(error);
80
+ process.exit(1);
81
+ }
82
+ }
83
+
84
+ main();
package/src/prompts.ts ADDED
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Interactive prompts for plugin configuration
3
+ */
4
+
5
+ import * as p from '@clack/prompts';
6
+ import pc from 'picocolors';
7
+ import { getGitUser } from './utils';
8
+
9
+ export interface PluginConfig {
10
+ name: string;
11
+ description: string;
12
+ category: 'trigger' | 'action' | 'transform' | 'flow';
13
+ author: string;
14
+ }
15
+
16
+ const CATEGORIES = [
17
+ {
18
+ value: 'trigger',
19
+ label: 'Trigger',
20
+ hint: 'Starts workflows (sensors, timers, webhooks)',
21
+ },
22
+ {
23
+ value: 'action',
24
+ label: 'Action',
25
+ hint: 'Performs operations (send, control, notify)',
26
+ },
27
+ {
28
+ value: 'transform',
29
+ label: 'Transform',
30
+ hint: 'Processes data (map, filter, format)',
31
+ },
32
+ {
33
+ value: 'flow',
34
+ label: 'Flow',
35
+ hint: 'Controls flow (condition, delay, split)',
36
+ },
37
+ ] as const;
38
+
39
+ /**
40
+ * Validate plugin name (kebab-case)
41
+ */
42
+ function validatePluginName(name: string): string | undefined {
43
+ if (!name) return 'Plugin name is required';
44
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) {
45
+ return 'Plugin name must be kebab-case (e.g., my-plugin)';
46
+ }
47
+ if (name.startsWith('-') || name.endsWith('-')) {
48
+ return 'Plugin name cannot start or end with a hyphen';
49
+ }
50
+ return undefined;
51
+ }
52
+
53
+ /**
54
+ * Prompt for plugin configuration
55
+ */
56
+ export async function promptForConfig(pluginName?: string): Promise<PluginConfig> {
57
+ p.intro(pc.bgCyan(pc.black(' create-brika ')));
58
+
59
+ // Validate provided name
60
+ if (pluginName) {
61
+ const error = validatePluginName(pluginName);
62
+ if (error) {
63
+ p.cancel(error);
64
+ throw new Error('cancelled');
65
+ }
66
+ }
67
+
68
+ const gitUser = await getGitUser();
69
+
70
+ const answers = await p.group(
71
+ {
72
+ name: () => {
73
+ if (pluginName) {
74
+ p.log.info(`Plugin name: ${pc.cyan(pluginName)}`);
75
+ return Promise.resolve(pluginName);
76
+ }
77
+ return p.text({
78
+ message: 'What is your plugin name?',
79
+ placeholder: 'my-plugin',
80
+ validate: validatePluginName,
81
+ });
82
+ },
83
+ description: ({ results }) =>
84
+ p.text({
85
+ message: 'Description',
86
+ placeholder: `A BRIKA plugin for ${results.name}`,
87
+ defaultValue: `A BRIKA plugin for ${results.name}`,
88
+ }),
89
+ category: () =>
90
+ p.select({
91
+ message: 'What type of plugin is this?',
92
+ options: CATEGORIES.map((c) => ({
93
+ value: c.value,
94
+ label: c.label,
95
+ hint: c.hint,
96
+ })),
97
+ initialValue: 'action',
98
+ }),
99
+ author: () =>
100
+ p.text({
101
+ message: 'Author',
102
+ placeholder: gitUser || 'Your Name',
103
+ defaultValue: gitUser,
104
+ }),
105
+ },
106
+ {
107
+ onCancel: () => {
108
+ p.cancel('Operation cancelled.');
109
+ throw new Error('cancelled');
110
+ },
111
+ }
112
+ );
113
+
114
+ return {
115
+ name: answers.name as string,
116
+ description: answers.description as string,
117
+ category: answers.category as PluginConfig['category'],
118
+ author: answers.author as string,
119
+ };
120
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Scaffold a new BRIKA plugin from file-based templates
3
+ */
4
+
5
+ import * as p from '@clack/prompts';
6
+ import * as fs from 'fs/promises';
7
+ import * as path from 'path';
8
+ import pc from 'picocolors';
9
+ import type { PluginConfig } from './prompts';
10
+ import { renderTemplate, runCommand, toCamelCase, toPascalCase } from './utils';
11
+
12
+ export interface ScaffoldOptions extends PluginConfig {
13
+ git: boolean;
14
+ install: boolean;
15
+ }
16
+
17
+ /**
18
+ * Template variables available in all template files
19
+ */
20
+ interface TemplateVars {
21
+ name: string;
22
+ packageName: string;
23
+ description: string;
24
+ category: string;
25
+ author: string;
26
+ blockId: string;
27
+ blockNamePascal: string;
28
+ blockNameCamel: string;
29
+ sdkVersion: string;
30
+ }
31
+
32
+ /**
33
+ * Fetch the latest version of a package from npm
34
+ */
35
+ async function fetchLatestVersion(packageName: string): Promise<string> {
36
+ const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
37
+ if (!response.ok) {
38
+ throw new Error(`Failed to fetch ${packageName} version: ${response.status}`);
39
+ }
40
+ const data = (await response.json()) as { version: string };
41
+ return data.version;
42
+ }
43
+
44
+ /**
45
+ * File rename mappings (template name -> output name)
46
+ */
47
+ const FILE_RENAMES: Record<string, string> = {
48
+ _gitignore: '.gitignore',
49
+ '_package.json': 'package.json',
50
+ };
51
+
52
+ /**
53
+ * Get output filename, handling .template extension
54
+ */
55
+ function getOutputName(filename: string): string {
56
+ // Remove .template extension if present
57
+ if (filename.endsWith('.template')) {
58
+ filename = filename.slice(0, -'.template'.length);
59
+ }
60
+ // Apply rename mappings
61
+ return FILE_RENAMES[filename] ?? filename;
62
+ }
63
+
64
+ /**
65
+ * Create template variables from config
66
+ */
67
+ function createTemplateVars(config: PluginConfig, sdkVersion: string): TemplateVars {
68
+ return {
69
+ name: config.name,
70
+ packageName: `@brika/plugin-${config.name}`,
71
+ description: config.description,
72
+ category: config.category,
73
+ author: config.author,
74
+ blockId: config.name,
75
+ blockNamePascal: toPascalCase(config.name),
76
+ blockNameCamel: toCamelCase(config.name),
77
+ sdkVersion,
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Get the template directory path
83
+ */
84
+ function getTemplateDir(): string {
85
+ return path.resolve(import.meta.dir, '..', 'template');
86
+ }
87
+
88
+ /**
89
+ * Process a single file: read, render, and write
90
+ */
91
+ async function processFile(
92
+ srcPath: string,
93
+ destPath: string,
94
+ vars: Record<string, string>
95
+ ): Promise<void> {
96
+ const content = await fs.readFile(srcPath, 'utf-8');
97
+ const rendered = renderTemplate(content, vars);
98
+ await fs.writeFile(destPath, rendered, 'utf-8');
99
+ }
100
+
101
+ /**
102
+ * Recursively copy and render template files
103
+ */
104
+ async function copyTemplateDir(
105
+ srcDir: string,
106
+ destDir: string,
107
+ vars: Record<string, string>
108
+ ): Promise<void> {
109
+ await fs.mkdir(destDir, { recursive: true });
110
+ const entries = await fs.readdir(srcDir, { withFileTypes: true });
111
+
112
+ for (const entry of entries) {
113
+ const srcPath = path.join(srcDir, entry.name);
114
+ const destName = getOutputName(entry.name);
115
+ const destPath = path.join(destDir, destName);
116
+
117
+ if (entry.isDirectory()) {
118
+ await copyTemplateDir(srcPath, destPath, vars);
119
+ } else {
120
+ await processFile(srcPath, destPath, vars);
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Scaffold a new plugin
127
+ */
128
+ export async function scaffold(options: ScaffoldOptions): Promise<void> {
129
+ const { git, install, ...config } = options;
130
+ const targetDir = path.resolve(process.cwd(), config.name);
131
+
132
+ // Check if directory exists
133
+ try {
134
+ await fs.access(targetDir);
135
+ p.cancel(`Directory ${pc.cyan(config.name)} already exists`);
136
+ throw new Error('cancelled');
137
+ } catch (error) {
138
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
139
+ throw error;
140
+ }
141
+ }
142
+
143
+ const templateDir = getTemplateDir();
144
+ const spinner = p.spinner();
145
+
146
+ // Fetch latest SDK version
147
+ spinner.start('Fetching latest SDK version');
148
+ const sdkVersion = await fetchLatestVersion('@brika/sdk');
149
+ spinner.stop(`Using SDK version ${pc.cyan(sdkVersion)}`);
150
+
151
+ const vars = createTemplateVars(config, sdkVersion) as unknown as Record<string, string>;
152
+
153
+ // Create files from template
154
+ spinner.start('Creating plugin files');
155
+ await copyTemplateDir(templateDir, targetDir, vars);
156
+ spinner.stop('Created plugin files');
157
+
158
+ // Initialize git repository
159
+ if (git) {
160
+ spinner.start('Initializing git repository');
161
+ const success = await runCommand(['git', 'init'], targetDir);
162
+ spinner.stop(success ? 'Initialized git repository' : 'Skipped git init');
163
+ }
164
+
165
+ // Install dependencies
166
+ if (install) {
167
+ spinner.start('Installing dependencies');
168
+ const success = await runCommand(['bun', 'install'], targetDir);
169
+ if (success) {
170
+ spinner.stop('Installed dependencies');
171
+ } else {
172
+ spinner.stop('Failed to install dependencies');
173
+ p.log.warn('Run `bun install` manually to install dependencies');
174
+ }
175
+ }
176
+
177
+ // Show summary
178
+ p.note(
179
+ [
180
+ `${pc.cyan('package.json')} Plugin manifest with blocks`,
181
+ `${pc.cyan('src/index.ts')} Block definitions`,
182
+ `${pc.cyan('tsconfig.json')} TypeScript config`,
183
+ `${pc.cyan('README.md')} Documentation`,
184
+ `${pc.cyan('locales/')} i18n translations`,
185
+ ].join('\n'),
186
+ 'Created files'
187
+ );
188
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Utility functions for create-brika CLI
3
+ */
4
+
5
+ /**
6
+ * Convert kebab-case to PascalCase
7
+ */
8
+ export function toPascalCase(str: string): string {
9
+ return str
10
+ .split('-')
11
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
12
+ .join('');
13
+ }
14
+
15
+ /**
16
+ * Convert kebab-case to camelCase
17
+ */
18
+ export function toCamelCase(str: string): string {
19
+ const pascal = toPascalCase(str);
20
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
21
+ }
22
+
23
+ /**
24
+ * Render template string with variables using {{variable}} syntax
25
+ */
26
+ export function renderTemplate(content: string, vars: Record<string, string>): string {
27
+ return content.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? '');
28
+ }
29
+
30
+ /**
31
+ * Get git user name
32
+ */
33
+ export async function getGitUser(): Promise<string> {
34
+ try {
35
+ const proc = Bun.spawn(['git', 'config', 'user.name'], {
36
+ stdout: 'pipe',
37
+ });
38
+ const text = await new Response(proc.stdout).text();
39
+ return text.trim();
40
+ } catch {
41
+ return '';
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Run a command in a directory
47
+ */
48
+ export async function runCommand(cmd: string[], cwd: string): Promise<boolean> {
49
+ try {
50
+ const proc = Bun.spawn(cmd, {
51
+ cwd,
52
+ stdout: 'ignore',
53
+ stderr: 'ignore',
54
+ });
55
+ await proc.exited;
56
+ return proc.exitCode === 0;
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
@@ -0,0 +1,48 @@
1
+ # {{packageName}}
2
+
3
+ {{description}}
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add {{packageName}}
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ Add the plugin to your `brika.yml`:
14
+
15
+ ```yaml
16
+ plugins:
17
+ "{{packageName}}":
18
+ enabled: true
19
+ ```
20
+
21
+ ## Blocks
22
+
23
+ ### {{blockNamePascal}}
24
+
25
+ {{description}}
26
+
27
+ **Inputs:**
28
+ - `in` - Input data to process
29
+
30
+ **Outputs:**
31
+ - `out` - Processed output
32
+
33
+ **Configuration:**
34
+ - `enabled` (boolean) - Enable processing (default: true)
35
+
36
+ ## Development
37
+
38
+ ```bash
39
+ # Link for local development
40
+ bun link
41
+
42
+ # Type check
43
+ bun run tsc
44
+ ```
45
+
46
+ ## License
47
+
48
+ MIT
@@ -0,0 +1,4 @@
1
+ node_modules/
2
+ dist/
3
+ *.log
4
+ .DS_Store
@@ -0,0 +1,40 @@
1
+ {
2
+ "$schema": "https://schema.brika.dev/plugin.schema.json",
3
+ "name": "{{packageName}}",
4
+ "version": "0.1.0",
5
+ "description": "{{description}}",
6
+ "author": "{{author}}",
7
+ "keywords": [
8
+ "brika",
9
+ "plugin",
10
+ "{{name}}"
11
+ ],
12
+ "engines": {
13
+ "brika": "^{{sdkVersion}}"
14
+ },
15
+ "type": "module",
16
+ "main": "./src/index.ts",
17
+ "exports": {
18
+ ".": "./src/index.ts"
19
+ },
20
+ "scripts": {
21
+ "link": "bun link",
22
+ "tsc": "bunx --bun tsc --noEmit"
23
+ },
24
+ "blocks": [
25
+ {
26
+ "id": "{{blockId}}",
27
+ "name": "{{blockNamePascal}}",
28
+ "description": "{{description}}",
29
+ "category": "{{category}}",
30
+ "icon": "box",
31
+ "color": "#3b82f6"
32
+ }
33
+ ],
34
+ "dependencies": {
35
+ "@brika/sdk": "^{{sdkVersion}}"
36
+ },
37
+ "devDependencies": {
38
+ "bun-types": "^1.3.5"
39
+ }
40
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "{{blockNamePascal}}",
3
+ "description": "{{description}}",
4
+ "blocks": {
5
+ "{{blockId}}": {
6
+ "name": "{{blockNamePascal}}",
7
+ "description": "{{description}}"
8
+ }
9
+ },
10
+ "fields": {
11
+ "enabled": {
12
+ "label": "Enabled",
13
+ "description": "Enable processing"
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * {{blockNamePascal}} Plugin for BRIKA
3
+ *
4
+ * {{description}}
5
+ */
6
+
7
+ import {
8
+ defineReactiveBlock,
9
+ input,
10
+ log,
11
+ onStop,
12
+ output,
13
+ z,
14
+ } from "@brika/sdk";
15
+
16
+ // ─────────────────────────────────────────────────────────────────────────────
17
+ // {{blockNamePascal}} Block
18
+ // ─────────────────────────────────────────────────────────────────────────────
19
+
20
+ export const {{blockNameCamel}} = defineReactiveBlock(
21
+ {
22
+ id: "{{blockId}}",
23
+ inputs: {
24
+ in: input(z.generic(), { name: "Input" }),
25
+ },
26
+ outputs: {
27
+ out: output(z.passthrough("in"), { name: "Output" }),
28
+ },
29
+ config: z.object({
30
+ // Add your configuration options here
31
+ enabled: z.boolean().default(true).describe("Enable processing"),
32
+ }),
33
+ },
34
+ ({ inputs, outputs, config, log }) => {
35
+ inputs.in.on((data) => {
36
+ if (!config.enabled) {
37
+ log("debug", "Processing disabled, skipping");
38
+ return;
39
+ }
40
+
41
+ log("info", "Processing data", { data });
42
+
43
+ // TODO: Add your block logic here
44
+ outputs.out.emit(data);
45
+ });
46
+ }
47
+ );
48
+
49
+ // ─────────────────────────────────────────────────────────────────────────────
50
+ // Lifecycle
51
+ // ─────────────────────────────────────────────────────────────────────────────
52
+
53
+ onStop(() => {
54
+ log.info("{{blockNamePascal}} plugin stopping");
55
+ });
56
+
57
+ log.info("{{blockNamePascal}} plugin loaded");
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "noEmit": true,
10
+ "composite": true,
11
+ "types": ["bun-types"]
12
+ },
13
+ "include": ["src"]
14
+ }