Compare commits

..

No commits in common. "b6c0dde776e1c7884f445f22b9a09634311531aa" and "415cbd0ccdc120bfe95659cbb87e456ba02958aa" have entirely different histories.

16 changed files with 358 additions and 406 deletions

View File

@ -1,4 +0,0 @@
#!/bin/sh
bun run check || exit 1
bun run build || exit 1

View File

@ -8,7 +8,7 @@
3. From the root of this repo, run: 3. From the root of this repo, run:
bun remote:install bun remote:install`
4. When it's done (it'll reboot) visit: 4. When it's done (it'll reboot) visit:
@ -24,7 +24,6 @@ And to make sure DNS is working:
Running the server will create `~/nose` for you to play with. Running the server will create `~/nose` for you to play with.
git config core.hooksPath .githooks
bun install bun install
bun dev bun dev
open localhost:3000 open localhost:3000
@ -85,7 +84,6 @@ https://wakamaifondue.com/
- [x] public tunnel lives through reboots - [x] public tunnel lives through reboots
- [o] tunnel to the terminal -- Not doing this for now! - [o] tunnel to the terminal -- Not doing this for now!
- [x] web browser - [x] web browser
- [x] browser commands
- [x] remember your "mode" - [x] remember your "mode"
- [x] status bar on terminal UX - [x] status bar on terminal UX
- [x] quickly open the current webapp - [x] quickly open the current webapp
@ -100,22 +98,3 @@ https://wakamaifondue.com/
- [ ] sounds - [ ] sounds
- [ ] maps - [ ] maps
- [ ] etc...? - [ ] etc...?
## Future Goals (need to be split into Phases)
- [ ] `nose` can find your NOSEputer via mDNS/Bonjour
- [ ] Shrimp!
- [ ] CodeMirror Editor
- [ ] cron tasks
- [ ] prompt()
- [ ] confirm()
- [ ] faster boot (turn off stuff like CUPS)
- [ ] SSD
- [ ] NOSE App Store -- "There's an app for that" (browse + install carts)
- [ ] System dashboard (CPU, memory)
- [ ] Write Your Own Daemons (MUD? FTPd?)
- [ ] Daemon Dashboard
- [ ] cloud backup for projects
- [ ] "sleep" / "wake" (disable monitor)
- [ ] themes
- [ ] pixel art boot animation

View File

@ -1,19 +1,23 @@
// Print all the commands, or a subset. /// <reference lib="dom" />
// Print all the commands.
import { commands } from "@/commands" import { commands } from "@/js/commands"
export default async function (prefix?: string) { export default function (prefix?: string) {
let cmds = prefix ? await matchingCommands(prefix) : Object.keys(await commands()) let cmds = prefix ? matchingCommands(prefix) : commands
return { html: "<div>" + cmds.map(cmd => `<a href="#help ${cmd}">${cmd}</a>`).join("") + "</div>" } return { html: "<div>" + Object.keys(cmds).map(cmd => `<a href="#help ${cmd}">${cmd}</a>`).join("") + "</div>" }
} }
async function matchingCommands(cmd: string): Promise<string[]> { function matchingCommands(cmd: string): string {
let matched: string[] = [] let matched: string[] = []
console.log(Object.keys(commands))
for (const command of Object.keys(await commands())) for (const command of Object.keys(commands)) {
console.log("cmd->", command)
if (command.startsWith(cmd)) matched.push(command) if (command.startsWith(cmd)) matched.push(command)
}
return matched
return matched.join(" ")
} }

View File

@ -2,25 +2,17 @@
// //
// (Hopefully.) // (Hopefully.)
import { commandPath } from "@/commands" import { commandPath, commands } from "@/commands"
import type { CommandOutput } from "@/shared/types"
import { moduleExports, type ExportInfo } from "@/sniffer"
import commands from "./commands"
export default async function (cmd: string): Promise<CommandOutput> { export default async function (cmd: string): Promise<string> {
if (!cmd) return "usage: help <command>" if (!cmd) return "usage: help <command>"
const path = commandPath(cmd) const path = commandPath(cmd)
if (!path) return await commands(cmd) if (!path) { return matchingCommands(cmd) }
const signatures = await moduleExports(path)
const signature = signatures.default
const code = (await Bun.file(path).text()).split("\n") const code = (await Bun.file(path).text()).split("\n")
let docs = [] let docs = []
docs.push(usage(cmd, signature), "")
for (const line of code) { for (const line of code) {
if (line.startsWith("///")) { if (line.startsWith("///")) {
docs.push("Runs in the browser.\n") docs.push("Runs in the browser.\n")
@ -35,17 +27,11 @@ export default async function (cmd: string): Promise<CommandOutput> {
return docs.join("\n") return docs.join("\n")
} }
function usage(cmd: string, signature?: ExportInfo) { async function matchingCommands(cmd: string): Promise<string> {
let out: string[] = [`usage: ${cmd}`] let matched: string[] = []
for (const command of Object.keys(await commands())) {
if (signature?.kind === "function" && signature.signatures.length) { if (command.startsWith(cmd)) matched.push(command)
const params = signature.signatures[0]!.params
for (const param of params) {
let desc = `${param.name}=`
desc += param.default ? `${param.default}` : `<${param.type}>`
out.push(desc)
}
} }
return out.join(" ") return matched.join(" ")
} }

View File

@ -1,5 +1,3 @@
// Print information about the current session.
import { sessionGet } from "@/session" import { sessionGet } from "@/session"
import { highlightToHTML } from "../lib/highlight" import { highlightToHTML } from "../lib/highlight"

View File

@ -1,5 +1,5 @@
//// ////
// version: 178711c // version: 3e67d66
// src/js/dom.ts // src/js/dom.ts
var content2 = $("content"); var content2 = $("content");
@ -24,26 +24,32 @@ var $$ = (tag, innerHTML = "") => {
return el; return el;
}; };
// src/js/focus.ts
function initFocus() {
window.addEventListener("click", focusHandler);
focusInput();
}
function focusInput() {
cmdInput.focus();
}
function focusHandler(e) {
const target = e.target;
if (!(target instanceof HTMLElement)) {
focusInput();
return;
}
if (["INPUT", "TEXTAREA", "CANVAS", "A"].includes(target.tagName))
return false;
const selection = window.getSelection() || "";
if (selection.toString() === "")
focusInput();
e.preventDefault();
}
// src/shared/utils.ts // src/shared/utils.ts
function randomId() { function randomId() {
return Math.random().toString(36).slice(7); return Math.random().toString(36).slice(7);
} }
async function importUrl(url) {
url += url.includes("?") ? "&" : "?";
url += "t=" + (nodeEnv() === "production" ? gitSHA() : Date.now());
console.log("-> import", url);
return import(url);
}
function nodeEnv() {
if (typeof process !== "undefined" && process.env?.NODE_ENV)
return "development";
if (typeof globalThis !== "undefined" && globalThis.NODE_ENV)
return globalThis.NODE_ENV;
return;
}
function gitSHA() {
return globalThis.GIT_SHA;
}
// src/js/scrollback.ts // src/js/scrollback.ts
var statusColors = { var statusColors = {
@ -160,6 +166,179 @@ function handleOutput(msg) {
addOutput(id, result.output); addOutput(id, result.output);
} }
// src/js/browser.ts
var HEIGHT = 540;
var WIDTH = 960;
var controls = $("browser-controls");
var address = $("browser-address");
var iframe;
var realUrl = "";
var showInput = true;
function isBrowsing() {
return document.querySelector("iframe.browser.active") !== null;
}
function openBrowser(url, openedVia = "click") {
showInput = openedVia === "click";
iframe = $$("iframe.browser.active");
iframe.src = url;
iframe.sandbox.add("allow-scripts", "allow-same-origin", "allow-forms");
iframe.height = String(HEIGHT);
iframe.width = String(WIDTH);
iframe.tabIndex = 0;
window.addEventListener("message", handleAppMessage);
window.addEventListener("keydown", handleBrowserKeydown);
controls.addEventListener("click", handleClick);
iframe.addEventListener("load", handlePageLoad);
controls.style.display = "";
const main = document.querySelector("#content");
main?.prepend(iframe);
setAddress(url);
}
function closeBrowser() {
window.removeEventListener("keydown", handleBrowserKeydown);
window.removeEventListener("message", handleAppMessage);
controls.removeEventListener("click", handleClick);
iframe.removeEventListener("load", handlePageLoad);
const id = randomId();
if (showInput)
addInput(id, "browse " + realUrl, "ok");
iframe.style.transformOrigin = "top left";
iframe.style.transform = "scale(0.5)";
iframe.style.pointerEvents = "none";
iframe.tabIndex = -1;
iframe.classList.remove("fullscreen", "active");
scrollback.append(iframe);
controls.style.display = "none";
focusInput();
}
function handleAppMessage(event) {
const origin = event.origin;
if (!origin.includes("localhost") && !origin.match(/\.local$/)) {
return;
}
const { type, data } = event.data;
switch (type) {
case "NAV_READY":
break;
case "URL_CHANGED":
setAddress(data.url);
break;
case "TITLE_CHANGED":
console.log("Page title:", data.title);
break;
case "NAV_BLOCKED":
showNavigationError(data.url, data.reason);
break;
case "KEYDOWN":
const keyEvent = new KeyboardEvent("keydown", {
key: data.key,
ctrlKey: data.ctrlKey,
shiftKey: data.shiftKey,
altKey: data.altKey,
metaKey: data.metaKey,
bubbles: true
});
window.dispatchEvent(keyEvent);
break;
}
}
function handleBrowserKeydown(e) {
if (e.key === "Escape" || e.ctrlKey && e.key === "c") {
e.preventDefault();
closeBrowser();
}
}
function handleClick(e) {
const target = e.target;
if (!(target instanceof HTMLElement))
return;
switch (target.id) {
case "back-button":
navigateBack();
break;
case "forward-button":
navigateForward();
break;
case "stop-button":
stopLoading();
break;
case "reload-button":
reloadBrowser();
break;
case "fullscreen-button":
fullscreenBrowser();
break;
case "close-button":
e.stopImmediatePropagation();
closeBrowser();
break;
default:
return;
}
e.preventDefault();
}
function handlePageLoad() {}
function setAddress(url) {
realUrl = url;
address.textContent = url.replace(/https?:\/\//, "");
}
function navigateBack() {
sendNavCommand("back");
}
function navigateForward() {
sendNavCommand("forward");
}
function reloadBrowser() {
sendNavCommand("reload");
}
function stopLoading() {
sendNavCommand("stop");
}
function fullscreenBrowser() {
controls.style.display = "none";
iframe.classList.add("fullscreen");
document.body.append(iframe);
}
function sendNavCommand(action) {
if (!iframe.contentWindow)
return;
iframe.contentWindow.postMessage({
type: "NAV_COMMAND",
action
}, "*");
}
function showNavigationError(url, reason) {
alert(`NAVIGATION BLOCKED
${url}
${reason}`);
}
// src/js/resize.ts
var content3 = document.getElementById("content");
function initResize() {
window.addEventListener("resize", resize);
resize();
}
function resize() {
if (document.body.dataset.mode === "tall") {
resizeTall();
} else {
resizeCinema();
}
}
function resizeTall() {
const scale = Math.min(1, window.innerWidth / 960);
content3.style.transformOrigin = "top center";
content3.style.transform = `scaleX(${scale})`;
}
function resizeCinema() {
const scale = Math.min(window.innerWidth / 960, window.innerHeight / 540);
content3.style.transformOrigin = "center center";
content3.style.transform = `scale(${scale})`;
}
// src/js/webapp.ts // src/js/webapp.ts
var apps = []; var apps = [];
function cacheApps(a) { function cacheApps(a) {
@ -168,6 +347,14 @@ function cacheApps(a) {
apps.sort(); apps.sort();
window.dispatchEvent(new CustomEvent("apps:change")); window.dispatchEvent(new CustomEvent("apps:change"));
} }
function currentAppUrl() {
const project = sessionStore.get("project") || "root";
if (!apps.includes(project))
return;
const hostname = sessionStore.get("hostname") || "localhost";
const s = hostname.startsWith("localhost") ? "" : "s";
return `http${s}://${project}.${hostname}`;
}
// src/js/session.ts // src/js/session.ts
var sessionId = randomId(); var sessionId = randomId();
@ -183,7 +370,7 @@ function handleSessionStart(msg) {
sessionStore.set("hostname", msg.data.hostname); sessionStore.set("hostname", msg.data.hostname);
updateProjectName(msg.data.project); updateProjectName(msg.data.project);
updateCwd(msg.data.cwd); updateCwd(msg.data.cwd);
mode(msg.data.mode); browserCommands.mode?.(msg.data.mode);
} }
function handleSessionUpdate(msg) { function handleSessionUpdate(msg) {
const data = msg.data; const data = msg.data;
@ -410,32 +597,10 @@ class GameContext {
} }
} }
// src/js/focus.ts
function initFocus() {
window.addEventListener("click", focusHandler);
focusInput();
}
function focusInput() {
cmdInput.focus();
}
function focusHandler(e) {
const target = e.target;
if (!(target instanceof HTMLElement)) {
focusInput();
return;
}
if (["INPUT", "TEXTAREA", "CANVAS", "A"].includes(target.tagName))
return false;
const selection = window.getSelection() || "";
if (selection.toString() === "")
focusInput();
e.preventDefault();
}
// src/js/game.ts // src/js/game.ts
var FPS = 30; var FPS = 30;
var HEIGHT = 540; var HEIGHT2 = 540;
var WIDTH = 960; var WIDTH2 = 960;
var oldMode = "cinema"; var oldMode = "cinema";
var running = false; var running = false;
var canvas; var canvas;
@ -455,14 +620,14 @@ async function handleGameStart(msg) {
const name = msg.data; const name = msg.data;
let game; let game;
try { try {
game = await importUrl(`/source/${name}?session=${sessionId}}`); game = await import(`/source/${name}?session=${sessionId}`);
} catch (err) { } catch (err) {
setStatus(msgId, "error"); setStatus(msgId, "error");
addOutput(msgId, `Error: ${err.message ? err.message : err}`); addOutput(msgId, `Error: ${err.message ? err.message : err}`);
return; return;
} }
if (document.body.dataset.mode === "tall") { if (document.body.dataset.mode === "tall") {
mode(); browserCommands.mode?.();
oldMode = "tall"; oldMode = "tall";
} }
canvas = createCanvas(); canvas = createCanvas();
@ -477,8 +642,8 @@ async function handleGameStart(msg) {
function createCanvas() { function createCanvas() {
const canvas2 = $$("canvas.game.active"); const canvas2 = $$("canvas.game.active");
canvas2.id = randomId(); canvas2.id = randomId();
canvas2.height = HEIGHT; canvas2.height = HEIGHT2;
canvas2.width = WIDTH; canvas2.width = WIDTH2;
canvas2.tabIndex = 0; canvas2.tabIndex = 0;
const main = document.querySelector("main"); const main = document.querySelector("main");
main?.classList.add("game"); main?.classList.add("game");
@ -545,12 +710,12 @@ function endGame() {
window.removeEventListener("keyup", handleKeyup); window.removeEventListener("keyup", handleKeyup);
window.removeEventListener("resize", resizeCanvas); window.removeEventListener("resize", resizeCanvas);
if (oldMode === "tall") if (oldMode === "tall")
mode(); browserCommands.mode?.();
const main = document.querySelector("main"); const main = document.querySelector("main");
main?.classList.remove("game"); main?.classList.remove("game");
canvas.classList.remove("active"); canvas.classList.remove("active");
canvas.style.height = HEIGHT / 2 + "px"; canvas.style.height = HEIGHT2 / 2 + "px";
canvas.style.width = WIDTH / 2 + "px"; canvas.style.width = WIDTH2 / 2 + "px";
const output2 = $$("li.output"); const output2 = $$("li.output");
output2.append(canvas); output2.append(canvas);
insert(output2); insert(output2);
@ -646,88 +811,61 @@ function retryConnection() {
setTimeout(startConnection, 2000); setTimeout(startConnection, 2000);
} }
// src/js/history.ts // src/js/statusbar.ts
var history = ["one", "two", "three"]; var STATUS_MSG_LENGTH = 3000;
var idx = -1; var statusbar = $("statusbar");
var savedInput = ""; var statusbarMsg = $("statusbar-msg");
function initHistory() { var timer;
cmdInput.addEventListener("keydown", navigateHistory); function status(msg) {
showStatusMsg();
statusbarMsg.textContent = msg;
if (timer)
clearTimeout(timer);
timer = setTimeout(hideStatusMsg, STATUS_MSG_LENGTH);
} }
function addToHistory(input) { function showStatusMsg() {
if (history.length === 0 || history[0] === input) statusbar.classList.add("showing-msg");
return;
history.unshift(input);
resetHistory();
} }
function resetHistory() { function hideStatusMsg() {
idx = -1; statusbar.className = "";
savedInput = "";
}
function navigateHistory(e) {
if (cmdLine.dataset.extended)
return;
if (e.key === "ArrowUp" || e.ctrlKey && e.key === "p") {
e.preventDefault();
if (idx >= history.length - 1)
return;
if (idx === -1)
savedInput = cmdInput.value;
cmdInput.value = history[++idx] || "";
if (idx >= history.length)
idx = history.length - 1;
} else if (e.key === "ArrowDown" || e.ctrlKey && e.key === "n") {
e.preventDefault();
if (idx <= 0) {
cmdInput.value = savedInput;
idx = -1;
return;
}
cmdInput.value = history[--idx] || "";
if (idx < -1)
idx = -1;
} else if (idx !== -1) {
resetHistory();
}
}
// src/js/shell.ts
async function runCommand(input, showInput = true) {
if (!input.trim())
return;
if (input.includes(";")) {
input.split(";").forEach(async (cmd2) => await runCommand(cmd2.trim()));
return;
}
const id = randomId();
addToHistory(input);
if (showInput)
addInput(id, input);
const [cmd = "", ...args] = input.split(" ");
if (commands[cmd]?.type === "browser") {
const mod = await importUrl(`/source/${cmd}`);
if (!mod.default) {
addOutput(id, `no default export in ${cmd}`);
setStatus(id, "error");
return;
}
const result = mod.default(...args);
if (result)
addOutput(id, result);
setStatus(id, "ok");
} else {
send({ id, type: "input", data: input });
}
} }
// src/js/commands.ts // src/js/commands.ts
var commands = {}; var commands = [];
var browserCommands = {
browse: (url) => {
const currentUrl = url ?? currentAppUrl();
if (currentUrl) {
openBrowser(currentUrl, "command");
} else {
setTimeout(() => setStatus(latestId(), "error"), 0);
return "usage: browse <url>";
}
},
"browser-session": () => sessionId,
clear: () => scrollback.innerHTML = "",
commands: () => {
return { html: "<div>" + commands.map((cmd) => `<a href="#help ${cmd}">${cmd}</a>`).join("") + "</div>" };
},
fullscreen: () => document.body.requestFullscreen(),
mode: (mode) => {
if (!mode) {
mode = document.body.dataset.mode === "tall" ? "cinema" : "tall";
send({ type: "session:update", data: { "ui:mode": mode } });
}
content2.style.display = "";
document.body.dataset.mode = mode;
resize();
focusInput();
},
status: (msg) => status(msg),
reload: () => window.location.reload()
};
function cacheCommands(cmds) { function cacheCommands(cmds) {
for (const key in commands) commands.length = 0;
delete commands[key]; commands.push(...cmds);
Object.assign(commands, cmds); commands.push(...Object.keys(browserCommands));
} commands.sort();
async function mode(mode2) {
await runCommand(mode2 ? `mode ${mode2}` : "mode", false);
} }
// src/js/completion.ts // src/js/completion.ts
@ -739,7 +877,7 @@ function handleCompletion(e) {
return; return;
e.preventDefault(); e.preventDefault();
const input = cmdInput.value; const input = cmdInput.value;
for (const command of Object.keys(commands)) { for (const command of commands) {
if (command.startsWith(input)) { if (command.startsWith(input)) {
cmdInput.value = command; cmdInput.value = command;
return; return;
@ -1069,153 +1207,70 @@ function handleGamepad() {
requestAnimationFrame(handleGamepad); requestAnimationFrame(handleGamepad);
} }
// src/js/browser.ts // src/js/history.ts
var HEIGHT2 = 540; var history = ["one", "two", "three"];
var WIDTH2 = 960; var idx = -1;
var controls = $("browser-controls"); var savedInput = "";
var address = $("browser-address"); function initHistory() {
var iframe; cmdInput.addEventListener("keydown", navigateHistory);
var realUrl = "";
var showInput = true;
function isBrowsing() {
return document.querySelector("iframe.browser.active") !== null;
} }
function openBrowser(url, openedVia = "click") { function addToHistory(input) {
showInput = openedVia === "click"; if (history.length === 0 || history[0] === input)
iframe = $$("iframe.browser.active");
iframe.src = url;
iframe.sandbox.add("allow-scripts", "allow-same-origin", "allow-forms");
iframe.height = String(HEIGHT2);
iframe.width = String(WIDTH2);
iframe.tabIndex = 0;
window.addEventListener("message", handleAppMessage);
window.addEventListener("keydown", handleBrowserKeydown);
controls.addEventListener("click", handleClick);
iframe.addEventListener("load", handlePageLoad);
controls.style.display = "";
const main = document.querySelector("#content");
main?.prepend(iframe);
setAddress(url);
}
function closeBrowser() {
window.removeEventListener("keydown", handleBrowserKeydown);
window.removeEventListener("message", handleAppMessage);
controls.removeEventListener("click", handleClick);
iframe.removeEventListener("load", handlePageLoad);
const id = randomId();
if (showInput)
addInput(id, "browse " + realUrl, "ok");
iframe.style.transformOrigin = "top left";
iframe.style.transform = "scale(0.5)";
iframe.style.pointerEvents = "none";
iframe.tabIndex = -1;
iframe.classList.remove("fullscreen", "active");
scrollback.append(iframe);
controls.style.display = "none";
focusInput();
}
function handleAppMessage(event) {
const origin = event.origin;
if (!origin.includes("localhost") && !origin.match(/\.local$/)) {
return; return;
} history.unshift(input);
const { type, data } = event.data; resetHistory();
switch (type) {
case "NAV_READY":
break;
case "URL_CHANGED":
setAddress(data.url);
break;
case "TITLE_CHANGED":
console.log("Page title:", data.title);
break;
case "NAV_BLOCKED":
showNavigationError(data.url, data.reason);
break;
case "KEYDOWN":
const keyEvent = new KeyboardEvent("keydown", {
key: data.key,
ctrlKey: data.ctrlKey,
shiftKey: data.shiftKey,
altKey: data.altKey,
metaKey: data.metaKey,
bubbles: true
});
window.dispatchEvent(keyEvent);
break;
}
} }
function handleBrowserKeydown(e) { function resetHistory() {
if (e.key === "Escape" || e.ctrlKey && e.key === "c") { idx = -1;
savedInput = "";
}
function navigateHistory(e) {
if (cmdLine.dataset.extended)
return;
if (e.key === "ArrowUp" || e.ctrlKey && e.key === "p") {
e.preventDefault(); e.preventDefault();
closeBrowser(); if (idx >= history.length - 1)
}
}
function handleClick(e) {
const target = e.target;
if (!(target instanceof HTMLElement))
return;
switch (target.id) {
case "back-button":
navigateBack();
break;
case "forward-button":
navigateForward();
break;
case "stop-button":
stopLoading();
break;
case "reload-button":
reloadBrowser();
break;
case "fullscreen-button":
fullscreenBrowser();
break;
case "close-button":
e.stopImmediatePropagation();
closeBrowser();
break;
default:
return; return;
if (idx === -1)
savedInput = cmdInput.value;
cmdInput.value = history[++idx] || "";
if (idx >= history.length)
idx = history.length - 1;
} else if (e.key === "ArrowDown" || e.ctrlKey && e.key === "n") {
e.preventDefault();
if (idx <= 0) {
cmdInput.value = savedInput;
idx = -1;
return;
}
cmdInput.value = history[--idx] || "";
if (idx < -1)
idx = -1;
} else if (idx !== -1) {
resetHistory();
} }
e.preventDefault();
} }
function handlePageLoad() {}
function setAddress(url) { // src/js/shell.ts
realUrl = url; async function runCommand(input) {
address.textContent = url.replace(/https?:\/\//, ""); if (!input.trim())
}
function navigateBack() {
sendNavCommand("back");
}
function navigateForward() {
sendNavCommand("forward");
}
function reloadBrowser() {
sendNavCommand("reload");
}
function stopLoading() {
sendNavCommand("stop");
}
function fullscreenBrowser() {
controls.style.display = "none";
iframe.classList.add("fullscreen");
document.body.append(iframe);
}
function sendNavCommand(action) {
if (!iframe.contentWindow)
return; return;
iframe.contentWindow.postMessage({ if (input.includes(";")) {
type: "NAV_COMMAND", input.split(";").forEach(async (cmd2) => await runCommand(cmd2.trim()));
action return;
}, "*"); }
} const id = randomId();
function showNavigationError(url, reason) { addToHistory(input);
alert(`NAVIGATION BLOCKED addInput(id, input);
const [cmd = "", ...args] = input.split(" ");
${url} if (browserCommands[cmd]) {
const result = await browserCommands[cmd](...args);
${reason}`); if (result)
addOutput(id, result);
setStatus(id, "ok");
} else {
send({ id, type: "input", data: input });
}
} }
// src/js/hyperlink.ts // src/js/hyperlink.ts
@ -1293,34 +1348,10 @@ function clearInput() {
delete cmdLine.dataset.extended; delete cmdLine.dataset.extended;
} }
// src/js/resize.ts
var content3 = document.getElementById("content");
function initResize() {
window.addEventListener("resize", resize);
resize();
}
function resize() {
if (document.body.dataset.mode === "tall") {
resizeTall();
} else {
resizeCinema();
}
}
function resizeTall() {
const scale = Math.min(1, window.innerWidth / 960);
content3.style.transformOrigin = "top center";
content3.style.transform = `scaleX(${scale})`;
}
function resizeCinema() {
const scale = Math.min(window.innerWidth / 960, window.innerHeight / 540);
content3.style.transformOrigin = "center center";
content3.style.transform = `scale(${scale})`;
}
// src/js/vram.ts // src/js/vram.ts
var vramCounter = $("vram-size"); var vramCounter = $("vram-size");
var startVramCounter = () => { var startVramCounter = () => {
const timer = setInterval(() => { const timer2 = setInterval(() => {
const count = parseInt(vramCounter.textContent) + 1; const count = parseInt(vramCounter.textContent) + 1;
let val = count + "KB"; let val = count + "KB";
if (count < 10) if (count < 10)
@ -1328,7 +1359,7 @@ var startVramCounter = () => {
vramCounter.textContent = val; vramCounter.textContent = val;
if (count >= 64) { if (count >= 64) {
vramCounter.textContent += " OK"; vramCounter.textContent += " OK";
clearInterval(timer); clearInterval(timer2);
} }
}, 15); }, 15);
}; };

View File

@ -1,23 +1,8 @@
#!/bin/sh #!/bin/sh
SHA=$(git rev-parse --short HEAD) SHA=$(git rev-parse --short HEAD)
# Build to a temporary file
bun build ./src/js/main.js \ bun build ./src/js/main.js \
--outfile ./public/bundle.tmp.js \ --outfile ./public/bundle.js \
--target browser --target browser
printf "////\n// version: %s\n\n" "$SHA" | cat - ./public/bundle.js > ./public/bundle.tmp.js
# If bundle.js doesn't exist yet, fake it it mv ./public/bundle.tmp.js ./public/bundle.js
if [ ! -f ./public/bundle.js ]; then
touch ./public/bundle.js
fi
# Strip version comments from both files and compare
tail -n +4 ./public/bundle.js > ./public/bundle.old.content.tmp
tail -n +1 ./public/bundle.tmp.js > ./public/bundle.new.content.tmp
if ! cmp -s ./public/bundle.old.content.tmp ./public/bundle.new.content.tmp; then
printf "////\n// version: %s\n\n" "$SHA" | cat - ./public/bundle.tmp.js > ./public/bundle.js
fi
rm ./public/bundle.tmp.js ./public/bundle.old.content.tmp ./public/bundle.new.content.tmp

View File

@ -82,7 +82,7 @@ export async function commandSource(name: string): Promise<string> {
export async function loadCommandModule(cmd: string) { export async function loadCommandModule(cmd: string) {
const path = commandPath(cmd) const path = commandPath(cmd)
if (!path) return if (!path) return
return await importUrl(path) return await importUrl(path + "?t+" + Date.now())
} }
let noseDirWatcher let noseDirWatcher

View File

@ -8,7 +8,7 @@ import { send } from "./websocket"
import { setState } from "./state" import { setState } from "./state"
export async function dispatchMessage(ws: any, msg: Message) { export async function dispatchMessage(ws: any, msg: Message) {
// console.log("<- receive", msg) console.log("<- receive", msg)
switch (msg.type) { switch (msg.type) {
case "input": case "input":
await inputMessage(ws, msg); break await inputMessage(ws, msg); break

View File

@ -12,12 +12,6 @@ export const Layout: FC = async ({ children, title }) => (
<link href="/css/main.css" rel="stylesheet" /> <link href="/css/main.css" rel="stylesheet" />
<link href="/css/game.css" rel="stylesheet" /> <link href="/css/game.css" rel="stylesheet" />
<script dangerouslySetInnerHTML={{
__html: `
window.NODE_ENV = ${process.env.NODE_ENV ? "'" + process.env.NODE_ENV + "'" : 'undefined'};
window.GIT_SHA = "${GIT_SHA}";
`}} />
<script type="importmap" dangerouslySetInnerHTML={{ __html: `{ "imports": { "@/": "/" } }` }} /> <script type="importmap" dangerouslySetInnerHTML={{ __html: `{ "imports": { "@/": "/" } }` }} />
<script src={process.env.NODE_ENV === "production" ? `/bundle.js?${GIT_SHA}` : "/js/main.js"} type="module" async></script> <script src={process.env.NODE_ENV === "production" ? `/bundle.js?${GIT_SHA}` : "/js/main.js"} type="module" async></script>
</head> </head>

View File

@ -35,7 +35,7 @@ export async function handleGameStart(msg: GameStartMessage) {
let game let game
try { try {
game = await importUrl(`/source/${name}?session=${sessionId}}`) game = await importUrl(`/source/${name}?session=${sessionId}&t=${Date.now()}`)
} catch (err: any) { } catch (err: any) {
setStatus(msgId, "error") setStatus(msgId, "error")
addOutput(msgId, `Error: ${err.message ? err.message : err}`) addOutput(msgId, `Error: ${err.message ? err.message : err}`)

View File

@ -25,7 +25,7 @@ export async function runCommand(input: string, showInput = true) {
const [cmd = "", ...args] = input.split(" ") const [cmd = "", ...args] = input.split(" ")
if (commands[cmd]?.type === "browser") { if (commands[cmd]?.type === "browser") {
const mod = await importUrl(`/source/${cmd}`) const mod = await importUrl(`/source/${cmd}?t=${Date.now()}`)
if (!mod.default) { if (!mod.default) {
addOutput(id, `no default export in ${cmd}`) addOutput(id, `no default export in ${cmd}`)
setStatus(id, "error") setStatus(id, "error")

View File

@ -40,27 +40,7 @@ export function unique<T>(array: T[]): T[] {
return [...new Set(array)] return [...new Set(array)]
} }
// import a typescript module. caches in production, doesn't in dev.
export async function importUrl(url: string) { export async function importUrl(url: string) {
url += url.includes("?") ? "&" : "?"
url += "t=" + (nodeEnv() === "production" ? gitSHA() : Date.now())
console.log("-> import", url) console.log("-> import", url)
return import(url) return import(url)
}
// "production" or nuttin
function nodeEnv(): string | undefined {
if (typeof process !== 'undefined' && process.env?.NODE_ENV)
return process.env.NODE_ENV
if (typeof globalThis !== 'undefined' && (globalThis as any).NODE_ENV)
return (globalThis as any).NODE_ENV
return undefined
}
// should always be set
function gitSHA(): string {
return (globalThis as any).GIT_SHA
} }

View File

@ -30,7 +30,8 @@ export type ExportInfo =
let prevProgram: ts.Program | undefined let prevProgram: ts.Program | undefined
export async function moduleExports(file: string): Promise<Record<string, ExportInfo>> {
export async function allExports(file: string): Promise<ExportInfo[]> {
const program = ts.createProgram([file], { const program = ts.createProgram([file], {
target: ts.ScriptTarget.ESNext, target: ts.ScriptTarget.ESNext,
module: ts.ModuleKind.ESNext, module: ts.ModuleKind.ESNext,
@ -47,7 +48,7 @@ export async function moduleExports(file: string): Promise<Record<string, Export
if (!sf) throw new SniffError(`File not found: ${file}`) if (!sf) throw new SniffError(`File not found: ${file}`)
const moduleSym = (sf as any).symbol as ts.Symbol | undefined const moduleSym = (sf as any).symbol as ts.Symbol | undefined
if (!moduleSym) return {} if (!moduleSym) return []
const exportSymbols = checker.getExportsOfModule(moduleSym) const exportSymbols = checker.getExportsOfModule(moduleSym)
const result: ExportInfo[] = [] const result: ExportInfo[] = []
@ -91,9 +92,7 @@ export async function moduleExports(file: string): Promise<Record<string, Export
} }
} }
return Object.fromEntries( return result
result.map(item => [item.name, item])
)
} }
if (import.meta.main) { if (import.meta.main) {
@ -102,5 +101,5 @@ if (import.meta.main) {
console.error("usage: sniff <path>") console.error("usage: sniff <path>")
process.exit(1) process.exit(1)
} }
console.log(await moduleExports(path)) console.log(await allExports(path))
} }

View File

@ -74,7 +74,7 @@ async function findApp(name: string): Promise<App | undefined> {
async function loadApp(path: string): Promise<App | undefined> { async function loadApp(path: string): Promise<App | undefined> {
if (!await Bun.file(path).exists()) return if (!await Bun.file(path).exists()) return
const mod = await importUrl(path) const mod = await importUrl(path + `?t=${Date.now()}`)
if (mod?.default) if (mod?.default)
return mod.default as App return mod.default as App
} }

View File

@ -6,7 +6,7 @@ import type { Message } from "./shared/types"
const wsConnections: any[] = [] const wsConnections: any[] = []
export function send(ws: any, msg: Message) { export function send(ws: any, msg: Message) {
// console.log("-> send", msg) console.log("-> send", msg)
ws.send(JSON.stringify(msg)) ws.send(JSON.stringify(msg))
} }