Compare commits
No commits in common. "6d05b6888f0e7fde0285949d021d57732a611650" and "cae2c1a9b1f44b0f472785586563ba90bec41741" have entirely different histories.
6d05b6888f
...
cae2c1a9b1
396
CLAUDE.md
396
CLAUDE.md
|
|
@ -1,319 +1,111 @@
|
||||||
---
|
---
|
||||||
description: NOSE Pluto - A Commodore 64-inspired web terminal with Bun-based shell
|
description: Use Bun instead of Node.js, npm, pnpm, or vite.
|
||||||
globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
|
globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
|
||||||
alwaysApply: true
|
alwaysApply: false
|
||||||
---
|
---
|
||||||
|
|
||||||
# NOSE Pluto
|
Default to using Bun instead of Node.js.
|
||||||
|
|
||||||
NOSE Pluto is a web-based terminal inspired by the Commodore 64, running on a Bun-based shell server. It hosts Val Town-style web apps for your home network, runs custom commands written in TypeScript, and lives on a Raspberry Pi 5.
|
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
||||||
|
- Use `bun test` instead of `jest` or `vitest`
|
||||||
|
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
||||||
|
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
||||||
|
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
||||||
|
- Bun automatically loads .env, so don't use dotenv.
|
||||||
|
|
||||||
## Architecture Overview
|
## APIs
|
||||||
|
|
||||||
- **Server**: Bun + Hono serving a C64-inspired terminal over WebSockets
|
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
||||||
- **Client**: Browser-based terminal UI with virtual 960×540 (16:9) screen that scales
|
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
||||||
- **Commands**: TypeScript files that run on the server and return output to the client
|
- `Bun.redis` for Redis. Don't use `ioredis`.
|
||||||
- **Projects**: Multi-project system with isolated commands, webapps, and state
|
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
||||||
- **Webapps**: Subdomain-based apps (e.g., `myapp.nose-pluto.local`)
|
- `WebSocket` is built-in. Don't use `ws`.
|
||||||
- **Games**: PICO-8-style game engine with `init()`, `update()`, `draw()` API
|
|
||||||
- **Session**: AsyncLocalStorage-based session management (1 browser tab = 1 session)
|
|
||||||
|
|
||||||
## Technology Stack
|
|
||||||
|
|
||||||
Use Bun exclusively instead of Node.js:
|
|
||||||
|
|
||||||
- `bun <file>` instead of `node <file>` or `ts-node <file>`
|
|
||||||
- `bun test` instead of `jest` or `vitest`
|
|
||||||
- `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
|
||||||
- `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
|
||||||
- `bun run <script>` instead of `npm run <script>`
|
|
||||||
- Bun automatically loads .env, so don't use dotenv
|
|
||||||
|
|
||||||
### Bun APIs
|
|
||||||
|
|
||||||
- `Bun.serve()` supports WebSockets, HTTPS, and routes (don't use `express`)
|
|
||||||
- `bun:sqlite` for SQLite (don't use `better-sqlite3`)
|
|
||||||
- `Bun.redis` for Redis (don't use `ioredis`)
|
|
||||||
- `Bun.sql` for Postgres (don't use `pg` or `postgres.js`)
|
|
||||||
- `WebSocket` is built-in (don't use `ws`)
|
|
||||||
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
||||||
- `Bun.$\`ls\`` instead of execa
|
- Bun.$`ls` instead of execa.
|
||||||
|
|
||||||
## Key Concepts
|
|
||||||
|
|
||||||
### Commands
|
|
||||||
|
|
||||||
Commands are TypeScript files in `bin/` directories that export a `default` function:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// bin/hello.ts
|
|
||||||
export default async function (name: string): Promise<string> {
|
|
||||||
return `Hello, ${name}!`
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Commands can return:
|
|
||||||
- `string` - plain text output
|
|
||||||
- `{ text: string, script?: string }` - text with optional client-side JS
|
|
||||||
- `{ html: string, script?: string }` - HTML with optional client-side JS
|
|
||||||
- `{ script: string }` - client-side JS only
|
|
||||||
- `{ error: string }` - error message
|
|
||||||
- `{ game: string }` - launch a game
|
|
||||||
|
|
||||||
Commands can also `throw` to display an error.
|
|
||||||
|
|
||||||
**Command Locations** (searched in order):
|
|
||||||
1. `~/nose/<project>/bin/` - project-specific commands
|
|
||||||
2. `~/nose/root/bin/` - global user commands (NOSE_ROOT_BIN)
|
|
||||||
3. `./bin/` - built-in system commands (NOSE_BIN)
|
|
||||||
|
|
||||||
**Documentation**: Commands use `//` comments at the top of the file for inline help:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// This command does something cool.
|
|
||||||
// Usage: mycmd <arg1> <arg2>
|
|
||||||
|
|
||||||
export default async function (arg1: string, arg2: string) {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Use `/// <reference lib="dom" />` on the first line to mark commands that run in the browser.
|
|
||||||
|
|
||||||
### Projects
|
|
||||||
|
|
||||||
Projects live in `~/nose/<project-name>/` and have:
|
|
||||||
- `bin/` - project-specific commands
|
|
||||||
- `index.ts` or `index.tsx` - webapp entry point (optional)
|
|
||||||
- `pub/` - static files served at subdomain (optional)
|
|
||||||
|
|
||||||
The `root` project is special - it's the default project and its `bin/` contains global commands.
|
|
||||||
|
|
||||||
**Project Structure**:
|
|
||||||
```
|
|
||||||
~/nose/
|
|
||||||
root/ # default project
|
|
||||||
bin/ # global commands
|
|
||||||
myproject/ # custom project
|
|
||||||
bin/ # project commands
|
|
||||||
index.ts # webapp (served at myproject.nose-pluto.local)
|
|
||||||
pub/ # static files
|
|
||||||
```
|
|
||||||
|
|
||||||
### Webapps
|
|
||||||
|
|
||||||
Webapps are served on subdomains (e.g., `myapp.nose-pluto.local`). They can:
|
|
||||||
- Export a `default` Hono app for dynamic routes
|
|
||||||
- Export a `default` function that returns a Response, string, or JSX
|
|
||||||
- Serve static files from `pub/` directory
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// ~/nose/myapp/index.ts
|
|
||||||
import { Hono } from "hono"
|
|
||||||
|
|
||||||
const app = new Hono()
|
|
||||||
|
|
||||||
app.get("/", c => c.text("Hello from myapp!"))
|
|
||||||
|
|
||||||
export default app
|
|
||||||
```
|
|
||||||
|
|
||||||
Static files in `pub/` are served automatically (e.g., `pub/index.html` → `myapp.nose-pluto.local/`).
|
|
||||||
|
|
||||||
### Games
|
|
||||||
|
|
||||||
Games are commands with `export const game = true` and three exports:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
/// <reference lib="dom" />
|
|
||||||
export const game = true
|
|
||||||
|
|
||||||
import type { GameContext, InputState } from "@/shared/game"
|
|
||||||
|
|
||||||
export function init() {
|
|
||||||
// Initialize game state
|
|
||||||
}
|
|
||||||
|
|
||||||
export function update(delta: number, input: InputState) {
|
|
||||||
// Update game logic (60 FPS)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function draw(game: GameContext) {
|
|
||||||
// Draw to 960×540 canvas
|
|
||||||
game.clear()
|
|
||||||
game.rectfill(0, 0, 100, 100, "red")
|
|
||||||
game.text("Score: 0", 10, 10, "white")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**GameContext API**: `clear()`, `text()`, `centerText()`, `rect()`, `rectfill()`, `circ()`, `circfill()`, `line()`, `oval()`, `ovalfill()`, `rrect()`, `rrectfill()`, `trianglefill()`, `polygonfill()`
|
|
||||||
|
|
||||||
**InputState**: Tracks keyboard/gamepad with `pressed`, `prevPressed`, `justPressed`, `justReleased` Sets.
|
|
||||||
|
|
||||||
### Session Management
|
|
||||||
|
|
||||||
Sessions use AsyncLocalStorage for context isolation:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { sessionGet, sessionSet } from "@/session"
|
|
||||||
|
|
||||||
// Get session data
|
|
||||||
const project = sessionGet("project")
|
|
||||||
const cwd = sessionGet("cwd")
|
|
||||||
|
|
||||||
// Set session data (auto-syncs to client)
|
|
||||||
sessionSet("project", "myproject")
|
|
||||||
sessionSet("cwd", "/some/path")
|
|
||||||
```
|
|
||||||
|
|
||||||
Session keys: `sessionId`, `taskId`, `project`, `cwd`, `ws`
|
|
||||||
|
|
||||||
## File Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
server.tsx # Main Hono server
|
|
||||||
websocket.ts # WebSocket connection management
|
|
||||||
dispatch.ts # Client→server message router
|
|
||||||
shell.ts # Command execution (server-side)
|
|
||||||
commands.ts # Command discovery and loading
|
|
||||||
session.ts # Session management (AsyncLocalStorage)
|
|
||||||
project.ts # Project helpers
|
|
||||||
webapp.ts # Webapp hosting
|
|
||||||
state.ts # Persistent state storage
|
|
||||||
config.ts # Environment configuration
|
|
||||||
|
|
||||||
html/
|
|
||||||
layout.tsx # Base HTML layout
|
|
||||||
terminal.tsx # Terminal UI components
|
|
||||||
|
|
||||||
js/
|
|
||||||
main.ts # Client entry point
|
|
||||||
websocket.ts # WebSocket client
|
|
||||||
shell.ts # Command execution (client-side)
|
|
||||||
scrollback.ts # Terminal scrollback buffer
|
|
||||||
input.ts # Input handling
|
|
||||||
completion.ts # Tab completion
|
|
||||||
history.ts # Command history
|
|
||||||
editor.ts # File editor
|
|
||||||
game.ts # Game engine client
|
|
||||||
browser.ts # Embedded browser
|
|
||||||
|
|
||||||
shared/
|
|
||||||
types.ts # Shared TypeScript types
|
|
||||||
game.ts # Game engine types and context
|
|
||||||
utils.ts # Shared utilities
|
|
||||||
|
|
||||||
bin/ # Built-in commands
|
|
||||||
public/ # Static assets (CSS, fonts, images)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development Workflow
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Local development with hot reload
|
|
||||||
bun dev
|
|
||||||
|
|
||||||
# Production mode (no DNS)
|
|
||||||
bun prod-nodns
|
|
||||||
|
|
||||||
# Production mode (with DNS)
|
|
||||||
bun prod
|
|
||||||
|
|
||||||
# Type checking
|
|
||||||
bun check
|
|
||||||
|
|
||||||
# Deploy to Raspberry Pi
|
|
||||||
bun deploy
|
|
||||||
```
|
|
||||||
|
|
||||||
**Environment Variables**:
|
|
||||||
- `NOSE_DIR` - Directory for projects (default: `~/nose`)
|
|
||||||
- `NODE_ENV` - Set to `production` for production mode
|
|
||||||
- `NO_DNS` - Disable DNS features in production
|
|
||||||
- `BUN_HOT` - Enable hot reload (set by `bun dev`)
|
|
||||||
|
|
||||||
## Important Patterns
|
|
||||||
|
|
||||||
### Path Aliases
|
|
||||||
|
|
||||||
Use `@/` for imports from `src/` when writing commands:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { sessionGet } from "@/session"
|
|
||||||
import { commands } from "@/commands"
|
|
||||||
import type { Message } from "@/shared/types"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Command Patterns
|
|
||||||
|
|
||||||
**Simple text command**:
|
|
||||||
```ts
|
|
||||||
export default function () {
|
|
||||||
return "Hello, world!"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**HTML output**:
|
|
||||||
```ts
|
|
||||||
export default function () {
|
|
||||||
return { html: "<h1>Hello</h1>" }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**JSX output** (auto-converted):
|
|
||||||
```tsx
|
|
||||||
export default function () {
|
|
||||||
return <h1>Hello</h1>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Client-side script**:
|
|
||||||
```ts
|
|
||||||
export default function () {
|
|
||||||
return {
|
|
||||||
text: "Running script...",
|
|
||||||
script: "alert('Hello from browser!')"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### WebSocket Messages
|
|
||||||
|
|
||||||
Messages between client and server use typed message objects:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// Client → Server
|
|
||||||
{ type: "input", id: "abc123", session: "0", data: "ls" }
|
|
||||||
{ type: "save-file", id: "/path/to/file", session: "0", data: "content" }
|
|
||||||
{ type: "session:update", data: { project: "myproject" } }
|
|
||||||
|
|
||||||
// Server → Client
|
|
||||||
{ type: "output", id: "abc123", data: { status: "ok", output: "result" } }
|
|
||||||
{ type: "commands", data: ["ls", "cd", "help"] }
|
|
||||||
{ type: "apps", data: ["myapp", "otherapp"] }
|
|
||||||
{ type: "game:start", id: "abc123", data: "snake" }
|
|
||||||
{ type: "session:start", data: { NOSE_DIR, project, cwd, mode, hostname } }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
Use `bun test`:
|
Use `bun test` to run tests.
|
||||||
|
|
||||||
```ts
|
```ts#index.test.ts
|
||||||
import { test, expect } from "bun:test"
|
import { test, expect } from "bun:test";
|
||||||
|
|
||||||
test("command output", () => {
|
test("hello world", () => {
|
||||||
expect(1).toBe(1)
|
expect(1).toBe(1);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
||||||
|
|
||||||
|
Server:
|
||||||
|
|
||||||
|
```ts#index.ts
|
||||||
|
import index from "./index.html"
|
||||||
|
|
||||||
|
Bun.serve({
|
||||||
|
routes: {
|
||||||
|
"/": index,
|
||||||
|
"/api/users/:id": {
|
||||||
|
GET: (req) => {
|
||||||
|
return new Response(JSON.stringify({ id: req.params.id }));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// optional websocket support
|
||||||
|
websocket: {
|
||||||
|
open: (ws) => {
|
||||||
|
ws.send("Hello, world!");
|
||||||
|
},
|
||||||
|
message: (ws, message) => {
|
||||||
|
ws.send(message);
|
||||||
|
},
|
||||||
|
close: (ws) => {
|
||||||
|
// handle close
|
||||||
|
}
|
||||||
|
},
|
||||||
|
development: {
|
||||||
|
hmr: true,
|
||||||
|
console: true,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
## Notes
|
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
||||||
|
|
||||||
- Virtual screen size is fixed at 960×540 (16:9) in "cinema" mode and scales to fit the display
|
```html#index.html
|
||||||
- Virtual screen size is fixed at 960x100%vh in "tall" mode and scales to fit the display
|
<html>
|
||||||
- Font: C64 Pro Mono (see `public/vendor/`)
|
<body>
|
||||||
- Commands are hot-reloaded when files change
|
<h1>Hello, world!</h1>
|
||||||
- WebSocket connections are managed per-session (1 tab = 1 session)
|
<script type="module" src="./frontend.tsx"></script>
|
||||||
- Session state persists across reconnects
|
</body>
|
||||||
- The `help` command extracts documentation from `//` comments in commands
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
With the following `frontend.tsx`:
|
||||||
|
|
||||||
|
```tsx#frontend.tsx
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// import .css files directly and it works
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
|
const root = createRoot(document.body);
|
||||||
|
|
||||||
|
export default function Frontend() {
|
||||||
|
return <h1>Hello, world!</h1>;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.render(<Frontend />);
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, run index.ts
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bun --hot ./index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.
|
||||||
|
|
|
||||||
|
|
@ -82,11 +82,11 @@ https://wakamaifondue.com/
|
||||||
- [ ] game/bin/www cartridges
|
- [ ] game/bin/www cartridges
|
||||||
- [x] public tunnel for your NOSE webapps
|
- [x] public tunnel for your NOSE webapps
|
||||||
- [x] public tunnel lives through reboots
|
- [x] public tunnel lives through reboots
|
||||||
- [o] tunnel to the terminal -- Not doing this for now!
|
- [ ] tunnel to the terminal
|
||||||
- [x] web browser
|
- [x] web browser
|
||||||
- [x] remember your "mode"
|
- [x] remember your "mode"
|
||||||
- [x] status bar on terminal UX
|
- [x] status bar on terminal UX
|
||||||
- [x] quickly open the current webapp
|
- [ ] quickly open the current webapp
|
||||||
- [x] "project"-based rehaul
|
- [x] "project"-based rehaul
|
||||||
- [x] self updating NOSE server
|
- [x] self updating NOSE server
|
||||||
- [x] `pub/` static hosting in webapps
|
- [x] `pub/` static hosting in webapps
|
||||||
|
|
|
||||||
15
bin/help.ts
15
bin/help.ts
|
|
@ -2,13 +2,13 @@
|
||||||
//
|
//
|
||||||
// (Hopefully.)
|
// (Hopefully.)
|
||||||
|
|
||||||
import { commandPath, commands } from "@/commands"
|
import { commandPath } from "@/commands"
|
||||||
|
|
||||||
export default async function (cmd: string): Promise<string> {
|
export default async function (cmd: string) {
|
||||||
if (!cmd) return "usage: help <command>"
|
if (!cmd) return "usage: help <command>"
|
||||||
|
|
||||||
const path = commandPath(cmd)
|
const path = commandPath(cmd)
|
||||||
if (!path) { return matchingCommands(cmd) }
|
if (!path) throw `${cmd} not found`
|
||||||
|
|
||||||
const code = (await Bun.file(path).text()).split("\n")
|
const code = (await Bun.file(path).text()).split("\n")
|
||||||
let docs = []
|
let docs = []
|
||||||
|
|
@ -26,12 +26,3 @@ export default async function (cmd: string): Promise<string> {
|
||||||
|
|
||||||
return docs.join("\n")
|
return docs.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
async function matchingCommands(cmd: string): Promise<string> {
|
|
||||||
let matched: string[] = []
|
|
||||||
for (const command of Object.keys(await commands())) {
|
|
||||||
if (command.startsWith(cmd)) matched.push(command)
|
|
||||||
}
|
|
||||||
|
|
||||||
return matched.join(" ")
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
// Create a new project.
|
// Create a new project.
|
||||||
|
//
|
||||||
|
// We should probably rename this...
|
||||||
|
|
||||||
import { mkdirSync, writeFileSync } from "fs"
|
import { mkdirSync, writeFileSync } from "fs"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
|
|
@ -2,12 +2,12 @@
|
||||||
// Manages the commands on disk, in NOSE_ROOT_BIN and NOSE_BIN
|
// Manages the commands on disk, in NOSE_ROOT_BIN and NOSE_BIN
|
||||||
|
|
||||||
import { Glob } from "bun"
|
import { Glob } from "bun"
|
||||||
import { watch, readFileSync } from "fs"
|
import { watch } from "fs"
|
||||||
import { join, basename } from "path"
|
import { join } from "path"
|
||||||
import { isFile } from "./utils"
|
import { isFile } from "./utils"
|
||||||
import { sendAll } from "./websocket"
|
import { sendAll } from "./websocket"
|
||||||
import { expectDir } from "./utils"
|
import { expectDir } from "./utils"
|
||||||
import type { Command, Commands } from "./shared/types"
|
import { unique } from "./shared/utils"
|
||||||
import { projectBin, projectName } from "./project"
|
import { projectBin, projectName } from "./project"
|
||||||
import { DEFAULT_PROJECT, NOSE_DIR, NOSE_ROOT_BIN, NOSE_BIN } from "./config"
|
import { DEFAULT_PROJECT, NOSE_DIR, NOSE_ROOT_BIN, NOSE_BIN } from "./config"
|
||||||
|
|
||||||
|
|
@ -15,40 +15,24 @@ export function initCommands() {
|
||||||
startWatchers()
|
startWatchers()
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function commands(project = DEFAULT_PROJECT): Promise<Commands> {
|
export async function commands(project = DEFAULT_PROJECT): Promise<string[]> {
|
||||||
let binCmds = await findCommands(NOSE_BIN)
|
let cmds = (await findCommands(NOSE_BIN))
|
||||||
let rootCmds = await findCommands(NOSE_ROOT_BIN)
|
.concat(await findCommands(NOSE_ROOT_BIN))
|
||||||
let projCmds = project === DEFAULT_PROJECT ? new Map : await findCommands(projectBin())
|
|
||||||
|
|
||||||
return Object.fromEntries(
|
if (project !== DEFAULT_PROJECT)
|
||||||
[
|
cmds = cmds.concat(await findCommands(projectBin()))
|
||||||
...Object.entries(binCmds),
|
|
||||||
...Object.entries(rootCmds),
|
return unique(cmds).sort()
|
||||||
...Object.entries(projCmds)
|
|
||||||
]
|
|
||||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function findCommands(path: string): Promise<Commands> {
|
export async function findCommands(path: string): Promise<string[]> {
|
||||||
const glob = new Glob("**/*.{ts,tsx}")
|
const glob = new Glob("**/*.{ts,tsx}")
|
||||||
let obj: Commands = {}
|
let list: string[] = []
|
||||||
|
|
||||||
for await (const file of glob.scan(path))
|
for await (const file of glob.scan(path))
|
||||||
obj[file.replace(/\.tsx?$/, "")] = describeCommand(join(path, file))
|
list.push(file.replace(".tsx", "").replace(".ts", ""))
|
||||||
|
|
||||||
return obj
|
return list
|
||||||
}
|
|
||||||
|
|
||||||
function describeCommand(path: string): Command {
|
|
||||||
const code = readFileSync(path, "utf8")
|
|
||||||
let game = /^export const game = true$/mg.test(code)
|
|
||||||
let browser = /^\/\/\/ ?<reference lib=['"]dom['"]\s*\/mg>$/.test(code)
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: basename(path).replace(/\.tsx?$/, ""),
|
|
||||||
type: game ? "game" : browser ? "browser" : "server"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function commandPath(cmd: string): string | undefined {
|
export function commandPath(cmd: string): string | undefined {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
////
|
////
|
||||||
// temporary hack for browser commands
|
// temporary hack for browser commands
|
||||||
|
|
||||||
import type { CommandOutput, Commands, Command } from "../shared/types"
|
import type { CommandOutput } from "../shared/types"
|
||||||
import { openBrowser } from "./browser"
|
import { openBrowser } from "./browser"
|
||||||
import { scrollback, content } from "./dom"
|
import { scrollback, content } from "./dom"
|
||||||
import { focusInput } from "./focus"
|
import { focusInput } from "./focus"
|
||||||
|
|
@ -12,7 +12,7 @@ import { status } from "./statusbar"
|
||||||
import { setStatus, latestId } from "./scrollback"
|
import { setStatus, latestId } from "./scrollback"
|
||||||
import { currentAppUrl } from "./webapp"
|
import { currentAppUrl } from "./webapp"
|
||||||
|
|
||||||
export const commands: Commands = {}
|
export const commands: string[] = []
|
||||||
|
|
||||||
export const browserCommands: Record<string, (...args: string[]) => void | Promise<void> | CommandOutput> = {
|
export const browserCommands: Record<string, (...args: string[]) => void | Promise<void> | CommandOutput> = {
|
||||||
browse: (url?: string) => {
|
browse: (url?: string) => {
|
||||||
|
|
@ -27,7 +27,7 @@ export const browserCommands: Record<string, (...args: string[]) => void | Promi
|
||||||
"browser-session": () => sessionId,
|
"browser-session": () => sessionId,
|
||||||
clear: () => scrollback.innerHTML = "",
|
clear: () => scrollback.innerHTML = "",
|
||||||
commands: () => {
|
commands: () => {
|
||||||
return { html: "<div>" + Object.keys(commands).map(cmd => `<a href="#help ${cmd}">${cmd}</a>`).join("") + "</div>" }
|
return { html: "<div>" + commands.map(cmd => `<a href="#help ${cmd}">${cmd}</a>`).join("") + "</div>" }
|
||||||
},
|
},
|
||||||
fullscreen: () => document.body.requestFullscreen(),
|
fullscreen: () => document.body.requestFullscreen(),
|
||||||
mode: (mode?: string) => {
|
mode: (mode?: string) => {
|
||||||
|
|
@ -45,9 +45,9 @@ export const browserCommands: Record<string, (...args: string[]) => void | Promi
|
||||||
reload: () => window.location.reload(),
|
reload: () => window.location.reload(),
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cacheCommands(cmds: Commands) {
|
export function cacheCommands(cmds: string[]) {
|
||||||
for (const key in commands)
|
commands.length = 0
|
||||||
delete commands[key]
|
commands.push(...cmds)
|
||||||
|
commands.push(...Object.keys(browserCommands))
|
||||||
Object.assign(commands, cmds)
|
commands.sort()
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,7 @@ function handleCompletion(e: KeyboardEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const input = cmdInput.value
|
const input = cmdInput.value
|
||||||
|
|
||||||
for (const command of Object.keys(commands)) {
|
for (const command of commands) {
|
||||||
if (command.startsWith(input)) {
|
if (command.startsWith(input)) {
|
||||||
cmdInput.value = command
|
cmdInput.value = command
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
export type Message =
|
export type Message =
|
||||||
| ErrorMessage
|
| ErrorMessage
|
||||||
| InputMessage
|
| InputMessage
|
||||||
|
|
@ -29,7 +28,7 @@ export type ErrorMessage = {
|
||||||
|
|
||||||
export type CommandsMessage = {
|
export type CommandsMessage = {
|
||||||
type: "commands"
|
type: "commands"
|
||||||
data: Commands
|
data: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AppsMessage = {
|
export type AppsMessage = {
|
||||||
|
|
@ -85,10 +84,3 @@ export type StreamMessage = {
|
||||||
session: string
|
session: string
|
||||||
data: CommandOutput
|
data: CommandOutput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Commands = Record<string, Command>
|
|
||||||
|
|
||||||
export type Command = {
|
|
||||||
name: string
|
|
||||||
type: "server" | "browser" | "game"
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user