loccon
Version:
A simple local context storage and management tool with CLI and web interfaces. Store, search, and organize code snippets, notes, and development contexts with sharded JSON storage.
697 lines (617 loc) • 19.7 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.startWebServer = startWebServer;
const express_1 = __importDefault(require("express"));
const path = __importStar(require("path"));
const fs = __importStar(require("fs-extra"));
const context_1 = require("../core/context");
const routes_1 = require("./api/routes");
const childProcess = __importStar(require("child_process"));
async function startWebServer(port = 5069, storagePath, openBrowser = true) {
const app = (0, express_1.default)();
// Middleware
app.use(express_1.default.json());
app.use(express_1.default.urlencoded({ extended: true }));
// Create context manager
const contextManager = await context_1.ContextManager.create(storagePath);
// API routes
app.use('/api', (0, routes_1.createAPIRoutes)(contextManager));
// Serve static files
const staticPath = path.join(__dirname, 'static');
// Check if static files exist, if not create basic ones
await ensureStaticFiles(staticPath);
app.use(express_1.default.static(staticPath));
// Catch-all route to serve index.html for SPA
app.get('*', (req, res) => {
res.sendFile(path.join(staticPath, 'index.html'));
});
// Start server
const server = app.listen(port, () => {
console.log(`Loccon web interface running at http://localhost:${port}`);
console.log(`Storage path: ${contextManager.getStoragePath()}`);
if (openBrowser) {
openBrowserToUrl(`http://localhost:${port}`);
}
});
// Handle graceful shutdown
process.on('SIGINT', () => {
console.log('\nShutting down server...');
server.close(() => {
process.exit(0);
});
});
return new Promise((resolve) => {
server.on('listening', resolve);
});
}
async function ensureStaticFiles(staticPath) {
await fs.ensureDir(staticPath);
const indexPath = path.join(staticPath, 'index.html');
const stylePath = path.join(staticPath, 'style.css');
const scriptPath = path.join(staticPath, 'app.js');
// Create basic HTML file if it doesn't exist
if (!await fs.pathExists(indexPath)) {
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Loccon - Local Context Manager</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="app">
<header>
<h1>Loccon</h1>
<p>Local Context Storage & Management</p>
</header>
<main>
<section id="add-section">
<h2>Add New Context</h2>
<form id="add-form">
<input type="text" id="tag-input" placeholder="Context tag" required>
<textarea id="content-input" placeholder="Context content" required rows="4"></textarea>
<input type="text" id="categories-input" placeholder="Categories (comma-separated)">
<button type="submit">Add Context</button>
</form>
</section>
<section id="search-section">
<h2>Search</h2>
<div class="search-controls">
<input type="text" id="search-input" placeholder="Search contexts...">
<label>
<input type="checkbox" id="fuzzy-search"> Fuzzy search
</label>
<button id="search-btn">Search</button>
<button id="clear-search">Clear</button>
</div>
</section>
<section id="contexts-section">
<h2>Contexts</h2>
<div id="contexts-container">
<!-- Contexts will be loaded here -->
</div>
</section>
</main>
<div id="modal" class="modal">
<div class="modal-content">
<span class="close">×</span>
<h3 id="modal-title">Edit Context</h3>
<form id="edit-form">
<input type="hidden" id="edit-tag">
<textarea id="edit-content" rows="6" required></textarea>
<input type="text" id="edit-categories" placeholder="Categories (comma-separated)">
<div class="modal-buttons">
<button type="submit">Save</button>
<button type="button" id="cancel-edit">Cancel</button>
</div>
</form>
</div>
</div>
</div>
<script src="app.js"></script>
</body>
</html>`;
await fs.writeFile(indexPath, html);
}
// Create basic CSS file if it doesn't exist
if (!await fs.pathExists(stylePath)) {
const css = `/* Basic styles for Loccon web interface */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f5f5f5;
}
#app {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
margin-bottom: 40px;
padding: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
header h1 {
color: #2c3e50;
font-size: 2.5em;
margin-bottom: 10px;
}
header p {
color: #7f8c8d;
font-size: 1.1em;
}
section {
background: white;
margin-bottom: 30px;
padding: 25px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h2 {
color: #2c3e50;
margin-bottom: 20px;
border-bottom: 2px solid #3498db;
padding-bottom: 10px;
}
/* Form styles */
form {
display: flex;
flex-direction: column;
gap: 15px;
}
input, textarea {
padding: 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
input:focus, textarea:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
}
button {
padding: 12px 24px;
background: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background 0.2s;
}
button:hover {
background: #2980b9;
}
button.danger {
background: #e74c3c;
}
button.danger:hover {
background: #c0392b;
}
/* Search controls */
.search-controls {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.search-controls input[type="text"] {
flex: 1;
min-width: 200px;
}
/* Context cards */
.context-card {
border: 1px solid #ddd;
border-radius: 6px;
padding: 20px;
margin-bottom: 15px;
background: #fafafa;
}
.context-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 15px;
}
.context-tag {
font-weight: bold;
font-size: 1.1em;
color: #2c3e50;
}
.context-actions {
display: flex;
gap: 8px;
}
.context-actions button {
padding: 6px 12px;
font-size: 12px;
}
.context-content {
margin-bottom: 15px;
white-space: pre-wrap;
background: white;
padding: 15px;
border-radius: 4px;
border-left: 4px solid #3498db;
}
.context-meta {
font-size: 0.9em;
color: #7f8c8d;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
}
.context-categories {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.category-tag {
background: #ecf0f1;
padding: 4px 8px;
border-radius: 12px;
font-size: 0.8em;
color: #2c3e50;
}
/* Modal styles */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
}
.modal-content {
background-color: white;
margin: 10% auto;
padding: 30px;
border-radius: 8px;
width: 90%;
max-width: 600px;
position: relative;
}
.close {
position: absolute;
right: 20px;
top: 15px;
color: #aaa;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover {
color: #000;
}
.modal-buttons {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 20px;
}
/* Responsive design */
(max-width: 768px) {
#app {
padding: 10px;
}
.search-controls {
flex-direction: column;
align-items: stretch;
}
.context-header {
flex-direction: column;
gap: 10px;
}
.context-meta {
flex-direction: column;
}
}
/* Loading and empty states */
.loading {
text-align: center;
padding: 40px;
color: #7f8c8d;
}
.empty-state {
text-align: center;
padding: 40px;
color: #7f8c8d;
}
.empty-state h3 {
margin-bottom: 10px;
color: #95a5a6;
}`;
await fs.writeFile(stylePath, css);
}
// Create basic JavaScript file if it doesn't exist
if (!await fs.pathExists(scriptPath)) {
const js = `// Loccon Web Interface JavaScript
class LocconApp {
constructor() {
this.contexts = {};
this.currentSearch = '';
this.init();
}
init() {
this.bindEvents();
this.loadContexts();
}
bindEvents() {
// Add form
document.getElementById('add-form').addEventListener('submit', (e) => {
e.preventDefault();
this.handleAddContext();
});
// Search
document.getElementById('search-btn').addEventListener('click', () => {
this.handleSearch();
});
document.getElementById('search-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.handleSearch();
}
});
document.getElementById('clear-search').addEventListener('click', () => {
this.clearSearch();
});
// Modal
document.getElementById('cancel-edit').addEventListener('click', () => {
this.closeModal();
});
document.querySelector('.close').addEventListener('click', () => {
this.closeModal();
});
document.getElementById('edit-form').addEventListener('submit', (e) => {
e.preventDefault();
this.handleUpdateContext();
});
// Close modal on outside click
window.addEventListener('click', (e) => {
const modal = document.getElementById('modal');
if (e.target === modal) {
this.closeModal();
}
});
}
async loadContexts() {
try {
const response = await fetch('/api/contexts');
this.contexts = await response.json();
this.renderContexts(this.contexts);
} catch (error) {
this.showError('Failed to load contexts: ' + error.message);
}
}
async handleAddContext() {
const tag = document.getElementById('tag-input').value.trim();
const content = document.getElementById('content-input').value.trim();
const categories = document.getElementById('categories-input').value.trim();
if (!tag || !content) {
alert('Tag and content are required');
return;
}
try {
const response = await fetch('/api/contexts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tag, content, categories })
});
if (response.ok) {
document.getElementById('add-form').reset();
await this.loadContexts();
this.showSuccess('Context added successfully');
} else {
const error = await response.json();
this.showError(error.error);
}
} catch (error) {
this.showError('Failed to add context: ' + error.message);
}
}
async handleSearch() {
const query = document.getElementById('search-input').value.trim();
const fuzzy = document.getElementById('fuzzy-search').checked;
if (!query) {
this.loadContexts();
return;
}
try {
const response = await fetch(\`/api/search?q=\${encodeURIComponent(query)}&fuzzy=\${fuzzy}\`);
const results = await response.json();
const searchContexts = {};
results.forEach(result => {
searchContexts[result.tag] = result.entry;
});
this.renderContexts(searchContexts);
this.currentSearch = query;
} catch (error) {
this.showError('Search failed: ' + error.message);
}
}
clearSearch() {
document.getElementById('search-input').value = '';
document.getElementById('fuzzy-search').checked = false;
this.currentSearch = '';
this.loadContexts();
}
renderContexts(contexts) {
const container = document.getElementById('contexts-container');
const entries = Object.entries(contexts);
if (entries.length === 0) {
container.innerHTML = \`
<div class="empty-state">
<h3>No contexts found</h3>
<p>\${this.currentSearch ? 'Try a different search term' : 'Add your first context above'}</p>
</div>
\`;
return;
}
container.innerHTML = entries.map(([tag, entry]) => \`
<div class="context-card">
<div class="context-header">
<div class="context-tag">\${this.escapeHtml(tag)}</div>
<div class="context-actions">
<button onclick="app.editContext('\${tag}')">Edit</button>
<button class="danger" onclick="app.deleteContext('\${tag}')">Delete</button>
</div>
</div>
<div class="context-content">\${this.escapeHtml(entry.content)}</div>
<div class="context-meta">
<div>
<strong>Created:</strong> \${new Date(entry.metadata.created).toLocaleString()}
<br>
<strong>Modified:</strong> \${new Date(entry.metadata.modified).toLocaleString()}
</div>
<div class="context-categories">
\${entry.metadata.categories.map(cat =>
\`<span class="category-tag">\${this.escapeHtml(cat)}</span>\`
).join('')}
</div>
</div>
</div>
\`).join('');
}
editContext(tag) {
const context = this.contexts[tag];
if (!context) return;
document.getElementById('edit-tag').value = tag;
document.getElementById('edit-content').value = context.content;
document.getElementById('edit-categories').value = context.metadata.categories.join(', ');
document.getElementById('modal-title').textContent = \`Edit Context: \${tag}\`;
document.getElementById('modal').style.display = 'block';
}
async handleUpdateContext() {
const tag = document.getElementById('edit-tag').value;
const content = document.getElementById('edit-content').value.trim();
const categories = document.getElementById('edit-categories').value.trim();
if (!content) {
alert('Content is required');
return;
}
try {
const response = await fetch(\`/api/contexts/\${encodeURIComponent(tag)}\`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content, categories })
});
if (response.ok) {
this.closeModal();
await this.loadContexts();
this.showSuccess('Context updated successfully');
} else {
const error = await response.json();
this.showError(error.error);
}
} catch (error) {
this.showError('Failed to update context: ' + error.message);
}
}
async deleteContext(tag) {
if (!confirm(\`Are you sure you want to delete the context '\${tag}'?\`)) {
return;
}
try {
const response = await fetch(\`/api/contexts/\${encodeURIComponent(tag)}\`, {
method: 'DELETE'
});
if (response.ok) {
await this.loadContexts();
this.showSuccess('Context deleted successfully');
} else {
const error = await response.json();
this.showError(error.error);
}
} catch (error) {
this.showError('Failed to delete context: ' + error.message);
}
}
closeModal() {
document.getElementById('modal').style.display = 'none';
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
showSuccess(message) {
// Simple alert for now - could be enhanced with proper notifications
alert(message);
}
showError(message) {
alert('Error: ' + message);
}
}
// Initialize app when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.app = new LocconApp();
});`;
await fs.writeFile(scriptPath, js);
}
}
function openBrowserToUrl(url) {
const platform = process.platform;
let command;
switch (platform) {
case 'darwin': // macOS
command = 'open';
break;
case 'win32': // Windows
command = 'start';
break;
default: // Linux and others
command = 'xdg-open';
break;
}
childProcess.spawn(command, [url], { detached: true, stdio: 'ignore' });
}
//# sourceMappingURL=server.js.map