upload command
This commit is contained in:
parent
329d36a878
commit
3ce1bce5f0
|
|
@ -3,9 +3,10 @@
|
||||||
// Show some debugging information.
|
// Show some debugging information.
|
||||||
|
|
||||||
import { NOSE_STARTED, NOSE_SYS_BIN, NOSE_DIR, GIT_SHA } from "@/config"
|
import { NOSE_STARTED, NOSE_SYS_BIN, NOSE_DIR, GIT_SHA } from "@/config"
|
||||||
|
import { highlightToHTML } from "../lib/highlight"
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
return [
|
return highlightToHTML([
|
||||||
`NODE_ENV=${process.env.NODE_ENV || "(none)"}`,
|
`NODE_ENV=${process.env.NODE_ENV || "(none)"}`,
|
||||||
`BUN_HOT=${process.env.BUN_HOT || "(none)"}`,
|
`BUN_HOT=${process.env.BUN_HOT || "(none)"}`,
|
||||||
`PORT=${process.env.PORT || "(none)"}`,
|
`PORT=${process.env.PORT || "(none)"}`,
|
||||||
|
|
@ -15,5 +16,5 @@ export default function () {
|
||||||
`NOSE_SYS_BIN=${NOSE_SYS_BIN}`,
|
`NOSE_SYS_BIN=${NOSE_SYS_BIN}`,
|
||||||
`NOSE_DIR=${NOSE_DIR}`,
|
`NOSE_DIR=${NOSE_DIR}`,
|
||||||
`GIT_SHA=${GIT_SHA.slice(0, 8)}`,
|
`GIT_SHA=${GIT_SHA.slice(0, 8)}`,
|
||||||
].join("\n")
|
].join("\n"))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
// Load a project so you can work on it.
|
// Load a project so you can work on it.
|
||||||
|
|
||||||
import { apps } from "@/webapp"
|
import { apps } from "@/webapp"
|
||||||
import { sessionGet } from "@/session"
|
import { sessionGet, sessionSet } from "@/session"
|
||||||
|
|
||||||
export default function (project: string) {
|
export default function (project: string) {
|
||||||
const state = sessionGet()
|
const state = sessionGet()
|
||||||
if (!project) throw `usage: load <project name>`
|
if (!project) throw `usage: load <project name>`
|
||||||
|
|
||||||
if (state && apps().includes(project)) {
|
if (state && apps().includes(project)) {
|
||||||
state.project = project
|
sessionSet("project", project)
|
||||||
state.cwd = ""
|
sessionSet("cwd", "")
|
||||||
} else {
|
} else {
|
||||||
return { error: `failed to load ${project}` }
|
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 { join } from "path"
|
||||||
|
import { NOSE_DATA } from "@/config"
|
||||||
|
import { highlightToHTML } from "../lib/highlight"
|
||||||
|
|
||||||
export default async function () {
|
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("")
|
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 {
|
export function tokenize(src: string): Program {
|
||||||
const tokens: Token[] = []
|
const tokens: Token[] = []
|
||||||
let i = 0
|
let i = 0
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,12 @@ export async function commandSource(name: string): Promise<string> {
|
||||||
return Bun.file(path).text()
|
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 sysCmdWatcher
|
||||||
let usrCmdWatcher
|
let usrCmdWatcher
|
||||||
function startWatchers() {
|
function startWatchers() {
|
||||||
|
|
|
||||||
|
|
@ -122,4 +122,29 @@ body[data-mode=tall] #content {
|
||||||
textarea {
|
textarea {
|
||||||
color: var(--c64-light-blue);
|
color: var(--c64-light-blue);
|
||||||
max-width: 97%;
|
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 { scrollback } from "./dom.js"
|
||||||
import { resize } from "./resize.js"
|
import { resize } from "./resize.js"
|
||||||
import { autoScroll } from "./scrollback.js"
|
import { autoScroll } from "./scrollback.js"
|
||||||
import { sessionID } from "./session.js"
|
import { sessionId } from "./session.js"
|
||||||
|
|
||||||
export const commands: string[] = []
|
export const commands: string[] = []
|
||||||
|
|
||||||
export const browserCommands: Record<string, () => any> = {
|
export const browserCommands: Record<string, () => any> = {
|
||||||
|
"browser-session": () => sessionId,
|
||||||
clear: () => scrollback.innerHTML = "",
|
clear: () => scrollback.innerHTML = "",
|
||||||
commands: () => commands.join(" "),
|
commands: () => commands.join(" "),
|
||||||
fullscreen: () => document.body.requestFullscreen(),
|
fullscreen: () => document.body.requestFullscreen(),
|
||||||
|
|
@ -18,7 +19,6 @@ export const browserCommands: Record<string, () => any> = {
|
||||||
autoScroll()
|
autoScroll()
|
||||||
},
|
},
|
||||||
reload: () => window.location.reload(),
|
reload: () => window.location.reload(),
|
||||||
session: () => sessionID,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cacheCommands(cmds: string[]) {
|
export function cacheCommands(cmds: string[]) {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ function handleCompletion(e: KeyboardEvent) {
|
||||||
const input = cmdInput.value
|
const input = cmdInput.value
|
||||||
|
|
||||||
for (const command of commands) {
|
for (const command of commands) {
|
||||||
console.log(input, command)
|
|
||||||
if (command.startsWith(input)) {
|
if (command.startsWith(input)) {
|
||||||
cmdInput.value = command
|
cmdInput.value = command
|
||||||
return
|
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")) {
|
} else if (e.key === "ArrowDown" || (e.ctrlKey && e.key === "n")) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
console.log(idx, savedInput)
|
|
||||||
if (idx <= 0) {
|
if (idx <= 0) {
|
||||||
cmdInput.value = savedInput
|
cmdInput.value = savedInput
|
||||||
idx = -1
|
idx = -1
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { initCompletion } from "./completion.js"
|
||||||
import { initCursor } from "./cursor.js"
|
import { initCursor } from "./cursor.js"
|
||||||
import { initEditor } from "./editor.js"
|
import { initEditor } from "./editor.js"
|
||||||
import { initFocus } from "./focus.js"
|
import { initFocus } from "./focus.js"
|
||||||
|
import { initForm } from "./form.js"
|
||||||
import { initGamepad } from "./gamepad.js"
|
import { initGamepad } from "./gamepad.js"
|
||||||
import { initHistory } from "./history.js"
|
import { initHistory } from "./history.js"
|
||||||
import { initHyperlink } from "./hyperlink.js"
|
import { initHyperlink } from "./hyperlink.js"
|
||||||
|
|
@ -14,6 +15,7 @@ import { startConnection } from "./websocket.js"
|
||||||
initCompletion()
|
initCompletion()
|
||||||
initCursor()
|
initCursor()
|
||||||
initFocus()
|
initFocus()
|
||||||
|
initForm()
|
||||||
initEditor()
|
initEditor()
|
||||||
initGamepad()
|
initGamepad()
|
||||||
initHistory()
|
initHistory()
|
||||||
|
|
|
||||||
|
|
@ -36,22 +36,15 @@ export function setStatus(id: string, status: InputStatus) {
|
||||||
const statusEl = document.querySelector(`[data-id="${id}"].input .status`)
|
const statusEl = document.querySelector(`[data-id="${id}"].input .status`)
|
||||||
if (!statusEl) return
|
if (!statusEl) return
|
||||||
|
|
||||||
statusEl.className = ""
|
const colors = {
|
||||||
|
waiting: "yellow",
|
||||||
switch (status) {
|
streaming: "purple",
|
||||||
case "waiting":
|
ok: "green",
|
||||||
statusEl.classList.add("yellow")
|
error: "red"
|
||||||
break
|
|
||||||
case "streaming":
|
|
||||||
statusEl.classList.add("purple")
|
|
||||||
break
|
|
||||||
case "ok":
|
|
||||||
statusEl.classList.add("green")
|
|
||||||
break
|
|
||||||
case "error":
|
|
||||||
statusEl.classList.add("red")
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
statusEl.classList.remove(...Object.values(colors))
|
||||||
|
statusEl.classList.add(colors[status])
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addOutput(id: string, output: CommandOutput) {
|
export function addOutput(id: string, output: CommandOutput) {
|
||||||
|
|
@ -80,7 +73,6 @@ export function addErrorMessage(message: string) {
|
||||||
addOutput("", { html: `<span class="red">${message}</span>` })
|
addOutput("", { html: `<span class="red">${message}</span>` })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function appendOutput(id: string, output: CommandOutput) {
|
export function appendOutput(id: string, output: CommandOutput) {
|
||||||
const item = document.querySelector(`[data-id="${id}"].output`)
|
const item = document.querySelector(`[data-id="${id}"].output`)
|
||||||
|
|
||||||
|
|
@ -125,14 +117,14 @@ function processOutput(output: CommandOutput): ["html" | "text", string] {
|
||||||
content = output
|
content = output
|
||||||
} else if (Array.isArray(output)) {
|
} else if (Array.isArray(output)) {
|
||||||
content = output.join(" ")
|
content = output.join(" ")
|
||||||
} else if ("html" in output) {
|
} else if (typeof output === "object" && "html" in output) {
|
||||||
html = true
|
html = true
|
||||||
content = output.html
|
content = output.html
|
||||||
if (output.script) eval(output.script)
|
if (output.script) eval(output.script)
|
||||||
} else if ("text" in output) {
|
} else if (typeof output === "object" && "text" in output) {
|
||||||
content = output.text
|
content = output.text
|
||||||
if (output.script) eval(output.script)
|
if (output.script) eval(output.script)
|
||||||
} else if ("script" in output) {
|
} else if (typeof output === "object" && "script" in output) {
|
||||||
eval(output.script!)
|
eval(output.script!)
|
||||||
} else {
|
} else {
|
||||||
content = JSON.stringify(output)
|
content = JSON.stringify(output)
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,4 @@
|
||||||
|
|
||||||
import { randomId } from "../shared/utils.js"
|
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.
|
// The terminal communicates with the shell via websockets.
|
||||||
|
|
||||||
import type { Message } from "../shared/types.js"
|
import type { Message } from "../shared/types.js"
|
||||||
import { sessionID } from "./session.js"
|
import { sessionId } from "./session.js"
|
||||||
import { handleMessage } from "./shell.js"
|
import { handleMessage } from "./shell.js"
|
||||||
import { addErrorMessage } from "./scrollback.js"
|
import { addErrorMessage } from "./scrollback.js"
|
||||||
|
|
||||||
|
|
@ -37,7 +37,7 @@ export function send(msg: Message) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!msg.session) msg.session = sessionID
|
if (!msg.session) msg.session = sessionId
|
||||||
ws?.readyState === 1 && ws.send(JSON.stringify(msg))
|
ws?.readyState === 1 && ws.send(JSON.stringify(msg))
|
||||||
console.log("-> send", 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 { transpile, isFile, tilde } from "./utils"
|
||||||
import { serveApp } from "./webapp"
|
import { serveApp } from "./webapp"
|
||||||
import { initDNS } from "./dns"
|
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 { send, addWebsocket, removeWebsocket, closeWebsockets } from "./websocket"
|
||||||
|
|
||||||
import { Layout } from "./html/layout"
|
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>))
|
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 { CommandResult, CommandOutput } from "./shared/types"
|
||||||
import type { Session } from "./session"
|
import type { Session } from "./session"
|
||||||
import { commandExists, commandPath } from "./commands"
|
import { commandExists, loadCommandModule } from "./commands"
|
||||||
import { ALS } from "./session"
|
import { ALS } from "./session"
|
||||||
|
|
||||||
const sessions: Map<string, Session> = new Map()
|
const sessions: Map<string, Session> = new Map()
|
||||||
|
|
@ -29,8 +29,13 @@ export async function runCommand(sessionId: string, taskId: string, input: strin
|
||||||
return { status, output }
|
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]> {
|
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)
|
if (module?.game)
|
||||||
return ["ok", { game: cmd }]
|
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)
|
let state = sessions.get(sessionId)
|
||||||
if (!state) {
|
if (!state) {
|
||||||
state = { sessionId: sessionId, project: "" }
|
state = { sessionId: sessionId, project: "" }
|
||||||
sessions.set(sessionId, state)
|
sessions.set(sessionId, state)
|
||||||
}
|
}
|
||||||
state.taskId = taskId
|
if (taskId)
|
||||||
|
state.taskId = taskId
|
||||||
if (ws) state.ws = ws
|
if (ws) state.ws = ws
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ export function initSneakers() {
|
||||||
for (const key in state) {
|
for (const key in state) {
|
||||||
if (key.startsWith(PREFIX)) {
|
if (key.startsWith(PREFIX)) {
|
||||||
const app = key.replace(PREFIX, "")
|
const app = key.replace(PREFIX, "")
|
||||||
console.log("sharing", app, state[key])
|
|
||||||
connectSneaker(app, state[key])
|
connectSneaker(app, state[key])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user