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);