diff --git a/bin/help.ts b/bin/help.ts index 8e213dc..dfcacb6 100644 --- a/bin/help.ts +++ b/bin/help.ts @@ -4,7 +4,7 @@ import { commandPath, commands } from "@/commands" -export default async function (cmd: string): string { +export default async function (cmd: string): Promise { if (!cmd) return "usage: help " const path = commandPath(cmd) @@ -27,9 +27,9 @@ export default async function (cmd: string): string { return docs.join("\n") } -async function matchingCommands(cmd: string): string { +async function matchingCommands(cmd: string): Promise { let matched: string[] = [] - for (const command of await commands()) { + for (const command of Object.keys(await commands())) { if (command.startsWith(cmd)) matched.push(command) } diff --git a/src/commands.ts b/src/commands.ts index 566007c..15e6fd3 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -2,12 +2,12 @@ // Manages the commands on disk, in NOSE_ROOT_BIN and NOSE_BIN import { Glob } from "bun" -import { watch } from "fs" -import { join } from "path" +import { watch, readFileSync } from "fs" +import { join, basename } from "path" import { isFile } from "./utils" import { sendAll } from "./websocket" import { expectDir } from "./utils" -import { unique } from "./shared/utils" +import type { Command, Commands } from "./shared/types" import { projectBin, projectName } from "./project" import { DEFAULT_PROJECT, NOSE_DIR, NOSE_ROOT_BIN, NOSE_BIN } from "./config" @@ -15,24 +15,40 @@ export function initCommands() { startWatchers() } -export async function commands(project = DEFAULT_PROJECT): Promise { - let cmds = (await findCommands(NOSE_BIN)) - .concat(await findCommands(NOSE_ROOT_BIN)) +export async function commands(project = DEFAULT_PROJECT): Promise { + let binCmds = await findCommands(NOSE_BIN) + let rootCmds = await findCommands(NOSE_ROOT_BIN) + let projCmds = project === DEFAULT_PROJECT ? new Map : await findCommands(projectBin()) - if (project !== DEFAULT_PROJECT) - cmds = cmds.concat(await findCommands(projectBin())) - - return unique(cmds).sort() + return Object.fromEntries( + [ + ...Object.entries(binCmds), + ...Object.entries(rootCmds), + ...Object.entries(projCmds) + ] + .sort((a, b) => a[0].localeCompare(b[0])) + ) } -export async function findCommands(path: string): Promise { +export async function findCommands(path: string): Promise { const glob = new Glob("**/*.{ts,tsx}") - let list: string[] = [] + let obj: Commands = {} for await (const file of glob.scan(path)) - list.push(file.replace(".tsx", "").replace(".ts", "")) + obj[file.replace(/\.tsx?$/, "")] = describeCommand(join(path, file)) - return list + return obj +} + +function describeCommand(path: string): Command { + const code = readFileSync(path, "utf8") + let game = /^export const game = true$/mg.test(code) + let browser = /^\/\/\/ ?$/.test(code) + + return { + name: basename(path).replace(/\.tsx?$/, ""), + type: game ? "game" : browser ? "browser" : "server" + } } export function commandPath(cmd: string): string | undefined { diff --git a/src/js/commands.ts b/src/js/commands.ts index a798759..7755b00 100644 --- a/src/js/commands.ts +++ b/src/js/commands.ts @@ -1,7 +1,7 @@ //// // temporary hack for browser commands -import type { CommandOutput } from "../shared/types" +import type { CommandOutput, Commands, Command } from "../shared/types" import { openBrowser } from "./browser" import { scrollback, content } from "./dom" import { focusInput } from "./focus" @@ -12,7 +12,7 @@ import { status } from "./statusbar" import { setStatus, latestId } from "./scrollback" import { currentAppUrl } from "./webapp" -export const commands: string[] = [] +export const commands: Commands = {} export const browserCommands: Record void | Promise | CommandOutput> = { browse: (url?: string) => { @@ -27,7 +27,7 @@ export const browserCommands: Record void | Promi "browser-session": () => sessionId, clear: () => scrollback.innerHTML = "", commands: () => { - return { html: "
" + commands.map(cmd => `${cmd}`).join("") + "
" } + return { html: "
" + Object.keys(commands).map(cmd => `${cmd}`).join("") + "
" } }, fullscreen: () => document.body.requestFullscreen(), mode: (mode?: string) => { @@ -45,9 +45,9 @@ export const browserCommands: Record void | Promi reload: () => window.location.reload(), } -export function cacheCommands(cmds: string[]) { - commands.length = 0 - commands.push(...cmds) - commands.push(...Object.keys(browserCommands)) - commands.sort() +export function cacheCommands(cmds: Commands) { + for (const key in commands) + delete commands[key] + + Object.assign(commands, cmds) } \ No newline at end of file diff --git a/src/js/completion.ts b/src/js/completion.ts index c6e295c..0f74707 100644 --- a/src/js/completion.ts +++ b/src/js/completion.ts @@ -1,4 +1,4 @@ -//// +//// // tab completion import { cmdInput } from "./dom" @@ -14,7 +14,7 @@ function handleCompletion(e: KeyboardEvent) { e.preventDefault() const input = cmdInput.value - for (const command of commands) { + for (const command of Object.keys(commands)) { if (command.startsWith(input)) { cmdInput.value = command return diff --git a/src/shared/types.ts b/src/shared/types.ts index 7b02c04..6f08438 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -1,3 +1,4 @@ + export type Message = | ErrorMessage | InputMessage @@ -28,7 +29,7 @@ export type ErrorMessage = { export type CommandsMessage = { type: "commands" - data: string[] + data: Commands } export type AppsMessage = { @@ -83,4 +84,11 @@ export type StreamMessage = { id: string session: string data: CommandOutput -} \ No newline at end of file +} + +export type Commands = Record + +export type Command = { + name: string + type: "server" | "browser" | "game" +}