upload command
This commit is contained in:
parent
329d36a878
commit
3ce1bce5f0
|
|
@ -3,9 +3,10 @@
|
|||
// Show some debugging information.
|
||||
|
||||
import { NOSE_STARTED, NOSE_SYS_BIN, NOSE_DIR, GIT_SHA } from "@/config"
|
||||
import { highlightToHTML } from "../lib/highlight"
|
||||
|
||||
export default function () {
|
||||
return [
|
||||
return highlightToHTML([
|
||||
`NODE_ENV=${process.env.NODE_ENV || "(none)"}`,
|
||||
`BUN_HOT=${process.env.BUN_HOT || "(none)"}`,
|
||||
`PORT=${process.env.PORT || "(none)"}`,
|
||||
|
|
@ -15,5 +16,5 @@ export default function () {
|
|||
`NOSE_SYS_BIN=${NOSE_SYS_BIN}`,
|
||||
`NOSE_DIR=${NOSE_DIR}`,
|
||||
`GIT_SHA=${GIT_SHA.slice(0, 8)}`,
|
||||
].join("\n")
|
||||
].join("\n"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
// Load a project so you can work on it.
|
||||
|
||||
import { apps } from "@/webapp"
|
||||
import { sessionGet } from "@/session"
|
||||
import { sessionGet, sessionSet } from "@/session"
|
||||
|
||||
export default function (project: string) {
|
||||
const state = sessionGet()
|
||||
if (!project) throw `usage: load <project name>`
|
||||
|
||||
if (state && apps().includes(project)) {
|
||||
state.project = project
|
||||
state.cwd = ""
|
||||
sessionSet("project", project)
|
||||
sessionSet("cwd", "")
|
||||
} else {
|
||||
return { error: `failed to load ${project}` }
|
||||
}
|
||||
|
|
|
|||
6
bin/session.tsx
Normal file
6
bin/session.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { sessionGet } from "@/session"
|
||||
import { highlightToHTML } from "../lib/highlight"
|
||||
|
||||
export default function () {
|
||||
return highlightToHTML(JSON.stringify(sessionGet(), null, 2))
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { NOSE_DATA } from "@/config"
|
||||
import { join } from "path"
|
||||
import { NOSE_DATA } from "@/config"
|
||||
import { highlightToHTML } from "../lib/highlight"
|
||||
|
||||
export default async function () {
|
||||
return JSON.parse(await Bun.file(join(NOSE_DATA, "state.json")).text())
|
||||
return highlightToHTML(await Bun.file(join(NOSE_DATA, "state.json")).text())
|
||||
}
|
||||
34
bin/upload.tsx
Normal file
34
bin/upload.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { join } from "path"
|
||||
import { projectDir } from "@/project"
|
||||
import { sessionGet } from "@/session"
|
||||
|
||||
export default function () {
|
||||
const project = sessionGet("project")
|
||||
if (!project) return { error: "No project loaded" }
|
||||
|
||||
return <>
|
||||
<form method="post" action="/upload">
|
||||
<input type="file" name="file" required={true} />
|
||||
<br />
|
||||
<br />
|
||||
<input type="submit" value="Upload" />
|
||||
</form>
|
||||
</>
|
||||
}
|
||||
|
||||
export async function POST(c: Context) {
|
||||
const cwd = sessionGet("cwd") || projectDir()
|
||||
if (!cwd) throw "No project loaded"
|
||||
|
||||
const form = await c.req.formData()
|
||||
const file = form.get("file")
|
||||
|
||||
if (file && file instanceof File) {
|
||||
const arrayBuffer = await file.arrayBuffer()
|
||||
await Bun.write(join(cwd, file.name), arrayBuffer)
|
||||
|
||||
return `Uploaded ${file.name}`
|
||||
}
|
||||
|
||||
return { error: "No file received" }
|
||||
}
|
||||
|
|
@ -42,6 +42,10 @@ export function highlight(code: string): string {
|
|||
return `<style> .string { color: #C62828; } .number { color: #C4A000; } .keyword { color: #7C3AED; } .comment { color: #E91E63; } </style>` + tokens.map(t => tokenToHTML(t)).join("")
|
||||
}
|
||||
|
||||
export function highlightToHTML(code: string): { html: string } {
|
||||
return { html: `<div style='white-space: pre;'>${highlight(code)}</div>` }
|
||||
}
|
||||
|
||||
export function tokenize(src: string): Program {
|
||||
const tokens: Token[] = []
|
||||
let i = 0
|
||||
|
|
|
|||
|
|
@ -47,6 +47,12 @@ export async function commandSource(name: string): Promise<string> {
|
|||
return Bun.file(path).text()
|
||||
}
|
||||
|
||||
export async function loadCommandModule(cmd: string) {
|
||||
const path = commandPath(cmd)
|
||||
if (!path) return
|
||||
return await import(path + "?t+" + Date.now())
|
||||
}
|
||||
|
||||
let sysCmdWatcher
|
||||
let usrCmdWatcher
|
||||
function startWatchers() {
|
||||
|
|
|
|||
|
|
@ -123,3 +123,28 @@ textarea {
|
|||
color: var(--c64-light-blue);
|
||||
max-width: 97%;
|
||||
}
|
||||
|
||||
form {
|
||||
padding: 10px;
|
||||
margin: 15px;
|
||||
background: var(--c64-light-blue);
|
||||
}
|
||||
|
||||
input[type="file"]::file-selector-button {
|
||||
font-family: 'C64ProMono', monospace;
|
||||
color: var(--c64-dark-blue);
|
||||
background: var(--white);
|
||||
border-radius: 2px;
|
||||
border: 2px solid #fff;
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
color: var(--c64-dark-blue);
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
color: var(--c64-dark-blue);
|
||||
padding: 5px;
|
||||
}
|
||||
|
|
@ -4,11 +4,12 @@
|
|||
import { scrollback } from "./dom.js"
|
||||
import { resize } from "./resize.js"
|
||||
import { autoScroll } from "./scrollback.js"
|
||||
import { sessionID } from "./session.js"
|
||||
import { sessionId } from "./session.js"
|
||||
|
||||
export const commands: string[] = []
|
||||
|
||||
export const browserCommands: Record<string, () => any> = {
|
||||
"browser-session": () => sessionId,
|
||||
clear: () => scrollback.innerHTML = "",
|
||||
commands: () => commands.join(" "),
|
||||
fullscreen: () => document.body.requestFullscreen(),
|
||||
|
|
@ -18,7 +19,6 @@ export const browserCommands: Record<string, () => any> = {
|
|||
autoScroll()
|
||||
},
|
||||
reload: () => window.location.reload(),
|
||||
session: () => sessionID,
|
||||
}
|
||||
|
||||
export function cacheCommands(cmds: string[]) {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ function handleCompletion(e: KeyboardEvent) {
|
|||
const input = cmdInput.value
|
||||
|
||||
for (const command of commands) {
|
||||
console.log(input, command)
|
||||
if (command.startsWith(input)) {
|
||||
cmdInput.value = command
|
||||
return
|
||||
|
|
|
|||
48
src/js/form.ts
Normal file
48
src/js/form.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
////
|
||||
// All forms are submitted via ajax.
|
||||
|
||||
import type { CommandResult, CommandOutput } from "../shared/types.js"
|
||||
import { sessionId } from "./session.js"
|
||||
import { setStatus, replaceOutput } from "./scrollback.js"
|
||||
import { focusInput } from "./focus.js"
|
||||
|
||||
export function initForm() {
|
||||
document.addEventListener("submit", submitHandler)
|
||||
}
|
||||
|
||||
export const submitHandler = async (e: SubmitEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
const form = e.target
|
||||
if (!(form instanceof HTMLFormElement)) return
|
||||
|
||||
const li = form.closest(".output")
|
||||
if (!(li instanceof HTMLLIElement)) return
|
||||
|
||||
const id = li.dataset.id
|
||||
if (!id) return
|
||||
|
||||
let output: CommandOutput
|
||||
let error = false
|
||||
|
||||
try {
|
||||
const fd = new FormData(form)
|
||||
|
||||
const data: CommandResult = await fetch("/cmd" + new URL(form.action).pathname, {
|
||||
method: "POST",
|
||||
headers: { "X-Session": sessionId }, // don't set Content-Type manually
|
||||
body: fd
|
||||
}).then(r => r.json())
|
||||
|
||||
output = data.output
|
||||
error = data.status === "error"
|
||||
} catch (e: any) {
|
||||
output = e.message || e.toString()
|
||||
error = true
|
||||
}
|
||||
|
||||
if (error) setStatus(id, "error")
|
||||
|
||||
replaceOutput(id, output)
|
||||
focusInput()
|
||||
}
|
||||
|
|
@ -40,7 +40,6 @@ function navigateHistory(e: KeyboardEvent) {
|
|||
} else if (e.key === "ArrowDown" || (e.ctrlKey && e.key === "n")) {
|
||||
e.preventDefault()
|
||||
|
||||
console.log(idx, savedInput)
|
||||
if (idx <= 0) {
|
||||
cmdInput.value = savedInput
|
||||
idx = -1
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { initCompletion } from "./completion.js"
|
|||
import { initCursor } from "./cursor.js"
|
||||
import { initEditor } from "./editor.js"
|
||||
import { initFocus } from "./focus.js"
|
||||
import { initForm } from "./form.js"
|
||||
import { initGamepad } from "./gamepad.js"
|
||||
import { initHistory } from "./history.js"
|
||||
import { initHyperlink } from "./hyperlink.js"
|
||||
|
|
@ -14,6 +15,7 @@ import { startConnection } from "./websocket.js"
|
|||
initCompletion()
|
||||
initCursor()
|
||||
initFocus()
|
||||
initForm()
|
||||
initEditor()
|
||||
initGamepad()
|
||||
initHistory()
|
||||
|
|
|
|||
|
|
@ -36,22 +36,15 @@ export function setStatus(id: string, status: InputStatus) {
|
|||
const statusEl = document.querySelector(`[data-id="${id}"].input .status`)
|
||||
if (!statusEl) return
|
||||
|
||||
statusEl.className = ""
|
||||
|
||||
switch (status) {
|
||||
case "waiting":
|
||||
statusEl.classList.add("yellow")
|
||||
break
|
||||
case "streaming":
|
||||
statusEl.classList.add("purple")
|
||||
break
|
||||
case "ok":
|
||||
statusEl.classList.add("green")
|
||||
break
|
||||
case "error":
|
||||
statusEl.classList.add("red")
|
||||
break
|
||||
const colors = {
|
||||
waiting: "yellow",
|
||||
streaming: "purple",
|
||||
ok: "green",
|
||||
error: "red"
|
||||
}
|
||||
|
||||
statusEl.classList.remove(...Object.values(colors))
|
||||
statusEl.classList.add(colors[status])
|
||||
}
|
||||
|
||||
export function addOutput(id: string, output: CommandOutput) {
|
||||
|
|
@ -80,7 +73,6 @@ export function addErrorMessage(message: string) {
|
|||
addOutput("", { html: `<span class="red">${message}</span>` })
|
||||
}
|
||||
|
||||
|
||||
export function appendOutput(id: string, output: CommandOutput) {
|
||||
const item = document.querySelector(`[data-id="${id}"].output`)
|
||||
|
||||
|
|
@ -125,14 +117,14 @@ function processOutput(output: CommandOutput): ["html" | "text", string] {
|
|||
content = output
|
||||
} else if (Array.isArray(output)) {
|
||||
content = output.join(" ")
|
||||
} else if ("html" in output) {
|
||||
} else if (typeof output === "object" && "html" in output) {
|
||||
html = true
|
||||
content = output.html
|
||||
if (output.script) eval(output.script)
|
||||
} else if ("text" in output) {
|
||||
} else if (typeof output === "object" && "text" in output) {
|
||||
content = output.text
|
||||
if (output.script) eval(output.script)
|
||||
} else if ("script" in output) {
|
||||
} else if (typeof output === "object" && "script" in output) {
|
||||
eval(output.script!)
|
||||
} else {
|
||||
content = JSON.stringify(output)
|
||||
|
|
|
|||
|
|
@ -4,4 +4,4 @@
|
|||
|
||||
import { randomId } from "../shared/utils.js"
|
||||
|
||||
export const sessionID = randomId()
|
||||
export const sessionId = randomId()
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
// The terminal communicates with the shell via websockets.
|
||||
|
||||
import type { Message } from "../shared/types.js"
|
||||
import { sessionID } from "./session.js"
|
||||
import { sessionId } from "./session.js"
|
||||
import { handleMessage } from "./shell.js"
|
||||
import { addErrorMessage } from "./scrollback.js"
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ export function send(msg: Message) {
|
|||
return
|
||||
}
|
||||
|
||||
if (!msg.session) msg.session = sessionID
|
||||
if (!msg.session) msg.session = sessionId
|
||||
ws?.readyState === 1 && ws.send(JSON.stringify(msg))
|
||||
console.log("-> send", msg)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ import { NOSE_ICON, NOSE_BIN, NOSE_WWW, NOSE_DATA, NOSE_DIR } from "./config"
|
|||
import { transpile, isFile, tilde } from "./utils"
|
||||
import { serveApp } from "./webapp"
|
||||
import { initDNS } from "./dns"
|
||||
import { commands, commandPath } from "./commands"
|
||||
import { commands, commandPath, loadCommandModule } from "./commands"
|
||||
import { runInSession, processExecOutput } from "./shell"
|
||||
import { send, addWebsocket, removeWebsocket, closeWebsockets } from "./websocket"
|
||||
|
||||
import { Layout } from "./html/layout"
|
||||
|
|
@ -89,6 +90,25 @@ app.get("/source/:name", async c => {
|
|||
})
|
||||
})
|
||||
|
||||
app.on(["GET", "POST"], ["/cmd/:name"], async c => {
|
||||
const sessionId = c.req.header("X-Session") || "0"
|
||||
const cmd = c.req.param("name")
|
||||
const method = c.req.method
|
||||
|
||||
try {
|
||||
const mod = await loadCommandModule(cmd)
|
||||
if (!mod || !mod[method])
|
||||
return c.json({ error: `No ${method} export in ${cmd}` }, 500)
|
||||
|
||||
return c.json(await runInSession(sessionId, async () => {
|
||||
const [status, output] = processExecOutput(await mod[method](c))
|
||||
return { status, output }
|
||||
}))
|
||||
} catch (e: any) {
|
||||
return c.json({ status: "error", output: e.message || e.toString() }, 500)
|
||||
}
|
||||
})
|
||||
|
||||
app.get("/", c => c.html(<Layout><Terminal /></Layout>))
|
||||
|
||||
//
|
||||
|
|
|
|||
14
src/shell.ts
14
src/shell.ts
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
import type { CommandResult, CommandOutput } from "./shared/types"
|
||||
import type { Session } from "./session"
|
||||
import { commandExists, commandPath } from "./commands"
|
||||
import { commandExists, loadCommandModule } from "./commands"
|
||||
import { ALS } from "./session"
|
||||
|
||||
const sessions: Map<string, Session> = new Map()
|
||||
|
|
@ -29,8 +29,13 @@ export async function runCommand(sessionId: string, taskId: string, input: strin
|
|||
return { status, output }
|
||||
}
|
||||
|
||||
export async function runInSession(sessionId: string, fn: () => Promise<any>) {
|
||||
const state = getState(sessionId)
|
||||
return await ALS.run(state, async () => await fn())
|
||||
}
|
||||
|
||||
async function exec(cmd: string, args: string[]): Promise<["ok" | "error", CommandOutput]> {
|
||||
const module = await import(commandPath(cmd) + "?t+" + Date.now())
|
||||
const module = await loadCommandModule(cmd)
|
||||
|
||||
if (module?.game)
|
||||
return ["ok", { game: cmd }]
|
||||
|
|
@ -62,13 +67,14 @@ export function processExecOutput(output: string | any): ["ok" | "error", Comman
|
|||
}
|
||||
}
|
||||
|
||||
function getState(sessionId: string, taskId: string, ws?: any): Session {
|
||||
function getState(sessionId: string, taskId?: string, ws?: any): Session {
|
||||
let state = sessions.get(sessionId)
|
||||
if (!state) {
|
||||
state = { sessionId: sessionId, project: "" }
|
||||
sessions.set(sessionId, state)
|
||||
}
|
||||
state.taskId = taskId
|
||||
if (taskId)
|
||||
state.taskId = taskId
|
||||
if (ws) state.ws = ws
|
||||
return state
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ export function initSneakers() {
|
|||
for (const key in state) {
|
||||
if (key.startsWith(PREFIX)) {
|
||||
const app = key.replace(PREFIX, "")
|
||||
console.log("sharing", app, state[key])
|
||||
connectSneaker(app, state[key])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user