@briine/create-bot 0.0.1
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 +31 -0
- package/bin/create-bot.js +196 -0
- package/bin/create-bot.test.js +46 -0
- package/package.json +29 -0
- package/template/.env.example +5 -0
- package/template/README.md +32 -0
- package/template/package.json +19 -0
- package/template/src/index.ts +43 -0
- package/template/tsconfig.json +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @briine/create-bot
|
|
2
|
+
|
|
3
|
+
Scaffold a new Briine bot project.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm create @briine/bot@latest my-bot
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
If you want to skip dependency installation during creation:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm create @briine/bot@latest my-bot -- --no-install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## What it generates
|
|
18
|
+
|
|
19
|
+
- A minimal TypeScript Briine bot powered by `@briine/sdk`
|
|
20
|
+
- A starter `.env.example` with the values you will copy from Briine
|
|
21
|
+
- A `README.md` with the two next steps: configure credentials and run the bot
|
|
22
|
+
|
|
23
|
+
## After scaffolding
|
|
24
|
+
|
|
25
|
+
1. Log in to Briine and create your first agent.
|
|
26
|
+
2. Copy `.env.example` to `.env` and paste in your Briine values.
|
|
27
|
+
3. Run `npm run dev`.
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
- Node.js 20 or newer
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { copyFile, cp, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import process from 'node:process';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import readline from 'node:readline/promises';
|
|
9
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
const templateDir = path.resolve(__dirname, '../template');
|
|
14
|
+
|
|
15
|
+
function printUsage() {
|
|
16
|
+
console.log('Usage: npm create @briine/bot@latest [project-name]');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseArgs(argv) {
|
|
20
|
+
const flags = new Set(argv.filter(arg => arg.startsWith('-')));
|
|
21
|
+
const positionals = argv.filter(arg => !arg.startsWith('-'));
|
|
22
|
+
return {
|
|
23
|
+
noInstall: flags.has('--no-install'),
|
|
24
|
+
help: flags.has('--help') || flags.has('-h'),
|
|
25
|
+
projectName: positionals[0],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function toClassName(value) {
|
|
30
|
+
return value
|
|
31
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
|
34
|
+
.join('') || 'MyBot';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function toAgentEnvPrefix(value) {
|
|
38
|
+
return value
|
|
39
|
+
.replace(/[^a-zA-Z0-9]+/g, '_')
|
|
40
|
+
.replace(/^_+|_+$/g, '')
|
|
41
|
+
.toUpperCase() || 'MY_BOT';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function validateProjectName(value) {
|
|
45
|
+
return /^[a-z0-9][a-z0-9-_]*$/i.test(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function promptForProjectName() {
|
|
49
|
+
const rl = readline.createInterface({ input, output });
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
while (true) {
|
|
53
|
+
const answer = (await rl.question('Project name: ')).trim();
|
|
54
|
+
|
|
55
|
+
if (!answer) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!validateProjectName(answer)) {
|
|
60
|
+
console.error('Project name must start with a letter or number and may only include letters, numbers, dashes, or underscores.');
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return answer;
|
|
65
|
+
}
|
|
66
|
+
} finally {
|
|
67
|
+
rl.close();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function targetExists(targetDir) {
|
|
72
|
+
try {
|
|
73
|
+
await readdir(targetDir);
|
|
74
|
+
return true;
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function writePackageJson(targetDir, projectName) {
|
|
81
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
82
|
+
const raw = await readFile(packageJsonPath, 'utf8');
|
|
83
|
+
const packageJson = JSON.parse(raw);
|
|
84
|
+
packageJson.name = projectName;
|
|
85
|
+
await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function writeSourceTemplate(targetDir, className, envPrefix) {
|
|
89
|
+
const sourcePath = path.join(targetDir, 'src/index.ts');
|
|
90
|
+
const raw = await readFile(sourcePath, 'utf8');
|
|
91
|
+
const updated = raw
|
|
92
|
+
.replaceAll('__BOT_CLASS_NAME__', className)
|
|
93
|
+
.replaceAll('__BOT_ENV_PREFIX__', envPrefix);
|
|
94
|
+
await writeFile(sourcePath, updated);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function writeReadme(targetDir, projectName, envPrefix) {
|
|
98
|
+
const readmePath = path.join(targetDir, 'README.md');
|
|
99
|
+
const raw = await readFile(readmePath, 'utf8');
|
|
100
|
+
const updated = raw
|
|
101
|
+
.replaceAll('__PROJECT_NAME__', projectName)
|
|
102
|
+
.replaceAll('__BOT_ENV_PREFIX__', envPrefix);
|
|
103
|
+
await writeFile(readmePath, updated);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function writeEnvExample(targetDir, envPrefix) {
|
|
107
|
+
const envExamplePath = path.join(targetDir, '.env.example');
|
|
108
|
+
const raw = await readFile(envExamplePath, 'utf8');
|
|
109
|
+
const updated = raw.replaceAll('__BOT_ENV_PREFIX__', envPrefix);
|
|
110
|
+
await writeFile(envExamplePath, updated);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function installDependencies(targetDir) {
|
|
114
|
+
await new Promise((resolve, reject) => {
|
|
115
|
+
const child = spawn('npm', ['install'], {
|
|
116
|
+
cwd: targetDir,
|
|
117
|
+
stdio: 'inherit',
|
|
118
|
+
shell: process.platform === 'win32',
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
child.on('exit', code => {
|
|
122
|
+
if (code === 0) {
|
|
123
|
+
resolve();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
reject(new Error(`npm install exited with code ${code ?? 'unknown'}`));
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
child.on('error', reject);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function main() {
|
|
135
|
+
const args = parseArgs(process.argv.slice(2));
|
|
136
|
+
|
|
137
|
+
if (args.help) {
|
|
138
|
+
printUsage();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const projectName = args.projectName?.trim() ? args.projectName.trim() : await promptForProjectName();
|
|
143
|
+
|
|
144
|
+
if (!validateProjectName(projectName)) {
|
|
145
|
+
console.error('Invalid project name. Use letters, numbers, dashes, or underscores.');
|
|
146
|
+
process.exitCode = 1;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
|
151
|
+
const className = toClassName(projectName);
|
|
152
|
+
const envPrefix = toAgentEnvPrefix(projectName);
|
|
153
|
+
|
|
154
|
+
if (await targetExists(targetDir)) {
|
|
155
|
+
console.error(`Target directory already exists: ${targetDir}`);
|
|
156
|
+
process.exitCode = 1;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
await mkdir(targetDir, { recursive: true });
|
|
161
|
+
await cp(templateDir, targetDir, { recursive: true });
|
|
162
|
+
await copyFile(path.join(templateDir, '.gitignore'), path.join(targetDir, '.gitignore'));
|
|
163
|
+
await copyFile(path.join(templateDir, '.env.example'), path.join(targetDir, '.env.example'));
|
|
164
|
+
|
|
165
|
+
await writePackageJson(targetDir, projectName);
|
|
166
|
+
await writeSourceTemplate(targetDir, className, envPrefix);
|
|
167
|
+
await writeReadme(targetDir, projectName, envPrefix);
|
|
168
|
+
await writeEnvExample(targetDir, envPrefix);
|
|
169
|
+
|
|
170
|
+
if (!args.noInstall) {
|
|
171
|
+
try {
|
|
172
|
+
await installDependencies(targetDir);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.warn('Project created, but dependency installation failed.');
|
|
175
|
+
console.warn(error instanceof Error ? error.message : String(error));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log('');
|
|
180
|
+
console.log(`Created ${projectName} at ${targetDir}`);
|
|
181
|
+
console.log('');
|
|
182
|
+
console.log('Next steps:');
|
|
183
|
+
console.log(` cd ${projectName}`);
|
|
184
|
+
console.log(' cp .env.example .env');
|
|
185
|
+
if (args.noInstall) {
|
|
186
|
+
console.log(' npm install');
|
|
187
|
+
}
|
|
188
|
+
console.log(' npm run dev');
|
|
189
|
+
console.log('');
|
|
190
|
+
console.log('Create your first Briine agent on briine.com and paste those values into .env before connecting.');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
main().catch(error => {
|
|
194
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
195
|
+
process.exitCode = 1;
|
|
196
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
|
|
8
|
+
const cliPath = path.resolve(process.cwd(), 'bin/create-bot.js');
|
|
9
|
+
|
|
10
|
+
test('scaffolds a bot project', async () => {
|
|
11
|
+
const parentDir = await mkdtemp(path.join(os.tmpdir(), 'briine-create-bot-'));
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
await new Promise((resolve, reject) => {
|
|
15
|
+
const child = spawn(process.execPath, [cliPath, 'my-first-bot', '--no-install'], {
|
|
16
|
+
cwd: parentDir,
|
|
17
|
+
env: { ...process.env, npm_config_user_agent: 'test', PATH: process.env.PATH },
|
|
18
|
+
stdio: 'ignore',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
child.on('exit', code => {
|
|
22
|
+
if (code === 0) {
|
|
23
|
+
resolve();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
reject(new Error(`CLI exited with code ${code ?? 'unknown'}`));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
child.on('error', reject);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const generatedPackage = JSON.parse(await readFile(path.join(parentDir, 'my-first-bot/package.json'), 'utf8'));
|
|
34
|
+
const generatedSource = await readFile(path.join(parentDir, 'my-first-bot/src/index.ts'), 'utf8');
|
|
35
|
+
const generatedReadme = await readFile(path.join(parentDir, 'my-first-bot/README.md'), 'utf8');
|
|
36
|
+
const generatedEnv = await readFile(path.join(parentDir, 'my-first-bot/.env.example'), 'utf8');
|
|
37
|
+
|
|
38
|
+
assert.equal(generatedPackage.name, 'my-first-bot');
|
|
39
|
+
assert.match(generatedSource, /class MyFirstBot extends BriineAgent/);
|
|
40
|
+
assert.match(generatedSource, /process\.env\.MY_FIRST_BOT_NAME!/);
|
|
41
|
+
assert.match(generatedReadme, /MY_FIRST_BOT_SECRET/);
|
|
42
|
+
assert.match(generatedEnv, /MY_FIRST_BOT_VERSION=0\.0\.1/);
|
|
43
|
+
} finally {
|
|
44
|
+
await rm(parentDir, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@briine/create-bot",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Scaffold a new Briine bot project.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "@briine",
|
|
7
|
+
"repository": "github:briinedev/create-bot",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"bin": {
|
|
10
|
+
"create-briine-bot": "./bin/create-bot.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"template"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node --test"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"briine",
|
|
24
|
+
"bot",
|
|
25
|
+
"create",
|
|
26
|
+
"scaffold",
|
|
27
|
+
"typescript"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# __PROJECT_NAME__
|
|
2
|
+
|
|
3
|
+
This project was generated with `npm create @briine/bot`.
|
|
4
|
+
|
|
5
|
+
## 1. Get your Briine credentials
|
|
6
|
+
|
|
7
|
+
Log in to Briine, create your first agent, and copy these values into `.env`:
|
|
8
|
+
|
|
9
|
+
- `USERNAME`
|
|
10
|
+
- `__BOT_ENV_PREFIX___NAME`
|
|
11
|
+
- `__BOT_ENV_PREFIX___VERSION`
|
|
12
|
+
- `__BOT_ENV_PREFIX___SECRET`
|
|
13
|
+
|
|
14
|
+
If you're running against a different Briine environment, update `API_HOST` as well.
|
|
15
|
+
|
|
16
|
+
## 2. Start the bot
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
cp .env.example .env
|
|
20
|
+
npm run dev
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Project structure
|
|
24
|
+
|
|
25
|
+
- `src/index.ts` contains your bot logic and the `Agent.register(...)` call.
|
|
26
|
+
- `.env` stores the credentials used to authenticate and queue.
|
|
27
|
+
|
|
28
|
+
## Next changes to make
|
|
29
|
+
|
|
30
|
+
- Replace the starter drafting strategy with your own character priorities.
|
|
31
|
+
- Replace the starter action logic with a real combat policy.
|
|
32
|
+
- Add logging around decisions once you start testing matches.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT_NAME__",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "tsx src/index.ts",
|
|
8
|
+
"check": "tsc --noEmit"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@briine/sdk": "^0.1.0",
|
|
12
|
+
"dotenv": "^17.4.2"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/node": "^26.1.1",
|
|
16
|
+
"tsx": "^4.23.1",
|
|
17
|
+
"typescript": "^7.0.2"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import BriineAgent, { Action, Character, MatchStatus, Spell } from '@briine/sdk';
|
|
3
|
+
|
|
4
|
+
class __BOT_CLASS_NAME__ extends BriineAgent {
|
|
5
|
+
chooseCharacter(available: Character[], _ally: Character[], _enemy: Character[]): Character {
|
|
6
|
+
return available[0];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
chooseSpells(available: Spell[], _ally: Character[], _enemy: Character[]): Spell[] {
|
|
10
|
+
return available;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
chooseAction(status: MatchStatus): Action {
|
|
14
|
+
const source = status.sources[0];
|
|
15
|
+
const target = status.targets[0];
|
|
16
|
+
const spell = source?.spells.find(candidate => candidate.available);
|
|
17
|
+
|
|
18
|
+
if (source && spell && target) {
|
|
19
|
+
return { source, target, action: spell };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (source && target && source.attacks[0]) {
|
|
23
|
+
return { source, target, action: source.attacks[0] };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
source,
|
|
28
|
+
target: source,
|
|
29
|
+
action: 'defend',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
BriineAgent.register(
|
|
35
|
+
new __BOT_CLASS_NAME__(
|
|
36
|
+
process.env.USERNAME!,
|
|
37
|
+
process.env.__BOT_ENV_PREFIX___NAME!,
|
|
38
|
+
process.env.__BOT_ENV_PREFIX___VERSION!,
|
|
39
|
+
process.env.__BOT_ENV_PREFIX___SECRET!,
|
|
40
|
+
true,
|
|
41
|
+
),
|
|
42
|
+
process.env.API_HOST!,
|
|
43
|
+
);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"types": ["node"],
|
|
10
|
+
"rootDir": "src",
|
|
11
|
+
"outDir": "dist"
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*.ts"]
|
|
14
|
+
}
|