Skip to content

Commit 67c0c77

Browse files
CLI tool + deployment hell
1 parent f8c7389 commit 67c0c77

File tree

11 files changed

+480
-39
lines changed

11 files changed

+480
-39
lines changed

packages/openmodes/api/index.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { getRenderWithCurrentVotes } from '../src/render.tsx';
2+
import path from 'path';
3+
import { promises as fs } from 'fs';
4+
import { Mutex } from 'async-mutex';
5+
import { fileURLToPath } from 'url';
6+
7+
const __filename = fileURLToPath(import.meta.url);
8+
const __dirname = path.dirname(__filename);
9+
10+
// Simple data storage (in-memory for now)
11+
const votes = {};
12+
const downloads = {};
13+
const voteMutex = new Mutex();
14+
const downloadMutex = new Mutex();
15+
16+
// Load built HTML
17+
let htmlTemplate;
18+
19+
async function loadTemplate() {
20+
if (!htmlTemplate) {
21+
const htmlPath = path.join(__dirname, '../dist/index.html');
22+
htmlTemplate = await fs.readFile(htmlPath, 'utf-8');
23+
}
24+
return htmlTemplate;
25+
}
26+
27+
export default async function handler(req, res) {
28+
const url = new URL(req.url, `https://${req.headers.host}`);
29+
30+
// API Routes
31+
if (url.pathname === '/api/vote' && req.method === 'POST') {
32+
const { modeId, direction, action } = req.body;
33+
34+
if (!modeId || !direction || !action) {
35+
return res.status(400).json({ error: 'Missing required fields' });
36+
}
37+
38+
return voteMutex.runExclusive(async () => {
39+
if (!votes[modeId]) votes[modeId] = { up: 0, down: 0 };
40+
41+
const change = action === 'add' ? 1 : -1;
42+
votes[modeId][direction] = Math.max(0, votes[modeId][direction] + change);
43+
44+
return res.json(votes[modeId]);
45+
});
46+
}
47+
48+
if (url.pathname === '/api/download' && req.method === 'POST') {
49+
const { modeId } = req.body;
50+
51+
if (!modeId) {
52+
return res.status(400).json({ error: 'Missing modeId' });
53+
}
54+
55+
return downloadMutex.runExclusive(async () => {
56+
downloads[modeId] = (downloads[modeId] || 0) + 1;
57+
return res.json({ downloads: downloads[modeId] });
58+
});
59+
}
60+
61+
if (url.pathname === '/api/votes') {
62+
return res.json(votes);
63+
}
64+
65+
if (url.pathname === '/api/downloads') {
66+
return res.json(downloads);
67+
}
68+
69+
// Static files from dist
70+
if (url.pathname === '/index.js') {
71+
const jsPath = path.join(__dirname, '../dist/index.js');
72+
const js = await fs.readFile(jsPath, 'utf-8');
73+
res.setHeader('Content-Type', 'application/javascript');
74+
return res.send(js);
75+
}
76+
77+
if (url.pathname === '/index.css') {
78+
const cssPath = path.join(__dirname, '../dist/index.css');
79+
const css = await fs.readFile(cssPath, 'utf-8');
80+
res.setHeader('Content-Type', 'text/css');
81+
return res.send(css);
82+
}
83+
84+
if (url.pathname === '/favicon.svg') {
85+
const svgPath = path.join(__dirname, '../dist/favicon.svg');
86+
const svg = await fs.readFile(svgPath, 'utf-8');
87+
res.setHeader('Content-Type', 'image/svg+xml');
88+
return res.send(svg);
89+
}
90+
91+
// Main page
92+
if (url.pathname === '/' || url.pathname === '') {
93+
const html = await loadTemplate();
94+
const rendered = getRenderWithCurrentVotes();
95+
const finalHtml = html.replace('<!--static-->', rendered);
96+
97+
res.setHeader('Content-Type', 'text/html');
98+
return res.send(finalHtml);
99+
}
100+
101+
return res.status(404).send('Not found');
102+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "module",
3+
"dependencies": {
4+
"async-mutex": "^0.5.0"
5+
}
6+
}

packages/openmodes/cli/install.js

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
#!/usr/bin/env node
2+
3+
import fs from 'fs';
4+
import path from 'path';
5+
import http from 'http';
6+
import { fileURLToPath } from 'url';
7+
8+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
9+
10+
async function fetchJson(url) {
11+
return new Promise((resolve, reject) => {
12+
http.get(url, (res) => {
13+
let data = '';
14+
res.on('data', (chunk) => {
15+
data += chunk;
16+
});
17+
res.on('end', () => {
18+
try {
19+
resolve(JSON.parse(data));
20+
} catch (e) {
21+
reject(e);
22+
}
23+
});
24+
}).on('error', reject);
25+
});
26+
}
27+
28+
function ensureDirectoryExists(dir) {
29+
if (!fs.existsSync(dir)) {
30+
fs.mkdirSync(dir, { recursive: true });
31+
}
32+
}
33+
34+
function updateOrCreateOpenCodeJson(modeData, modeId) {
35+
const currentDir = process.cwd();
36+
const openCodePath = path.join(currentDir, 'opencode.json');
37+
38+
let openCodeConfig = {
39+
"$schema": "https://opencode.ai/config.json",
40+
"instructions": [],
41+
"mcp": {},
42+
"provider": {},
43+
"mode": {}
44+
};
45+
46+
if (fs.existsSync(openCodePath)) {
47+
try {
48+
openCodeConfig = JSON.parse(fs.readFileSync(openCodePath, 'utf8'));
49+
} catch (e) {
50+
console.warn('⚠️ Could not parse existing opencode.json, creating new one');
51+
}
52+
}
53+
54+
if (!openCodeConfig.mcp) openCodeConfig.mcp = {};
55+
if (!openCodeConfig.mode) openCodeConfig.mode = {};
56+
57+
if (modeData.opencode_config && modeData.opencode_config.mode) {
58+
Object.entries(modeData.opencode_config.mode).forEach(([key, config]) => {
59+
const updatedConfig = { ...config };
60+
61+
if (updatedConfig.prompt) {
62+
updatedConfig.prompt = `{file:./.opencode/mode/${key}/${key}.mode.md}`;
63+
}
64+
65+
if (updatedConfig.instructions && Array.isArray(updatedConfig.instructions)) {
66+
updatedConfig.instructions = updatedConfig.instructions.map(instruction => {
67+
const filename = instruction.replace('./', '');
68+
return `./.opencode/mode/${key}/${filename}`;
69+
});
70+
}
71+
72+
openCodeConfig.mode[key] = updatedConfig;
73+
});
74+
}
75+
76+
fs.writeFileSync(openCodePath, JSON.stringify(openCodeConfig, null, '\t'));
77+
}
78+
79+
async function installMode(modeId) {
80+
try {
81+
console.log(`📦 Installing mode: ${modeId}`);
82+
83+
const url = `https://openmodes.dev/mode/${modeId}`;
84+
const modeData = await fetchJson(url);
85+
86+
// Remove URL keys from MCP configurations
87+
if (modeData.opencode_config && modeData.opencode_config.mode) {
88+
Object.values(modeData.opencode_config.mode).forEach(modeConfig => {
89+
if (modeConfig.mcp) {
90+
Object.values(modeConfig.mcp).forEach(mcpConfig => {
91+
delete mcpConfig.url;
92+
});
93+
}
94+
});
95+
}
96+
97+
const currentDir = process.cwd();
98+
const modeDir = path.join(currentDir, '.opencode', 'mode', modeId);
99+
ensureDirectoryExists(modeDir);
100+
101+
// Write mode files
102+
if (modeData.mode_prompt) {
103+
const promptPath = path.join(modeDir, `${modeId}.mode.md`);
104+
fs.writeFileSync(promptPath, modeData.mode_prompt);
105+
}
106+
107+
if (modeData.context_instructions && Array.isArray(modeData.context_instructions)) {
108+
modeData.context_instructions.forEach((instruction) => {
109+
const filename = `${instruction.title.toLowerCase()}.instructions.md`;
110+
const instructionPath = path.join(modeDir, filename);
111+
fs.writeFileSync(instructionPath, instruction.content);
112+
});
113+
}
114+
115+
updateOrCreateOpenCodeJson(modeData, modeId);
116+
117+
console.log(`✅ Successfully installed mode "${modeId}"`);
118+
119+
} catch (error) {
120+
console.error(`❌ Error installing mode "${modeId}":`, error.message);
121+
process.exit(1);
122+
}
123+
}
124+
125+
function removeMode(modeId) {
126+
try {
127+
console.log(`🗑️ Removing mode: ${modeId}`);
128+
129+
const currentDir = process.cwd();
130+
const modeDir = path.join(currentDir, '.opencode', 'mode', modeId);
131+
const openCodePath = path.join(currentDir, 'opencode.json');
132+
133+
if (fs.existsSync(modeDir)) {
134+
fs.rmSync(modeDir, { recursive: true, force: true });
135+
}
136+
137+
if (fs.existsSync(openCodePath)) {
138+
try {
139+
const openCodeConfig = JSON.parse(fs.readFileSync(openCodePath, 'utf8'));
140+
141+
if (openCodeConfig.mode && openCodeConfig.mode[modeId]) {
142+
delete openCodeConfig.mode[modeId];
143+
fs.writeFileSync(openCodePath, JSON.stringify(openCodeConfig, null, '\t'));
144+
}
145+
} catch (e) {
146+
console.error('⚠️ Error updating opencode.json:', e.message);
147+
}
148+
}
149+
150+
console.log(`✅ Successfully removed mode "${modeId}"`);
151+
152+
} catch (error) {
153+
console.error(`❌ Error removing mode "${modeId}":`, error.message);
154+
process.exit(1);
155+
}
156+
}
157+
158+
const args = process.argv.slice(2);
159+
const command = args[0];
160+
const modeId = args[1];
161+
162+
if (command === 'install' && modeId) {
163+
installMode(modeId);
164+
} else if (command === 'remove' && modeId) {
165+
removeMode(modeId);
166+
} else {
167+
console.log('Usage: openmodes <command> <mode-id>');
168+
console.log('');
169+
console.log('Commands:');
170+
console.log(' install <mode-id> Install a mode from openmodes.dev');
171+
console.log(' remove <mode-id> Remove an installed mode');
172+
console.log('');
173+
console.log('Examples:');
174+
console.log(' npx openmodes install archie');
175+
console.log(' npx openmodes remove archie');
176+
process.exit(1);
177+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "openmodes",
3+
"version": "1.0.0",
4+
"description": "CLI tool for installing OpenModes from openmodes.dev",
5+
"type": "module",
6+
"main": "install.js",
7+
"bin": {
8+
"openmodes": "./install.js"
9+
},
10+
"scripts": {
11+
"test": "echo \"Error: no test specified\" && exit 1"
12+
},
13+
"keywords": [
14+
"openmodes",
15+
"cli",
16+
"opencode",
17+
"ai",
18+
"modes"
19+
],
20+
"author": "spoon",
21+
"license": "MIT",
22+
"repository": {
23+
"type": "git",
24+
"url": "https://github.com/spoons-and-mirrors/models.dev.git",
25+
"directory": "packages/openmodes/cli"
26+
},
27+
"homepage": "https://openmodes.dev",
28+
"engines": {
29+
"node": ">=14.0.0"
30+
}
31+
}

packages/openmodes/modes/archie/opencode.json

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,32 @@
11
{
2-
"instructions": [
3-
"./adr.instructions.md",
4-
"./codemap.instructions.md",
5-
"./resources.instructions.md"
6-
],
7-
"mcp": {
8-
"context7": {
9-
"type": "local",
10-
"command": ["npx", "-y", "@upstash/context7-mcp"],
11-
"enabled": true,
12-
"url": "https://github.com/upstash/context7"
13-
},
14-
"think-tool": {
15-
"type": "local",
16-
"command": ["npx", "-y", "think-tool-mcp"],
17-
"enabled": true,
18-
"url": "https://github.com/abhinav-mangla/think-tool-mcp"
19-
},
20-
"repomix": {
21-
"type": "local",
22-
"command": ["npx", "-y", "repomix", "--mcp"],
23-
"enabled": true,
24-
"url": "https://github.com/yamadashy/repomix"
25-
}
26-
},
272
"mode": {
283
"archie": {
294
"prompt": "{file:./archie.mode.md}",
5+
"instructions": [
6+
"./adr.instructions.md",
7+
"./codemap.instructions.md",
8+
"./resources.instructions.md"
9+
],
10+
"mcp": {
11+
"context7": {
12+
"type": "local",
13+
"command": ["npx", "-y", "@upstash/context7-mcp"],
14+
"enabled": true,
15+
"url": "https://github.com/upstash/context7"
16+
},
17+
"think-tool": {
18+
"type": "local",
19+
"command": ["npx", "-y", "think-tool-mcp"],
20+
"enabled": true,
21+
"url": "https://github.com/abhinav-mangla/think-tool-mcp"
22+
},
23+
"repomix": {
24+
"type": "local",
25+
"command": ["npx", "-y", "repomix", "--mcp"],
26+
"enabled": true,
27+
"url": "https://github.com/yamadashy/repomix"
28+
}
29+
},
3030
"tools": {
3131
"repomix_file_system_read_file": false,
3232
"repomix_file_system_read_directory": false,

packages/openmodes/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"scripts": {
55
"dev": "bun run --hot ./src/server.ts",
66
"build": "NODE_ENV=production bun ./script/build.ts",
7+
"vercel-build": "NODE_ENV=production bun ./script/build.ts",
78
"build:dev": "bun ./script/build.ts",
89
"start": "NODE_ENV=production bun ./src/server.ts",
910
"clean": "rm -rf dist"
@@ -13,6 +14,7 @@
1314
"hono": "^4.8.0"
1415
},
1516
"devDependencies": {
16-
"@types/bun": "^1.2.16"
17+
"@types/bun": "^1.2.16",
18+
"@types/node": "^20.0.0"
1719
}
1820
}

0 commit comments

Comments
 (0)