Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
c6728bf
ESD-1623-fix(cli): delete the frontend-integration reference implemen…
Phil-Browne Jul 10, 2026
a83a7b4
fix(cli): remove stale committed web/vue-demo build output
Phil-Browne Jul 10, 2026
12b51e3
fix(wasm): wipe stale publish dir and clean wasm_exec.js properly
Phil-Browne Jul 10, 2026
6675d4e
Merge remote-tracking branch 'origin/main' into esd-1623-delete-front…
Phil-Browne Jul 10, 2026
c1f6f41
docs(wasm): clarify cmd/wasmhash caching section isn't part of the ac…
Phil-Browne Jul 10, 2026
0b6d200
docs(wasm): document interactive-mode prompt host contract
Phil-Browne Jul 13, 2026
e44a468
fix(wasm): strip debug info from the published WASM binary
Phil-Browne Jul 13, 2026
d70fb8b
docs(wasm): document the live output streaming API
Phil-Browne Jul 13, 2026
f4f7a1e
fix(build): remove web/wasm_exec.js in make clean
Phil-Browne Jul 13, 2026
376641f
docs(wasm): correct the build-web.sh publish hint
Phil-Browne Jul 13, 2026
e7cd0be
docs(wasm): align build-web.sh header with the publish note
Phil-Browne Jul 13, 2026
4602fd1
docs(wasm): correct CDN publish guidance to use the wasm-publish work…
Phil-Browne Jul 13, 2026
641c7a9
Merge remote-tracking branch 'origin/main' into esd-1623-delete-front…
Phil-Browne Jul 13, 2026
4b7211c
Merge remote-tracking branch 'origin/main' into esd-1623-delete-front…
Phil-Browne Jul 15, 2026
9eea4ef
Merge remote-tracking branch 'origin/main' into esd-1623-delete-front…
Phil-Browne Jul 15, 2026
54db16d
Remove duplicate blank line in .gitignore
Phil-Browne Jul 22, 2026
2b9afa0
Merge branch 'main' into esd-1623-delete-frontend-integration
Phil-Browne Jul 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,6 @@ testdata/
.env.local
wasm.sh
Comment thread
Phil-Browne marked this conversation as resolved.

# Node.js (not needed in the image)
web/node_modules/

# Front-end build artifacts not needed by the binary-only image.
frontend-integration/node_modules/
web/vue-demo/

# Temporary files
tmp/
temp/
Comment thread
Phil-Browne marked this conversation as resolved.
Expand Down
27 changes: 10 additions & 17 deletions .github/workflows/wasm-publish.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
name: Publish WASM to S3

# Builds the browser/WASM CLI static site and publishes it to S3 + CloudFront.
# Builds the browser/WASM CLI's static assets (megaport.wasm + wasm_exec.js) and
# publishes them to S3 + CloudFront.
#
# CONFIGURATION REQUIRED BEFORE MERGE (ESD-1506):
# AWS access is provisioned via the shared github_runners_iam Terraform module in
Expand All @@ -27,10 +28,9 @@ name: Publish WASM to S3
# See the "Check required configuration" step, which fails early with a clear message.
#
# WHAT GETS PUBLISHED:
# Stable filenames under each prefix: megaport.wasm (brotli, Content-Encoding: br),
# wasm_exec.js, and the rest of the static site. The portal references megaport.wasm
# and wasm_exec.js by URL, so the names are kept stable rather than content-hashed;
# the version/latest prefix provides the cache-busting instead.
# Stable filenames under each prefix: megaport.wasm (brotli, Content-Encoding: br) and
# wasm_exec.js. The portal references both by URL, so the names are kept stable rather
# than content-hashed; the version/latest prefix provides the cache-busting instead.
#
# PORTAL INTEGRATION:
# Point the portal's wasmUrl/wasmExecUrl at the latest/ alias while iterating, then
Expand Down Expand Up @@ -108,12 +108,6 @@ jobs:
go-version-file: 'go.mod'
cache: true

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20
cache: npm
cache-dependency-path: frontend-integration/package-lock.json

- name: Resolve version label
id: version
env:
Expand Down Expand Up @@ -142,7 +136,7 @@ jobs:
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "Publishing version: $version"

- name: Build static site
- name: Build static assets
run: make web-static

- name: Compress WASM
Expand All @@ -151,7 +145,7 @@ jobs:
# cmd/wasmcompress writes .br (and .gz) next to the source file (ESD-1268).
# CloudFront auto-compression caps at 10 MB; the raw wasm is well over that,
# so it must be pre-compressed at origin and served with Content-Encoding: br.
GOWORK=off go run ./cmd/wasmcompress web/vue-demo/megaport.wasm
GOWORK=off go run ./cmd/wasmcompress web/dist/megaport.wasm

- name: Compute integrity hashes
id: hashes
Expand Down Expand Up @@ -215,15 +209,14 @@ jobs:
PUBLISH_LATEST: ${{ inputs.publish_latest }}
run: |
set -euo pipefail
src=web/vue-demo
src=web/dist

publish() {
local dest="s3://${BUCKET}/${PREFIX}/$1"
local cache="$2"
echo "==> Publishing to ${dest}"
# Sync the static site; the wasm and wasm_exec.js are uploaded separately below
# so we can pin their Content-Type/Content-Encoding instead of relying on
# aws s3 sync's MIME inference.
# The wasm and wasm_exec.js are uploaded separately below so we can pin their
# Content-Type/Content-Encoding instead of relying on aws s3 sync's MIME inference.
aws s3 sync "$src/" "$dest/" --delete \
--exclude 'megaport.wasm' \
--exclude 'megaport.wasm.br' \
Expand Down
8 changes: 0 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,6 @@ web/megaport.wasm
*.wasm.br
*.wasm.gz

# Node.js / npm
node_modules/
web/node_modules/
package-lock.json
web/package-lock.json
# Committed so browser/WASM builds resolve identical dependency versions via npm ci.
!frontend-integration/package-lock.json

# Go build artifacts
/megaport-cli
megaport-cli
Expand Down
5 changes: 3 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,11 @@ wasm-smoke: wasm
wasm-compress: wasm
go run ./cmd/wasmcompress web/megaport.wasm

# Build the static browser/WASM site into web/vue-demo/ (for CDN hosting)
# Build the WASM binary + wasm_exec.js loader into web/dist/ (for CDN hosting)
web-static:
./scripts/build-web.sh

# Clean build artifacts
clean:
rm -f megaport-cli cover*.out coverage*.out coverage.html web/megaport.wasm web/megaport.wasm.br web/megaport.wasm.gz web/vue-demo/megaport.wasm web/vue-demo/megaport.wasm.br web/vue-demo/megaport.wasm.gz
rm -f megaport-cli cover*.out coverage*.out coverage.html web/megaport.wasm web/megaport.wasm.br web/megaport.wasm.gz web/wasm_exec.js
rm -rf web/dist
Comment thread
Phil-Browne marked this conversation as resolved.
129 changes: 92 additions & 37 deletions WASM_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,8 @@ Instead, whenever the API rejects a request with 401/403, whether that's because
token expired or because the host revoked it, the failing command's output contains
the substring `MEGAPORT_SESSION_EXPIRED`. The host is responsible for watching command
output for this marker and, on a match, prompting the user to re-authenticate and
calling `setAuthToken` again with a fresh token. See
[`frontend-integration/types/megaport-wasm.d.ts`](frontend-integration/types/megaport-wasm.d.ts)
for the full type signature and doc comments.
calling `setAuthToken` again with a fresh token. The registration and doc comments
for `setAuthToken` live inline in `internal/wasm/wasm.go`.

Separately, the browser-cached OAuth token used by credentials-based logins
(`window.tokenManager.getToken(environment)`, checked to avoid re-authenticating on
Expand Down Expand Up @@ -120,43 +119,102 @@ always returns `{ error: "synchronous execution is not supported; use executeMeg
It will be removed in a future release; new integrations should not call it.

Full type definitions for the whole JS surface (auth, config file, prompts, telemetry)
live in [`frontend-integration/types/megaport-wasm.d.ts`](frontend-integration/types/megaport-wasm.d.ts).
are documented inline in `internal/wasm/`; there is no separate reference front end in
this repo. Front-end integrators (e.g. the Portal) own their own wrapper around the
`window.executeMegaportCommandAsync` API described above.

## Building
## Interactive Mode

The browser CLI is two pieces: the WASM binary and the Vue front end that hosts it.
Some commands prompt for input (interactive `buy`/`update` flows, confirmations, secrets).
In the browser there is no stdin, so the WASM asks the host page for each value through a
small set of JavaScript functions. Wire these up and run the command through
`executeMegaportCommandAsync` (see [JavaScript API](#javascript-api)), or interactive
commands will never receive a response.

```bash
# WASM binary only (writes web/megaport.wasm)
make wasm
### Host functions

# Full static site: WASM + Vue front end, assembled into web/vue-demo/
make web-static # or: ./scripts/build-web.sh
The WASM registers these on `window` at startup:

| Function | Purpose |
|---|---|
| `registerPromptHandler(cb)` | Register a callback the WASM invokes with each prompt request. |
| `submitPromptResponse(id, response)` | Reply to the prompt `id` with the user's input (a string). |
| `cancelPrompt(id)` | Cancel the prompt `id`. A value prompt fails with a "prompt cancelled by user" error; a confirmation is treated as declined. |

### Prompt request shape

Your handler receives a single object:

```js
{
id: "prompt_1_1700000000000000000", // unique id; echo it back in submit/cancel
message: "Enter port name:", // text to show the user
type: "text", // "text" | "confirm" | "password" | "resource"
resourceType: "port" // set for resource and secret-resource prompts (port, mcr, vxc, ...), else ""
}
```

Mask the input when `type === "password"`: render an `<input type="password">` or otherwise
hide the characters. Password prompts and secret-resource prompts (for example VXC/MVE
passwords and pre-shared keys) set this type. Note that some other secret-bearing inputs
(such as partner auth/service/shared keys and MVE registration keys) are currently sent as
`type === "resource"`, so don't rely on the password type alone if you want to mask every
possible secret.

### Lifecycle

```js
registerPromptHandler((request) => {
const masked = request.type === 'password';
showPrompt(request.message, { masked }).then((answer) => {
if (answer === null) {
cancelPrompt(request.id); // user dismissed the prompt
} else {
submitPromptResponse(request.id, answer);
}
});
});

executeMegaportCommandAsync('vxc buy --interactive', (result) => {
console.log(result.output || result.error);
});
```

A prompt left unanswered times out after 10 minutes and the command receives an error.

### Live output streaming

By default a command's output arrives once, in the async callback's result. To render
output as it is produced instead, register a handler before running the command:

```js
registerOutputHandler((chunk) => terminal.write(chunk)); // chunk is a string
```

`web-static` needs the Go toolchain and Node/npm on `PATH`. It produces a self-contained
**`web/vue-demo/`** directory (Vue build + `megaport.wasm` + `wasm_exec.js`) ready to
publish to a CDN. See [`web/README.md`](web/README.md) for the wasm pre-compression and
cache-header details.
The contract:

## Local Development
- Only narrative output (progress and status messages) streams. Structured document
output (table/JSON/CSV/XML) is never streamed; it arrives once in the completion result.
- If your handler received at least one chunk without throwing, the completion result does
**not** repeat the streamed narrative, so don't render both.
- If the handler throws or no chunk was delivered, streaming is disabled for the rest of
that command and the completion result falls back to the full captured output
(already-streamed chunks may then appear twice).

The front end lives in `frontend-integration/` and has its own Vite dev server. The dev
server serves files from that directory's root and has no `public/` dir, so build the wasm
and copy the loader into `frontend-integration/` first, then start the server:
## Building

```bash
# From the repo root: build the wasm + loader into the dev server's root.
GOOS=js GOARCH=wasm go build -tags js,wasm -o frontend-integration/megaport.wasm .
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" frontend-integration/wasm_exec.js
# WASM binary only (writes web/megaport.wasm)
make wasm

cd frontend-integration
npm install
npm run dev:demo
# WASM binary + wasm_exec.js loader, assembled into web/dist/
make web-static # or: ./scripts/build-web.sh
```

Vite serves `megaport.wasm` with the correct `application/wasm` MIME type and reloads the
front end on change. Rerun the build command above after changing Go code.
`web-static` needs the Go toolchain on `PATH`. It produces a self-contained
**`web/dist/`** directory (`megaport.wasm` + `wasm_exec.js`) ready to publish to a CDN.
See [`web/README.md`](web/README.md) for the wasm pre-compression and cache-header
details.

## Enabling a Module for WASM

Expand Down Expand Up @@ -230,16 +288,13 @@ aws s3 cp web/megaport.wasm s3://media.megaport.com/portal/megaport-cli/megaport
aws s3 cp web/wasm_exec.js s3://media.megaport.com/portal/megaport-cli/wasm_exec.js
```

For CDN hosting (S3 + CloudFront) of the full static site, sync the assembled
`web/vue-demo/` directory and serve it from the site root:

```bash
make web-static
aws s3 sync web/vue-demo/ s3://<bucket>/<prefix>/ --delete
```

`--delete` prunes stale hashed assets from old builds, so point it at a prefix dedicated
to this site, since it removes anything else under that prefix.
For CDN hosting (S3 + CloudFront), publish via the `.github/workflows/wasm-publish.yaml`
workflow rather than a plain sync. It runs `make web-static`, brotli pre-compresses the
wasm (`cmd/wasmcompress`), then uploads `megaport.wasm` with `Content-Encoding: br` and
pins `Content-Type` on both the wasm and `wasm_exec.js`. It syncs only the remaining
static assets, so a bare `aws s3 sync web/dist/` would instead serve the wasm
uncompressed and let the CDN mis-infer its MIME type, which breaks
`WebAssembly.instantiateStreaming`.

## Troubleshooting

Expand Down
4 changes: 2 additions & 2 deletions cmd/wasmhash/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ func TestHashFileDeterministicAndContentSensitive(t *testing.T) {
}

func TestInsertHash(t *testing.T) {
got := insertHash(filepath.FromSlash("web/vue-demo/megaport.wasm"), "abcd1234")
want := filepath.FromSlash("web/vue-demo/megaport.abcd1234.wasm")
got := insertHash(filepath.FromSlash("web/dist/megaport.wasm"), "abcd1234")
want := filepath.FromSlash("web/dist/megaport.abcd1234.wasm")
if got != want {
t.Fatalf("insertHash = %q, want %q", got, want)
}
Expand Down
31 changes: 0 additions & 31 deletions frontend-integration/.gitignore

This file was deleted.

Loading
Loading