Compare commits
11 Commits
8bc8c7d96e
...
8a6e95a500
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a6e95a500 | ||
| ea7d538618 | |||
| bab54e74d7 | |||
| c8704cf8fd | |||
| 8bfb6f2105 | |||
| 4276363a37 | |||
| dda1cc1f21 | |||
| e88627693c | |||
| f9792eb31c | |||
| 89d850b55f | |||
| 07840aabd8 |
|
|
@ -22,6 +22,8 @@ And to make sure DNS is working:
|
||||||
|
|
||||||
## Local Dev
|
## Local Dev
|
||||||
|
|
||||||
|
Running the server will create `~/nose` for you to play with.
|
||||||
|
|
||||||
bun install
|
bun install
|
||||||
bun dev
|
bun dev
|
||||||
open localhost:3000
|
open localhost:3000
|
||||||
|
|
@ -79,10 +81,10 @@ https://wakamaifondue.com/
|
||||||
- [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
|
||||||
- [ ] tunnel to the terminal
|
- [ ] tunnel to the terminal
|
||||||
- [ ] remember your "mode"
|
- [x] remember your "mode"
|
||||||
- [ ] `nose` CLI
|
- [ ] `nose` CLI
|
||||||
- [ ] status bar on terminal UX
|
- [ ] status bar on terminal UX
|
||||||
- [ ] "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
|
||||||
- [x] upload files to projects
|
- [x] upload files to projects
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import { readdirSync } from "fs"
|
||||||
import { join, extname } from "path"
|
import { join, extname } from "path"
|
||||||
|
|
||||||
import type { CommandOutput } from "@/shared/types"
|
import type { CommandOutput } from "@/shared/types"
|
||||||
import { NOSE_WWW } from "@/config"
|
|
||||||
import { isBinaryFile } from "@/utils"
|
import { isBinaryFile } from "@/utils"
|
||||||
import { projectName, projectDir } from "@/project"
|
import { projectName, projectDir } from "@/project"
|
||||||
import { sessionGet } from "@/session"
|
import { sessionGet } from "@/session"
|
||||||
|
|
@ -21,10 +20,6 @@ export default async function (path: string) {
|
||||||
files.push(file.name)
|
files.push(file.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root === NOSE_WWW) {
|
|
||||||
files = files.filter(file => file.endsWith(`${project}.ts`) || file.endsWith(`${project}.tsx`))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!files.includes(path))
|
if (!files.includes(path))
|
||||||
return { error: `file not found: ${path}` }
|
return { error: `file not found: ${path}` }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import { readdirSync } from "fs"
|
||||||
import { join, extname } from "path"
|
import { join, extname } from "path"
|
||||||
|
|
||||||
import type { CommandOutput } from "@/shared/types"
|
import type { CommandOutput } from "@/shared/types"
|
||||||
import { NOSE_WWW } from "@/config"
|
|
||||||
import { isBinaryFile } from "@/utils"
|
import { isBinaryFile } from "@/utils"
|
||||||
import { countChar } from "@/shared/utils"
|
import { countChar } from "@/shared/utils"
|
||||||
import { projectName, projectDir } from "@/project"
|
import { projectName, projectDir } from "@/project"
|
||||||
|
|
@ -20,10 +19,6 @@ export default async function (path: string) {
|
||||||
files.push(file.name)
|
files.push(file.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root === NOSE_WWW) {
|
|
||||||
files = files.filter(file => file.endsWith(`${project}.ts`) || file.endsWith(`${project}.tsx`))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (files.includes(path))
|
if (files.includes(path))
|
||||||
return await readFile(join(root, path))
|
return await readFile(join(root, path))
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
//
|
//
|
||||||
// Show some debugging information.
|
// Show some debugging information.
|
||||||
|
|
||||||
import { NOSE_STARTED, NOSE_SYS_BIN, NOSE_DIR, GIT_SHA } from "@/config"
|
import { NOSE_STARTED, NOSE_ROOT_BIN, NOSE_BIN, NOSE_DATA, NOSE_DIR, GIT_SHA } from "@/config"
|
||||||
import { highlightToHTML } from "../lib/highlight"
|
import { highlightToHTML } from "../lib/highlight"
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
|
|
@ -14,7 +14,9 @@ export default function () {
|
||||||
`USER=${valueOrNone(process.env.USER)}`,
|
`USER=${valueOrNone(process.env.USER)}`,
|
||||||
`PWD=${valueOrNone(process.env.PWD)}`,
|
`PWD=${valueOrNone(process.env.PWD)}`,
|
||||||
`NOSE_STARTED=${NOSE_STARTED}`,
|
`NOSE_STARTED=${NOSE_STARTED}`,
|
||||||
`NOSE_SYS_BIN="${NOSE_SYS_BIN}"`,
|
`NOSE_BIN="${NOSE_BIN}"`,
|
||||||
|
`NOSE_ROOT_BIN="${NOSE_ROOT_BIN}"`,
|
||||||
|
`NOSE_DATA="${NOSE_DATA}"`,
|
||||||
`NOSE_DIR="${NOSE_DIR}"`,
|
`NOSE_DIR="${NOSE_DIR}"`,
|
||||||
`GIT_SHA="${GIT_SHA}"`,
|
`GIT_SHA="${GIT_SHA}"`,
|
||||||
].join("\n"))
|
].join("\n"))
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@
|
||||||
|
|
||||||
import { readdirSync } from "fs"
|
import { readdirSync } from "fs"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
import { NOSE_SYS_BIN } from "@/config"
|
import { NOSE_BIN } from "@/config"
|
||||||
|
|
||||||
export default async function () {
|
export default async function () {
|
||||||
let games = await Promise.all(readdirSync(NOSE_SYS_BIN, { withFileTypes: true }).map(async file => {
|
let games = await Promise.all(readdirSync(NOSE_BIN, { withFileTypes: true }).map(async file => {
|
||||||
if (!file.isFile()) return
|
if (!file.isFile()) return
|
||||||
|
|
||||||
const code = await Bun.file(join(NOSE_SYS_BIN, file.name)).text()
|
const code = await Bun.file(join(NOSE_BIN, file.name)).text()
|
||||||
|
|
||||||
if (/^export const game\s*=\s*true\s*;?\s*$/m.test(code))
|
if (/^export const game\s*=\s*true\s*;?\s*$/m.test(code))
|
||||||
return file.name.replace(".tsx", "").replace(".ts", "")
|
return file.name.replace(".tsx", "").replace(".ts", "")
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
// Look around.
|
// Look around.
|
||||||
|
|
||||||
import { readdirSync } from "fs"
|
import { readdirSync } from "fs"
|
||||||
import { NOSE_WWW } from "@/config"
|
|
||||||
import { projectName, projectDir } from "@/project"
|
import { projectName, projectDir } from "@/project"
|
||||||
import { sessionGet } from "@/session"
|
import { sessionGet } from "@/session"
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
const project = projectName()
|
|
||||||
const root = sessionGet("cwd") || projectDir()
|
const root = sessionGet("cwd") || projectDir()
|
||||||
|
|
||||||
let files: string[] = []
|
let files: string[] = []
|
||||||
|
|
@ -15,10 +13,6 @@ export default function () {
|
||||||
files.push(file.isDirectory() ? `${file.name}/` : file.name)
|
files.push(file.isDirectory() ? `${file.name}/` : file.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (root === NOSE_WWW) {
|
|
||||||
files = files.filter(file => file.endsWith(`${project}.ts`) || file.endsWith(`${project}.tsx`))
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
{root !== projectDir() && <a href="#cd ..;ls">..</a>}
|
{root !== projectDir() && <a href="#cd ..;ls">..</a>}
|
||||||
{files.map(file =>
|
{files.map(file =>
|
||||||
|
|
|
||||||
16
bin/new.ts
16
bin/new.ts
|
|
@ -4,23 +4,21 @@
|
||||||
|
|
||||||
import { mkdirSync, writeFileSync } from "fs"
|
import { mkdirSync, writeFileSync } from "fs"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
import { apps } from "@/webapp"
|
import { projects } from "@/project"
|
||||||
import { NOSE_WWW } from "@/config"
|
import { NOSE_DIR } from "@/config"
|
||||||
import { isDir } from "@/utils"
|
|
||||||
|
|
||||||
import load from "./load"
|
import load from "./load"
|
||||||
|
|
||||||
export default function (project: string) {
|
export default function (project: string) {
|
||||||
if (!project) throw "usage: new <project name>"
|
if (!project) throw "usage: new <project name>"
|
||||||
|
|
||||||
if (apps().includes(project)) throw `${project} already exists`
|
if (projects().includes(project)) throw `${project} already exists`
|
||||||
|
|
||||||
if (!isDir(NOSE_WWW)) throw `no www dir! make one in a real shell:\n$ mkdir -p ${NOSE_WWW}`
|
const dir = join(NOSE_DIR, project, "bin")
|
||||||
|
mkdirSync(dir, { recursive: true })
|
||||||
mkdirSync(join(NOSE_WWW, project))
|
writeFileSync(join(dir, `index.ts`), `export default (c: Context) =>\n "Hello, world!"`)
|
||||||
writeFileSync(join(NOSE_WWW, project, `index.ts`), `export default (c: Context) =>\n "Hello, world!"`)
|
|
||||||
|
|
||||||
load(project)
|
load(project)
|
||||||
|
|
||||||
return `created ${project}`
|
return `Created ${project}`
|
||||||
}
|
}
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
// Print the currently loaded project.
|
// Print the currently loaded project.
|
||||||
|
|
||||||
import { sessionGet } from "@/session"
|
import { projectName } from "@/project"
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
const state = sessionGet()
|
return projectName()
|
||||||
if (!state) return { error: "no state" }
|
|
||||||
|
|
||||||
return state?.project || "none"
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
// Show the projects on this NOSEputer.
|
// Show the projects on this NOSEputer.
|
||||||
|
|
||||||
import { apps } from "@/webapp"
|
import { projects, projectName } from "@/project"
|
||||||
import { sessionGet } from "@/session"
|
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
const state = sessionGet()
|
const project = projectName()
|
||||||
if (!state) return { error: "no state" }
|
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
{apps().map(app => <a href={`#load ${app}`} class={app === state.project ? "magenta" : ""}>{app}</a>)}
|
{projects().map(app => <a href={`#load ${app}`} class={app === project ? "magenta" : ""}>{app}</a>)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
|
// Reboot the whole computer! Careful!
|
||||||
export default async function reboot() {
|
export default async function reboot() {
|
||||||
setTimeout(async () => await Bun.$`reboot`, 1000)
|
setTimeout(async () => await Bun.$`reboot`, 1000)
|
||||||
console.log("REBOOTING...")
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text: "Rebooting... This will take about 10 seconds.",
|
text: "Rebooting... This will take about 10 seconds.",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
|
// Restart the NOSE server.
|
||||||
export default function restart() {
|
export default function restart() {
|
||||||
setTimeout(() => process.exit(), 1000)
|
setTimeout(() => process.exit(), 1000)
|
||||||
console.log("RESTARTING...")
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text: "Restarting... This will take a second or two.",
|
text: "Restarting... This will take a second or two.",
|
||||||
|
|
|
||||||
|
|
@ -65,9 +65,6 @@ export function draw(game: GameContext) {
|
||||||
const offsetX = (game.width - boardW) / 2
|
const offsetX = (game.width - boardW) / 2
|
||||||
const offsetY = (game.height - boardH) / 2
|
const offsetY = (game.height - boardH) / 2
|
||||||
|
|
||||||
console.log("X", offsetX)
|
|
||||||
console.log("Y", offsetY)
|
|
||||||
|
|
||||||
const c = game.ctx
|
const c = game.ctx
|
||||||
c.save()
|
c.save()
|
||||||
c.translate(offsetX, offsetY)
|
c.translate(offsetX, offsetY)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// The git sha for the running server.
|
||||||
import { GIT_SHA } from "@/config"
|
import { GIT_SHA } from "@/config"
|
||||||
export default function () {
|
export default function () {
|
||||||
return GIT_SHA
|
return GIT_SHA
|
||||||
|
|
|
||||||
3
nose/chris/bin/chris.ts
Normal file
3
nose/chris/bin/chris.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default function () {
|
||||||
|
return "chris works!"
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
3
nose/corey/bin/corey.ts
Normal file
3
nose/corey/bin/corey.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default function () {
|
||||||
|
return "corey works!"
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 150 KiB |
3
nose/hello/bin/hello.tsx
Normal file
3
nose/hello/bin/hello.tsx
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default function () {
|
||||||
|
return <h1>HELLO!</h1>
|
||||||
|
}
|
||||||
2
nose/root/bin/root.ts
Normal file
2
nose/root/bin/root.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export default () =>
|
||||||
|
"root online"
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
////
|
////
|
||||||
// Manages the commands on disk, in NOSE_SYS_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 } from "fs"
|
import { watch } from "fs"
|
||||||
|
|
@ -7,34 +7,49 @@ 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 { NOSE_SYS_BIN, NOSE_BIN } from "./config"
|
import { unique } from "./shared/utils"
|
||||||
|
import { projectBin, projectName } from "./project"
|
||||||
|
import { DEFAULT_PROJECT, NOSE_DIR, NOSE_ROOT_BIN, NOSE_BIN } from "./config"
|
||||||
|
|
||||||
export function initCommands() {
|
export function initCommands() {
|
||||||
startWatchers()
|
startWatchers()
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function commands(): Promise<string[]> {
|
export async function commands(project = DEFAULT_PROJECT): Promise<string[]> {
|
||||||
return (await findCommands(NOSE_SYS_BIN)).concat(await findCommands(NOSE_BIN))
|
let cmds = (await findCommands(NOSE_BIN))
|
||||||
|
.concat(await findCommands(NOSE_ROOT_BIN))
|
||||||
|
|
||||||
|
if (project !== DEFAULT_PROJECT)
|
||||||
|
cmds = cmds.concat(await findCommands(projectBin()))
|
||||||
|
|
||||||
|
return unique(cmds).sort()
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function findCommands(path: string): Promise<string[]> {
|
export async function findCommands(path: string): Promise<string[]> {
|
||||||
const glob = new Glob("**/*.{ts,tsx}")
|
const glob = new Glob("**/*.{ts,tsx}")
|
||||||
let list: string[] = []
|
let list: string[] = []
|
||||||
|
|
||||||
for await (const file of glob.scan(path)) {
|
for await (const file of glob.scan(path))
|
||||||
list.push(file.replace(".tsx", "").replace(".ts", ""))
|
list.push(file.replace(".tsx", "").replace(".ts", ""))
|
||||||
}
|
|
||||||
|
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
export function commandPath(cmd: string): string | undefined {
|
export function commandPath(cmd: string): string | undefined {
|
||||||
return [
|
let paths = [
|
||||||
join(NOSE_SYS_BIN, cmd + ".ts"),
|
|
||||||
join(NOSE_SYS_BIN, cmd + ".tsx"),
|
|
||||||
join(NOSE_BIN, cmd + ".ts"),
|
join(NOSE_BIN, cmd + ".ts"),
|
||||||
join(NOSE_BIN, cmd + ".tsx")
|
join(NOSE_BIN, cmd + ".tsx"),
|
||||||
].find((path: string) => isFile(path))
|
join(NOSE_ROOT_BIN, cmd + ".ts"),
|
||||||
|
join(NOSE_ROOT_BIN, cmd + ".tsx"),
|
||||||
|
]
|
||||||
|
|
||||||
|
if (projectName() !== DEFAULT_PROJECT)
|
||||||
|
paths = paths.concat(
|
||||||
|
join(projectBin(), cmd + ".ts"),
|
||||||
|
join(projectBin(), cmd + ".tsx"),
|
||||||
|
)
|
||||||
|
|
||||||
|
return paths.find((path: string) => isFile(path))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function commandExists(cmd: string): boolean {
|
export function commandExists(cmd: string): boolean {
|
||||||
|
|
@ -53,16 +68,17 @@ export async function loadCommandModule(cmd: string) {
|
||||||
return await import(path + "?t+" + Date.now())
|
return await import(path + "?t+" + Date.now())
|
||||||
}
|
}
|
||||||
|
|
||||||
let sysCmdWatcher
|
let noseDirWatcher
|
||||||
let usrCmdWatcher
|
let binCmdWatcher
|
||||||
function startWatchers() {
|
function startWatchers() {
|
||||||
if (!expectDir(NOSE_BIN)) return
|
if (!expectDir(NOSE_BIN)) return
|
||||||
|
if (!expectDir(NOSE_ROOT_BIN)) return
|
||||||
|
|
||||||
sysCmdWatcher = watch(NOSE_SYS_BIN, async (event, filename) =>
|
binCmdWatcher = watch(NOSE_BIN, async (event, filename) => {
|
||||||
sendAll({ type: "commands", data: await commands() })
|
|
||||||
)
|
|
||||||
|
|
||||||
usrCmdWatcher = watch(NOSE_BIN, async (event, filename) => {
|
|
||||||
sendAll({ type: "commands", data: await commands() })
|
sendAll({ type: "commands", data: await commands() })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
noseDirWatcher = watch(NOSE_DIR, async (event, filename) =>
|
||||||
|
sendAll({ type: "commands", data: await commands() })
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -4,13 +4,12 @@ import { untilde } from "./utils"
|
||||||
|
|
||||||
export const NOSE_ICON = ` ͡° ͜ʖ ͡°`
|
export const NOSE_ICON = ` ͡° ͜ʖ ͡°`
|
||||||
|
|
||||||
export const NOSE_SYS_BIN = resolve("./bin")
|
export const NOSE_BIN = resolve("./bin")
|
||||||
|
export const NOSE_DATA = resolve("./data")
|
||||||
|
|
||||||
export const NOSE_DIR = resolve(untilde(process.env.NOSE_DIR || "./nose"))
|
export const NOSE_DIR = resolve(untilde(process.env.NOSE_DIR || "./nose"))
|
||||||
export const NOSE_BIN = join(NOSE_DIR, "bin")
|
export const DEFAULT_PROJECT = "root"
|
||||||
export const NOSE_WWW = join(NOSE_DIR, "www")
|
export const NOSE_ROOT_BIN = join(NOSE_DIR, DEFAULT_PROJECT, "bin")
|
||||||
|
|
||||||
export const NOSE_DATA = resolve("./data")
|
|
||||||
|
|
||||||
export const NOSE_STARTED = Date.now()
|
export const NOSE_STARTED = Date.now()
|
||||||
export const GIT_SHA = (await $`git rev-parse --short HEAD`.text()).trim()
|
export const GIT_SHA = (await $`git rev-parse --short HEAD`.text()).trim()
|
||||||
|
|
@ -73,6 +73,7 @@
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--cyan);
|
color: var(--cyan);
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:visited {
|
a:visited {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { basename } from "path"
|
||||||
import type { Message } from "./shared/types"
|
import type { Message } from "./shared/types"
|
||||||
import { runCommand } from "./shell"
|
import { runCommand } from "./shell"
|
||||||
import { send } from "./websocket"
|
import { send } from "./websocket"
|
||||||
|
import { setState } from "./state"
|
||||||
|
|
||||||
export async function dispatchMessage(ws: any, msg: Message) {
|
export async function dispatchMessage(ws: any, msg: Message) {
|
||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
|
|
@ -14,6 +15,9 @@ export async function dispatchMessage(ws: any, msg: Message) {
|
||||||
case "save-file":
|
case "save-file":
|
||||||
await saveFileMessage(ws, msg); break
|
await saveFileMessage(ws, msg); break
|
||||||
|
|
||||||
|
case "ui:mode":
|
||||||
|
setState("ui:mode", msg.data); break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
send(ws, { type: "error", data: `unknown message: ${msg.type}` })
|
send(ws, { type: "error", data: `unknown message: ${msg.type}` })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
import { watch } from "fs"
|
import { watch } from "fs"
|
||||||
import { apps } from "./webapp"
|
import { apps } from "./webapp"
|
||||||
import { expectDir } from "./utils"
|
import { expectDir } from "./utils"
|
||||||
import { NOSE_WWW } from "./config"
|
import { NOSE_DIR } from "./config"
|
||||||
import { expectShellCmd } from "./utils"
|
import { expectShellCmd } from "./utils"
|
||||||
|
|
||||||
export const dnsEntries: Record<string, any> = {}
|
export const dnsEntries: Record<string, any> = {}
|
||||||
|
|
@ -47,11 +47,11 @@ export function publishAppDNS(app: string) {
|
||||||
return dnsEntries[app]
|
return dnsEntries[app]
|
||||||
}
|
}
|
||||||
|
|
||||||
let wwwWatcher
|
let dnsWatcher
|
||||||
function startWatcher() {
|
function startWatcher() {
|
||||||
if (!expectDir(NOSE_WWW)) return
|
if (!expectDir(NOSE_DIR)) return
|
||||||
|
|
||||||
wwwWatcher = watch(NOSE_WWW, (event, filename) => {
|
dnsWatcher = watch(NOSE_DIR, (event, filename) => {
|
||||||
const www = apps()
|
const www = apps()
|
||||||
www.forEach(publishAppDNS)
|
www.forEach(publishAppDNS)
|
||||||
for (const name in dnsEntries)
|
for (const name in dnsEntries)
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,6 @@ export function routes(def: Record<string, Handler>): Hono {
|
||||||
const method = parts[0] || "GET"
|
const method = parts[0] || "GET"
|
||||||
const path = parts[1] || "/"
|
const path = parts[1] || "/"
|
||||||
|
|
||||||
console.log(method, path, def[key])
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
app.on(method, path, async c => toResponse(await def[key](c)))
|
app.on(method, path, async c => toResponse(await def[key](c)))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ export const Layout: FC = async ({ children, title }) => (
|
||||||
</head>
|
</head>
|
||||||
<body data-mode="tall">
|
<body data-mode="tall">
|
||||||
<main>
|
<main>
|
||||||
<div id="content">
|
<div id="content" style="display:none">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,30 @@
|
||||||
////
|
////
|
||||||
// temporary hack for browser commands
|
// temporary hack for browser commands
|
||||||
|
|
||||||
import { scrollback } from "./dom.js"
|
import type { CommandOutput } from "../shared/types"
|
||||||
import { resize } from "./resize.js"
|
import { scrollback, content } from "./dom"
|
||||||
import { autoScroll } from "./scrollback.js"
|
import { resize } from "./resize"
|
||||||
import { sessionId } from "./session.js"
|
import { autoScroll } from "./scrollback"
|
||||||
|
import { sessionId } from "./session"
|
||||||
|
import { send } from "./websocket"
|
||||||
|
|
||||||
export const commands: string[] = []
|
export const commands: string[] = []
|
||||||
|
|
||||||
export const browserCommands: Record<string, () => any> = {
|
export const browserCommands: Record<string, (...args: string[]) => void | Promise<void> | CommandOutput> = {
|
||||||
"browser-session": () => sessionId,
|
"browser-session": () => sessionId,
|
||||||
clear: () => scrollback.innerHTML = "",
|
clear: () => scrollback.innerHTML = "",
|
||||||
commands: () => commands.join(" "),
|
commands: () => {
|
||||||
|
return { html: "<div>" + commands.map(cmd => `<a href="#help ${cmd}">${cmd}</a>`).join("") + "</div>" }
|
||||||
|
},
|
||||||
fullscreen: () => document.body.requestFullscreen(),
|
fullscreen: () => document.body.requestFullscreen(),
|
||||||
mode: () => {
|
mode: (mode?: string) => {
|
||||||
document.body.dataset.mode = document.body.dataset.mode === "tall" ? "cinema" : "tall"
|
if (!mode) {
|
||||||
|
mode = document.body.dataset.mode === "tall" ? "cinema" : "tall"
|
||||||
|
send({ type: "ui:mode", data: mode })
|
||||||
|
}
|
||||||
|
|
||||||
|
content.style.display = ""
|
||||||
|
document.body.dataset.mode = mode
|
||||||
resize()
|
resize()
|
||||||
autoScroll()
|
autoScroll()
|
||||||
},
|
},
|
||||||
|
|
@ -26,4 +36,5 @@ export function cacheCommands(cmds: string[]) {
|
||||||
commands.push(...cmds)
|
commands.push(...cmds)
|
||||||
commands.push(...Object.keys(browserCommands))
|
commands.push(...Object.keys(browserCommands))
|
||||||
commands.sort()
|
commands.sort()
|
||||||
|
console.log("CMDS", commands)
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
////
|
////
|
||||||
// tab completion
|
// tab completion
|
||||||
|
|
||||||
import { cmdInput } from "./dom.js"
|
import { cmdInput } from "./dom"
|
||||||
import { commands } from "./commands.js"
|
import { commands } from "./commands"
|
||||||
|
|
||||||
export function initCompletion() {
|
export function initCompletion() {
|
||||||
cmdInput.addEventListener("keydown", handleCompletion)
|
cmdInput.addEventListener("keydown", handleCompletion)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
////
|
////
|
||||||
// Blinking c64 cursor
|
// Blinking c64 cursor
|
||||||
|
|
||||||
import { cmdInput, $ } from "./dom.js"
|
import { cmdInput, $ } from "./dom"
|
||||||
|
|
||||||
const cursor = "Û"
|
const cursor = "Û"
|
||||||
let cmdCursor: HTMLTextAreaElement
|
let cmdCursor: HTMLTextAreaElement
|
||||||
|
|
|
||||||
32
src/js/dispatch.ts
Normal file
32
src/js/dispatch.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import type { Message } from "@/shared/types"
|
||||||
|
import { cacheCommands } from "./commands"
|
||||||
|
import { handleOutput } from "./scrollback"
|
||||||
|
import { handleStreamStart, handleStreamAppend, handleStreamReplace, handleStreamEnd } from "./stream"
|
||||||
|
import { handleGameStart } from "./game"
|
||||||
|
import { browserCommands } from "./commands"
|
||||||
|
|
||||||
|
// message received from server
|
||||||
|
export async function dispatchMessage(msg: Message) {
|
||||||
|
switch (msg.type) {
|
||||||
|
case "output":
|
||||||
|
handleOutput(msg); break
|
||||||
|
case "commands":
|
||||||
|
cacheCommands(msg.data as string[]); break
|
||||||
|
case "error":
|
||||||
|
console.error(msg.data); break
|
||||||
|
case "stream:start":
|
||||||
|
handleStreamStart(msg); break
|
||||||
|
case "stream:end":
|
||||||
|
handleStreamEnd(msg); break
|
||||||
|
case "stream:append":
|
||||||
|
handleStreamAppend(msg); break
|
||||||
|
case "stream:replace":
|
||||||
|
handleStreamReplace(msg); break
|
||||||
|
case "game:start":
|
||||||
|
await handleGameStart(msg); break
|
||||||
|
case "ui:mode":
|
||||||
|
browserCommands.mode?.(msg.data as string); break
|
||||||
|
default:
|
||||||
|
console.error("unknown message type", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// DOM helpers and cached elements
|
// DOM helpers and cached elements
|
||||||
|
|
||||||
// elements we know will be there... right?
|
// elements we know will be there... right?
|
||||||
|
export const content = $("content") as HTMLDivElement
|
||||||
export const cmdLine = $("command-line") as HTMLDivElement
|
export const cmdLine = $("command-line") as HTMLDivElement
|
||||||
export const cmdInput = $("command-textbox") as HTMLTextAreaElement
|
export const cmdInput = $("command-textbox") as HTMLTextAreaElement
|
||||||
export const scrollback = $("scrollback") as HTMLUListElement
|
export const scrollback = $("scrollback") as HTMLUListElement
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
////
|
||||||
|
// Hack... works with the `upload` command.
|
||||||
|
// Dragging a file to the terminal fills in the <input type="file"/> on screen.
|
||||||
|
|
||||||
export function initDrop() {
|
export function initDrop() {
|
||||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||||
document.body.addEventListener(eventName, preventDefaults, false);
|
document.body.addEventListener(eventName, preventDefaults, false);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { scrollback } from "./dom.js"
|
import { scrollback } from "./dom"
|
||||||
import { send } from "./websocket.js"
|
import { send } from "./websocket"
|
||||||
import { focusInput } from "./focus.js"
|
import { focusInput } from "./focus"
|
||||||
|
|
||||||
const INDENT_SIZE = 2
|
const INDENT_SIZE = 2
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
////
|
////
|
||||||
// We try to keep the command textbox focused at all times.
|
// We try to keep the command textbox focused at all times.
|
||||||
|
|
||||||
import { cmdInput } from "./dom.js"
|
import { cmdInput } from "./dom"
|
||||||
|
|
||||||
export function initFocus() {
|
export function initFocus() {
|
||||||
window.addEventListener("click", focusHandler)
|
window.addEventListener("click", focusHandler)
|
||||||
focusInput()
|
setTimeout(() => focusInput(), 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function focusInput() {
|
export function focusInput() {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
////
|
////
|
||||||
// All forms are submitted via ajax.
|
// All forms are submitted via ajax.
|
||||||
|
|
||||||
import type { CommandResult, CommandOutput } from "../shared/types.js"
|
import type { CommandResult, CommandOutput } from "../shared/types"
|
||||||
import { sessionId } from "./session.js"
|
import { sessionId } from "./session"
|
||||||
import { setStatus, replaceOutput } from "./scrollback.js"
|
import { setStatus, replaceOutput } from "./scrollback"
|
||||||
import { focusInput } from "./focus.js"
|
import { focusInput } from "./focus"
|
||||||
|
|
||||||
export function initForm() {
|
export function initForm() {
|
||||||
document.addEventListener("submit", submitHandler)
|
document.addEventListener("submit", submitHandler)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import type { Message } from "../shared/types.js"
|
import type { Message } from "../shared/types"
|
||||||
import { GameContext, type InputState } from "../shared/game.js"
|
import { GameContext, type InputState } from "../shared/game"
|
||||||
import { focusInput } from "./focus.js"
|
import { focusInput } from "./focus"
|
||||||
import { $$ } from "./dom.js"
|
import { $$ } from "./dom"
|
||||||
import { randomId } from "../shared/utils.js"
|
import { randomId } from "../shared/utils"
|
||||||
import { setStatus, addOutput, insert } from "./scrollback.js"
|
import { setStatus, addOutput, insert } from "./scrollback"
|
||||||
import { browserCommands } from "./commands.js"
|
import { browserCommands } from "./commands"
|
||||||
|
|
||||||
const FPS = 30
|
const FPS = 30
|
||||||
const HEIGHT = 540
|
const HEIGHT = 540
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
////
|
////
|
||||||
// Command input history storage and navigation.
|
// Command input history storage and navigation.
|
||||||
|
|
||||||
import { cmdInput, cmdLine } from "./dom.js"
|
import { cmdInput, cmdLine } from "./dom"
|
||||||
|
|
||||||
const history: string[] = ["one", "two", "three"]
|
const history: string[] = ["one", "two", "three"]
|
||||||
let idx = -1
|
let idx = -1
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { runCommand } from "./shell.js"
|
import { runCommand } from "./shell"
|
||||||
import { focusInput } from "./focus.js"
|
import { focusInput } from "./focus"
|
||||||
|
|
||||||
export function initHyperlink() {
|
export function initHyperlink() {
|
||||||
window.addEventListener("click", handleClick)
|
window.addEventListener("click", handleClick)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClick(e: MouseEvent) {
|
async function handleClick(e: MouseEvent) {
|
||||||
const target = e.target
|
const target = e.target
|
||||||
|
|
||||||
if (!(target instanceof HTMLElement)) return
|
if (!(target instanceof HTMLElement)) return
|
||||||
|
|
@ -18,7 +18,7 @@ function handleClick(e: MouseEvent) {
|
||||||
|
|
||||||
if (href.startsWith("#")) {
|
if (href.startsWith("#")) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
runCommand(href.slice(1))
|
await runCommand(href.slice(1))
|
||||||
focusInput()
|
focusInput()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,17 +1,16 @@
|
||||||
////
|
////
|
||||||
// Terminal input is handled by a <textarea> and friends.
|
// Terminal input is handled by a <textarea> and friends.
|
||||||
|
|
||||||
import { cmdInput, cmdLine } from "./dom.js"
|
import { cmdInput, cmdLine } from "./dom"
|
||||||
import { runCommand } from "./shell.js"
|
import { runCommand } from "./shell"
|
||||||
import { resetHistory } from "./history.js"
|
import { resetHistory } from "./history"
|
||||||
import { countChar } from "../shared/utils.js"
|
|
||||||
|
|
||||||
export function initInput() {
|
export function initInput() {
|
||||||
cmdInput.addEventListener("keydown", inputHandler)
|
cmdInput.addEventListener("keydown", inputHandler)
|
||||||
cmdInput.addEventListener("paste", pasteHandler)
|
cmdInput.addEventListener("paste", pasteHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
function inputHandler(event: KeyboardEvent) {
|
async function inputHandler(event: KeyboardEvent) {
|
||||||
const target = event.target as HTMLElement
|
const target = event.target as HTMLElement
|
||||||
if (target?.id !== cmdInput.id) return
|
if (target?.id !== cmdInput.id) return
|
||||||
|
|
||||||
|
|
@ -25,7 +24,7 @@ function inputHandler(event: KeyboardEvent) {
|
||||||
cmdLine.dataset.extended = "true"
|
cmdLine.dataset.extended = "true"
|
||||||
} else if (event.key === "Enter") {
|
} else if (event.key === "Enter") {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
runCommand(cmdInput.value)
|
await runCommand(cmdInput.value)
|
||||||
clearInput()
|
clearInput()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
import { initCompletion } from "./completion.js"
|
import { initCompletion } from "./completion"
|
||||||
import { initCursor } from "./cursor.js"
|
import { initCursor } from "./cursor"
|
||||||
import { initDrop } from "./drop.js"
|
import { initDrop } from "./drop"
|
||||||
import { initEditor } from "./editor.js"
|
import { initEditor } from "./editor"
|
||||||
import { initFocus } from "./focus.js"
|
import { initFocus } from "./focus"
|
||||||
import { initForm } from "./form.js"
|
import { initForm } from "./form"
|
||||||
import { initGamepad } from "./gamepad.js"
|
import { initGamepad } from "./gamepad"
|
||||||
import { initHistory } from "./history.js"
|
import { initHistory } from "./history"
|
||||||
import { initHyperlink } from "./hyperlink.js"
|
import { initHyperlink } from "./hyperlink"
|
||||||
import { initInput } from "./input.js"
|
import { initInput } from "./input"
|
||||||
import { initResize } from "./resize.js"
|
import { initResize } from "./resize"
|
||||||
import { initScrollback } from "./scrollback.js"
|
import { initScrollback } from "./scrollback"
|
||||||
import { startVramCounter } from "./vram.js"
|
import { startVramCounter } from "./vram"
|
||||||
import { startConnection } from "./websocket.js"
|
import { startConnection } from "./websocket"
|
||||||
|
|
||||||
initCompletion()
|
initCompletion()
|
||||||
initCursor()
|
initCursor()
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
// The scrollback shows your history of interacting with the shell.
|
// The scrollback shows your history of interacting with the shell.
|
||||||
// input, output, etc
|
// input, output, etc
|
||||||
|
|
||||||
import type { CommandOutput } from "../shared/types.js"
|
import type { Message, CommandOutput, CommandResult } from "../shared/types"
|
||||||
import { scrollback, cmdInput, $$ } from "./dom.js"
|
import { scrollback, cmdInput, $$ } from "./dom"
|
||||||
import { randomId } from "../shared/utils.js"
|
import { randomId } from "../shared/utils"
|
||||||
|
|
||||||
type InputStatus = "waiting" | "streaming" | "ok" | "error"
|
type InputStatus = "waiting" | "streaming" | "ok" | "error"
|
||||||
|
|
||||||
|
|
@ -142,3 +142,9 @@ function handleInputClick(e: MouseEvent) {
|
||||||
cmdInput.value = target.textContent
|
cmdInput.value = target.textContent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function handleOutput(msg: Message) {
|
||||||
|
const result = msg.data as CommandResult
|
||||||
|
setStatus(msg.id!, result.status)
|
||||||
|
addOutput(msg.id!, result.output)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,6 @@
|
||||||
// Each browser tab is a shell session. This means you can run multiple sessions
|
// Each browser tab is a shell session. This means you can run multiple sessions
|
||||||
// in the same browser.
|
// in the same browser.
|
||||||
|
|
||||||
import { randomId } from "../shared/utils.js"
|
import { randomId } from "../shared/utils"
|
||||||
|
|
||||||
export const sessionId = randomId()
|
export const sessionId = randomId()
|
||||||
|
|
@ -1,19 +1,17 @@
|
||||||
////
|
////
|
||||||
// The shell runs on the server and processes input, returning output.
|
// The shell runs on the server and processes input, returning output.
|
||||||
|
|
||||||
import type { Message, CommandResult, CommandOutput } from "../shared/types.js"
|
import { addInput, setStatus, addOutput } from "./scrollback"
|
||||||
import { addInput, setStatus, addOutput, appendOutput, replaceOutput } from "./scrollback.js"
|
import { send } from "./websocket"
|
||||||
import { send } from "./websocket.js"
|
import { randomId } from "../shared/utils"
|
||||||
import { randomId } from "../shared/utils.js"
|
import { addToHistory } from "./history"
|
||||||
import { addToHistory } from "./history.js"
|
import { browserCommands } from "./commands"
|
||||||
import { browserCommands, cacheCommands } from "./commands.js"
|
|
||||||
import { handleGameStart } from "./game.js"
|
|
||||||
|
|
||||||
export function runCommand(input: string) {
|
export async function runCommand(input: string) {
|
||||||
if (!input.trim()) return
|
if (!input.trim()) return
|
||||||
|
|
||||||
if (input.includes(";")) {
|
if (input.includes(";")) {
|
||||||
input.split(";").forEach(cmd => runCommand(cmd.trim()))
|
input.split(";").forEach(async cmd => await runCommand(cmd.trim()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -22,68 +20,14 @@ export function runCommand(input: string) {
|
||||||
addToHistory(input)
|
addToHistory(input)
|
||||||
addInput(id, input)
|
addInput(id, input)
|
||||||
|
|
||||||
const [cmd = "", ..._args] = input.split(" ")
|
const [cmd = "", ...args] = input.split(" ")
|
||||||
|
|
||||||
if (browserCommands[cmd]) {
|
if (browserCommands[cmd]) {
|
||||||
const result = browserCommands[cmd]()
|
const result = await browserCommands[cmd](...args)
|
||||||
if (typeof result === "string")
|
if (result) addOutput(id, result)
|
||||||
addOutput(id, result)
|
|
||||||
setStatus(id, "ok")
|
setStatus(id, "ok")
|
||||||
} else {
|
} else {
|
||||||
send({ id, type: "input", data: input })
|
send({ id, type: "input", data: input })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// message received from server
|
|
||||||
export async function handleMessage(msg: Message) {
|
|
||||||
switch (msg.type) {
|
|
||||||
case "output":
|
|
||||||
handleOutput(msg); break
|
|
||||||
case "commands":
|
|
||||||
cacheCommands(msg.data as string[]); break
|
|
||||||
case "error":
|
|
||||||
console.error(msg.data); break
|
|
||||||
case "stream:start":
|
|
||||||
handleStreamStart(msg); break
|
|
||||||
case "stream:end":
|
|
||||||
handleStreamEnd(msg); break
|
|
||||||
case "stream:append":
|
|
||||||
handleStreamAppend(msg); break
|
|
||||||
case "stream:replace":
|
|
||||||
handleStreamReplace(msg); break
|
|
||||||
case "game:start":
|
|
||||||
await handleGameStart(msg); break
|
|
||||||
default:
|
|
||||||
console.error("unknown message type", msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleOutput(msg: Message) {
|
|
||||||
const result = msg.data as CommandResult
|
|
||||||
setStatus(msg.id!, result.status)
|
|
||||||
addOutput(msg.id!, result.output)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleStreamStart(msg: Message) {
|
|
||||||
const id = msg.id!
|
|
||||||
|
|
||||||
const status = document.querySelector(`[data-id="${id}"].input .status`)
|
|
||||||
if (!status) return
|
|
||||||
|
|
||||||
addOutput(id, msg.data as CommandOutput)
|
|
||||||
|
|
||||||
status.classList.remove("yellow")
|
|
||||||
status.classList.add("purple")
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleStreamAppend(msg: Message) {
|
|
||||||
appendOutput(msg.id!, msg.data as CommandOutput)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleStreamReplace(msg: Message) {
|
|
||||||
replaceOutput(msg.id!, msg.data as CommandOutput)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleStreamEnd(_msg: Message) {
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
||||||
25
src/js/stream.ts
Normal file
25
src/js/stream.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import type { Message, CommandOutput } from "@/shared/types"
|
||||||
|
import { addOutput, appendOutput, replaceOutput } from "./scrollback"
|
||||||
|
|
||||||
|
export function handleStreamStart(msg: Message) {
|
||||||
|
const id = msg.id!
|
||||||
|
|
||||||
|
const status = document.querySelector(`[data-id="${id}"].input .status`)
|
||||||
|
if (!status) return
|
||||||
|
|
||||||
|
addOutput(id, msg.data as CommandOutput)
|
||||||
|
|
||||||
|
status.classList.remove("yellow")
|
||||||
|
status.classList.add("purple")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleStreamAppend(msg: Message) {
|
||||||
|
appendOutput(msg.id!, msg.data as CommandOutput)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleStreamReplace(msg: Message) {
|
||||||
|
replaceOutput(msg.id!, msg.data as CommandOutput)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleStreamEnd(_msg: Message) {
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
////
|
////
|
||||||
// Fun vram counter at startup.
|
// Fun vram counter at startup.
|
||||||
|
|
||||||
import { $ } from "./dom.js"
|
import { $ } from "./dom"
|
||||||
|
|
||||||
const vramCounter = $("vram-size")!
|
const vramCounter = $("vram-size")!
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
////
|
////
|
||||||
// The terminal communicates with the shell via websockets.
|
// The terminal communicates with the shell via websockets.
|
||||||
|
|
||||||
import type { Message } from "../shared/types.js"
|
import type { Message } from "../shared/types"
|
||||||
import { sessionId } from "./session.js"
|
import { sessionId } from "./session"
|
||||||
import { handleMessage } from "./shell.js"
|
import { dispatchMessage } from "./dispatch"
|
||||||
import { addErrorMessage } from "./scrollback.js"
|
import { addErrorMessage } from "./scrollback"
|
||||||
|
|
||||||
const MAX_RETRIES = 5
|
const MAX_RETRIES = 5
|
||||||
let retries = 0
|
let retries = 0
|
||||||
|
|
@ -15,7 +15,7 @@ let ws: WebSocket | null = null
|
||||||
|
|
||||||
// open our websocket connection
|
// open our websocket connection
|
||||||
export function startConnection() {
|
export function startConnection() {
|
||||||
const url = new URL('/ws', location.href)
|
const url = new URL(`/ws?session=${sessionId}`, location.href)
|
||||||
url.protocol = url.protocol.replace('http', 'ws')
|
url.protocol = url.protocol.replace('http', 'ws')
|
||||||
ws = new WebSocket(url)
|
ws = new WebSocket(url)
|
||||||
|
|
||||||
|
|
@ -45,7 +45,7 @@ export function send(msg: Message) {
|
||||||
async function receive(e: MessageEvent) {
|
async function receive(e: MessageEvent) {
|
||||||
const data = JSON.parse(e.data) as Message
|
const data = JSON.parse(e.data) as Message
|
||||||
console.log("<- receive", data)
|
console.log("<- receive", data)
|
||||||
await handleMessage(data)
|
await dispatchMessage(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// close it... plz don't do this, though
|
// close it... plz don't do this, though
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,37 @@
|
||||||
////
|
////
|
||||||
// Helpers for working with projects in the CLI.
|
// Helpers for working with projects in the CLI.
|
||||||
|
|
||||||
|
import { join } from "path"
|
||||||
import { readdirSync, type Dirent } from "fs"
|
import { readdirSync, type Dirent } from "fs"
|
||||||
import { sessionGet } from "./session"
|
import { sessionGet } from "./session"
|
||||||
import { appDir } from "./webapp"
|
import { DEFAULT_PROJECT, NOSE_DIR } from "./config"
|
||||||
|
import { isDir } from "./utils"
|
||||||
|
|
||||||
export function projectName(): string {
|
export function projectName(): string {
|
||||||
const state = sessionGet()
|
const state = sessionGet()
|
||||||
if (!state) throw "no state"
|
if (!state) throw "no state"
|
||||||
|
|
||||||
const project = state.project
|
return state.project || DEFAULT_PROJECT
|
||||||
if (!project) throw "no project loaded"
|
|
||||||
|
|
||||||
return project
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function projectDir(): string {
|
export function projects(): string[] {
|
||||||
const root = appDir(projectName())
|
return readdirSync(NOSE_DIR, { withFileTypes: true })
|
||||||
if (!root) throw "error loading project"
|
.filter(file => file.isDirectory())
|
||||||
|
.map(dir => dir.name)
|
||||||
|
.sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function projectDir(name = projectName()): string {
|
||||||
|
const root = join(NOSE_DIR, name)
|
||||||
|
if (!isDir(root))
|
||||||
|
throw `no project found at ${root}`
|
||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|
||||||
export function projectFiles(): Dirent[] {
|
export function projectBin(name = projectName()): string {
|
||||||
return readdirSync(projectDir(), { recursive: true, withFileTypes: true })
|
return join(projectDir(), "bin")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function projectFiles(name = projectName()): Dirent[] {
|
||||||
|
return readdirSync(projectDir(name), { recursive: true, withFileTypes: true })
|
||||||
}
|
}
|
||||||
|
|
@ -7,7 +7,7 @@ import { prettyJSON } from "hono/pretty-json"
|
||||||
import color from "kleur"
|
import color from "kleur"
|
||||||
|
|
||||||
import type { Message } from "./shared/types"
|
import type { Message } from "./shared/types"
|
||||||
import { NOSE_ICON, NOSE_BIN, NOSE_WWW, NOSE_DATA, NOSE_DIR } from "./config"
|
import { NOSE_ICON, NOSE_BIN, NOSE_DATA, NOSE_DIR, NOSE_ROOT_BIN } from "./config"
|
||||||
import { transpile, isFile, tilde, isDir } from "./utils"
|
import { transpile, isFile, tilde, isDir } from "./utils"
|
||||||
import { serveApp } from "./webapp"
|
import { serveApp } from "./webapp"
|
||||||
import { commands, commandPath, loadCommandModule } from "./commands"
|
import { commands, commandPath, loadCommandModule } from "./commands"
|
||||||
|
|
@ -16,6 +16,7 @@ import { send, addWebsocket, removeWebsocket, closeWebsockets } from "./websocke
|
||||||
import { initSneakers, disconnectSneakers } from "./sneaker"
|
import { initSneakers, disconnectSneakers } from "./sneaker"
|
||||||
import { dispatchMessage } from "./dispatch"
|
import { dispatchMessage } from "./dispatch"
|
||||||
import { fatal } from "./fatal"
|
import { fatal } from "./fatal"
|
||||||
|
import { getState } from "./state"
|
||||||
|
|
||||||
import { Layout } from "./html/layout"
|
import { Layout } from "./html/layout"
|
||||||
import { Terminal } from "./html/terminal"
|
import { Terminal } from "./html/terminal"
|
||||||
|
|
@ -126,11 +127,16 @@ app.get("/", c => c.html(<Layout><Terminal /></Layout>))
|
||||||
// websocket
|
// websocket
|
||||||
//
|
//
|
||||||
|
|
||||||
app.get("/ws", upgradeWebSocket(async c => {
|
app.get("/ws", c => {
|
||||||
return {
|
const _sessionId = c.req.query("session")
|
||||||
|
|
||||||
|
return upgradeWebSocket(c, {
|
||||||
async onOpen(_e, ws) {
|
async onOpen(_e, ws) {
|
||||||
addWebsocket(ws)
|
addWebsocket(ws)
|
||||||
send(ws, { type: "commands", data: await commands() })
|
send(ws, { type: "commands", data: await commands() })
|
||||||
|
|
||||||
|
const mode = getState("ui:mode")
|
||||||
|
if (mode) send(ws, { type: "ui:mode", data: mode })
|
||||||
},
|
},
|
||||||
async onMessage(event, ws) {
|
async onMessage(event, ws) {
|
||||||
let data: Message | undefined
|
let data: Message | undefined
|
||||||
|
|
@ -148,8 +154,8 @@ app.get("/ws", upgradeWebSocket(async c => {
|
||||||
await dispatchMessage(ws, data)
|
await dispatchMessage(ws, data)
|
||||||
},
|
},
|
||||||
onClose: (event, ws) => removeWebsocket(ws)
|
onClose: (event, ws) => removeWebsocket(ws)
|
||||||
}
|
})
|
||||||
}))
|
})
|
||||||
|
|
||||||
//
|
//
|
||||||
// hot reload mode cleanup
|
// hot reload mode cleanup
|
||||||
|
|
@ -187,10 +193,10 @@ if (process.env.NODE_ENV === "production") {
|
||||||
//
|
//
|
||||||
|
|
||||||
console.log(color.cyan(NOSE_ICON))
|
console.log(color.cyan(NOSE_ICON))
|
||||||
|
console.log(color.blue(" NOSE_BIN:"), color.yellow(tilde(NOSE_BIN)))
|
||||||
console.log(color.blue(" NOSE_DATA:"), color.yellow(tilde(NOSE_DATA)))
|
console.log(color.blue(" NOSE_DATA:"), color.yellow(tilde(NOSE_DATA)))
|
||||||
console.log(color.blue(" NOSE_DIR:"), color.yellow(tilde(NOSE_DIR)))
|
console.log(color.blue(" NOSE_DIR:"), color.yellow(tilde(NOSE_DIR)))
|
||||||
console.log(color.blue("NOSE_BIN:"), color.yellow(tilde(NOSE_BIN)))
|
console.log(color.blue("NOSE_ROOT_BIN:"), color.yellow(tilde(NOSE_ROOT_BIN)))
|
||||||
console.log(color.blue("NOSE_WWW:"), color.yellow(tilde(NOSE_WWW)))
|
|
||||||
|
|
||||||
await initNoseDir()
|
await initNoseDir()
|
||||||
initCommands()
|
initCommands()
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,16 @@ export type Session = {
|
||||||
const g = globalThis as typeof globalThis & { __thread?: AsyncLocalStorage<Session> }
|
const g = globalThis as typeof globalThis & { __thread?: AsyncLocalStorage<Session> }
|
||||||
export const ALS = g.__thread ??= new AsyncLocalStorage<Session>()
|
export const ALS = g.__thread ??= new AsyncLocalStorage<Session>()
|
||||||
|
|
||||||
|
const sessions: Map<string, Session> = new Map()
|
||||||
|
|
||||||
|
export async function sessionRun(sessionId: string, fn: () => void | Promise<void>) {
|
||||||
|
const state = sessionStore(sessionId)
|
||||||
|
return await ALS.run(state, async () => fn())
|
||||||
|
}
|
||||||
|
|
||||||
export function sessionGet(key?: keyof Session): Session | any | undefined {
|
export function sessionGet(key?: keyof Session): Session | any | undefined {
|
||||||
const store = ALS.getStore()
|
const store = ALS.getStore()
|
||||||
if (!store) return
|
if (!store) throw "sessionGet() called outside of ALS.run"
|
||||||
|
|
||||||
if (key) return store[key]
|
if (key) return store[key]
|
||||||
|
|
||||||
|
|
@ -26,6 +33,18 @@ export function sessionGet(key?: keyof Session): Session | any | undefined {
|
||||||
|
|
||||||
export function sessionSet(key: keyof Session, value: any) {
|
export function sessionSet(key: keyof Session, value: any) {
|
||||||
const store = ALS.getStore()
|
const store = ALS.getStore()
|
||||||
if (!store) return
|
if (!store) throw "sessionSet() called outside of ALS.run"
|
||||||
store[key] = value
|
store[key] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sessionStore(sessionId: string, taskId?: string, ws?: any): Session {
|
||||||
|
let state = sessions.get(sessionId)
|
||||||
|
if (!state) {
|
||||||
|
state = { sessionId: sessionId, project: "" }
|
||||||
|
sessions.set(sessionId, state)
|
||||||
|
}
|
||||||
|
if (taskId)
|
||||||
|
state.taskId = taskId
|
||||||
|
if (ws) state.ws = ws
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ export type Message = {
|
||||||
export type MessageType = "error" | "input" | "output" | "commands" | "save-file"
|
export type MessageType = "error" | "input" | "output" | "commands" | "save-file"
|
||||||
| "game:start"
|
| "game:start"
|
||||||
| "stream:start" | "stream:end" | "stream:append" | "stream:replace"
|
| "stream:start" | "stream:end" | "stream:append" | "stream:replace"
|
||||||
|
| "ui:mode"
|
||||||
|
|
||||||
export type CommandOutput = string | string[]
|
export type CommandOutput = string | string[]
|
||||||
| { text: string, script?: string }
|
| { text: string, script?: string }
|
||||||
|
|
|
||||||
|
|
@ -34,3 +34,8 @@ export function randomIndex<T>(list: T[]): number | undefined {
|
||||||
if (!list.length) return
|
if (!list.length) return
|
||||||
return rng(0, list.length - 1)
|
return rng(0, list.length - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// unique([1,1,2,2,3,3]) #=> [1,2,3]
|
||||||
|
export function unique<T>(array: T[]): T[] {
|
||||||
|
return [...new Set(array)]
|
||||||
|
}
|
||||||
28
src/shell.ts
28
src/shell.ts
|
|
@ -2,21 +2,14 @@
|
||||||
// Runs commands and such on the server.
|
// Runs commands and such on the server.
|
||||||
// This is the "shell" - the "terminal" is the browser UI.
|
// This is the "shell" - the "terminal" is the browser UI.
|
||||||
|
|
||||||
import type { CommandResult, CommandOutput } from "./shared/types"
|
import type { CommandResult } from "./shared/types"
|
||||||
import type { Session } from "./session"
|
import { sessionStore } from "./session"
|
||||||
import { commandExists, loadCommandModule } from "./commands"
|
import { commandExists, loadCommandModule } from "./commands"
|
||||||
import { ALS } from "./session"
|
import { ALS } from "./session"
|
||||||
|
|
||||||
const sessions: Map<string, Session> = new Map()
|
|
||||||
|
|
||||||
export async function runCommand(sessionId: string, taskId: string, input: string, ws?: any): Promise<CommandResult> {
|
export async function runCommand(sessionId: string, taskId: string, input: string, ws?: any): Promise<CommandResult> {
|
||||||
const [cmd = "", ...args] = input.split(" ")
|
const [cmd = "", ...args] = input.split(" ")
|
||||||
|
|
||||||
if (!commandExists(cmd))
|
|
||||||
return { status: "error", output: `${cmd} not found` }
|
|
||||||
|
|
||||||
return runCommandFn({ sessionId, taskId, ws }, async () => exec(cmd, args))
|
return runCommandFn({ sessionId, taskId, ws }, async () => exec(cmd, args))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runCommandFn(
|
export async function runCommandFn(
|
||||||
|
|
@ -24,7 +17,7 @@ export async function runCommandFn(
|
||||||
fn: () => Promise<CommandResult>
|
fn: () => Promise<CommandResult>
|
||||||
): Promise<CommandResult> {
|
): Promise<CommandResult> {
|
||||||
try {
|
try {
|
||||||
const state = getState(sessionId, taskId, ws)
|
const state = sessionStore(sessionId, taskId, ws)
|
||||||
return processExecOutput(await ALS.run(state, async () => fn()))
|
return processExecOutput(await ALS.run(state, async () => fn()))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { status: "error", output: errorMessage(err) }
|
return { status: "error", output: errorMessage(err) }
|
||||||
|
|
@ -32,6 +25,9 @@ export async function runCommandFn(
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exec(cmd: string, args: string[]): Promise<CommandResult> {
|
async function exec(cmd: string, args: string[]): Promise<CommandResult> {
|
||||||
|
if (!commandExists(cmd))
|
||||||
|
return { status: "error", output: `${cmd} not found` }
|
||||||
|
|
||||||
const module = await loadCommandModule(cmd)
|
const module = await loadCommandModule(cmd)
|
||||||
|
|
||||||
if (module?.game)
|
if (module?.game)
|
||||||
|
|
@ -67,18 +63,6 @@ export function processExecOutput(output: string | any): CommandResult {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getState(sessionId: string, taskId?: string, ws?: any): Session {
|
|
||||||
let state = sessions.get(sessionId)
|
|
||||||
if (!state) {
|
|
||||||
state = { sessionId: sessionId, project: "" }
|
|
||||||
sessions.set(sessionId, state)
|
|
||||||
}
|
|
||||||
if (taskId)
|
|
||||||
state.taskId = taskId
|
|
||||||
if (ws) state.ws = ws
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
function errorMessage(error: Error | any): string {
|
function errorMessage(error: Error | any): string {
|
||||||
if (!(error instanceof Error))
|
if (!(error instanceof Error))
|
||||||
return String(error)
|
return String(error)
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ export function expectDir(path: string): boolean {
|
||||||
export async function expectShellCmd(cmd: string): Promise<boolean> {
|
export async function expectShellCmd(cmd: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
await $`which ${cmd}`
|
await $`which ${cmd}`
|
||||||
console.log("WHICH", cmd)
|
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
setFatal(`Missing critical dependency: avahi-publish`)
|
setFatal(`Missing critical dependency: avahi-publish`)
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@
|
||||||
import type { Child } from "hono/jsx"
|
import type { Child } from "hono/jsx"
|
||||||
import { type Context, Hono } from "hono"
|
import { type Context, Hono } from "hono"
|
||||||
import { renderToString } from "hono/jsx/dom/server"
|
import { renderToString } from "hono/jsx/dom/server"
|
||||||
import { join, dirname } from "path"
|
import { join } from "path"
|
||||||
import { readdirSync } from "fs"
|
import { readdirSync } from "fs"
|
||||||
|
|
||||||
import { NOSE_WWW } from "./config"
|
import { NOSE_DIR } from "./config"
|
||||||
import { isFile, isDir } from "./utils"
|
import { isFile, isDir } from "./utils"
|
||||||
|
|
||||||
export type Handler = (r: Context) => string | Child | Response | Promise<Response>
|
export type Handler = (r: Context) => string | Child | Response | Promise<Response>
|
||||||
|
|
@ -35,39 +35,32 @@ export async function serveApp(c: Context, subdomain: string): Promise<Response>
|
||||||
export function apps(): string[] {
|
export function apps(): string[] {
|
||||||
const apps: string[] = []
|
const apps: string[] = []
|
||||||
|
|
||||||
for (const entry of readdirSync(NOSE_WWW))
|
for (const entry of readdirSync(NOSE_DIR))
|
||||||
apps.push(entry.replace(/\.tsx?/, ""))
|
if (isApp(entry))
|
||||||
|
apps.push(entry)
|
||||||
|
|
||||||
return apps.sort()
|
return apps.sort()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isApp(name: string): boolean {
|
||||||
|
return isFile(join(NOSE_DIR, name, "index.ts"))
|
||||||
|
|| isFile(join(NOSE_DIR, name, "index.tsx"))
|
||||||
|
|| isDir(join(NOSE_DIR, name, "pub"))
|
||||||
|
}
|
||||||
|
|
||||||
export function appDir(name: string): string | undefined {
|
export function appDir(name: string): string | undefined {
|
||||||
const path = [
|
if (isApp(name))
|
||||||
`${name}.ts`,
|
return join(NOSE_DIR, name)
|
||||||
`${name}.tsx`,
|
|
||||||
name
|
|
||||||
]
|
|
||||||
.map(path => join(NOSE_WWW, path))
|
|
||||||
.flat()
|
|
||||||
.filter(path => /\.tsx?$/.test(path) ? isFile(path) : isDir(path))[0]
|
|
||||||
|
|
||||||
if (!path) return
|
|
||||||
|
|
||||||
return /\.tsx?$/.test(path) ? dirname(path) : path
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findApp(name: string): Promise<App | undefined> {
|
async function findApp(name: string): Promise<App | undefined> {
|
||||||
const paths = [
|
const paths = [
|
||||||
`${name}.ts`,
|
|
||||||
`${name}.tsx`,
|
|
||||||
join(name, "index.ts"),
|
join(name, "index.ts"),
|
||||||
join(name, "index.tsx")
|
join(name, "index.tsx")
|
||||||
]
|
]
|
||||||
|
|
||||||
let app
|
|
||||||
|
|
||||||
for (const path of paths) {
|
for (const path of paths) {
|
||||||
app = await loadApp(join(NOSE_WWW, path))
|
const app = await loadApp(join(NOSE_DIR, path))
|
||||||
if (app) return app
|
if (app) return app
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user