Compare commits

...

4 Commits

Author SHA1 Message Date
0e601a254e fix opencode and gitea integration 2026-04-05 21:09:15 +00:00
30ccdab8af chore(opencode-sandbox): add SSH config, git safe directory, and test setup 2026-04-05 20:16:52 +00:00
299de83f2f chore(opencode-sandbox): fix SSH key permissions and remove read-only flag 2026-04-05 18:55:28 +00:00
32e5eb8906 chore(opencode-sandbox): add SSH auth and workspace mount for Gitea
- Mount host workspace at /home/ga/workspace
- Add SSH key and known_hosts for Gitea auth
- Configure git user identity in container
2026-04-05 18:19:34 +00:00
8 changed files with 160 additions and 13 deletions

View File

@@ -7,9 +7,14 @@ RUN pacman -Syu --noconfirm && \
npm \ npm \
git \ git \
iptables \ iptables \
openssh \
opencode \ opencode \
&& pacman -Scc --noconfirm && pacman -Scc --noconfirm
# Git safe directory system-wide config
RUN git config --system --add safe.directory /workspace
RUN git config --system --add safe.directory '*'
# Firewall script — blocks everything except your Gitea instance # Firewall script — blocks everything except your Gitea instance
COPY entrypoint.sh /entrypoint.sh COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh

View File

@@ -9,9 +9,11 @@ services:
- NET_ADMIN # required for iptables in entrypoint - NET_ADMIN # required for iptables in entrypoint
- NET_RAW - NET_RAW
volumes: volumes:
# Your monorepo — opencode's working directory # Your monorepo
- ../:/workspace - /home/ga/workspace:/workspace
# Persist opencode auth so you don't re-login every time # Mount entire SSH directory from host
- /home/ga/.ssh:/root/.ssh
# Persist opencode auth
- opencode-auth:/root/.local/share/opencode - opencode-auth:/root/.local/share/opencode
environment: environment:
- OPENCODE_API_KEY=${OPENCODE_API_KEY} - OPENCODE_API_KEY=${OPENCODE_API_KEY}

View File

@@ -1,2 +1,24 @@
#!/bin/bash #!/bin/bash
# Fix SSH directory permissions
chmod 700 /root/.ssh
chmod 600 /root/.ssh/id_ed25519
# Copy known_hosts to tmp if it's read-only (from host mount)
cp /root/.ssh/known_hosts /tmp/known_hosts 2>/dev/null || touch /tmp/known_hosts
chmod 600 /tmp/known_hosts
# Add Gitea to known_hosts
ssh-keyscan -H git.processhub.work >> /tmp/known_hosts 2>/dev/null || true
# Configure SSH
export GIT_SSH_COMMAND="ssh -i /root/.ssh/id_ed25519 -o UserKnownHostsFile=/tmp/known_hosts -o StrictHostKeyChecking=accept-new"
# Git config
git config --global user.email "gabriel.pereira@protonmail.com"
git config --global user.name "gabspereira"
git config --system --add safe.directory '*'
git config --global --add safe.directory /workspace
# Run opencode
exec opencode exec opencode

View File

@@ -0,0 +1,8 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
rootDir: '.',
testMatch: ['**/tests/**/*.test.ts'],
verbose: true,
forceExit: true,
}

View File

@@ -0,0 +1,17 @@
{
"name": "opencode-sandbox",
"version": "1.0.0",
"private": true,
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
},
"devDependencies": {
"@types/jest": "29.5.12",
"@types/node": "20.11.0",
"jest": "29.7.0",
"ts-jest": "29.1.2",
"typescript": "5.3.3"
}
}

View File

@@ -0,0 +1,84 @@
import { exec } from 'child_process'
import { promisify } from 'util'
import * as fs from 'fs'
import * as path from 'path'
const execAsync = promisify(exec)
describe('SSH/Gitea Integration', () => {
const SSH_DIR = '/root/.ssh'
const SSH_KEY = path.join(SSH_DIR, 'id_ed25519')
const KNOWN_HOSTS = path.join(SSH_DIR, 'known_hosts')
describe('SSH Key Setup', () => {
test('SSH directory exists', () => {
expect(fs.existsSync(SSH_DIR)).toBe(true)
})
test('SSH directory has correct permissions (0700)', () => {
const stats = fs.statSync(SSH_DIR)
const mode = stats.mode & 0o777
expect(mode).toBe(0o700)
})
test('SSH key file exists', () => {
expect(fs.existsSync(SSH_KEY)).toBe(true)
})
test('SSH key has correct permissions (0600)', () => {
const stats = fs.statSync(SSH_KEY)
const mode = stats.mode & 0o777
expect(mode).toBe(0o600)
})
})
describe('Gitea Server', () => {
test('known_hosts file exists', () => {
expect(fs.existsSync(KNOWN_HOSTS)).toBe(true)
})
test('known_hosts contains git.processhub.work', () => {
const content = fs.readFileSync(KNOWN_HOSTS, 'utf8')
expect(content).toContain('git.processhub.work')
})
})
describe('Git Configuration', () => {
test('git user.email is configured', async () => {
const { stdout } = await execAsync('git config --global user.email')
expect(stdout.trim()).toBe('gabriel.pereira@protonmail.com')
})
test('git user.name is configured', async () => {
const { stdout } = await execAsync('git config --global user.name')
expect(stdout.trim()).toBe('gabspereira')
})
})
describe('Gitea SSH Connection', () => {
test('SSH connection to Gitea succeeds', async () => {
try {
const { stdout, stderr } = await execAsync(
'ssh -T -o ConnectTimeout=10 git@git.processhub.work',
{ timeout: 15000 }
)
const output = stdout + stderr
expect(output).toContain('successfully authenticated')
} catch (error: any) {
const output = error.stdout + error.stderr
expect(output).toContain('gabspereira')
expect(output).toContain('successfully authenticated')
}
}, 20000)
})
describe('Git Remote Access', () => {
test('Can access Gitea repository', async () => {
const { stdout, stderr } = await execAsync(
'git ls-remote git@git.processhub.work:gabspereira/workspace.git HEAD',
{ timeout: 15000 }
)
expect(stdout).toMatch(/[a-f0-9]+\s+HEAD/)
}, 20000)
})
})

View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"types": ["node", "jest"]
},
"include": ["tests/**/*"],
"exclude": ["node_modules"]
}

View File

@@ -9,7 +9,7 @@ This workspace is a monorepo containing data engineering projects and WhatsApp C
``` ```
workspace/ workspace/
├── apps/ ├── apps/
│ ├── whatsapp-crm/ # Next.js CRM with Kanban board │ ├── whatsapp-crm/ # Next.js CRM with Kanban board (primary app)
│ ├── whatsapp-sync/ # WhatsApp message sync service │ ├── whatsapp-sync/ # WhatsApp message sync service
│ ├── whatsapp-reader/ # WhatsApp message reader │ ├── whatsapp-reader/ # WhatsApp message reader
│ └── timesfm-forecast/ # Time series forecasting app │ └── timesfm-forecast/ # Time series forecasting app
@@ -52,6 +52,7 @@ npm run dev # node --watch src/index.js
npm run sync # node src/sync.js npm run sync # node src/sync.js
npm run test # Jest npm run test # Jest
npm run test:watch # Jest watch mode npm run test:watch # Jest watch mode
npm run test:coverage # Jest with coverage
``` ```
### whatsapp-reader ### whatsapp-reader
@@ -113,7 +114,6 @@ export async function POST(request: Request) {
try { try {
const body = await request.json() const body = await request.json()
const validated = CreateContactInput.parse(body) const validated = CreateContactInput.parse(body)
// ... process
} catch (error) { } catch (error) {
if (error instanceof ZodError) { if (error instanceof ZodError) {
return Response.json({ error: error.errors }, { status: 400 }) return Response.json({ error: error.errors }, { status: 400 })
@@ -138,14 +138,9 @@ interface Props {
} }
export default function ComponentName({ contacts, onContactClick }: Props) { export default function ComponentName({ contacts, onContactClick }: Props) {
// hooks first, then effects, then render
const [state, setState] = useState(false) const [state, setState] = useState(false)
return ( return <div>{/* JSX */}</div>
<div>
{/* JSX */}
</div>
)
} }
``` ```
@@ -166,7 +161,6 @@ test('should update contact stage', async () => {
render(<KanbanBoard {...props} />) render(<KanbanBoard {...props} />)
await user.click(screen.getByText('Move to Next Stage')) await user.click(screen.getByText('Move to Next Stage'))
expect(onStageChange).toHaveBeenCalledWith(1, 'DECIDINDO') expect(onStageChange).toHaveBeenCalledWith(1, 'DECIDINDO')
}) })
``` ```
@@ -188,4 +182,4 @@ test('should update contact stage', async () => {
- whatsapp-crm uses Next.js 14 App Router - whatsapp-crm uses Next.js 14 App Router
- Database is sql.js (WebAssembly SQLite) - runs in browser - Database is sql.js (WebAssembly SQLite) - runs in browser
- Authentication uses WhatsApp Web.js QR code scanning - Authentication uses WhatsApp Web.js QR code scanning
- STAGES constant defines the Kanban pipeline (defined in `src/lib/types.ts`) - STAGES constant defines the Kanban pipeline (defined in `src/lib/types.ts`)