Compare commits
4 Commits
e8bb767884
...
0e601a254e
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e601a254e | |||
| 30ccdab8af | |||
| 299de83f2f | |||
| 32e5eb8906 |
@@ -7,9 +7,14 @@ RUN pacman -Syu --noconfirm && \
|
||||
npm \
|
||||
git \
|
||||
iptables \
|
||||
openssh \
|
||||
opencode \
|
||||
&& 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
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
@@ -9,9 +9,11 @@ services:
|
||||
- NET_ADMIN # required for iptables in entrypoint
|
||||
- NET_RAW
|
||||
volumes:
|
||||
# Your monorepo — opencode's working directory
|
||||
- ../:/workspace
|
||||
# Persist opencode auth so you don't re-login every time
|
||||
# Your monorepo
|
||||
- /home/ga/workspace:/workspace
|
||||
# Mount entire SSH directory from host
|
||||
- /home/ga/.ssh:/root/.ssh
|
||||
# Persist opencode auth
|
||||
- opencode-auth:/root/.local/share/opencode
|
||||
environment:
|
||||
- OPENCODE_API_KEY=${OPENCODE_API_KEY}
|
||||
|
||||
@@ -1,2 +1,24 @@
|
||||
#!/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
|
||||
|
||||
8
.opencode-sandbox/jest.config.js
Normal file
8
.opencode-sandbox/jest.config.js
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
rootDir: '.',
|
||||
testMatch: ['**/tests/**/*.test.ts'],
|
||||
verbose: true,
|
||||
forceExit: true,
|
||||
}
|
||||
17
.opencode-sandbox/package.json
Normal file
17
.opencode-sandbox/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
84
.opencode-sandbox/tests/ssh.test.ts
Normal file
84
.opencode-sandbox/tests/ssh.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
15
.opencode-sandbox/tsconfig.json
Normal file
15
.opencode-sandbox/tsconfig.json
Normal 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"]
|
||||
}
|
||||
12
AGENTS.md
12
AGENTS.md
@@ -9,7 +9,7 @@ This workspace is a monorepo containing data engineering projects and WhatsApp C
|
||||
```
|
||||
workspace/
|
||||
├── 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-reader/ # WhatsApp message reader
|
||||
│ └── 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 test # Jest
|
||||
npm run test:watch # Jest watch mode
|
||||
npm run test:coverage # Jest with coverage
|
||||
```
|
||||
|
||||
### whatsapp-reader
|
||||
@@ -113,7 +114,6 @@ export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const validated = CreateContactInput.parse(body)
|
||||
// ... process
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
return Response.json({ error: error.errors }, { status: 400 })
|
||||
@@ -138,14 +138,9 @@ interface Props {
|
||||
}
|
||||
|
||||
export default function ComponentName({ contacts, onContactClick }: Props) {
|
||||
// hooks first, then effects, then render
|
||||
const [state, setState] = useState(false)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* JSX */}
|
||||
</div>
|
||||
)
|
||||
return <div>{/* JSX */}</div>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -166,7 +161,6 @@ test('should update contact stage', async () => {
|
||||
render(<KanbanBoard {...props} />)
|
||||
|
||||
await user.click(screen.getByText('Move to Next Stage'))
|
||||
|
||||
expect(onStageChange).toHaveBeenCalledWith(1, 'DECIDINDO')
|
||||
})
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user