Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions __tests__/bootstrap-cli.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { EventEmitter } from 'node:events'
import fs from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { describe, expect, it, vi } from 'vitest'

import { runBootstrap } from '../scripts/cli/bootstrap.mjs'

function createSuccessfulSpawn() {
return vi.fn(() => {
const child = new EventEmitter()
queueMicrotask(() => child.emit('close', 0))
return child
})
}

describe('open-stellar bootstrap', () => {
it('scaffolds an agent project and runs npm install', async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'open-stellar-bootstrap-'))
const spawnCommand = createSuccessfulSpawn()

await runBootstrap(['my-agent'], {
cwd: tempDir,
spawnCommand,
})

const projectDir = path.join(tempDir, 'my-agent')
const expectedFiles = [
'package.json',
'tsconfig.json',
'next.config.mjs',
'.env.example',
'README.md',
'lib/agent.ts',
'app/page.tsx',
'app/layout.tsx',
]

await Promise.all(
expectedFiles.map(async (fileName) => {
await expect(fs.stat(path.join(projectDir, fileName))).resolves.toBeTruthy()
}),
)

await expect(fs.readFile(path.join(projectDir, 'package.json'), 'utf8')).resolves.toContain('"name": "my-agent"')
await expect(fs.readFile(path.join(projectDir, 'README.md'), 'utf8')).resolves.toContain('# my-agent')
await expect(fs.readFile(path.join(projectDir, '.env.example'), 'utf8')).resolves.toContain('ANTHROPIC_API_KEY=')
expect(spawnCommand).toHaveBeenCalledWith('npm', ['install'], expect.objectContaining({ cwd: projectDir }))
})

it('errors when the project directory already exists', async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'open-stellar-bootstrap-'))
await fs.mkdir(path.join(tempDir, 'my-agent'))

await expect(
runBootstrap(['my-agent'], {
cwd: tempDir,
spawnCommand: createSuccessfulSpawn(),
}),
).rejects.toThrow('Directory "my-agent" already exists.')
})
})
7 changes: 5 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "my-project",
"name": "open-stellar",
"version": "0.1.0",
"private": true,
"engines": {
Expand Down
90 changes: 90 additions & 0 deletions scripts/cli/bootstrap.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { spawn } from 'node:child_process'

const __dirname = path.dirname(fileURLToPath(import.meta.url))
const TEMPLATE_DIR = path.resolve(__dirname, '../templates/agent')

async function pathExists(targetPath) {
try {
await fs.access(targetPath)
return true
} catch {
return false
}
}

function installDependencies(projectDir, spawnCommand) {
return new Promise((resolve, reject) => {
const child = spawnCommand('npm', ['install'], {
cwd: projectDir,
shell: process.platform === 'win32',
stdio: 'inherit',
})

child.on('error', reject)
child.on('close', (code) => {
if (code === 0) {
resolve()
return
}

reject(new Error(`npm install failed with exit code ${code}`))
})
})
}

function printSuccess(projectName) {
console.log('')
console.log(`Created ${projectName}`)
console.log('')
console.log('Next steps:')
console.log(` cd ${projectName} && npm run dev`)
console.log('')
}

export async function runBootstrap(argv = process.argv.slice(2), options = {}) {
const [projectName] = argv
const cwd = options.cwd ?? process.cwd()
const spawnCommand = options.spawnCommand ?? spawn

if (!projectName) {
throw new Error('Usage: npx open-stellar bootstrap <project-name>')
}

const targetDir = path.resolve(cwd, projectName)

if (await pathExists(targetDir)) {
throw new Error(`Directory "${projectName}" already exists.`)
}

await fs.cp(TEMPLATE_DIR, targetDir, { recursive: true })

const replacements = {
__PROJECT_NAME__: projectName,
}

for (const fileName of ['package.json', 'README.md']) {
const filePath = path.join(targetDir, fileName)
let content = await fs.readFile(filePath, 'utf8')

for (const [token, value] of Object.entries(replacements)) {
content = content.split(token).join(value)
}

await fs.writeFile(filePath, content, 'utf8')
}

await installDependencies(targetDir, spawnCommand)
printSuccess(projectName)
}

if (process.argv[1] === fileURLToPath(import.meta.url)) {
try {
await runBootstrap()
} catch (error) {
console.error(error instanceof Error ? error.message : error)
process.exit(1)
}
}
17 changes: 17 additions & 0 deletions scripts/cli/open-stellar.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env node

import { runBootstrap } from './bootstrap.mjs'

const [command, ...args] = process.argv.slice(2)

try {
if (command === 'bootstrap') {
await runBootstrap(args)
} else {
console.error('Usage: npx open-stellar bootstrap <project-name>')
process.exit(1)
}
} catch (error) {
console.error(error instanceof Error ? error.message : error)
process.exit(1)
}
56 changes: 56 additions & 0 deletions scripts/templates/agent/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Stellar Network - use testnet for local development.
STELLAR_NETWORK=testnet

# Development mode - disables auth and uses testnet-only flows locally.
DEV_MODE=true

# Anthropic API key for agent execution.
ANTHROPIC_API_KEY=sk-ant-your-key-here

# Moltbot gateway token for internal communication.
MOLTBOT_GATEWAY_TOKEN=dev-test-token-12345

# Optional Cloudflare Access client ID when DEV_MODE is false.
CF_ACCESS_CLIENT_ID=

# Optional Cloudflare Access team domain for protected routes.
CF_ACCESS_TEAM_DOMAIN=

# Optional Cloudflare Access audience tag for protected routes.
CF_ACCESS_AUD=

# Optional debug routes at /debug/*.
DEBUG_ROUTES=false

# Optional Stellar testnet public key that receives paid cosmetic purchases.
STELLAR_TREASURY_ADDRESS=

# Optional Better Stack / Logtail source token for structured API logs.
LOGTAIL_SOURCE_TOKEN=

# Optional custom BNB RPC URL for x402 settlement.
NEXT_PUBLIC_BNB_RPC_URL=

# Optional custom Base RPC URL for x402 settlement.
NEXT_PUBLIC_BASE_RPC_URL=

# Optional OpenAI API key if using OpenAI instead of Anthropic.
OPENAI_API_KEY=

# Optional Telegram channel token.
TELEGRAM_BOT_TOKEN=

# Optional Discord channel token.
DISCORD_BOT_TOKEN=

# Optional Slack bot token.
SLACK_BOT_TOKEN=

# Optional Slack app token.
SLACK_APP_TOKEN=

# Optional shared secret for CDP browser automation.
CDP_SECRET=

# Optional deployed worker URL for CDP browser automation.
WORKER_URL=
14 changes: 14 additions & 0 deletions scripts/templates/agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# __PROJECT_NAME__

Minimal Open Stellar agent project scaffolded with `npx open-stellar bootstrap`.

## Quick start

```bash
cp .env.example .env.local
npm run dev
```

## Agent

Edit `lib/agent.ts` to register your agent name, capabilities, and handler.
15 changes: 15 additions & 0 deletions scripts/templates/agent/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Metadata } from 'next'
import type { ReactNode } from 'react'

export const metadata: Metadata = {
title: 'Open Stellar Agent',
description: 'A minimal Open Stellar agent project.',
}

export default function RootLayout({ children }: { children: ReactNode }) {

Check warning on line 9 in scripts/templates/agent/app/layout.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=Bitcoindefi_Open-Stellar&issues=AZ9rzWB4M-GMQlnKZpRz&open=AZ9rzWB4M-GMQlnKZpRz&pullRequest=444
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
19 changes: 19 additions & 0 deletions scripts/templates/agent/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { agent } from '@/lib/agent'

export default function Page() {
return (
<main style={{ maxWidth: 760, margin: '64px auto', padding: '0 24px', fontFamily: 'system-ui, sans-serif' }}>
<p style={{ margin: 0, color: '#4f46e5', fontWeight: 700 }}>Open Stellar Agent</p>
<h1 style={{ margin: '12px 0', fontSize: 44, lineHeight: 1.05 }}>{agent.name}</h1>
<p style={{ color: '#475569', fontSize: 18 }}>{agent.description}</p>
<section style={{ marginTop: 32 }}>
<h2 style={{ fontSize: 18 }}>Capabilities</h2>
<ul>
{agent.capabilities.map((capability) => (
<li key={capability}>{capability}</li>
))}
</ul>
</section>
</main>
)
}
17 changes: 17 additions & 0 deletions scripts/templates/agent/lib/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export type AgentRegistration = {
id: string
name: string
description: string
capabilities: string[]
run: (input: string) => Promise<string>
}

export const agent: AgentRegistration = {
id: 'starter-agent',
name: 'Starter Agent',
description: 'A minimal Open Stellar agent ready for local development.',
capabilities: ['status', 'echo'],
async run(input: string): Promise<string> {
return `Starter Agent received: ${input}`
},
}
5 changes: 5 additions & 0 deletions scripts/templates/agent/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited.
// See https://nextjs.org/docs/app/api-reference/config/typescript for more information.
4 changes: 4 additions & 0 deletions scripts/templates/agent/next.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {}

export default nextConfig
23 changes: 23 additions & 0 deletions scripts/templates/agent/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "__PROJECT_NAME__",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --webpack",
"build": "next build --webpack",
"start": "next start",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"next": "16.2.0",
"react": "19.2.4",
"react-dom": "19.2.4",
"zod": "^3.24.1"
},
"devDependencies": {
"@types/node": "^22",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"typescript": "5.7.3"
}
}
27 changes: 27 additions & 0 deletions scripts/templates/agent/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
Loading