From 4e3b56acab1f84d2c5268232d6781f181b03c929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cort=C3=B3n=20Cobas?= <104267232+carloscortonc@users.noreply.github.com> Date: Sun, 1 Feb 2026 20:52:09 +0100 Subject: [PATCH 1/4] feat: new path module --- site/src/builtins/ls.js | 27 ++++++++++++++++++++++----- site/src/builtins/pwd.js | 4 ++-- site/src/fs.js | 21 ++++++++++++--------- site/src/path.js | 30 ++++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 16 deletions(-) create mode 100644 site/src/path.js diff --git a/site/src/builtins/ls.js b/site/src/builtins/ls.js index 145074f..e66823c 100644 --- a/site/src/builtins/ls.js +++ b/site/src/builtins/ls.js @@ -1,11 +1,28 @@ import fs from "../fs.js"; +import pathmodule from "../path.js"; export const ls = { - definition: {}, + definition: { files: { type: "string", positional: true, default: [] } }, cliOptions: {}, - action: async () => { - const cwd = fs.getCwd(); - const contents = await fs.readDir(cwd); - process.stdout.write(contents.map((c) => c.name).join(" ")); + action: async (params) => { + const rfiles = params.files.length ? params.files : ["."]; + const fps = rfiles.map((f) => pathmodule.resolve(pathmodule.getCwd(), f)); + const files = await Promise.all(fps.map((fp) => fs.info(fp).then((r) => [fp, r]))); + // Log error for not-found locations + for (const f of files.filter((e) => !e[1])) { + Cli.logger.error("No such file or directory: ".concat(f[0])); + process.exitCode = 1; + } + // List contents for requested files + const efiles = files.filter((e) => e[1]).map((e) => ({ ...e[1], path: e[0] })); + for (let i = 0; i < efiles.length; i++) { + const f = efiles[i]; + const contents = f.type == "file" ? [{ name: f.name }] : await fs.readDir(f.path); + // Include section title if type=directory & files>1 + const title = f.type == "directory" && files.length > 1 ? f.path.concat(":\n") : ""; + process.stdout.write( + "".concat(title, contents.map((c) => c.name).join(" "), i < efiles.length - 1 ? "\n\n" : ""), + ); + } }, }; diff --git a/site/src/builtins/pwd.js b/site/src/builtins/pwd.js index e595ccb..c8de20d 100644 --- a/site/src/builtins/pwd.js +++ b/site/src/builtins/pwd.js @@ -1,7 +1,7 @@ -import fs from "../fs"; +import path from "../path"; export const pwd = { definition: {}, cliOptions: {}, - action: () => process.stdout.write(fs.getCwd()), + action: () => process.stdout.write(path.getCwd()), }; diff --git a/site/src/fs.js b/site/src/fs.js index 358b8be..9e26442 100644 --- a/site/src/fs.js +++ b/site/src/fs.js @@ -1,22 +1,14 @@ import kernel from "./kernel.js"; +import pathmodule from "./path.js"; const root = await navigator.storage.getDirectory(); class FileSystem { - cwd = "/"; // Store a dedicated handle for stdin fd (stdout and stderr are sent to main thread) to be used in `readFileSync` stdinHandle = null; // Pending write operation wp = Promise.resolve(); - getCwd() { - return this.cwd; - } - - setCwd(cwd) { - this.cwd = cwd; - } - async #getDirHandle(pathOrParts, create) { const parts = Array.isArray(pathOrParts) ? pathOrParts : pathOrParts.split("/").filter(Boolean); let h = root; @@ -52,6 +44,17 @@ class FileSystem { const h = await this.#getFileHandle(path); return h.remove(); } + + async info(path) { + const target = pathmodule.basename(path); + const parent = pathmodule.resolve(path, ".."); + if (parent == path) { + return { type: "directory", name: path }; + } + const dir = await this.readDir(parent); + return dir.find((e) => e.name === target); + } + async #readFile(path) { const handle = await this.#getFileHandle(path); return handle diff --git a/site/src/path.js b/site/src/path.js new file mode 100644 index 0000000..668af97 --- /dev/null +++ b/site/src/path.js @@ -0,0 +1,30 @@ +class Path { + cwd = "/"; + + getCwd() { + return this.cwd; + } + + async setCwd(path) { + this.cwd = path || "/"; + } + + resolve(...parts) { + const base = parts[0].replace(/\/*$/, "/"); + return ( + parts + .slice(1) + .reduce((acc, p) => new URL(p, acc), new URL("https://_" + base)) + .pathname.replace(/\/*$/, "") || "/" + ); + } + + basename(path) { + if (path === "/") return "/"; + return /[^\/]+$/.exec(path)?.[0]; + } +} + +const path = new Path(); + +export default path; From b562f9c2964de4a7cb46dcc7d52f9f6146e4b3f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cort=C3=B3n=20Cobas?= <104267232+carloscortonc@users.noreply.github.com> Date: Sun, 1 Feb 2026 20:53:19 +0100 Subject: [PATCH 2/4] feat: cd cmd --- site/src/builtins/cd.js | 18 ++++++++++++++++++ site/src/builtins/index.js | 1 + 2 files changed, 19 insertions(+) create mode 100644 site/src/builtins/cd.js diff --git a/site/src/builtins/cd.js b/site/src/builtins/cd.js new file mode 100644 index 0000000..00291e3 --- /dev/null +++ b/site/src/builtins/cd.js @@ -0,0 +1,18 @@ +import fs from "../fs.js"; +import pathmodule from "../path.js"; + +export const cd = { + definition: { path: { type: "string", positional: 0 } }, + cliOptions: {}, + action: async ({ path }) => { + if (!path) return; + const fp = pathmodule.resolve(pathmodule.getCwd(), path); + const e = await fs.info(fp); + if (!e || e.type !== "directory") { + const m = !e ? "not such file or directory" : "not a directory"; + Cli.logger.error(m.concat(": ", path)); + return process.exit(1); + } + return pathmodule.setCwd(fp); + }, +}; diff --git a/site/src/builtins/index.js b/site/src/builtins/index.js index 7e987de..687ea60 100644 --- a/site/src/builtins/index.js +++ b/site/src/builtins/index.js @@ -3,3 +3,4 @@ export * from "./sleep"; export * from "./printenv"; export * from "./pwd"; export * from "./ls"; +export * from "./cd"; From d57ae71cb5f5c52c0034b05ea6cbe242ef8130ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cort=C3=B3n=20Cobas?= <104267232+carloscortonc@users.noreply.github.com> Date: Sun, 1 Feb 2026 20:53:39 +0100 Subject: [PATCH 3/4] feat: cat cmd --- site/src/builtins/cat.js | 18 ++++++++++++++++++ site/src/builtins/index.js | 1 + 2 files changed, 19 insertions(+) create mode 100644 site/src/builtins/cat.js diff --git a/site/src/builtins/cat.js b/site/src/builtins/cat.js new file mode 100644 index 0000000..911f063 --- /dev/null +++ b/site/src/builtins/cat.js @@ -0,0 +1,18 @@ +import fs from "../fs"; +import pathmodule from "../path"; + +export const cat = { + definition: { path: { type: "string", positional: 0 } }, + cliOptions: {}, + action: async ({ path }) => { + if (!path) return; + const fp = pathmodule.resolve(pathmodule.getCwd(), path); + const e = await fs.info(fp); + if (!e || e.type !== "file") { + const m = !e ? "not such file or directory" : "is a directory"; + Cli.logger.error(m.concat(": ", path)); + return process.exit(1); + } + return fs.readFile(fp).then(process.stdout.write); + }, +}; diff --git a/site/src/builtins/index.js b/site/src/builtins/index.js index 687ea60..7f8b4bf 100644 --- a/site/src/builtins/index.js +++ b/site/src/builtins/index.js @@ -4,3 +4,4 @@ export * from "./printenv"; export * from "./pwd"; export * from "./ls"; export * from "./cd"; +export * from "./cat"; From 0ef30effa94b3ec7c795c7785a8f5b20f6a3324d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Cort=C3=B3n=20Cobas?= <104267232+carloscortonc@users.noreply.github.com> Date: Sun, 1 Feb 2026 21:44:09 +0100 Subject: [PATCH 4/4] feat: implement basic bash prompt --- site/src/bash/index.js | 2 ++ site/src/bash/interpreter.js | 2 +- site/src/bash/prompt.js | 11 +++++++++++ site/src/main.js | 23 ++++++++++++++++++++--- site/src/renderer.js | 2 +- 5 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 site/src/bash/index.js create mode 100644 site/src/bash/prompt.js diff --git a/site/src/bash/index.js b/site/src/bash/index.js new file mode 100644 index 0000000..8d89dff --- /dev/null +++ b/site/src/bash/index.js @@ -0,0 +1,2 @@ +export { execute } from "./interpreter"; +export { prompt } from "./prompt"; diff --git a/site/src/bash/interpreter.js b/site/src/bash/interpreter.js index 75114cd..90bb135 100644 --- a/site/src/bash/interpreter.js +++ b/site/src/bash/interpreter.js @@ -128,7 +128,7 @@ async function executeAst(node, opts = {}) { } } -export default async function execute(input) { +export async function execute(input) { const r = grammar.match(input); if (!r.succeeded()) { // Render error directly diff --git a/site/src/bash/prompt.js b/site/src/bash/prompt.js new file mode 100644 index 0000000..fc5b89a --- /dev/null +++ b/site/src/bash/prompt.js @@ -0,0 +1,11 @@ +import path from "../path"; + +// https://www.gnu.org/software/bash/manual/bash.html#Controlling-the-Prompt-1 +export function prompt() { + let template = process.env.PS1 || ""; + const r = { "\\u": process.env.USER, "\\w": path.getCwd() }; + for (const [k, v] of Object.entries(r)) { + template = template.replace(k, v); + } + return template; +} diff --git a/site/src/main.js b/site/src/main.js index af297d7..dcaea69 100644 --- a/site/src/main.js +++ b/site/src/main.js @@ -3,7 +3,8 @@ import handleKey from "./key-handler.js"; import * as renderer from "./renderer.js"; import * as history from "./history.js"; import fs from "./fs.js"; -import execute from "./bash/interpreter.js"; +import path from "./path.js"; +import { execute, prompt } from "./bash"; import * as builtincmds from "./builtins"; import kernel from "./kernel.js"; import "./index.css"; @@ -20,7 +21,9 @@ const blob = new Blob([blobSource], { type: "text/javascript" }); window.cliHandlerUrl = URL.createObjectURL(blob); require("url").pathToFileURL = () => ({ href: cliHandlerUrl }); -let [i, o, oa, lm] = ["input", "output", "output-after", "theme"].map((id) => document.getElementById(id)); +let [i, sp, o, oa, lm] = ["input", "sprompt", "output", "output-after", "theme"].map((id) => + document.getElementById(id), +); document.addEventListener("click", () => i.focus()); o.addEventListener("click", (e) => e.stopPropagation()); lm.addEventListener("click", () => @@ -28,9 +31,12 @@ lm.addEventListener("click", () => ); // Initialize FS -await fs.init({ "/README.md": "Hello" }); +await fs.init({ "/users/guest/README.md": "Welcome!" }); require("fs").readFileSync = fs.readFileSync.bind(fs); +// Initialize path +path.setCwd("/"); + // Define `stdin.isTTY` as if kernel's fd=0's type is TTY Object.defineProperty(process.stdin, "isTTY", { get() { @@ -42,8 +48,18 @@ Object.defineProperty(process.stdin, "isTTY", { Object.assign(process.env, { SHELL: "cliersh", USER: "guest", + PS1: "\\u:\\w $", }); +// Method for updating bash prompt +const updatePrompt = () => { + let p = prompt(); + if (window.CLI_PROMPT === p) return; + window.CLI_PROMPT = p; + sp.innerText = p; +}; +updatePrompt(); + handleKey(i, { Enter: () => { let inputValue = i.value; @@ -52,6 +68,7 @@ handleKey(i, { renderer.renderInput(inputValue); i.value = ""; execute(inputValue).finally(() => { + updatePrompt(); renderer.flushOutput(); }); }, diff --git a/site/src/renderer.js b/site/src/renderer.js index 49f551e..9fb09c7 100644 --- a/site/src/renderer.js +++ b/site/src/renderer.js @@ -7,7 +7,7 @@ const OUTPUT_ID = "exec"; export function renderInput(value) { const input = document.createElement("div"); input.className = "input-wrapper"; - input.innerHTML = `$${value}`; + input.innerHTML = `${window.CLI_PROMPT}${value}`; sp.classList.add("executing"); clearOutput(oa); o.appendChild(input);