From eac572dcaf61cd38f3c31f574ae133b9fc8768f8 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 27 Jul 2026 09:44:56 +0900 Subject: [PATCH] Split pages and commands into editable modules --- README.md | 9 +++++- src/commands.js | 64 ++++++++++++++++++---------------------- src/commands/announce.js | 2 ++ src/commands/chat.js | 2 ++ src/commands/clear.js | 2 ++ src/commands/contact.js | 2 ++ src/commands/help.js | 6 ++++ src/commands/home.js | 2 ++ src/commands/open.js | 2 ++ src/commands/pages.js | 3 ++ src/commands/quit.js | 2 ++ src/commands/reload.js | 2 ++ src/commands/theme.js | 2 ++ src/pages/about.js | 10 +++++++ src/pages/home.js | 11 +++++++ src/pages/index.js | 8 +++++ src/pages/links.js | 10 +++++++ src/pages/projects.js | 10 +++++++ 18 files changed, 112 insertions(+), 37 deletions(-) create mode 100644 src/commands/announce.js create mode 100644 src/commands/chat.js create mode 100644 src/commands/clear.js create mode 100644 src/commands/contact.js create mode 100644 src/commands/help.js create mode 100644 src/commands/home.js create mode 100644 src/commands/open.js create mode 100644 src/commands/pages.js create mode 100644 src/commands/quit.js create mode 100644 src/commands/reload.js create mode 100644 src/commands/theme.js create mode 100644 src/pages/about.js create mode 100644 src/pages/home.js create mode 100644 src/pages/index.js create mode 100644 src/pages/links.js create mode 100644 src/pages/projects.js diff --git a/README.md b/README.md index 935ba08..f648e0f 100644 --- a/README.md +++ b/README.md @@ -27,4 +27,11 @@ Run `npm run notify` on the owner machine (or use `NOTIFICATION_PORT`) to receiv ## Extending -Pages are data-driven in `config.json`; commands are isolated in `src/commands.js`. Pong is intentionally left as the next module: it can use the same session registry and owner-online check. +Pages and commands are source modules: + +- `src/pages/home.js`, `about.js`, `projects.js`, and `links.js` are the premade editable pages. +- `src/pages/index.js` is the page registry. Add an import and registry entry when creating a new page; its filename becomes the SSH command. +- `src/commands/` contains the premade command modules (`chat`, `contact`, `announce`, and so on). +- `src/commands.js` is the command registry. Add an import there to expose a new command. + +Each page exports `name`, `title`, and `render({ config, ctx, api })`. The renderer returns terminal text, so you can build extensive custom pages while inheriting the shared header, theme, and prompt. Pong is intentionally left as the next module: it can use the same session registry and owner-online check. diff --git a/src/commands.js b/src/commands.js index 11c08d0..4397280 100644 --- a/src/commands.js +++ b/src/commands.js @@ -1,44 +1,36 @@ -import { color, wrap } from './terminal.js'; +import * as home from './commands/home.js'; +import * as help from './commands/help.js'; +import * as pages from './commands/pages.js'; +import * as open from './commands/open.js'; +import * as contact from './commands/contact.js'; +import * as chat from './commands/chat.js'; +import * as clearCommand from './commands/clear.js'; +import * as quit from './commands/quit.js'; +import * as reload from './commands/reload.js'; +import * as theme from './commands/theme.js'; +import * as announce from './commands/announce.js'; +import { pageMap } from './pages/index.js'; + +// Add a command module import here to make a new command available. +const commands = [home, help, pages, open, contact, chat, clearCommand, quit, reload, theme, announce]; export function createCommandRouter(api) { - const commands = new Map(); - const add = command => commands.set(command.name, command); - const page = name => { - const item = api.config.pages?.[name]; - if (!item) return `${color('red', `No page named “${name}”.`)}\nTry: pages`; - return wrap((item.lines || []).join('\n')).join('\n'); + const commandMap = new Map(commands.map(command => [command.name, command])); + const runPage = (ctx, name) => { + const page = pageMap.get(name); + return page ? page.render({ ...api, api, ctx }) : `No page named “${name}”. Try: pages`; }; - - add({ name: 'home', description: 'Return home', run: () => { - const p = api.config.persona || {}; - const status = api.ownerOnline() ? color('green', 'Owner is online') : color(api.config.theme?.muted || 'brightBlack', 'Owner is offline'); - return `${color(api.config.theme?.accent || 'yellow', `Welcome to ${p.name || 'my portfolio'}`)}\n\n${wrap(p.bio || 'You are exploring a portfolio over SSH. Type “help” to get started.').join('\n')}\n\n${status}\n\nCommands: home · pages · open · chat · contact · clear · quit`; - }}); - add({ name: 'help', description: 'Show this help', run: ctx => { - const actions = [...commands.values()].filter(c => !c.admin || api.isAdmin(ctx)).map(c => `${c.name.padEnd(18)} ${c.description}`); - const pages = Object.entries(api.config.pages || {}).filter(([name]) => !commands.has(name)).map(([name, item]) => `${name.padEnd(18)} Open ${item.title || name}`); - return [...actions, ...pages].join('\n'); - }}); - add({ name: 'pages', description: 'List available pages', run: () => Object.entries(api.config.pages || {}).map(([name, item]) => `${color(api.config.theme?.accent || 'yellow', name.padEnd(14))} ${item.title || ''}`).join('\n') || 'No pages configured.' }); - add({ name: 'open', description: 'Open a page', run: (ctx, arg) => arg ? page(arg) : 'Usage: open ' }); - add({ name: 'contact', description: 'Show contact details', run: () => `Email: ${api.config.persona?.email || 'not configured'}` }); - add({ name: 'chat', description: 'Chat with the owner/visitors', run: (ctx, arg) => { - if (!arg) return 'Usage: chat '; - const author = api.isAdmin(ctx) ? `${api.config.persona?.name || 'Owner'} [owner]` : (ctx.username || 'visitor'); - const line = `${author}: ${arg}`; api.broadcast(line); api.sendNotification(line); return api.ownerOnline() ? 'Message sent to the owner.' : 'Message posted; the owner is currently offline.'; - }}); - add({ name: 'clear', description: 'Clear the screen', run: () => '' }); - add({ name: 'quit', description: 'Disconnect', run: ctx => { ctx.channel?.end(); return ''; } }); - add({ name: 'reload', description: 'Reload configuration', admin: true, run: () => { api.reload(); return 'Configuration reloaded.'; } }); - add({ name: 'theme', description: 'Set runtime primary color', admin: true, run: (ctx, arg) => { if (!arg) return 'Usage: theme '; api.config.theme.primary = arg; return `Primary color set to ${arg} (runtime only).`; } }); - add({ name: 'announce', description: 'Broadcast an announcement', admin: true, run: (ctx, arg) => { if (!arg) return 'Usage: announce '; api.broadcast(`OWNER: ${arg}`); api.sendNotification(`OWNER: ${arg}`); return 'Announcement sent.'; } }); - return { - home: ctx => commands.get('home').run(ctx, ''), + home: ctx => runPage(ctx, 'home'), execute(ctx, input) { - const [name, ...rest] = input.trim().split(/\s+/); const command = commands.get(name || 'home') || (api.config.pages?.[name] ? { run: () => page(name) } : null); - if (!command || (command.admin && !api.isAdmin(ctx))) return `Unknown command: ${name}. Try “help”.`; - return command.run(ctx, rest.join(' ')); + const [name, ...rest] = input.trim().split(/\s+/); const arg = rest.join(' '); + const command = commandMap.get(name || 'home') || pageMap.get(name); + if (!command) return `Unknown command: ${name}. Try “help”.`; + if (command.admin && !api.isAdmin(ctx)) return `Unknown command: ${name}. Try “help”.`; + if (pageMap.has(name) && !command.run) return runPage(ctx, name); + const result = command.run({ ...api, api, ctx, arg, config: api.config, pageMap, visibleCommands: commands }); + if (typeof result === 'string' && result.startsWith('__PAGE__:')) return runPage(ctx, result.slice(9)); + return result; } }; } diff --git a/src/commands/announce.js b/src/commands/announce.js new file mode 100644 index 0000000..6d8028b --- /dev/null +++ b/src/commands/announce.js @@ -0,0 +1,2 @@ +export const name = 'announce'; export const description = 'Broadcast an announcement'; export const admin = true; +export function run({ arg, api }) { if (!arg) return 'Usage: announce '; api.broadcast(`OWNER: ${arg}`); api.sendNotification(`OWNER: ${arg}`); return 'Announcement sent.'; } diff --git a/src/commands/chat.js b/src/commands/chat.js new file mode 100644 index 0000000..a2e0bc2 --- /dev/null +++ b/src/commands/chat.js @@ -0,0 +1,2 @@ +export const name = 'chat'; export const description = 'Chat with the owner/visitors'; +export function run({ arg, ctx, api }) { if (!arg) return 'Usage: chat '; const author = api.isAdmin(ctx) ? `${api.config.persona?.name || 'Owner'} [owner]` : (ctx.username || 'visitor'); const line = `${author}: ${arg}`; api.broadcast(line); api.sendNotification(line); return api.ownerOnline() ? 'Message sent to the owner.' : 'Message posted; the owner is currently offline.'; } diff --git a/src/commands/clear.js b/src/commands/clear.js new file mode 100644 index 0000000..f588e0c --- /dev/null +++ b/src/commands/clear.js @@ -0,0 +1,2 @@ +export const name = 'clear'; export const description = 'Clear the screen'; +export function run() { return ''; } diff --git a/src/commands/contact.js b/src/commands/contact.js new file mode 100644 index 0000000..d11046d --- /dev/null +++ b/src/commands/contact.js @@ -0,0 +1,2 @@ +export const name = 'contact'; export const description = 'Show contact details'; +export function run({ config }) { return `Email: ${config.persona?.email || 'not configured'}`; } diff --git a/src/commands/help.js b/src/commands/help.js new file mode 100644 index 0000000..ec2856b --- /dev/null +++ b/src/commands/help.js @@ -0,0 +1,6 @@ +export const name = 'help'; export const description = 'Show this help'; +export function run({ visibleCommands, pageMap, isAdmin, ctx }) { + const actions = visibleCommands.filter(command => !command.admin || isAdmin(ctx)).map(command => `${command.name.padEnd(18)} ${command.description}`); + const pages = [...pageMap.values()].filter(page => !visibleCommands.some(command => command.name === page.name)).map(page => `${page.name.padEnd(18)} Open ${page.title || page.name}`); + return [...actions, ...pages].join('\n'); +} diff --git a/src/commands/home.js b/src/commands/home.js new file mode 100644 index 0000000..2b0e744 --- /dev/null +++ b/src/commands/home.js @@ -0,0 +1,2 @@ +export const name = 'home'; export const description = 'Return home'; +export function run({ pageMap, config, ownerOnline, ctx, api }) { return pageMap.get('home').render({ config, ownerOnline, ctx, api }); } diff --git a/src/commands/open.js b/src/commands/open.js new file mode 100644 index 0000000..f8c4db9 --- /dev/null +++ b/src/commands/open.js @@ -0,0 +1,2 @@ +export const name = 'open'; export const description = 'Open a page'; +export function run({ arg }) { return arg ? `__PAGE__:${arg}` : 'Usage: open '; } diff --git a/src/commands/pages.js b/src/commands/pages.js new file mode 100644 index 0000000..61e45c0 --- /dev/null +++ b/src/commands/pages.js @@ -0,0 +1,3 @@ +import { color } from '../terminal.js'; +export const name = 'pages'; export const description = 'List available pages'; +export function run({ pageMap, config }) { return [...pageMap.values()].map(page => `${color(config.theme?.accent || 'yellow', page.name.padEnd(14))} ${page.title || page.name}`).join('\n') || 'No pages configured.'; } diff --git a/src/commands/quit.js b/src/commands/quit.js new file mode 100644 index 0000000..d26f1cd --- /dev/null +++ b/src/commands/quit.js @@ -0,0 +1,2 @@ +export const name = 'quit'; export const description = 'Disconnect'; +export function run({ ctx }) { ctx.channel?.end(); return ''; } diff --git a/src/commands/reload.js b/src/commands/reload.js new file mode 100644 index 0000000..184db01 --- /dev/null +++ b/src/commands/reload.js @@ -0,0 +1,2 @@ +export const name = 'reload'; export const description = 'Reload configuration'; export const admin = true; +export function run({ api }) { api.reload(); return 'Configuration reloaded.'; } diff --git a/src/commands/theme.js b/src/commands/theme.js new file mode 100644 index 0000000..cf8c142 --- /dev/null +++ b/src/commands/theme.js @@ -0,0 +1,2 @@ +export const name = 'theme'; export const description = 'Set runtime primary color'; export const admin = true; +export function run({ arg, config }) { if (!arg) return 'Usage: theme '; config.theme.primary = arg; return `Primary color set to ${arg} (runtime only).`; } diff --git a/src/pages/about.js b/src/pages/about.js new file mode 100644 index 0000000..45bc76e --- /dev/null +++ b/src/pages/about.js @@ -0,0 +1,10 @@ +import { wrap } from '../terminal.js'; + +export const name = 'about'; +export const title = 'About'; + +// Edit this function to customize your About page. +export function render({ config }) { + const persona = config.persona || {}; + return wrap((persona.about || 'Tell visitors who you are.') + '\n\n' + (persona.bio || 'This is your space on the internet.')).join('\n'); +} diff --git a/src/pages/home.js b/src/pages/home.js new file mode 100644 index 0000000..75c0db8 --- /dev/null +++ b/src/pages/home.js @@ -0,0 +1,11 @@ +import { color, wrap } from '../terminal.js'; + +export const name = 'home'; +export const title = 'Home'; + +// Edit this function to customize the landing page. +export function render({ config, ownerOnline }) { + const p = config.persona || {}; + const status = ownerOnline() ? color('green', 'Owner is online') : color(config.theme?.muted || 'brightBlack', 'Owner is offline'); + return `${color(config.theme?.accent || 'yellow', `Welcome to ${p.name || 'my portfolio'}`)}\n\n${wrap(p.bio || 'You are exploring a portfolio over SSH. Type “help” to get started.').join('\n')}\n\n${status}\n\nCommands: home · pages · open · chat · contact · clear · quit`; +} diff --git a/src/pages/index.js b/src/pages/index.js new file mode 100644 index 0000000..4f1ce8a --- /dev/null +++ b/src/pages/index.js @@ -0,0 +1,8 @@ +import * as home from './home.js'; +import * as about from './about.js'; +import * as projects from './projects.js'; +import * as links from './links.js'; + +// Add new page modules to this registry to make them available as commands. +export const pages = [home, about, projects, links]; +export const pageMap = new Map(pages.map(page => [page.name, page])); diff --git a/src/pages/links.js b/src/pages/links.js new file mode 100644 index 0000000..96b00ca --- /dev/null +++ b/src/pages/links.js @@ -0,0 +1,10 @@ +export const name = 'links'; +export const title = 'Links'; + +// Edit these links directly, or move them into your config file. +export function render({ config }) { + return (config.pages?.links?.lines || [ + 'GitHub: https://github.com/yourname', + 'Website: https://example.com' + ]).join('\n'); +} diff --git a/src/pages/projects.js b/src/pages/projects.js new file mode 100644 index 0000000..b894fc6 --- /dev/null +++ b/src/pages/projects.js @@ -0,0 +1,10 @@ +export const name = 'projects'; +export const title = 'Projects'; + +// Replace these entries with your own work, or load them from config. +export function render({ config }) { + return (config.pages?.projects?.lines || [ + 'Project One — what it does', + 'Project Two — what you learned' + ]).join('\n'); +}