Compare commits
3 Commits
8b8f17a9fc
...
94b0eb4dad
| Author | SHA1 | Date | |
|---|---|---|---|
| 94b0eb4dad | |||
| d1e2e7d7a4 | |||
| 312abc11d8 |
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
||||||
|
data/
|
||||||
# dependencies (bun install)
|
# dependencies (bun install)
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,11 @@ import { NOSE_WWW } from "app/src/config"
|
||||||
import { isBinaryFile } from "app/src/utils"
|
import { isBinaryFile } from "app/src/utils"
|
||||||
import { highlight } from "../lib/highlight"
|
import { highlight } from "../lib/highlight"
|
||||||
import { projectName, projectDir } from "@/project"
|
import { projectName, projectDir } from "@/project"
|
||||||
import { getState } from "@/session"
|
import { sessionGet } from "@/session"
|
||||||
|
|
||||||
export default async function (path: string) {
|
export default async function (path: string) {
|
||||||
const project = projectName()
|
const project = projectName()
|
||||||
const root = getState("cwd") || projectDir()
|
const root = sessionGet("cwd") || projectDir()
|
||||||
|
|
||||||
let files: string[] = []
|
let files: string[] = []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,14 @@
|
||||||
import { dirname, resolve, isAbsolute } from "path"
|
import { dirname, resolve, isAbsolute } from "path"
|
||||||
import { statSync } from "fs"
|
import { statSync } from "fs"
|
||||||
import { projectDir } from "@/project"
|
import { projectDir } from "@/project"
|
||||||
import { getState, setState } from "@/session"
|
import { sessionGet, sessionSet } from "@/session"
|
||||||
|
|
||||||
export default async function (path?: string) {
|
export default async function (path?: string) {
|
||||||
const root = projectDir()
|
const root = projectDir()
|
||||||
const cwd = getState("cwd") || root
|
const cwd = sessionGet("cwd") || root
|
||||||
|
|
||||||
if (!path || path.trim() === "") {
|
if (!path || path.trim() === "") {
|
||||||
setState("cwd", root)
|
sessionSet("cwd", root)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -21,7 +21,7 @@ export default async function (path?: string) {
|
||||||
if (cwd !== root) {
|
if (cwd !== root) {
|
||||||
const parent = dirname(cwd)
|
const parent = dirname(cwd)
|
||||||
if (parent.startsWith(root)) {
|
if (parent.startsWith(root)) {
|
||||||
setState("cwd", parent)
|
sessionSet("cwd", parent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|
@ -35,7 +35,7 @@ export default async function (path?: string) {
|
||||||
try {
|
try {
|
||||||
const stat = statSync(target)
|
const stat = statSync(target)
|
||||||
if (stat.isDirectory()) {
|
if (stat.isDirectory()) {
|
||||||
setState("cwd", target)
|
sessionSet("cwd", target)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,11 @@ import { NOSE_WWW } from "app/src/config"
|
||||||
import { isBinaryFile } from "app/src/utils"
|
import { isBinaryFile } from "app/src/utils"
|
||||||
import { countChar } from "app/src/shared/utils"
|
import { countChar } from "app/src/shared/utils"
|
||||||
import { projectName, projectDir } from "@/project"
|
import { projectName, projectDir } from "@/project"
|
||||||
import { getState } from "@/session"
|
import { sessionGet } from "@/session"
|
||||||
|
|
||||||
export default async function (path: string) {
|
export default async function (path: string) {
|
||||||
const project = projectName()
|
const project = projectName()
|
||||||
const root = getState("cwd") || projectDir()
|
const root = sessionGet("cwd") || projectDir()
|
||||||
|
|
||||||
let files: string[] = []
|
let files: string[] = []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
// Load a project so you can work on it.
|
// Load a project so you can work on it.
|
||||||
|
|
||||||
import { apps } from "app/src/webapp"
|
import { apps } from "app/src/webapp"
|
||||||
import { getState } from "@/session"
|
import { sessionGet } from "@/session"
|
||||||
|
|
||||||
export default function (project: string) {
|
export default function (project: string) {
|
||||||
const state = getState()
|
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)) {
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,11 @@
|
||||||
import { readdirSync } from "fs"
|
import { readdirSync } from "fs"
|
||||||
import { NOSE_WWW } from "app/src/config"
|
import { NOSE_WWW } from "app/src/config"
|
||||||
import { projectName, projectDir } from "@/project"
|
import { projectName, projectDir } from "@/project"
|
||||||
import { getState } from "@/session"
|
import { sessionGet } from "@/session"
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
const project = projectName()
|
const project = projectName()
|
||||||
const root = getState("cwd") || projectDir()
|
const root = sessionGet("cwd") || projectDir()
|
||||||
|
|
||||||
let files: string[] = []
|
let files: string[] = []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,12 @@ import { mkdirSync } from "fs"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
import { projectDir } from "@/project"
|
import { projectDir } from "@/project"
|
||||||
import { readdirSync } from "fs"
|
import { readdirSync } from "fs"
|
||||||
import { getState } from "@/session"
|
import { sessionGet } from "@/session"
|
||||||
|
|
||||||
export default async function (path: string) {
|
export default async function (path: string) {
|
||||||
if (path.endsWith("/")) path = path.slice(0, path.length - 1)
|
if (path.endsWith("/")) path = path.slice(0, path.length - 1)
|
||||||
|
|
||||||
const root = getState("cwd") || projectDir()
|
const root = sessionGet("cwd") || projectDir()
|
||||||
for (const file of readdirSync(root, { withFileTypes: true }))
|
for (const file of readdirSync(root, { withFileTypes: true }))
|
||||||
if (file.name === path) throw `${path} exists`
|
if (file.name === path) throw `${path} exists`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
// Print the currently loaded project.
|
// Print the currently loaded project.
|
||||||
|
|
||||||
import { getState } from "@/session"
|
import { sessionGet } from "@/session"
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
const state = getState()
|
const state = sessionGet()
|
||||||
if (!state) return { error: "no state" }
|
if (!state) return { error: "no state" }
|
||||||
|
|
||||||
return state?.project || "none"
|
return state?.project || "none"
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
// Show the projects on this NOSEputer.
|
// Show the projects on this NOSEputer.
|
||||||
|
|
||||||
import { apps } from "app/src/webapp"
|
import { apps } from "app/src/webapp"
|
||||||
import { getState } from "@/session"
|
import { sessionGet } from "@/session"
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
const state = getState()
|
const state = sessionGet()
|
||||||
if (!state) return { error: "no state" }
|
if (!state) return { error: "no state" }
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
import { dirname } from "path"
|
import { dirname } from "path"
|
||||||
import { projectDir } from "@/project"
|
import { projectDir } from "@/project"
|
||||||
import { getState } from "@/session"
|
import { sessionGet } from "@/session"
|
||||||
|
|
||||||
export default async function () {
|
export default async function () {
|
||||||
const root = projectDir()
|
const root = projectDir()
|
||||||
return (getState("cwd") || root).replace(dirname(root), "")
|
return (sessionGet("cwd") || root).replace(dirname(root), "")
|
||||||
}
|
}
|
||||||
|
|
@ -3,13 +3,13 @@
|
||||||
import { unlinkSync, readdirSync } from "fs"
|
import { unlinkSync, readdirSync } from "fs"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
import { projectDir } from "@/project"
|
import { projectDir } from "@/project"
|
||||||
import { getState } from "@/session"
|
import { sessionGet } from "@/session"
|
||||||
|
|
||||||
export default function (path: string) {
|
export default function (path: string) {
|
||||||
let target = ""
|
let target = ""
|
||||||
if (path.endsWith("/")) path = path.slice(0, path.length - 1)
|
if (path.endsWith("/")) path = path.slice(0, path.length - 1)
|
||||||
|
|
||||||
const root = getState("cwd") || projectDir()
|
const root = sessionGet("cwd") || projectDir()
|
||||||
for (const file of readdirSync(root, { withFileTypes: true }))
|
for (const file of readdirSync(root, { withFileTypes: true }))
|
||||||
if (file.name === path) {
|
if (file.name === path) {
|
||||||
if (file.isDirectory())
|
if (file.isDirectory())
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,13 @@
|
||||||
import { rmdirSync, readdirSync } from "fs"
|
import { rmdirSync, readdirSync } from "fs"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
import { projectDir } from "@/project"
|
import { projectDir } from "@/project"
|
||||||
import { getState } from "@/session"
|
import { sessionGet } from "@/session"
|
||||||
|
|
||||||
export default function (path: string) {
|
export default function (path: string) {
|
||||||
let target = ""
|
let target = ""
|
||||||
if (path.endsWith("/")) path = path.slice(0, path.length - 1)
|
if (path.endsWith("/")) path = path.slice(0, path.length - 1)
|
||||||
|
|
||||||
const root = getState("cwd") || projectDir()
|
const root = sessionGet("cwd") || projectDir()
|
||||||
for (const file of readdirSync(root, { withFileTypes: true }))
|
for (const file of readdirSync(root, { withFileTypes: true }))
|
||||||
if (file.name === path) {
|
if (file.name === path) {
|
||||||
if (file.isFile())
|
if (file.isFile())
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,12 @@
|
||||||
import { apps } from "app/src/webapp"
|
import { apps } from "app/src/webapp"
|
||||||
import { connectSneaker, sneakers, sneakerUrl } from "app/src/sneaker"
|
import { connectSneaker, sneakers, sneakerUrl } from "app/src/sneaker"
|
||||||
|
|
||||||
export default async function (app: string, subdomain = "") {
|
export default async function (app: string) {
|
||||||
if (!app) {
|
if (!app) {
|
||||||
let out = `usage: share <app> [subdomain]`
|
let out = `usage: share <app> [subdomain]`
|
||||||
const apps = sneakers()
|
const apps = sneakers()
|
||||||
if (apps.length) {
|
if (apps.length) {
|
||||||
|
out += "\n\nUse `unshare` to stop sharing an app."
|
||||||
out += "\n\nsharing\n" + apps.map(app => {
|
out += "\n\nsharing\n" + apps.map(app => {
|
||||||
const url = sneakerUrl(app)
|
const url = sneakerUrl(app)
|
||||||
return `${app}: <a href="${url}">${url}</a>`
|
return `${app}: <a href="${url}">${url}</a>`
|
||||||
|
|
@ -20,6 +21,6 @@ export default async function (app: string, subdomain = "") {
|
||||||
return { error: `${app} not found` }
|
return { error: `${app} not found` }
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = sneakerUrl(await connectSneaker(app, subdomain))
|
const url = sneakerUrl(await connectSneaker(app))
|
||||||
return { html: `<a href="${url}">${url}</a>` }
|
return { html: `<a href="${url}">${url}</a>` }
|
||||||
}
|
}
|
||||||
6
app/nose/bin/state.ts
Normal file
6
app/nose/bin/state.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { NOSE_DATA } from "@/config"
|
||||||
|
import { join } from "path"
|
||||||
|
|
||||||
|
export default async function () {
|
||||||
|
return JSON.parse(await Bun.file(join(NOSE_DATA, "state.json")).text())
|
||||||
|
}
|
||||||
|
|
@ -3,12 +3,12 @@
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
import { readdirSync } from "fs"
|
import { readdirSync } from "fs"
|
||||||
import { projectDir } from "@/project"
|
import { projectDir } from "@/project"
|
||||||
import { getState } from "@/session"
|
import { sessionGet } from "@/session"
|
||||||
|
|
||||||
export default async function (path: string) {
|
export default async function (path: string) {
|
||||||
if (path.endsWith("/")) path = path.slice(0, path.length - 1)
|
if (path.endsWith("/")) path = path.slice(0, path.length - 1)
|
||||||
|
|
||||||
const root = getState("cwd") || projectDir()
|
const root = sessionGet("cwd") || projectDir()
|
||||||
for (const file of readdirSync(root, { withFileTypes: true }))
|
for (const file of readdirSync(root, { withFileTypes: true }))
|
||||||
if (file.name === path) throw `${path} exists`
|
if (file.name === path) throw `${path} exists`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,5 +10,7 @@ export const NOSE_DIR = resolve("..")
|
||||||
export const NOSE_BIN = join(NOSE_DIR, "bin")
|
export const NOSE_BIN = join(NOSE_DIR, "bin")
|
||||||
export const NOSE_WWW = join(NOSE_DIR, "www")
|
export const NOSE_WWW = join(NOSE_DIR, "www")
|
||||||
|
|
||||||
|
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 HEAD`.text()).trim()
|
export const GIT_SHA = (await $`git rev-parse HEAD`.text()).trim()
|
||||||
27
app/src/mutex.ts
Normal file
27
app/src/mutex.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
////
|
||||||
|
// Simple mutex for concurrent file writes
|
||||||
|
|
||||||
|
export class Mutex {
|
||||||
|
private queue: (() => void)[] = []
|
||||||
|
private locked = false
|
||||||
|
|
||||||
|
async lock(): Promise<() => void> {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const unlock = () => {
|
||||||
|
const next = this.queue.shift()
|
||||||
|
if (next) {
|
||||||
|
next()
|
||||||
|
} else {
|
||||||
|
this.locked = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.locked) {
|
||||||
|
this.queue.push(() => resolve(unlock))
|
||||||
|
} else {
|
||||||
|
this.locked = true
|
||||||
|
resolve(unlock)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
// Helpers for working with projects in the CLI.
|
// Helpers for working with projects in the CLI.
|
||||||
|
|
||||||
import { readdirSync, type Dirent } from "fs"
|
import { readdirSync, type Dirent } from "fs"
|
||||||
import { getState } from "./session"
|
import { sessionGet } from "./session"
|
||||||
import { appDir } from "./webapp"
|
import { appDir } from "./webapp"
|
||||||
|
|
||||||
export function projectName(): string {
|
export function projectName(): string {
|
||||||
const state = getState()
|
const state = sessionGet()
|
||||||
if (!state) throw "no state"
|
if (!state) throw "no state"
|
||||||
|
|
||||||
const project = state.project
|
const project = state.project
|
||||||
|
|
|
||||||
|
|
@ -7,17 +7,17 @@ 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 } from "./config"
|
import { NOSE_ICON, NOSE_BIN, NOSE_WWW, NOSE_DATA } 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, commandSource, commandPath } from "./commands"
|
import { commands, commandPath } from "./commands"
|
||||||
import { send, addWebsocket, removeWebsocket, closeWebsockets } from "./websocket"
|
import { send, addWebsocket, removeWebsocket, closeWebsockets } from "./websocket"
|
||||||
|
|
||||||
import { Layout } from "./html/layout"
|
import { Layout } from "./html/layout"
|
||||||
import { Terminal } from "./html/terminal"
|
import { Terminal } from "./html/terminal"
|
||||||
import { dispatchMessage } from "./dispatch"
|
import { dispatchMessage } from "./dispatch"
|
||||||
import "./sneaker"
|
import { initSneakers, disconnectSneakers } from "./sneaker"
|
||||||
|
|
||||||
//
|
//
|
||||||
// Hono setup
|
// Hono setup
|
||||||
|
|
@ -129,6 +129,7 @@ if (process.env.BUN_HOT) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
globalThis.__hot_reload_cleanup = () => {
|
globalThis.__hot_reload_cleanup = () => {
|
||||||
closeWebsockets()
|
closeWebsockets()
|
||||||
|
disconnectSneakers()
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const sig of ["SIGINT", "SIGTERM"] as const) {
|
for (const sig of ["SIGINT", "SIGTERM"] as const) {
|
||||||
|
|
@ -138,19 +139,27 @@ if (process.env.BUN_HOT) {
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
initDNS()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// production mode
|
||||||
|
//
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === "production") {
|
||||||
|
initDNS()
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// server start
|
// server start
|
||||||
//
|
//
|
||||||
|
|
||||||
console.log(color.cyan(NOSE_ICON))
|
console.log(color.cyan(NOSE_ICON))
|
||||||
|
console.log(color.blue("NOSE_DATA:"), color.yellow(tilde(NOSE_DATA)))
|
||||||
console.log(color.blue("NOSE_BIN:"), color.yellow(tilde(NOSE_BIN)))
|
console.log(color.blue("NOSE_BIN:"), color.yellow(tilde(NOSE_BIN)))
|
||||||
console.log(color.blue("NOSE_WWW:"), color.yellow(tilde(NOSE_WWW)))
|
console.log(color.blue("NOSE_WWW:"), color.yellow(tilde(NOSE_WWW)))
|
||||||
|
|
||||||
|
initSneakers()
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
port: process.env.PORT || 3000,
|
port: process.env.PORT || 3000,
|
||||||
hostname: "0.0.0.0",
|
hostname: "0.0.0.0",
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ 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>()
|
||||||
|
|
||||||
export function getState(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) return
|
||||||
|
|
||||||
|
|
@ -24,7 +24,7 @@ export function getState(key?: keyof Session): Session | any | undefined {
|
||||||
return store
|
return store
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setState(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) return
|
||||||
store[key] = value
|
store[key] = value
|
||||||
|
|
|
||||||
|
|
@ -83,5 +83,5 @@ function errorMessage(error: Error | any): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
function isJSX(obj: any): boolean {
|
function isJSX(obj: any): boolean {
|
||||||
return 'tag' in obj && 'props' in obj && 'children' in obj
|
return typeof obj === 'object' && 'tag' in obj && 'props' in obj && 'children' in obj
|
||||||
}
|
}
|
||||||
|
|
@ -3,12 +3,15 @@
|
||||||
// with the public internet. It requires a sneaker server, usually hosted by us.
|
// with the public internet. It requires a sneaker server, usually hosted by us.
|
||||||
|
|
||||||
import nose from "./server"
|
import nose from "./server"
|
||||||
|
import { clearState, setState, getState } from "app/src/state"
|
||||||
|
|
||||||
const SNEAKER_URL = "nose.space"
|
const SNEAKER_URL = "nose.space"
|
||||||
const SNEAKER_TLS = true
|
const SNEAKER_TLS = true
|
||||||
// const SNEAKER_URL = "localhost:3100"
|
// const SNEAKER_URL = "localhost:3100"
|
||||||
// const SNEAKER_TLS = false
|
// const SNEAKER_TLS = false
|
||||||
|
|
||||||
|
const PREFIX = "sneaker:"
|
||||||
|
|
||||||
type Connection = {
|
type Connection = {
|
||||||
subdomain: string
|
subdomain: string
|
||||||
ws: any
|
ws: any
|
||||||
|
|
@ -16,6 +19,19 @@ type Connection = {
|
||||||
}
|
}
|
||||||
const conns: Record<string, Connection> = {}
|
const conns: Record<string, Connection> = {}
|
||||||
|
|
||||||
|
export function initSneakers() {
|
||||||
|
const state = getState()
|
||||||
|
if (!state) return
|
||||||
|
|
||||||
|
for (const key in state) {
|
||||||
|
if (key.startsWith(PREFIX)) {
|
||||||
|
const app = key.replace(PREFIX, "")
|
||||||
|
console.log("sharing", app, state[key])
|
||||||
|
connectSneaker(app, state[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function sneakerUrl(appOrSubdomain: string): string {
|
export function sneakerUrl(appOrSubdomain: string): string {
|
||||||
let conn = conns[appOrSubdomain]
|
let conn = conns[appOrSubdomain]
|
||||||
if (!conn) {
|
if (!conn) {
|
||||||
|
|
@ -29,7 +45,6 @@ export function sneakerUrl(appOrSubdomain: string): string {
|
||||||
return "none"
|
return "none"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let url = "http" + (SNEAKER_TLS ? "s" : "") + "://"
|
let url = "http" + (SNEAKER_TLS ? "s" : "") + "://"
|
||||||
url += conn.subdomain + "." + SNEAKER_URL
|
url += conn.subdomain + "." + SNEAKER_URL
|
||||||
|
|
||||||
|
|
@ -40,10 +55,19 @@ export function sneakers(): string[] {
|
||||||
return Object.keys(conns)
|
return Object.keys(conns)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function disconnectSneaker(app: string): boolean {
|
export async function disconnectSneakers() {
|
||||||
|
for (const app in conns) {
|
||||||
|
const conn = conns[app]!
|
||||||
|
conn.close = true
|
||||||
|
conn.ws.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function disconnectSneaker(app: string): Promise<boolean> {
|
||||||
if (!sneakers().includes(app) || !conns[app])
|
if (!sneakers().includes(app) || !conns[app])
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
await clearState(`${PREFIX}${app}`)
|
||||||
conns[app].close = true
|
conns[app].close = true
|
||||||
conns[app].ws.close()
|
conns[app].ws.close()
|
||||||
|
|
||||||
|
|
@ -60,6 +84,8 @@ export async function connectSneaker(app: string, subdomain = ""): Promise<strin
|
||||||
if (subdomain) url += `&subdomain=${subdomain}`
|
if (subdomain) url += `&subdomain=${subdomain}`
|
||||||
|
|
||||||
const ws = new WebSocket(url)
|
const ws = new WebSocket(url)
|
||||||
|
ws.binaryType = 'arraybuffer'
|
||||||
|
|
||||||
let resolve: (v: string) => void
|
let resolve: (v: string) => void
|
||||||
let promise = new Promise<string>(res => resolve = res)
|
let promise = new Promise<string>(res => resolve = res)
|
||||||
|
|
||||||
|
|
@ -76,6 +102,7 @@ export async function connectSneaker(app: string, subdomain = ""): Promise<strin
|
||||||
|
|
||||||
if (msg.subdomain) {
|
if (msg.subdomain) {
|
||||||
conns[app] = { subdomain: msg.subdomain, ws, close: false }
|
conns[app] = { subdomain: msg.subdomain, ws, close: false }
|
||||||
|
await setState(`${PREFIX}${app}`, msg.subdomain)
|
||||||
subdomain = msg.subdomain
|
subdomain = msg.subdomain
|
||||||
resolve(msg.subdomain)
|
resolve(msg.subdomain)
|
||||||
return
|
return
|
||||||
|
|
@ -90,25 +117,57 @@ export async function connectSneaker(app: string, subdomain = ""): Promise<strin
|
||||||
|
|
||||||
const res = await nose.fetch(req)
|
const res = await nose.fetch(req)
|
||||||
|
|
||||||
const body = await res.text()
|
|
||||||
const headers: Record<string, string> = {}
|
const headers: Record<string, string> = {}
|
||||||
res.headers.forEach((v, k) => (headers[k] = v))
|
res.headers.forEach((v, k) => (headers[k] = v))
|
||||||
|
|
||||||
ws.send(JSON.stringify({
|
const contentType = res.headers.get('content-type') || ''
|
||||||
id: msg.id,
|
const isBinary = isBinaryContentType(contentType)
|
||||||
status: res.status,
|
|
||||||
headers,
|
let body: string | ArrayBuffer
|
||||||
body,
|
let responseData: any
|
||||||
}))
|
|
||||||
|
if (isBinary) {
|
||||||
|
const arrayBuffer = await res.arrayBuffer()
|
||||||
|
const base64 = Buffer.from(arrayBuffer).toString('base64')
|
||||||
|
|
||||||
|
responseData = {
|
||||||
|
id: msg.id,
|
||||||
|
status: res.status,
|
||||||
|
headers,
|
||||||
|
body: base64,
|
||||||
|
isBinary: true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
body = await res.text()
|
||||||
|
|
||||||
|
responseData = {
|
||||||
|
id: msg.id,
|
||||||
|
status: res.status,
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
isBinary: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.send(JSON.stringify(responseData))
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
ws.send(JSON.stringify({
|
ws.send(JSON.stringify({
|
||||||
id: msg.id,
|
id: msg.id,
|
||||||
status: 500,
|
status: 500,
|
||||||
headers: { "content-type": "text/plain" },
|
headers: { "content-type": "text/plain" },
|
||||||
body: "error: " + err.message,
|
body: "error: " + err.message,
|
||||||
|
isBinary: false
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isBinaryContentType(contentType: string): boolean {
|
||||||
|
const binaryTypes = [
|
||||||
|
'image/', 'audio/', 'video/', 'application/octet-stream',
|
||||||
|
'application/pdf', 'application/zip', 'font/'
|
||||||
|
]
|
||||||
|
return binaryTypes.some(type => contentType.startsWith(type))
|
||||||
|
}
|
||||||
|
|
|
||||||
42
app/src/state.ts
Normal file
42
app/src/state.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
////
|
||||||
|
// Server state, shared by all sessions.
|
||||||
|
|
||||||
|
import { join } from "path"
|
||||||
|
import { readFileSync } from "fs"
|
||||||
|
import { NOSE_DATA } from "./config"
|
||||||
|
import { Mutex } from "./mutex"
|
||||||
|
|
||||||
|
const statePath = join(NOSE_DATA, "state.json")
|
||||||
|
const mutex = new Mutex()
|
||||||
|
|
||||||
|
export function getState(key?: string): any {
|
||||||
|
return key ? readState()?.[key] : readState()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setState(key: string, value: any) {
|
||||||
|
const state = readState()
|
||||||
|
state[key] = value
|
||||||
|
await saveState(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clearState(key: string) {
|
||||||
|
await setState(key, undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveState(newState: any) {
|
||||||
|
const unlock = await mutex.lock()
|
||||||
|
try {
|
||||||
|
await Bun.write(statePath, JSON.stringify(newState, null, 2))
|
||||||
|
} finally {
|
||||||
|
unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readState(): Record<string, any> {
|
||||||
|
try {
|
||||||
|
return JSON.parse(readFileSync(statePath, 'utf8'))
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.code === "ENOENT") return {}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { send as sendWs } from "./websocket"
|
import { send as sendWs } from "./websocket"
|
||||||
import { getState } from "./session"
|
import { sessionGet } from "./session"
|
||||||
import { processExecOutput } from "./shell"
|
import { processExecOutput } from "./shell"
|
||||||
import type { Child } from "hono/jsx"
|
import type { Child } from "hono/jsx"
|
||||||
import type { CommandOutput, Message } from "./shared/types"
|
import type { CommandOutput, Message } from "./shared/types"
|
||||||
|
|
@ -9,7 +9,7 @@ type StreamFns = { replace: StreamFn, append: StreamFn }
|
||||||
type StreamParamFn = (fns: StreamFns) => Promise<void>
|
type StreamParamFn = (fns: StreamFns) => Promise<void>
|
||||||
|
|
||||||
export async function stream(initOrFn: StreamParamFn | string | any, fn?: StreamParamFn) {
|
export async function stream(initOrFn: StreamParamFn | string | any, fn?: StreamParamFn) {
|
||||||
const state = getState()
|
const state = sessionGet()
|
||||||
if (!state) throw "stream() called outside runCommand()"
|
if (!state) throw "stream() called outside runCommand()"
|
||||||
|
|
||||||
let error = false
|
let error = false
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user