From b995e3927088f4f5fa4cf1cc96d8ad74fe37aa15 Mon Sep 17 00:00:00 2001 From: Moses Narrow <36607567+0pcom@users.noreply.github.com> Date: Thu, 20 Aug 2026 17:07:49 -0500 Subject: [PATCH] feat(cli): drop the `web` browser-UI command from the default binary The default skywire binary (repo-root main -> cmd/skywire-cli/commands) registered the `web` command, which served the CLI as a browser UI and embedded cmd/skywire/commands/web/static/b.wasm (~111 KB) unconditionally into both the repo-root binary and ./cmd/skywire. The command is redundant: the CLI/cobra help tree is already reachable via the websh terminal in the hypervisor UI and the wasm-visor UI. Remove it from the command tree and delete the self-contained package (server, its own wasm/main.go source, and static/ assets incl. b.wasm). - cmd/skywire-cli/commands/root.go: drop the import, the GroupID line, and the AddCommand entry. - cmd/skywire/commands/web/: removed entirely (self-contained; no other Go package imported it, and its wasm builder is not shared with any other tooling). - Makefile build-wasm lane: drop ./cmd/skywire/commands/web/wasm from the compile-check list. - docs: drop the command's README and its index entry. --- Makefile | 2 +- cmd/skywire-cli/commands/root.go | 3 - cmd/skywire/commands/web/Makefile | 24 - cmd/skywire/commands/web/procgroup_unix.go | 25 - cmd/skywire/commands/web/procgroup_windows.go | 23 - cmd/skywire/commands/web/static/b.wasm | Bin 113213 -> 0 bytes cmd/skywire/commands/web/static/index.html | 212 ---- cmd/skywire/commands/web/static/wasm_exec.js | 559 ----------- cmd/skywire/commands/web/wasm/main.go | 909 ------------------ cmd/skywire/commands/web/web.go | 505 ---------- docs/skywire/README.md | 1 - docs/skywire/web/README.md | 51 - 12 files changed, 1 insertion(+), 2313 deletions(-) delete mode 100644 cmd/skywire/commands/web/Makefile delete mode 100644 cmd/skywire/commands/web/procgroup_unix.go delete mode 100644 cmd/skywire/commands/web/procgroup_windows.go delete mode 100755 cmd/skywire/commands/web/static/b.wasm delete mode 100644 cmd/skywire/commands/web/static/index.html delete mode 100644 cmd/skywire/commands/web/static/wasm_exec.js delete mode 100644 cmd/skywire/commands/web/wasm/main.go delete mode 100644 cmd/skywire/commands/web/web.go delete mode 100644 docs/skywire/web/README.md diff --git a/Makefile b/Makefile index 804f65790c..f0fbbcd0c1 100644 --- a/Makefile +++ b/Makefile @@ -344,7 +344,7 @@ clean: ## Clean project: remove created binaries and apps build-wasm: ## Compile-check every js/wasm binary (GOOS=js GOARCH=wasm), no run — mirrors the CI wasm lane @echo "compile-checking js/wasm binaries..." - @for p in ./pkg/tpviz/wasm ./cmd/dmsg-wasm ./cmd/wasm-visor ./cmd/wasm-visor-probe ./cmd/websh-probe ./cmd/skywire/commands/web/wasm; do \ + @for p in ./pkg/tpviz/wasm ./cmd/dmsg-wasm ./cmd/wasm-visor ./cmd/wasm-visor-probe ./cmd/websh-probe; do \ echo " GOOS=js GOARCH=wasm go build $$p"; \ GOOS=js GOARCH=wasm go build -mod=vendor -o /dev/null "$$p" || exit 1; \ done diff --git a/cmd/skywire-cli/commands/root.go b/cmd/skywire-cli/commands/root.go index 74c81d9e07..a14468ef86 100644 --- a/cmd/skywire-cli/commands/root.go +++ b/cmd/skywire-cli/commands/root.go @@ -42,7 +42,6 @@ import ( cliversion "github.com/skycoin/skywire/cmd/skywire-cli/commands/version" clivisor "github.com/skycoin/skywire/cmd/skywire-cli/commands/visor" clivpn "github.com/skycoin/skywire/cmd/skywire-cli/commands/vpn" - "github.com/skycoin/skywire/cmd/skywire/commands/web" "github.com/skycoin/skywire/pkg/buildinfo" "github.com/skycoin/skywire/pkg/calvin" "github.com/skycoin/skywire/pkg/cliout" @@ -110,7 +109,6 @@ func init() { cliutil.RootCmd.GroupID = groupUtil cligot.RootCmd.GroupID = groupUtil cliversion.RootCmd.GroupID = groupUtil - web.RootCmd.GroupID = groupUtil // Install flag-aware `help` command (supports -r/-t/-d). Covers // the case where `skywire cli` is invoked as a subcommand of the @@ -150,7 +148,6 @@ func init() { cliutil.RootCmd, cligot.RootCmd, cliversion.RootCmd, - web.RootCmd, // Top-level shortcuts: high-traffic verbs reachable without // the `visor` middle word. Long forms keep working at diff --git a/cmd/skywire/commands/web/Makefile b/cmd/skywire/commands/web/Makefile deleted file mode 100644 index e47289b5a7..0000000000 --- a/cmd/skywire/commands/web/Makefile +++ /dev/null @@ -1,24 +0,0 @@ -# Makefile for the `skywire web` WASM client. -# -# Targets: -# make build = build static/b.wasm via TinyGo -# make clean = remove static/b.wasm -# -# The Go server (web.go) embeds static/b.wasm via //go:embed. An -# empty b.wasm is acceptable — the page renders a "b.wasm not built" -# banner — but a production binary should ship a real bundle, so -# the top-level skywire Makefile should chain into this target as -# part of a release-binary build (TODO: wire into Makefile:build). - -WASM_OUT := static/b.wasm - -.PHONY: build clean - -build: $(WASM_OUT) - -$(WASM_OUT): wasm/main.go - tinygo build -target wasm -no-debug -o $@ ./wasm - @echo "built $@ ($(shell stat -c %s $@) bytes)" - -clean: - rm -f $(WASM_OUT) diff --git a/cmd/skywire/commands/web/procgroup_unix.go b/cmd/skywire/commands/web/procgroup_unix.go deleted file mode 100644 index 6079e53987..0000000000 --- a/cmd/skywire/commands/web/procgroup_unix.go +++ /dev/null @@ -1,25 +0,0 @@ -//go:build !windows - -// Package web cmd/skywire/commands/web/procgroup_unix.go c4-vis-cli -package web - -import ( - "os" - "os/exec" - "syscall" -) - -// setProcGroup puts the child in its own process group so the whole group can -// be signaled on cancel / client-disconnect (matters for visor halt, dmsg curl -// downloads, etc). -func setProcGroup(cmd *exec.Cmd) { - cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} -} - -// killProcGroup sends SIGINT to the child's process group for graceful cleanup. -func killProcGroup(p *os.Process) { - if p == nil { - return - } - _ = syscall.Kill(-p.Pid, syscall.SIGINT) //nolint:errcheck -} diff --git a/cmd/skywire/commands/web/procgroup_windows.go b/cmd/skywire/commands/web/procgroup_windows.go deleted file mode 100644 index 94cc689ee4..0000000000 --- a/cmd/skywire/commands/web/procgroup_windows.go +++ /dev/null @@ -1,23 +0,0 @@ -//go:build windows - -// Package web cmd/skywire/commands/web/procgroup_windows.go c4-vis-cli -package web - -import ( - "os" - "os/exec" -) - -// setProcGroup is a no-op on Windows — there are no POSIX process groups, and -// the default CreateProcess behavior is sufficient for the web command's -// subprocess lifecycle. -func setProcGroup(_ *exec.Cmd) {} - -// killProcGroup terminates the child process. Windows has no SIGINT-to-group -// equivalent, so this is a best-effort hard kill of the subprocess. -func killProcGroup(p *os.Process) { - if p == nil { - return - } - _ = p.Kill() //nolint:errcheck -} diff --git a/cmd/skywire/commands/web/static/b.wasm b/cmd/skywire/commands/web/static/b.wasm deleted file mode 100755 index f73d10359245ddeffb7c19f69d1267a89ec29203..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 113213 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 dBjfy@mCQG8%X6db56ydek6^ - - - - -skywire web - - - -
-

Loading skywire web…

-
- - - - diff --git a/cmd/skywire/commands/web/static/wasm_exec.js b/cmd/skywire/commands/web/static/wasm_exec.js deleted file mode 100644 index 3d926ce524..0000000000 --- a/cmd/skywire/commands/web/static/wasm_exec.js +++ /dev/null @@ -1,559 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. -// -// This file has been modified for use by the TinyGo compiler. - -(() => { - // Map multiple JavaScript environments to a single common API, - // preferring web standards over Node.js API. - // - // Environments considered: - // - Browsers - // - Node.js - // - Electron - // - Parcel - - if (typeof global !== "undefined") { - // global already exists - } else if (typeof window !== "undefined") { - window.global = window; - } else if (typeof self !== "undefined") { - self.global = self; - } else { - throw new Error("cannot export Go (neither global, window nor self is defined)"); - } - - if (!global.require && typeof require !== "undefined") { - global.require = require; - } - - if (!global.fs && global.require) { - global.fs = require("node:fs"); - } - - const enosys = () => { - const err = new Error("not implemented"); - err.code = "ENOSYS"; - return err; - }; - - if (!global.fs) { - let outputBuf = ""; - global.fs = { - constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused - writeSync(fd, buf) { - outputBuf += decoder.decode(buf); - const nl = outputBuf.lastIndexOf("\n"); - if (nl != -1) { - console.log(outputBuf.substr(0, nl)); - outputBuf = outputBuf.substr(nl + 1); - } - return buf.length; - }, - write(fd, buf, offset, length, position, callback) { - if (offset !== 0 || length !== buf.length || position !== null) { - callback(enosys()); - return; - } - const n = this.writeSync(fd, buf); - callback(null, n); - }, - chmod(path, mode, callback) { callback(enosys()); }, - chown(path, uid, gid, callback) { callback(enosys()); }, - close(fd, callback) { callback(enosys()); }, - fchmod(fd, mode, callback) { callback(enosys()); }, - fchown(fd, uid, gid, callback) { callback(enosys()); }, - fstat(fd, callback) { callback(enosys()); }, - fsync(fd, callback) { callback(null); }, - ftruncate(fd, length, callback) { callback(enosys()); }, - lchown(path, uid, gid, callback) { callback(enosys()); }, - link(path, link, callback) { callback(enosys()); }, - lstat(path, callback) { callback(enosys()); }, - mkdir(path, perm, callback) { callback(enosys()); }, - open(path, flags, mode, callback) { callback(enosys()); }, - read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, - readdir(path, callback) { callback(enosys()); }, - readlink(path, callback) { callback(enosys()); }, - rename(from, to, callback) { callback(enosys()); }, - rmdir(path, callback) { callback(enosys()); }, - stat(path, callback) { callback(enosys()); }, - symlink(path, link, callback) { callback(enosys()); }, - truncate(path, length, callback) { callback(enosys()); }, - unlink(path, callback) { callback(enosys()); }, - utimes(path, atime, mtime, callback) { callback(enosys()); }, - }; - } - - if (!global.process) { - global.process = { - getuid() { return -1; }, - getgid() { return -1; }, - geteuid() { return -1; }, - getegid() { return -1; }, - getgroups() { throw enosys(); }, - pid: -1, - ppid: -1, - umask() { throw enosys(); }, - cwd() { throw enosys(); }, - chdir() { throw enosys(); }, - } - } - - if (!global.crypto) { - const nodeCrypto = require("node:crypto"); - global.crypto = { - getRandomValues(b) { - nodeCrypto.randomFillSync(b); - }, - }; - } - - if (!global.performance) { - global.performance = { - now() { - const [sec, nsec] = process.hrtime(); - return sec * 1000 + nsec / 1000000; - }, - }; - } - - if (!global.TextEncoder) { - global.TextEncoder = require("node:util").TextEncoder; - } - - if (!global.TextDecoder) { - global.TextDecoder = require("node:util").TextDecoder; - } - - // End of polyfills for common API. - - const encoder = new TextEncoder("utf-8"); - const decoder = new TextDecoder("utf-8"); - let reinterpretBuf = new DataView(new ArrayBuffer(8)); - var logLine = []; - const wasmExit = {}; // thrown to exit via proc_exit (not an error) - - global.Go = class { - constructor() { - this._callbackTimeouts = new Map(); - this._nextCallbackTimeoutID = 1; - - const mem = () => { - // The buffer may change when requesting more memory. - return new DataView(this._inst.exports.memory.buffer); - } - - const unboxValue = (v_ref) => { - reinterpretBuf.setBigInt64(0, v_ref, true); - const f = reinterpretBuf.getFloat64(0, true); - if (f === 0) { - return undefined; - } - if (!isNaN(f)) { - return f; - } - - const id = v_ref & 0xffffffffn; - return this._values[id]; - } - - - const loadValue = (addr) => { - let v_ref = mem().getBigUint64(addr, true); - return unboxValue(v_ref); - } - - const boxValue = (v) => { - const nanHead = 0x7FF80000n; - - if (typeof v === "number") { - if (isNaN(v)) { - return nanHead << 32n; - } - if (v === 0) { - return (nanHead << 32n) | 1n; - } - reinterpretBuf.setFloat64(0, v, true); - return reinterpretBuf.getBigInt64(0, true); - } - - switch (v) { - case undefined: - return 0n; - case null: - return (nanHead << 32n) | 2n; - case true: - return (nanHead << 32n) | 3n; - case false: - return (nanHead << 32n) | 4n; - } - - let id = this._ids.get(v); - if (id === undefined) { - id = this._idPool.pop(); - if (id === undefined) { - id = BigInt(this._values.length); - } - this._values[id] = v; - this._goRefCounts[id] = 0; - this._ids.set(v, id); - } - this._goRefCounts[id]++; - let typeFlag = 1n; - switch (typeof v) { - case "string": - typeFlag = 2n; - break; - case "symbol": - typeFlag = 3n; - break; - case "function": - typeFlag = 4n; - break; - } - return id | ((nanHead | typeFlag) << 32n); - } - - const storeValue = (addr, v) => { - let v_ref = boxValue(v); - mem().setBigUint64(addr, v_ref, true); - } - - const loadSlice = (array, len, cap) => { - return new Uint8Array(this._inst.exports.memory.buffer, array, len); - } - - const loadSliceOfValues = (array, len, cap) => { - const a = new Array(len); - for (let i = 0; i < len; i++) { - a[i] = loadValue(array + i * 8); - } - return a; - } - - const loadString = (ptr, len) => { - return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len)); - } - - const timeOrigin = Date.now() - performance.now(); - const wasi_EBADF = 8; - const wasi_ENOSYS = 52; - this.importObject = { - wasi_snapshot_preview1: { - // https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md - fd_write: function(fd, iovs_ptr, iovs_len, nwritten_ptr) { - let nwritten = 0; - if (fd == 1) { - for (let iovs_i=0; iovs_i wasi_ENOSYS, - fd_close: () => wasi_ENOSYS, - fd_fdstat_get: () => wasi_ENOSYS, - fd_prestat_get: () => wasi_EBADF, // wasi-libc relies on this errno value - fd_prestat_dir_name: () => wasi_ENOSYS, - fd_seek: () => wasi_ENOSYS, - path_open: () => wasi_ENOSYS, - proc_exit: (code) => { - this.exited = true; - this.exitCode = code; - this._resolveExitPromise(); - throw wasmExit; - }, - random_get: (bufPtr, bufLen) => { - crypto.getRandomValues(loadSlice(bufPtr, bufLen)); - return 0; - }, - }, - gojs: { - // func ticks() int64 - "runtime.ticks": () => { - return BigInt((timeOrigin + performance.now()) * 1e6); - }, - - // func sleepTicks(timeout int64) - "runtime.sleepTicks": (timeout) => { - // Do not sleep, only reactivate scheduler after the given timeout. - setTimeout(() => { - if (this.exited) return; - try { - this._inst.exports.go_scheduler(); - } catch (e) { - if (e !== wasmExit) throw e; - } - }, Number(timeout)/1e6); - }, - - // func finalizeRef(v ref) - "syscall/js.finalizeRef": (v_ref) => { - // Note: TinyGo does not support finalizers so this is only called - // for one specific case, by js.go:jsString. and can/might leak memory. - const id = v_ref & 0xffffffffn; - if (this._goRefCounts?.[id] !== undefined) { - this._goRefCounts[id]--; - if (this._goRefCounts[id] === 0) { - const v = this._values[id]; - this._values[id] = null; - this._ids.delete(v); - this._idPool.push(id); - } - } else { - console.error("syscall/js.finalizeRef: unknown id", id); - } - }, - - // func stringVal(value string) ref - "syscall/js.stringVal": (value_ptr, value_len) => { - value_ptr >>>= 0; - const s = loadString(value_ptr, value_len); - return boxValue(s); - }, - - // func valueGet(v ref, p string) ref - "syscall/js.valueGet": (v_ref, p_ptr, p_len) => { - let prop = loadString(p_ptr, p_len); - let v = unboxValue(v_ref); - let result = Reflect.get(v, prop); - return boxValue(result); - }, - - // func valueSet(v ref, p string, x ref) - "syscall/js.valueSet": (v_ref, p_ptr, p_len, x_ref) => { - const v = unboxValue(v_ref); - const p = loadString(p_ptr, p_len); - const x = unboxValue(x_ref); - Reflect.set(v, p, x); - }, - - // func valueDelete(v ref, p string) - "syscall/js.valueDelete": (v_ref, p_ptr, p_len) => { - const v = unboxValue(v_ref); - const p = loadString(p_ptr, p_len); - Reflect.deleteProperty(v, p); - }, - - // func valueIndex(v ref, i int) ref - "syscall/js.valueIndex": (v_ref, i) => { - return boxValue(Reflect.get(unboxValue(v_ref), i)); - }, - - // valueSetIndex(v ref, i int, x ref) - "syscall/js.valueSetIndex": (v_ref, i, x_ref) => { - Reflect.set(unboxValue(v_ref), i, unboxValue(x_ref)); - }, - - // func valueCall(v ref, m string, args []ref) (ref, bool) - "syscall/js.valueCall": (ret_addr, v_ref, m_ptr, m_len, args_ptr, args_len, args_cap) => { - const v = unboxValue(v_ref); - const name = loadString(m_ptr, m_len); - const args = loadSliceOfValues(args_ptr, args_len, args_cap); - try { - const m = Reflect.get(v, name); - storeValue(ret_addr, Reflect.apply(m, v, args)); - mem().setUint8(ret_addr + 8, 1); - } catch (err) { - storeValue(ret_addr, err); - mem().setUint8(ret_addr + 8, 0); - } - }, - - // func valueInvoke(v ref, args []ref) (ref, bool) - "syscall/js.valueInvoke": (ret_addr, v_ref, args_ptr, args_len, args_cap) => { - try { - const v = unboxValue(v_ref); - const args = loadSliceOfValues(args_ptr, args_len, args_cap); - storeValue(ret_addr, Reflect.apply(v, undefined, args)); - mem().setUint8(ret_addr + 8, 1); - } catch (err) { - storeValue(ret_addr, err); - mem().setUint8(ret_addr + 8, 0); - } - }, - - // func valueNew(v ref, args []ref) (ref, bool) - "syscall/js.valueNew": (ret_addr, v_ref, args_ptr, args_len, args_cap) => { - const v = unboxValue(v_ref); - const args = loadSliceOfValues(args_ptr, args_len, args_cap); - try { - storeValue(ret_addr, Reflect.construct(v, args)); - mem().setUint8(ret_addr + 8, 1); - } catch (err) { - storeValue(ret_addr, err); - mem().setUint8(ret_addr+ 8, 0); - } - }, - - // func valueLength(v ref) int - "syscall/js.valueLength": (v_ref) => { - return unboxValue(v_ref).length; - }, - - // valuePrepareString(v ref) (ref, int) - "syscall/js.valuePrepareString": (ret_addr, v_ref) => { - const s = String(unboxValue(v_ref)); - const str = encoder.encode(s); - storeValue(ret_addr, str); - mem().setInt32(ret_addr + 8, str.length, true); - }, - - // valueLoadString(v ref, b []byte) - "syscall/js.valueLoadString": (v_ref, slice_ptr, slice_len, slice_cap) => { - const str = unboxValue(v_ref); - loadSlice(slice_ptr, slice_len, slice_cap).set(str); - }, - - // func valueInstanceOf(v ref, t ref) bool - "syscall/js.valueInstanceOf": (v_ref, t_ref) => { - return unboxValue(v_ref) instanceof unboxValue(t_ref); - }, - - // func copyBytesToGo(dst []byte, src ref) (int, bool) - "syscall/js.copyBytesToGo": (ret_addr, dest_addr, dest_len, dest_cap, src_ref) => { - let num_bytes_copied_addr = ret_addr; - let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable - - const dst = loadSlice(dest_addr, dest_len); - const src = unboxValue(src_ref); - if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { - mem().setUint8(returned_status_addr, 0); // Return "not ok" status - return; - } - const toCopy = src.subarray(0, dst.length); - dst.set(toCopy); - mem().setUint32(num_bytes_copied_addr, toCopy.length, true); - mem().setUint8(returned_status_addr, 1); // Return "ok" status - }, - - // copyBytesToJS(dst ref, src []byte) (int, bool) - // Originally copied from upstream Go project, then modified: - // https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416 - "syscall/js.copyBytesToJS": (ret_addr, dst_ref, src_addr, src_len, src_cap) => { - let num_bytes_copied_addr = ret_addr; - let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable - - const dst = unboxValue(dst_ref); - const src = loadSlice(src_addr, src_len); - if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { - mem().setUint8(returned_status_addr, 0); // Return "not ok" status - return; - } - const toCopy = src.subarray(0, dst.length); - dst.set(toCopy); - mem().setUint32(num_bytes_copied_addr, toCopy.length, true); - mem().setUint8(returned_status_addr, 1); // Return "ok" status - }, - } - }; - - // Go 1.20 uses 'env'. Go 1.21 uses 'gojs'. - // For compatibility, we use both as long as Go 1.20 is supported. - this.importObject.env = this.importObject.gojs; - } - - async run(instance) { - this._inst = instance; - this._values = [ // JS values that Go currently has references to, indexed by reference id - NaN, - 0, - null, - true, - false, - global, - this, - ]; - this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id - this._ids = new Map(); // mapping from JS values to reference ids - this._idPool = []; // unused ids that have been garbage collected - this.exited = false; // whether the Go program has exited - this.exitCode = 0; - - if (this._inst.exports._start) { - let exitPromise = new Promise((resolve, reject) => { - this._resolveExitPromise = resolve; - }); - - // Run program, but catch the wasmExit exception that's thrown - // to return back here. - try { - this._inst.exports._start(); - } catch (e) { - if (e !== wasmExit) throw e; - } - - await exitPromise; - return this.exitCode; - } else { - this._inst.exports._initialize(); - } - } - - _resume() { - if (this.exited) { - throw new Error("Go program has already exited"); - } - try { - this._inst.exports.resume(); - } catch (e) { - if (e !== wasmExit) throw e; - } - if (this.exited) { - this._resolveExitPromise(); - } - } - - _makeFuncWrapper(id) { - const go = this; - return function () { - const event = { id: id, this: this, args: arguments }; - go._pendingEvent = event; - go._resume(); - return event.result; - }; - } - } - - if ( - global.require && - global.require.main === module && - global.process && - global.process.versions && - !global.process.versions.electron - ) { - if (process.argv.length != 3) { - console.error("usage: go_js_wasm_exec [wasm binary] [arguments]"); - process.exit(1); - } - - const go = new Go(); - WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then(async (result) => { - let exitCode = await go.run(result.instance); - process.exit(exitCode); - }).catch((err) => { - console.error(err); - process.exit(1); - }); - } -})(); diff --git a/cmd/skywire/commands/web/wasm/main.go b/cmd/skywire/commands/web/wasm/main.go deleted file mode 100644 index c617761ab9..0000000000 --- a/cmd/skywire/commands/web/wasm/main.go +++ /dev/null @@ -1,909 +0,0 @@ -//go:build js && wasm - -// Package main cmd/skywire/commands/web/wasm/main.go c4-vis-cli -// -// Renders a single shell-like prompt: `skywire $ `. As the -// operator types, the WASM client parses the line into command path -// + flags + args, looks up the matching cobra node in /api/tree, -// and shows its help (Short, Long, Flags, Example) below the input. -// Pressing Enter executes the line via POST /api/run + SSE stream. -// Tab autocompletes the current token (subcommand or flag name). -// Up/Down recall previous lines. -// -// All DOM manipulation via syscall/js — no JS framework, no -// client-side state outside this module. -// -// Build: tinygo build -target wasm -no-debug -o ../static/b.wasm . -package main - -import ( - "sort" - "strings" - "syscall/js" -) - -var ( - tree map[string]node - history []string - histIdx int // -1 = current input -) - -func main() { - d := js.Global().Get("document") - if d.Get("readyState").String() == "loading" { - var ready js.Func - ready = js.FuncOf(func(_ js.Value, _ []js.Value) interface{} { - boot() - ready.Release() - return nil - }) - d.Call("addEventListener", "DOMContentLoaded", ready) - } else { - boot() - } - select {} -} - -func boot() { - loadTree() -} - -type node struct { - Path string - Name string - Short string - Long string - Example string - Children []string - Flags []flag - Runnable bool -} - -type flag struct { - Name string - Shorthand string - Type string - Default string - Usage string -} - -func loadTree() { - then := js.FuncOf(func(_ js.Value, args []js.Value) interface{} { - args[0].Call("text").Call("then", js.FuncOf(func(_ js.Value, args []js.Value) interface{} { - tree = parseTree(args[0].String()) - renderShell() - update("") - return nil - })) - return nil - }) - js.Global().Call("fetch", "/api/tree").Call("then", then) -} - -// renderShell paints the single-column shell layout: prompt input -// on top, help/completion panel mid, output panel bottom. Input -// focus is captured on load + every click anywhere in the page. -func renderShell() { - d := js.Global().Get("document") - app := d.Call("getElementById", "app") - if app.IsUndefined() || app.IsNull() { - d.Get("body").Set("innerHTML", `
missing #app in index.html
`) - return - } - app.Set("innerHTML", ` -
-
- skywire $ - -
-
-
- -
`) - - input := d.Call("getElementById", "sh-input") - input.Call("focus") - - // Refocus on any click outside an interactive element. Keeps the - // keyboard captured for the prompt without explicit Ctrl+L style - // gestures. - bodyClick := js.FuncOf(func(_ js.Value, args []js.Value) interface{} { - target := args[0].Get("target") - tag := target.Get("tagName").String() - if tag != "BUTTON" && tag != "A" { - input.Call("focus") - } - return nil - }) - d.Get("body").Call("addEventListener", "click", bodyClick) - - inputCb := js.FuncOf(func(_ js.Value, _ []js.Value) interface{} { - update(input.Get("value").String()) - return nil - }) - input.Call("addEventListener", "input", inputCb) - - keyCb := js.FuncOf(func(_ js.Value, args []js.Value) interface{} { - ev := args[0] - key := ev.Get("key").String() - switch key { - case "Enter": - ev.Call("preventDefault") - line := strings.TrimSpace(input.Get("value").String()) - if line == "" { - return nil - } - history = append(history, line) - histIdx = -1 - runLine(line) - input.Set("value", "") - update("") - case "Tab": - ev.Call("preventDefault") - completed := autocomplete(input.Get("value").String()) - if completed != "" { - input.Set("value", completed) - update(completed) - } - case "ArrowUp": - if len(history) == 0 { - return nil - } - ev.Call("preventDefault") - if histIdx == -1 { - histIdx = len(history) - } - if histIdx > 0 { - histIdx-- - } - input.Set("value", history[histIdx]) - update(history[histIdx]) - case "ArrowDown": - if histIdx == -1 { - return nil - } - ev.Call("preventDefault") - histIdx++ - if histIdx >= len(history) { - histIdx = -1 - input.Set("value", "") - update("") - } else { - input.Set("value", history[histIdx]) - update(history[histIdx]) - } - } - return nil - }) - input.Call("addEventListener", "keydown", keyCb) -} - -// update re-renders the help + completions panel based on the -// current input line. Called on every keystroke and on history -// navigation. -func update(line string) { - d := js.Global().Get("document") - - path, finishedTokens, lastToken := resolvePath(line) - n, ok := tree[path] - if !ok { - n = tree[""] - path = "" - } - - // Help panel: name + Short + Long + Example + Flags table. - help := d.Call("getElementById", "sh-help") - help.Set("innerHTML", "") - - header := d.Call("createElement", "div") - header.Set("className", "sh-help-header") - cmdSoFar := "skywire" - if path != "" { - cmdSoFar += " " + strings.ReplaceAll(path, ".", " ") - } - header.Set("textContent", cmdSoFar) - help.Call("appendChild", header) - - if n.Short != "" { - s := d.Call("createElement", "div") - s.Set("className", "sh-help-short") - s.Set("textContent", n.Short) - help.Call("appendChild", s) - } - if n.Long != "" { - pre := d.Call("createElement", "pre") - pre.Set("className", "sh-help-long") - pre.Set("textContent", n.Long) - help.Call("appendChild", pre) - } - if n.Runnable && len(n.Flags) > 0 { - flagsH := d.Call("createElement", "div") - flagsH.Set("className", "sh-help-section") - flagsH.Set("textContent", "Flags") - help.Call("appendChild", flagsH) - table := d.Call("createElement", "table") - table.Set("className", "sh-flags") - for _, fl := range n.Flags { - tr := d.Call("createElement", "tr") - tdName := d.Call("createElement", "td") - tdName.Set("className", "sh-flag-name") - name := "--" + fl.Name - if fl.Shorthand != "" { - name = "-" + fl.Shorthand + ", " + name - } - tdName.Set("textContent", name) - tdType := d.Call("createElement", "td") - tdType.Set("className", "sh-flag-type") - tdType.Set("textContent", fl.Type) - tdUsage := d.Call("createElement", "td") - tdUsage.Set("className", "sh-flag-usage") - usage := fl.Usage - if fl.Default != "" && fl.Default != "false" && fl.Default != "[]" { - usage += " (default " + fl.Default + ")" - } - tdUsage.Set("textContent", usage) - tr.Call("appendChild", tdName) - tr.Call("appendChild", tdType) - tr.Call("appendChild", tdUsage) - table.Call("appendChild", tr) - } - help.Call("appendChild", table) - } - if n.Example != "" { - eh := d.Call("createElement", "div") - eh.Set("className", "sh-help-section") - eh.Set("textContent", "Examples") - help.Call("appendChild", eh) - pre := d.Call("createElement", "pre") - pre.Set("className", "sh-help-example") - pre.Set("textContent", n.Example) - help.Call("appendChild", pre) - } - - // Completions panel: list of subcommands (if non-leaf) or flag - // suggestions (if last token starts with "-"). - compPanel := d.Call("getElementById", "sh-completions") - compPanel.Set("innerHTML", "") - suggestions := []string{} - if strings.HasPrefix(lastToken, "-") && !finishedTokens { - // Flag completion at the current path. - flagPrefix := strings.TrimLeft(lastToken, "-") - for _, fl := range n.Flags { - if flagPrefix == "" || strings.HasPrefix(fl.Name, flagPrefix) { - suggestions = append(suggestions, "--"+fl.Name) - } - } - } else { - // Subcommand completion. - for _, childPath := range n.Children { - c := tree[childPath] - if lastToken == "" || strings.HasPrefix(c.Name, lastToken) { - suggestions = append(suggestions, c.Name) - } - } - } - sort.Strings(suggestions) - if len(suggestions) > 0 { - hdr := d.Call("createElement", "div") - hdr.Set("className", "sh-comp-header") - hdr.Set("textContent", "↹ Tab — completions") - compPanel.Call("appendChild", hdr) - row := d.Call("createElement", "div") - row.Set("className", "sh-comp-row") - for _, s := range suggestions { - pill := d.Call("createElement", "span") - pill.Set("className", "sh-comp-pill") - pill.Set("textContent", s) - row.Call("appendChild", pill) - } - compPanel.Call("appendChild", row) - } -} - -// resolvePath walks the input line token-by-token, advancing through -// the cobra tree until a token isn't a known subcommand name. Returns -// the dot-path matched, whether the last token is "finished" (a -// trailing space means yes), and the last (possibly partial) token -// for completion logic. -func resolvePath(line string) (string, bool, string) { - finished := strings.HasSuffix(line, " ") - tokens := strings.Fields(line) - path := "" - consumed := 0 - for i, t := range tokens { - if strings.HasPrefix(t, "-") { - // Hit a flag — stop walking the path. - break - } - candidate := path - if candidate != "" { - candidate += "." - } - candidate += t - if _, ok := tree[candidate]; !ok { - break - } - path = candidate - consumed = i + 1 - } - lastToken := "" - if !finished && len(tokens) > 0 { - lastToken = tokens[len(tokens)-1] - if consumed == len(tokens) { - // Last token was a complete subcommand → empty partial. - lastToken = "" - } - } - return path, finished, lastToken -} - -// autocomplete returns the input line with the last token expanded -// to the unique completion, or empty if no unique match. Common -// prefix expansion isn't done — yet. -func autocomplete(line string) string { - path, finished, lastToken := resolvePath(line) - n, ok := tree[path] - if !ok { - return "" - } - - var candidates []string - isFlag := strings.HasPrefix(lastToken, "-") - if isFlag { - prefix := strings.TrimLeft(lastToken, "-") - for _, fl := range n.Flags { - if strings.HasPrefix(fl.Name, prefix) { - candidates = append(candidates, "--"+fl.Name) - } - } - } else if !finished { - for _, childPath := range n.Children { - c := tree[childPath] - if strings.HasPrefix(c.Name, lastToken) { - candidates = append(candidates, c.Name) - } - } - } - if len(candidates) == 0 { - // Trailing space: list children of current path → noop here, - // the completions panel shows them; tab doesn't change input. - return "" - } - if len(candidates) == 1 { - // Replace the last token with the unique candidate + space. - fields := strings.Fields(line) - if !finished && len(fields) > 0 { - fields = fields[:len(fields)-1] - } - fields = append(fields, candidates[0]) - return strings.Join(fields, " ") + " " - } - // Multi-candidate: extend to common prefix. - common := candidates[0] - for _, c := range candidates[1:] { - common = commonPrefix(common, c) - } - if len(common) > len(lastToken) { - fields := strings.Fields(line) - if !finished && len(fields) > 0 { - fields = fields[:len(fields)-1] - } - fields = append(fields, common) - return strings.Join(fields, " ") - } - return "" -} - -func commonPrefix(a, b string) string { - n := len(a) - if len(b) < n { - n = len(b) - } - i := 0 - for i < n && a[i] == b[i] { - i++ - } - return a[:i] -} - -// runLine parses a full input line into command path + flags + args -// and POSTs to /api/run. Output streams via SSE into #sh-output. -func runLine(line string) { - path, flags, args := parseLine(line) - if _, ok := tree[path]; !ok { - appendOutput("error: unknown command\n") - return - } - - body := `{"path":` + jsonString(path) - if len(flags) > 0 { - body += `,"flags":{` - first := true - for k, v := range flags { - if !first { - body += "," - } - body += jsonString(k) + ":" + jsonString(v) - first = false - } - body += `}` - } - if len(args) > 0 { - body += `,"args":[` - for i, a := range args { - if i > 0 { - body += "," - } - body += jsonString(a) - } - body += `]` - } - body += `}` - - d := js.Global().Get("document") - out := d.Call("getElementById", "sh-output") - wrap := d.Call("getElementById", "sh-output-wrap") - wrap.Set("hidden", false) - label := d.Call("getElementById", "sh-output-label") - label.Set("textContent", "$ skywire "+strings.ReplaceAll(path, ".", " ")) - // Append history separator instead of clearing — operator - // expects a transcript view, not a single-shot REPL. - if cur := out.Get("textContent").String(); cur != "" { - out.Set("textContent", cur+"\n") - } - out.Set("textContent", out.Get("textContent").String()+"$ "+line+"\n") - - cancelBtn := d.Call("getElementById", "sh-cancel") - cancelBtn.Set("hidden", true) - - headers := js.Global().Get("Object").New() - headers.Set("Content-Type", "application/json") - opts := js.Global().Get("Object").New() - opts.Set("method", "POST") - opts.Set("body", body) - opts.Set("headers", headers) - then := js.FuncOf(func(_ js.Value, fetchArgs []js.Value) interface{} { - fetchArgs[0].Call("json").Call("then", js.FuncOf(func(_ js.Value, jsonArgs []js.Value) interface{} { - id := jsonArgs[0].Get("id").String() - subscribe(id) - return nil - })) - return nil - }) - js.Global().Call("fetch", "/api/run", opts).Call("then", then) -} - -// parseLine breaks a typed line into (command-path, flag map, -// positional args). Tokens with --name or --name=value are flags; -// the next-token-after-flag-name (when no =) is the flag's value -// unless it itself starts with --. Bool flags accept --name or -// --name=true. Single-dash shorthand isn't handled yet (operator -// types --long-name for now). -func parseLine(line string) (string, map[string]string, []string) { - tokens := strings.Fields(line) - // Find path prefix. - path := "" - i := 0 - for ; i < len(tokens); i++ { - t := tokens[i] - if strings.HasPrefix(t, "-") { - break - } - candidate := path - if candidate != "" { - candidate += "." - } - candidate += t - if _, ok := tree[candidate]; !ok { - break - } - path = candidate - } - flags := map[string]string{} - args := []string{} - for i < len(tokens) { - t := tokens[i] - if strings.HasPrefix(t, "--") { - name := strings.TrimPrefix(t, "--") - if eq := strings.Index(name, "="); eq >= 0 { - flags[name[:eq]] = name[eq+1:] - i++ - continue - } - // Bool detection — if the flag's typed-tree type is bool - // and the next token starts with -- (or doesn't exist), - // treat as standalone --flag (true). - if isBoolFlag(path, name) { - if i+1 < len(tokens) && !strings.HasPrefix(tokens[i+1], "-") { - // Operator wrote --bool value — accept the value. - flags[name] = tokens[i+1] - i += 2 - } else { - flags[name] = "true" - i++ - } - continue - } - if i+1 < len(tokens) { - flags[name] = tokens[i+1] - i += 2 - } else { - flags[name] = "" - i++ - } - continue - } - args = append(args, t) - i++ - } - return path, flags, args -} - -func isBoolFlag(path, flagName string) bool { - n, ok := tree[path] - if !ok { - return false - } - for _, fl := range n.Flags { - if fl.Name == flagName { - return fl.Type == "bool" - } - } - return false -} - -func subscribe(id string) { - d := js.Global().Get("document") - out := d.Call("getElementById", "sh-output") - cancelBtn := d.Call("getElementById", "sh-cancel") - cancelBtn.Set("hidden", false) - - es := js.Global().Get("EventSource").New("/api/sse/" + id) - stdout := js.FuncOf(func(_ js.Value, args []js.Value) interface{} { - data := args[0].Get("data").String() - out.Set("textContent", out.Get("textContent").String()+data+"\n") - out.Set("scrollTop", out.Get("scrollHeight")) - return nil - }) - exitCB := js.FuncOf(func(_ js.Value, args []js.Value) interface{} { - code := args[0].Get("data").String() - out.Set("textContent", out.Get("textContent").String()+"[exit "+code+"]\n") - es.Call("close") - cancelBtn.Set("hidden", true) - return nil - }) - es.Call("addEventListener", "stdout", stdout) - es.Call("addEventListener", "exit", exitCB) - - cancelClick := js.FuncOf(func(_ js.Value, _ []js.Value) interface{} { - opts := js.Global().Get("Object").New() - opts.Set("method", "POST") - js.Global().Call("fetch", "/api/cancel/"+id, opts) - return nil - }) - cancelBtn.Call("addEventListener", "click", cancelClick) -} - -func appendOutput(s string) { - out := js.Global().Get("document").Call("getElementById", "sh-output") - wrap := js.Global().Get("document").Call("getElementById", "sh-output-wrap") - wrap.Set("hidden", false) - out.Set("textContent", out.Get("textContent").String()+s) -} - -// jsonString escapes s for inclusion in a JSON document. Hand-rolled -// to avoid encoding/json's reflect dependency. -func jsonString(s string) string { - b := strings.Builder{} - b.WriteByte('"') - for _, r := range s { - switch r { - case '"': - b.WriteString(`\"`) - case '\\': - b.WriteString(`\\`) - case '\n': - b.WriteString(`\n`) - case '\r': - b.WriteString(`\r`) - case '\t': - b.WriteString(`\t`) - default: - if r < 0x20 { - b.WriteString("\\u00") - const hex = "0123456789abcdef" - b.WriteByte(hex[r>>4]) - b.WriteByte(hex[r&0xF]) - } else { - b.WriteRune(r) - } - } - } - b.WriteByte('"') - return b.String() -} - -// parseTree hand-rolled JSON decoder — see equivalent in the -// install-page generator's WASM client. Doesn't use encoding/json -// (drags reflect) and we don't need a general decoder here. -func parseTree(s string) map[string]node { - p := &parser{s: s} - out := map[string]node{} - p.expect('{') - for { - p.ws() - if p.peek() == '}' { - p.next() - break - } - k := p.parseString() - p.ws() - p.expect(':') - v := p.parseNode() - out[k] = v - p.ws() - if p.peek() == ',' { - p.next() - continue - } - } - return out -} - -type parser struct { - s string - i int -} - -func (p *parser) ws() { - for p.i < len(p.s) { - c := p.s[p.i] - if c == ' ' || c == '\t' || c == '\n' || c == '\r' { - p.i++ - continue - } - break - } -} - -func (p *parser) peek() byte { - if p.i >= len(p.s) { - return 0 - } - return p.s[p.i] -} - -func (p *parser) next() byte { - c := p.peek() - p.i++ - return c -} - -func (p *parser) expect(c byte) { - p.ws() - if p.peek() != c { - return - } - p.next() -} - -func (p *parser) parseString() string { - p.ws() - if p.peek() != '"' { - return "" - } - p.next() - b := strings.Builder{} - for p.i < len(p.s) { - c := p.next() - if c == '"' { - return b.String() - } - if c == '\\' { - esc := p.next() - switch esc { - case '"', '\\', '/': - b.WriteByte(esc) - case 'n': - b.WriteByte('\n') - case 'r': - b.WriteByte('\r') - case 't': - b.WriteByte('\t') - case 'u': - if p.i+4 > len(p.s) { - return b.String() - } - h := p.s[p.i : p.i+4] - p.i += 4 - r := 0 - for _, c := range h { - r <<= 4 - switch { - case c >= '0' && c <= '9': - r |= int(c - '0') - case c >= 'a' && c <= 'f': - r |= int(c-'a') + 10 - case c >= 'A' && c <= 'F': - r |= int(c-'A') + 10 - } - } - b.WriteRune(rune(r)) - default: - b.WriteByte(esc) - } - continue - } - b.WriteByte(c) - } - return b.String() -} - -func (p *parser) parseBool() bool { - p.ws() - if p.i+4 <= len(p.s) && p.s[p.i:p.i+4] == "true" { - p.i += 4 - return true - } - if p.i+5 <= len(p.s) && p.s[p.i:p.i+5] == "false" { - p.i += 5 - } - return false -} - -func (p *parser) skipValue() { - p.ws() - c := p.peek() - switch c { - case '"': - p.parseString() - case '{', '[': - open := c - closeB := byte('}') - if open == '[' { - closeB = ']' - } - depth := 1 - p.next() - for p.i < len(p.s) && depth > 0 { - c = p.next() - if c == '"' { - p.i-- - p.parseString() - continue - } - if c == open { - depth++ - } else if c == closeB { - depth-- - } - } - default: - for p.i < len(p.s) { - c = p.peek() - if c == ',' || c == '}' || c == ']' || c == ' ' || c == '\n' || c == '\t' || c == '\r' { - return - } - p.next() - } - } -} - -func (p *parser) parseStringArray() []string { - out := []string{} - p.ws() - if p.peek() != '[' { - return out - } - p.next() - for { - p.ws() - if p.peek() == ']' { - p.next() - return out - } - out = append(out, p.parseString()) - p.ws() - if p.peek() == ',' { - p.next() - continue - } - } -} - -func (p *parser) parseFlags() []flag { - out := []flag{} - p.ws() - if p.peek() != '[' { - return out - } - p.next() - for { - p.ws() - if p.peek() == ']' { - p.next() - return out - } - f := flag{} - p.expect('{') - for { - p.ws() - if p.peek() == '}' { - p.next() - break - } - k := p.parseString() - p.ws() - p.expect(':') - switch k { - case "name": - f.Name = p.parseString() - case "shorthand": - f.Shorthand = p.parseString() - case "type": - f.Type = p.parseString() - case "default": - f.Default = p.parseString() - case "usage": - f.Usage = p.parseString() - default: - p.skipValue() - } - p.ws() - if p.peek() == ',' { - p.next() - } - } - out = append(out, f) - p.ws() - if p.peek() == ',' { - p.next() - } - } -} - -func (p *parser) parseNode() node { - n := node{} - p.ws() - p.expect('{') - for { - p.ws() - if p.peek() == '}' { - p.next() - return n - } - k := p.parseString() - p.ws() - p.expect(':') - switch k { - case "path": - n.Path = p.parseString() - case "name": - n.Name = p.parseString() - case "short": - n.Short = p.parseString() - case "long": - n.Long = p.parseString() - case "example": - n.Example = p.parseString() - case "children": - n.Children = p.parseStringArray() - case "flags": - n.Flags = p.parseFlags() - case "runnable": - n.Runnable = p.parseBool() - default: - p.skipValue() - } - p.ws() - if p.peek() == ',' { - p.next() - } - } -} diff --git a/cmd/skywire/commands/web/web.go b/cmd/skywire/commands/web/web.go deleted file mode 100644 index 92591ca823..0000000000 --- a/cmd/skywire/commands/web/web.go +++ /dev/null @@ -1,505 +0,0 @@ -// Package web cmd/skywire/commands/web/web.go c4-vis-cli -// -// Serves a browser-based UI for the entire skywire CLI tree. The -// page is a TinyGo-compiled WASM bundle (see ./wasm/main.go); the -// server walks the live cobra tree at startup, exposes it as JSON, -// and proxies command execution via subprocess + Server-Sent Events. -// -// Why a separate binary subcommand and not nested under `cli`: this -// command spawns `./skywire ` as a subprocess, so it's a -// PEER of cli, not a child — clearer in the help tree this way. -// -// Static assets (index.html, wasm_exec.js, b.wasm) are embedded via -// //go:embed so a release binary ships self-contained. b.wasm is -// produced by `make build` in this directory; absent that, the -// server still starts and serves the index page with a build-me -// banner. -package web - -import ( - "bufio" - "context" - _ "embed" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "os" - "os/exec" - "strings" - "sync" - "time" - - "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/spf13/pflag" -) - -//go:embed static/index.html -var indexHTML []byte - -//go:embed static/wasm_exec.js -var wasmExecJS []byte - -// b.wasm is optional — embedded if produced by `make build` in this -// dir, otherwise the embed will be empty and the loader page shows -// a "rebuild me" banner. The empty default keeps the package -// buildable from a fresh checkout without TinyGo installed. -// -//go:embed static/b.wasm -var bWasm []byte - -var ( - flagAddr string - flagToken string - flagAllow []string - flagSkywire string -) - -func init() { - RootCmd.Flags().StringVar(&flagAddr, "addr", "127.0.0.1:8088", - "HTTP bind address (use 'host:port'; default loopback-only)") - RootCmd.Flags().StringVar(&flagToken, "token", "", - "require ?token=... or Authorization: Bearer for every request (empty = no auth, only safe on loopback)") - RootCmd.Flags().StringSliceVar(&flagAllow, "allow", nil, - "allowlist of subcommand paths (dot-separated, e.g. 'cli.skychat.send'); empty = allow all non-hidden subcommands") - RootCmd.Flags().StringVar(&flagSkywire, "skywire", "", - "path to the skywire binary used for subprocess execution (empty = use the running binary)") -} - -// RootCmd is the cobra entry point. Mounted from -// cmd/skywire/commands/root.go. -var RootCmd = &cobra.Command{ - Use: "web", - Short: "Serve the skywire CLI as a browser-based UI", - Long: `Start a local HTTP server that renders the entire skywire -cobra command tree as a navigable web page. Click any subcommand -to see its help, fill in flags, and execute — output streams back -to the browser via Server-Sent Events. - -The page is a TinyGo-compiled WASM bundle (no JavaScript framework, -no client-side state outside the WASM module). The server walks -the live cobra tree at startup so the UI always reflects the -current binary's actual capabilities. - -Default binding is loopback-only. For LAN/remote access, pair ---addr 0.0.0.0:8088 with --token and require clients to -provide the token via ?token= or Authorization: Bearer. - -The --allow flag scopes which subcommands the UI exposes — useful -for hosted instances. Example: --allow cli.skychat,cli.dmsg.curl -exposes only chat send + dmsg curl. Empty allowlist = everything -non-hidden. - -Examples: - skywire web # localhost:8088, no auth - skywire web --addr 0.0.0.0:8088 --token foo # LAN, token-gated - skywire web --allow cli.skychat # only chat subcommands`, - SilenceErrors: true, - SilenceUsage: true, - DisableFlagsInUseLine: true, - RunE: func(cmd *cobra.Command, _ []string) error { - return serve(cmd.Context(), cmd.Root()) - }, -} - -func serve(ctx context.Context, root *cobra.Command) error { - skywireBin := flagSkywire - if skywireBin == "" { - exe, err := os.Executable() - if err != nil { - return fmt.Errorf("resolve own binary path: %w", err) - } - skywireBin = exe - } - tree := buildTree(root, flagAllow) - - mux := http.NewServeMux() - mux.Handle("GET /", authMiddleware(http.HandlerFunc(handleIndex))) - mux.Handle("GET /b.wasm", authMiddleware(http.HandlerFunc(handleWasm))) - mux.Handle("GET /wasm_exec.js", authMiddleware(http.HandlerFunc(handleWasmExec))) - mux.Handle("GET /api/tree", authMiddleware(handleTree(tree))) - - runs := newRunRegistry() - mux.Handle("POST /api/run", authMiddleware(handleRun(runs, skywireBin))) - mux.Handle("GET /api/sse/{id}", authMiddleware(handleSSE(runs))) - mux.Handle("POST /api/cancel/{id}", authMiddleware(handleCancel(runs))) - - srv := &http.Server{ - Addr: flagAddr, - Handler: mux, - ReadHeaderTimeout: 5 * time.Second, - } - - // Shutdown on ctx cancel — outer command's signal handler - // (cobra's default) plumbs SIGINT through. - go func() { //nolint:gosec // G118: shutdown timeout deliberately uses Background — the request ctx is being canceled - <-ctx.Done() - shutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - _ = srv.Shutdown(shutCtx) //nolint:errcheck - runs.cancelAll() - }() - - fmt.Fprintf(os.Stderr, "skywire web: serving on http://%s/\n", flagAddr) - if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { - return err - } - return nil -} - -// authMiddleware is the optional token gate. No-op when flagToken is -// empty — operator's responsibility to keep the bind loopback in -// that case. Loopback bindings (127.0.0.0/8, ::1) bypass the gate -// because the operator running localhost obviously authenticates -// themselves via OS access. -func authMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if flagToken == "" { - next.ServeHTTP(w, r) - return - } - // Loopback bypass — RemoteAddr is "host:port". - host, _, _ := strings.Cut(r.RemoteAddr, ":") - if host == "127.0.0.1" || host == "::1" || host == "localhost" { - next.ServeHTTP(w, r) - return - } - got := r.URL.Query().Get("token") - if got == "" { - auth := r.Header.Get("Authorization") - if strings.HasPrefix(auth, "Bearer ") { - got = strings.TrimPrefix(auth, "Bearer ") - } - } - if got != flagToken { - http.Error(w, "missing or invalid token", http.StatusUnauthorized) - return - } - next.ServeHTTP(w, r) - }) -} - -func handleIndex(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", "text/html; charset=utf-8") - _, _ = w.Write(indexHTML) //nolint:errcheck -} - -func handleWasm(w http.ResponseWriter, _ *http.Request) { - if len(bWasm) == 0 { - http.Error(w, "b.wasm not built — run `make build` in cmd/skywire/commands/web/", http.StatusNotFound) - return - } - w.Header().Set("Content-Type", "application/wasm") - _, _ = w.Write(bWasm) //nolint:errcheck -} - -func handleWasmExec(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", "application/javascript; charset=utf-8") - _, _ = w.Write(wasmExecJS) //nolint:errcheck -} - -// CommandNode is the JSON-friendly shape of one cobra command in the -// tree. Flat children list (not nested) so the WASM client can -// navigate via path lookups without recursing JSON. flags include -// only non-hidden user-facing flags. -type CommandNode struct { - Path string `json:"path"` // dot-separated, "" for root - Name string `json:"name"` - Short string `json:"short,omitempty"` - Long string `json:"long,omitempty"` - Example string `json:"example,omitempty"` - Use string `json:"use,omitempty"` // raw Use string from cobra - Children []string `json:"children,omitempty"` - Flags []FlagInfo `json:"flags,omitempty"` - Runnable bool `json:"runnable"` // has Run / RunE - Hidden bool `json:"hidden,omitempty"` -} - -// FlagInfo carries enough flag metadata for the WASM side to render -// an appropriate . Type strings match pflag's Value.Type() -// output ("string", "bool", "int", "duration", etc.). -type FlagInfo struct { - Name string `json:"name"` - Shorthand string `json:"shorthand,omitempty"` - Type string `json:"type"` - Default string `json:"default,omitempty"` - Usage string `json:"usage,omitempty"` -} - -// buildTree walks the cobra subcommand graph rooted at `root` and -// returns a path→node map. The empty-string key is the root itself. -// Hidden commands are included only when the operator's --allow -// list names them explicitly; the WASM client decides whether to -// surface them in the sidebar. -func buildTree(root *cobra.Command, allow []string) map[string]CommandNode { - allowSet := make(map[string]struct{}, len(allow)) - for _, a := range allow { - allowSet[a] = struct{}{} - } - out := make(map[string]CommandNode) - var walk func(c *cobra.Command, path string) - walk = func(c *cobra.Command, path string) { - // Non-allowlisted nodes are still walked so allowed descendants can - // mount under an implicit breadcrumb; the node itself is emitted with - // its flags/runnable bit regardless. - children := make([]string, 0, len(c.Commands())) - for _, sub := range c.Commands() { - if sub.Hidden && len(allow) == 0 { - continue - } - subPath := sub.Name() - if path != "" { - subPath = path + "." + sub.Name() - } - children = append(children, subPath) - walk(sub, subPath) - } - flags := []FlagInfo{} - c.Flags().VisitAll(func(f *pflag.Flag) { - if f.Hidden { - return - } - flags = append(flags, FlagInfo{ - Name: f.Name, - Shorthand: f.Shorthand, - Type: f.Value.Type(), - Default: f.DefValue, - Usage: f.Usage, - }) - }) - runnable := c.Run != nil || c.RunE != nil - out[path] = CommandNode{ - Path: path, - Name: c.Name(), - Short: c.Short, - Long: strings.TrimSpace(c.Long), - Example: strings.TrimSpace(c.Example), - Use: c.Use, - Children: children, - Flags: flags, - Runnable: runnable, - Hidden: c.Hidden, - } - } - walk(root, "") - return out -} - -func handleTree(tree map[string]CommandNode) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(tree) //nolint:errcheck - }) -} - -// runRegistry tracks running subprocesses so /api/sse/ can find -// their stdout stream and /api/cancel/ can SIGINT them. -type runRegistry struct { - mu sync.Mutex - runs map[string]*run -} - -type run struct { - cmd *exec.Cmd - output chan string // line-buffered stdout+stderr - done chan int // exit code - cancel context.CancelFunc - started time.Time -} - -func newRunRegistry() *runRegistry { return &runRegistry{runs: map[string]*run{}} } - -func (rr *runRegistry) put(id string, r *run) { - rr.mu.Lock() - defer rr.mu.Unlock() - rr.runs[id] = r -} - -func (rr *runRegistry) get(id string) (*run, bool) { - rr.mu.Lock() - defer rr.mu.Unlock() - r, ok := rr.runs[id] - return r, ok -} - -func (rr *runRegistry) delete(id string) { - rr.mu.Lock() - defer rr.mu.Unlock() - delete(rr.runs, id) -} - -func (rr *runRegistry) cancelAll() { - rr.mu.Lock() - defer rr.mu.Unlock() - for _, r := range rr.runs { - r.cancel() - } -} - -// RunRequest is the POST /api/run body — subpath plus user-entered -// flag values + positional args. -type RunRequest struct { - Path string `json:"path"` // dot-separated, mapped to space-separated argv - Flags map[string]string `json:"flags,omitempty"` // key = long flag name, value = string repr - Args []string `json:"args,omitempty"` // positional args -} - -func handleRun(runs *runRegistry, skywireBin string) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var req RunRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "decode body: "+err.Error(), http.StatusBadRequest) - return - } - argv := append([]string{}, strings.Split(req.Path, ".")...) - for k, v := range req.Flags { - argv = append(argv, "--"+k, v) - } - argv = append(argv, req.Args...) - - runCtx, cancel := context.WithCancel(context.Background()) //nolint:gosec // cancel routed to run - cmd := exec.CommandContext(runCtx, skywireBin, argv...) //nolint:gosec // skywireBin is the resolved self-binary path; argv is trusted CLI input - // Combined stdout+stderr to one pipe; we don't distinguish - // in the SSE stream (the wasm client just renders sequential - // lines, like a terminal would). - stdout, err := cmd.StdoutPipe() - if err != nil { - cancel() - http.Error(w, "stdout pipe: "+err.Error(), http.StatusInternalServerError) - return - } - cmd.Stderr = cmd.Stdout - // SIGINT on cancel so the subprocess gets a chance to clean - // up (matters for visor halt, dmsg curl downloads, etc). - setProcGroup(cmd) - - if err := cmd.Start(); err != nil { - cancel() - http.Error(w, "start: "+err.Error(), http.StatusInternalServerError) - return - } - - id := uuid.NewString() - runEntry := &run{ - cmd: cmd, - output: make(chan string, 256), - done: make(chan int, 1), - cancel: cancel, - started: time.Now(), - } - runs.put(id, runEntry) - - // Reader goroutine: line-split stdout into the channel. - go func() { - defer close(runEntry.output) - sc := bufio.NewScanner(stdout) - sc.Buffer(make([]byte, 0, 64*1024), 1024*1024) - for sc.Scan() { - select { - case runEntry.output <- sc.Text(): - case <-runCtx.Done(): - return - } - } - }() - // Wait goroutine: emit exit code, then drop from registry - // after a grace window so late SSE consumers can read it. - go func() { - err := cmd.Wait() - code := 0 - if err != nil { - var exitErr *exec.ExitError - if errors.As(err, &exitErr) { - code = exitErr.ExitCode() - } else { - code = -1 - } - } - runEntry.done <- code - close(runEntry.done) - // Grace: keep the entry around for 30s so a slow SSE - // reconnect can still pull the exit code. - time.AfterFunc(30*time.Second, func() { - runs.delete(id) - cancel() - }) - }() - - w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(map[string]string{"id": id}) //nolint:errcheck - }) -} - -func handleSSE(runs *runRegistry) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - id := r.PathValue("id") - runEntry, ok := runs.get(id) - if !ok { - http.Error(w, "no such run", http.StatusNotFound) - return - } - flusher, ok := w.(http.Flusher) - if !ok { - http.Error(w, "streaming not supported", http.StatusInternalServerError) - return - } - w.Header().Set("Content-Type", "text/event-stream") - w.Header().Set("Cache-Control", "no-cache") - w.Header().Set("Connection", "keep-alive") - w.WriteHeader(http.StatusOK) - flusher.Flush() - - // Browser disconnect → SIGINT to subprocess. The Setpgid - // above lets us kill the whole process group, catching any - // children the subprocess spawned (e.g., `skywire visor`'s - // app subprocesses). - notify := r.Context().Done() - - for { - select { - case line, ok := <-runEntry.output: - if !ok { - // Output channel closed → wait for exit code + - // emit it as a final event, then close. - select { - case code := <-runEntry.done: - fmt.Fprintf(w, "event: exit\ndata: %d\n\n", code) //nolint:errcheck - flusher.Flush() - case <-time.After(5 * time.Second): - fmt.Fprintf(w, "event: exit\ndata: -1\n\n") //nolint:errcheck - flusher.Flush() - } - return - } - writeSSE(w, "stdout", line) - flusher.Flush() - case <-notify: - // Client gone — kill the subprocess group. - killProcGroup(runEntry.cmd.Process) - return - } - } - }) -} - -func writeSSE(w io.Writer, event, data string) { - for _, line := range strings.Split(data, "\n") { - fmt.Fprintf(w, "event: %s\ndata: %s\n", event, line) //nolint:errcheck - } - fmt.Fprint(w, "\n") //nolint:errcheck -} - -func handleCancel(runs *runRegistry) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - id := r.PathValue("id") - runEntry, ok := runs.get(id) - if !ok { - http.Error(w, "no such run", http.StatusNotFound) - return - } - killProcGroup(runEntry.cmd.Process) - w.WriteHeader(http.StatusNoContent) - }) -} diff --git a/docs/skywire/README.md b/docs/skywire/README.md index 7d81bbd0aa..148ed5b2a3 100644 --- a/docs/skywire/README.md +++ b/docs/skywire/README.md @@ -22,7 +22,6 @@ skywire - [dmsg](dmsg/README.md) — DMSG services & utilities - [svc](svc/README.md) — Skywire services - [visor](visor/README.md) — Skywire Visor -- [web](web/README.md) — Serve the skywire CLI as a browser-based UI ## Flags diff --git a/docs/skywire/web/README.md b/docs/skywire/web/README.md deleted file mode 100644 index de6833d00b..0000000000 --- a/docs/skywire/web/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# skywire web - -[← skywire](../README.md) - -Start a local HTTP server that renders the entire skywire -cobra command tree as a navigable web page. Click any subcommand -to see its help, fill in flags, and execute — output streams back -to the browser via Server-Sent Events. - -The page is a TinyGo-compiled WASM bundle (no JavaScript framework, -no client-side state outside the WASM module). The server walks -the live cobra tree at startup so the UI always reflects the -current binary's actual capabilities. - -Default binding is loopback-only. For LAN/remote access, pair ---addr 0.0.0.0:8088 with --token and require clients to -provide the token via ?token= or Authorization: Bearer. - -The --allow flag scopes which subcommands the UI exposes — useful -for hosted instances. Example: --allow cli.skychat,cli.dmsg.curl -exposes only chat send + dmsg curl. Empty allowlist = everything -non-hidden. - -Examples: - skywire web # localhost:8088, no auth - skywire web --addr 0.0.0.0:8088 --token foo # LAN, token-gated - skywire web --allow cli.skychat # only chat subcommands - -## Usage - -``` -skywire web -``` - -## Flags - -``` - --addr string HTTP bind address (use 'host:port'; default loopback-only) (default "127.0.0.1:8088") - --allow strings allowlist of subcommand paths (dot-separated, e.g. 'cli.skychat.send'); empty = allow all non-hidden subcommands - --skywire string path to the skywire binary used for subprocess execution (empty = use the running binary) - --token string require ?token=... or Authorization: Bearer for every request (empty = no auth, only safe on loopback) -``` - -## Global Flags - -``` - -h, --help show help menu -``` - ---- -_Generated by `skywire doc` — do not edit by hand._