diff --git a/.changeset/calm-lamps-smile.md b/.changeset/calm-lamps-smile.md new file mode 100644 index 0000000..9eae560 --- /dev/null +++ b/.changeset/calm-lamps-smile.md @@ -0,0 +1,5 @@ +--- +"playground-cli": patch +--- + +Rename the deploy source-publishing option and related CLI language to moddable. diff --git a/.github/workflows/e2e-cleanup.yml b/.github/workflows/e2e-cleanup.yml index e0faecb..5b97102 100644 --- a/.github/workflows/e2e-cleanup.yml +++ b/.github/workflows/e2e-cleanup.yml @@ -17,20 +17,20 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Sweep rotating modable repos and domains + - name: Sweep rotating moddable repos and domains shell: bash env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | echo "::group::What this workflow sweeps" echo "Per spec §9c, this cron sweeps rotating per-run E2E state:" - echo " - GH repos matching 'e2e-cli-modable-*' older than 14 days" - echo " - Registry domains matching 'e2e-cli-modable-*' older than 14 days" + echo " - GH repos matching 'e2e-cli-moddable-*' older than 14 days" + echo " - Registry domains matching 'e2e-cli-moddable-*' older than 14 days" echo "" - echo "Phase 5e (modable) hasn't shipped yet, so there's nothing to sweep today." + echo "Phase 5e (moddable) hasn't shipped yet, so there's nothing to sweep today." echo "When Phase 5e lands, this step gets the actual sweep logic:" echo " gh repo list --topic e2e-test-fixture --limit 100 ..." - echo " bun tools/sweep-modable-domains.ts (would be added then)" + echo " bun tools/sweep-moddable-domains.ts (would be added then)" echo "::endgroup::" # Stub — exit 0. Replace with real sweep when Phase 5e adds rotating state. diff --git a/AGENTS.md b/AGENTS.md index ce77dae..d5e1fd5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -62,8 +62,8 @@ Read `CLAUDE.md` alongside this file when you need the full rationale for repo-s ## Mod And GitHub Behavior - `dot mod` is GitHub-tarball-only. Do not reintroduce `git clone`, `gh repo fork`, or tooling requirements for the public-repo path. -- `dot` never invokes `gh`. `dot deploy --modable` reads an existing `origin` and validates it's a public GitHub URL via `HEAD https://github.com/{o}/{r}`; missing `origin`, private repos, and non-GitHub URLs hard-fail with actionable messages from `src/utils/deploy/modable.ts`. Do not reintroduce auto-create, `gh auth` checks, or any `gh`-shell-out path — the user is responsible for setting up the public GitHub repo themselves. -- `metadata.repository` is written only when `--modable` is explicitly opted in. +- `dot` never invokes `gh`. `dot deploy --moddable` reads an existing `origin` and validates it's a public GitHub URL via `HEAD https://github.com/{o}/{r}`; missing `origin`, private repos, and non-GitHub URLs hard-fail with actionable messages from `src/utils/deploy/moddable.ts`. Do not reintroduce auto-create, `gh auth` checks, or any `gh`-shell-out path — the user is responsible for setting up the public GitHub repo themselves. +- `metadata.repository` is written only when `--moddable` is explicitly opted in. ## Sentry Telemetry diff --git a/CHANGELOG.md b/CHANGELOG.md index 007b21c..a1eadc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ ### Minor Changes -- 10b2abf: `dot deploy --modable` no longer auto-creates a GitHub repository. The CLI now requires the user to set up a public GitHub `origin` themselves and fails with a clear message if `origin` is unset, points to a private repo, or points to a non-GitHub URL. The `--repo-name` flag is removed, the `gh` CLI dependency is dropped (no longer installed by `dot init`, no longer probed for authentication), and `dot mod` now initialises an empty git history without a baseline commit so users can stage and commit their first revision however they like. +- 10b2abf: `dot deploy --moddable` no longer auto-creates a GitHub repository. The CLI now requires the user to set up a public GitHub `origin` themselves and fails with a clear message if `origin` is unset, points to a private repo, or points to a non-GitHub URL. The `--repo-name` flag is removed, the `gh` CLI dependency is dropped (no longer installed by `dot init`, no longer probed for authentication), and `dot mod` now initialises an empty git history without a baseline commit so users can stage and commit their first revision however they like. ## 0.18.2 @@ -28,13 +28,13 @@ ### Minor Changes -- 82036ef: Eliminates every remaining `api.github.com` call from the unauthenticated path so `dot mod`, `dot deploy --modable`, and `dot update` no longer contribute to GitHub's 60 req/hour anonymous-IP rate limit. On shared networks (hackathon WiFi, conference NATs) the CLI now works regardless of how many other users are on the same public IP. +- 82036ef: Eliminates every remaining `api.github.com` call from the unauthenticated path so `dot mod`, `dot deploy --moddable`, and `dot update` no longer contribute to GitHub's 60 req/hour anonymous-IP rate limit. On shared networks (hackathon WiFi, conference NATs) the CLI now works regardless of how many other users are on the same public IP. - - `dot deploy --modable` writes the deploying branch to metadata as `meta.branch` (read via `git rev-parse --abbrev-ref HEAD`). `dot mod` reads that field and constructs the codeload tarball URL directly, skipping the previous `api.github.com/repos/{o}/{r}` lookup. Old apps without `meta.branch` fall back to `main`. + - `dot deploy --moddable` writes the deploying branch to metadata as `meta.branch` (read via `git rev-parse --abbrev-ref HEAD`). `dot mod` reads that field and constructs the codeload tarball URL directly, skipping the previous `api.github.com/repos/{o}/{r}` lookup. Old apps without `meta.branch` fall back to `main`. - `assertPublicGitHubRepo` now issues a `HEAD https://github.com/{o}/{r}` against the regular HTML page rather than the API. Same public/private signal (200 vs 404) at zero API quota cost. Anti-abuse limits on the HTML surface are orders of magnitude more generous. - `dot update` resolves the latest CLI version through jsDelivr's `/resolved` endpoint instead of `api.github.com/.../releases/latest`. The binary download stays on `github.com/.../releases/download/...` (also non-API). - The `gh auth token` opportunistic-header utility and the end-of-`dot init` rate-limit advisory banner are removed — both were workarounds for API quota issues that no longer exist on the unauthenticated path. `gh auth login` is still required for the one remaining authenticated call site (`gh repo create --public --push` when a fresh modable repo is created), and `dot init`'s dependency-list row continues to advise it. + The `gh auth token` opportunistic-header utility and the end-of-`dot init` rate-limit advisory banner are removed — both were workarounds for API quota issues that no longer exist on the unauthenticated path. `gh auth login` is still required for the one remaining authenticated call site (`gh repo create --public --push` when a fresh moddable repo is created), and `dot init`'s dependency-list row continues to advise it. `install.sh` is updated to resolve the latest tag through jsDelivr first (with the github.com `releases/latest` redirect probe as fallback) so concurrent first-time installs at a hackathon — every attendee on the same NAT — never touch `api.github.com` at all. The previous `api.github.com/repos/.../releases?per_page=1` fallback is removed entirely. @@ -44,9 +44,9 @@ - eb9760c: Every `dot` invocation now shows a one-line "Update available" banner at the bottom when a newer release exists. The check resolves the latest version through jsDelivr's free public CDN (not GitHub's rate-limited API) with a 1 s timeout, so a flaky network never delays the command. Suppressed in CI / piped output, when running `dot update` itself, and when `DOT_NO_UPDATE_CHECK=1`. - `dot mod` and `dot deploy --modable` now opportunistically pass an `Authorization: Bearer ` header read from `gh auth token` when available — logged-in users get GitHub's per-user 5000/hour quota instead of contributing to the shared 60/hour anonymous-IP quota that gets exhausted quickly on hackathon WiFi. Anonymous users continue to work as before. + `dot mod` and `dot deploy --moddable` now opportunistically pass an `Authorization: Bearer ` header read from `gh auth token` when available — logged-in users get GitHub's per-user 5000/hour quota instead of contributing to the shared 60/hour anonymous-IP quota that gets exhausted quickly on hackathon WiFi. Anonymous users continue to work as before. - `dot deploy --modable` now fails with an explicit "GitHub rate limit exceeded — run `gh auth login`" error when the public-repo preflight is denied by the rate limiter, instead of silently passing the check and risking a private repo being published as modable. Ambiguous 403s and transient 5xx responses still skip the check (unchanged). + `dot deploy --moddable` now fails with an explicit "GitHub rate limit exceeded — run `gh auth login`" error when the public-repo preflight is denied by the rate limiter, instead of silently passing the check and risking a private repo being published as moddable. Ambiguous 403s and transient 5xx responses still skip the check (unchanged). `dot init` ends with an explicit advisory banner (visually consistent with the new "Update available" banner) explaining the IP-based GitHub rate limit and recommending `gh auth login`, but only when the user is not currently authed. The single-row dependency-list warning was too terse to convey why this matters on hackathon / shared-network setups. @@ -58,7 +58,7 @@ ### Patch Changes -- 88d78d3: `dot deploy --modable` now rejects private GitHub repositories at preflight with a clear error message instead of silently failing later. `dot mod` also surfaces a more actionable error when it encounters a private or non-existent repository instead of the misleading "pin one in metadata.branch" hint. +- 88d78d3: `dot deploy --moddable` now rejects private GitHub repositories at preflight with a clear error message instead of silently failing later. `dot mod` also surfaces a more actionable error when it encounters a private or non-existent repository instead of the misleading "pin one in metadata.branch" hint. ## 0.16.16 @@ -174,7 +174,7 @@ ### Patch Changes -- 7151157: Avoid GitHub auth and `git push` during `dot deploy --modable` when the project already has an `origin`; the existing repository URL is recorded directly. +- 7151157: Avoid GitHub auth and `git push` during `dot deploy --moddable` when the project already has an `origin`; the existing repository URL is recorded directly. ## 0.15.3 @@ -237,9 +237,9 @@ - b9ec23b: `dot mod` now downloads source as a fresh project from GitHub via HTTPS — multiple mods of the same starter no longer collide via GitHub's one-fork-per-account limit. `git` and `gh` are no longer required to mod an app. - `dot deploy --playground` now asks before publishing source. Pass `--modable` (or answer "yes" to the prompt) to publish a public GitHub source repo alongside the deploy so others can `dot mod` it. Use `--no-modable` to skip the prompt non-interactively. The default is non-modable. Pass `--repo-name ` to skip the repo-name prompt when creating a fresh repo. + `dot deploy --playground` now asks before publishing source. Pass `--moddable` (or answer "yes" to the prompt) to publish a public GitHub source repo alongside the deploy so others can `dot mod` it. Use `--no-moddable` to skip the prompt non-interactively. The default is non-moddable. Pass `--repo-name ` to skip the repo-name prompt when creating a fresh repo. - The interactive registry picker (`dot mod` with no domain) now hides apps that aren't modable. + The interactive registry picker (`dot mod` with no domain) now hides apps that aren't moddable. Removed: `dot mod --clone`, `--repo-name`, `--yes` flags (no longer needed). diff --git a/CLAUDE.md b/CLAUDE.md index 195ba62..56f42e0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,9 +23,9 @@ These are things that aren't self-evident from reading the code and have bitten - **Throttle TUI info updates** — bulletin-deploy logs per-chunk and builds (vite/next) stream thousands of lines/sec. Calling `setState` on every log event floods React's reconciler with so much backpressure the process can balloon past 20 GB and freeze the OS. `RunningStage` coalesces "latest info" updates to ≤10/sec via a ref + timer and caps line length at 160 chars. Any new hot-path event sink should do the same; don't hook raw per-line streams directly into Ink state. - **Process-guard safety net** (`src/utils/process-guard.ts`) — deploy pipelines open several long-lived WebSockets + child processes and any one of them can keep the event loop alive after the TUI visibly finishes, turning `dot` into a zombie that accumulates retry buffers indefinitely (seen climbing past 25 GB). We defend in depth: (1) `installSignalHandlers()` catches SIGINT/TERM/HUP + `unhandledRejection` and forces cleanup + exit within 3 s; (2) `scheduleHardExit()` installs an `unref`'d timer that kills the process if the event loop doesn't drain within a grace period; (3) `startMemoryWatchdog()` aborts if RSS exceeds 4 GB — a generous cap because legit deploys on Bun SEA binaries routinely touch 1–1.5 GB from runtime-metadata decoding + Bun's JSC heap + Ink yoga. Do NOT re-add a per-window growth detector: we tried 300 MB / 3 s and it false-positived on the single-burst metadata-loading spike, aborting deploys that would have succeeded. Set `DOT_MEMORY_TRACE=1` to stream per-sample RSS/heap/external stats — useful when diagnosing a real leak report. **Telemetry bootstrap** (`src/bootstrap.ts`) is the FIRST import in `src/index.ts`. It sets `BULLETIN_DEPLOY_USE_AMBIENT_SENTRY=1` and `BULLETIN_DEPLOY_HOST_APP=playground-cli` before `bulletin-deploy` can evaluate, then maps `DOT_TELEMETRY`/internal-context detection to `BULLETIN_DEPLOY_TELEMETRY`. Do not leave `BULLETIN_DEPLOY_TELEMETRY` unset while setting the host app: `bulletin-deploy` treats `playground-cli` as an internal host, which would enable deploy telemetry for external users. `BULLETIN_DEPLOY_MEM_REPORT` is not forced off by default anymore because upstream guards the Bun-incompatible memory-report path. Any new long-running command should register a cleanup hook via `onProcessShutdown()`. - **Parser MUST NOT emit an event per log line.** `DeployLogParser.feed()` is called for every console line bulletin-deploy prints — hundreds per deploy on the happy path, thousands if retries fire. We intentionally emit events ONLY for phase-banner matches and `[N/M]` chunk progress. Everything else returns `null`. Adding a catch-all `info` emit turns the parser into a firehose that allocates ~200 bytes × thousands of lines, and was a measurable contributor to chunk-upload memory pressure. -- **`dot mod` is GitHub-tarball-only and must stay that way.** `src/utils/mod/source.ts` downloads from `codeload.github.com` (no auth, no `git`/`gh` required for the public-repo case) and extracts via `node:zlib` + the pure-JS `tar` package. Do NOT re-introduce `git clone` or `gh repo fork` paths — both would re-add a hard tooling requirement and the fork path was specifically removed because GitHub caps you to one fork per source-repo per account, which broke "mod the same starter twice." A non-modable app (no `metadata.repository`) returns a hard error from `dot mod`; the interactive picker filters those out so the user never sees an unmoddable option. The picker does NOT pre-probe each app's repo visibility, because that would burn the 60 req/hr anonymous GitHub API quota on every `dot mod`. Instead, `runModCommand` lazy-probes the picked app once via `assertPublicGitHubRepo()` between picker dismount and `SetupScreen` mount; `dot deploy --modable` already rejects private repos at deploy time, so this fires only when a publisher has flipped visibility post-publish. -- **`dot` never invokes `gh`.** `dot deploy --modable` reads an existing `origin`, validates it's a public GitHub URL via `HEAD https://github.com/{o}/{r}`, and records it in metadata. There is no auto-create path: no `gh` install, no `gh auth status` check, no `gh repo create`. Missing `origin`, private repos, and non-GitHub URLs all hard-fail with actionable messages from `src/utils/deploy/modable.ts::resolveRepositoryUrl()`. We deliberately do NOT add an interactive `gh auth login` handoff — Ink owns stdout + raw-mode stdin and a `stdio: "inherit"` child would race `useInput` for keystrokes. The user is expected to set up the public GitHub repo themselves before re-running. Do not re-introduce a `gh` dependency or any auto-create path: it tangles `dot` with one source-host's CLI, surprises users with public repos created on their account, and the interactive-auth handoff is a known footgun. -- **`metadata.repository` is set ONLY when `--modable` is opted in.** Older code in `publishToPlayground` would silently probe `git remote get-url origin` and stuff whatever it found into the metadata, which surprised users who didn't realise their fork was being advertised. The contract: `runDeploy` takes an explicit `repositoryUrl: string | null`, and `publishToPlayground` writes the field iff that param is non-null. The CLI command is responsible for resolving the URL upstream via `src/utils/deploy/modable.ts::resolveRepositoryUrl()`, which uses an existing public GitHub `origin` URL or fails — it never pushes or creates anything on behalf of the user. +- **`dot mod` is GitHub-tarball-only and must stay that way.** `src/utils/mod/source.ts` downloads from `codeload.github.com` (no auth, no `git`/`gh` required for the public-repo case) and extracts via `node:zlib` + the pure-JS `tar` package. Do NOT re-introduce `git clone` or `gh repo fork` paths — both would re-add a hard tooling requirement and the fork path was specifically removed because GitHub caps you to one fork per source-repo per account, which broke "mod the same starter twice." A non-moddable app (no `metadata.repository`) returns a hard error from `dot mod`; the interactive picker filters those out so the user never sees an unmoddable option. The picker does NOT pre-probe each app's repo visibility, because that would burn the 60 req/hr anonymous GitHub API quota on every `dot mod`. Instead, `runModCommand` lazy-probes the picked app once via `assertPublicGitHubRepo()` between picker dismount and `SetupScreen` mount; `dot deploy --moddable` already rejects private repos at deploy time, so this fires only when a publisher has flipped visibility post-publish. +- **`dot` never invokes `gh`.** `dot deploy --moddable` reads an existing `origin`, validates it's a public GitHub URL via `HEAD https://github.com/{o}/{r}`, and records it in metadata. There is no auto-create path: no `gh` install, no `gh auth status` check, no `gh repo create`. Missing `origin`, private repos, and non-GitHub URLs all hard-fail with actionable messages from `src/utils/deploy/moddable.ts::resolveRepositoryUrl()`. We deliberately do NOT add an interactive `gh auth login` handoff — Ink owns stdout + raw-mode stdin and a `stdio: "inherit"` child would race `useInput` for keystrokes. The user is expected to set up the public GitHub repo themselves before re-running. Do not re-introduce a `gh` dependency or any auto-create path: it tangles `dot` with one source-host's CLI, surprises users with public repos created on their account, and the interactive-auth handoff is a known footgun. +- **`metadata.repository` is set ONLY when `--moddable` is opted in.** Older code in `publishToPlayground` would silently probe `git remote get-url origin` and stuff whatever it found into the metadata, which surprised users who didn't realise their fork was being advertised. The contract: `runDeploy` takes an explicit `repositoryUrl: string | null`, and `publishToPlayground` writes the field iff that param is non-null. The CLI command is responsible for resolving the URL upstream via `src/utils/deploy/moddable.ts::resolveRepositoryUrl()`, which uses an existing public GitHub `origin` URL or fails — it never pushes or creates anything on behalf of the user. - **`startMemoryWatchdog()` runs for both `dot deploy` and `dot mod`.** Mod's tarball download is a streaming pipe through `node:zlib` + `tar.extract()`, and a stuck IPFS gateway or a malformed tarball can leak buffers. Same 4 GB cap, same worker-thread sampler. Any new top-level command that does meaningful I/O should also call `startMemoryWatchdog()` and register `stopWatchdog` via `onProcessShutdown()`. ## Repo conventions @@ -65,5 +65,5 @@ These are things that aren't self-evident from reading the code and have bitten - **CI report job name:** `E2E Report` — aggregates per-leg conclusions, posts a sticky PR comment with marker ``, opens an auto-issue on schedule/release fail. - **Running tests:** see `docs/e2e-running-tests.md` for the full guide — local modes, vitest passthrough, reading results, GitHub triggers, and common operations FAQ. - **Bootstrap:** see `docs/e2e-bootstrap.md` for the maintainer-facing setup + recovery procedures. The tool itself is `tools/register-e2e-fixtures.ts`. -- **Cleanup cron:** `.github/workflows/e2e-cleanup.yml` runs Sunday 04:00 UTC. Stub today; will sweep rotating modable state when Phase 5e ships. +- **Cleanup cron:** `.github/workflows/e2e-cleanup.yml` runs Sunday 04:00 UTC. Stub today; will sweep rotating moddable state when Phase 5e ships. - **Design spec:** `docs-internal/2026-05-02-e2e-test-suite-design.md`. diff --git a/README.md b/README.md index 03adc13..7a9e5a6 100644 --- a/README.md +++ b/README.md @@ -59,11 +59,11 @@ Flags: - `--no-contract-build` — skip the contract compile step (`forge build --resolc`, `cargo-contract build`, `npx hardhat compile`) and deploy pre-built artifacts. Requires `--contracts` and headless mode (i.e. all of `--signer`, `--domain`, `--buildDir`, `--playground`). - `--playground` — publish to the playground registry so the app appears under "my apps". Interactive prompt (default: no) if omitted. - `--private` — publish to the playground with private (owner-only) visibility. Requires `--playground`. Not interactively prompted; pass the flag to opt in. -- `--modable` / `--no-modable` — publish the source repo URL alongside the deploy so others can `dot mod` it. Requires `--playground`. Interactive prompt (default: no) if omitted. The CLI reads your existing `origin` and records its URL in the Bulletin metadata; it never creates a repo or pushes for you. The deploy fails with an actionable message if `origin` is unset, points to a private repo, or points to anything other than GitHub (since `dot mod` only fetches from `codeload.github.com`). Set up the repo yourself before re-running: create a public repo on GitHub, then `git remote add origin https://github.com//` followed by `git push -u origin main`. (If you happen to have `gh` installed, `gh repo create my-app --public --source=. --push` does both in one shot — `dot` does not require `gh`.) +- `--moddable` / `--no-moddable` — publish the source repo URL alongside the deploy so others can `dot mod` it. Requires `--playground`. Interactive prompt (default: no) if omitted. The CLI reads your existing `origin` and records its URL in the Bulletin metadata; it never creates a repo or pushes for you. The deploy fails with an actionable message if `origin` is unset, points to a private repo, or points to anything other than GitHub (since `dot mod` only fetches from `codeload.github.com`). Set up the repo yourself before re-running: create a public repo on GitHub, then `git remote add origin https://github.com//` followed by `git push -u origin main`. (If you happen to have `gh` installed, `gh repo create my-app --public --source=. --push` does both in one shot — `dot` does not require `gh`.) - `--suri ` — override signer with a dev secret URI (e.g. `//Alice`). Useful for CI. - `--env ` — `testnet` (default) or `mainnet` (not yet supported). -Passing all four of `--signer`, `--domain`, `--buildDir`, and `--playground` runs in fully non-interactive mode. Any absent flag is filled in by the TUI prompt. `--modable`, `--private`, and `--contracts` are independently optional in both modes — their absence means a non-modable, public, frontend-only deploy. +Passing all four of `--signer`, `--domain`, `--buildDir`, and `--playground` runs in fully non-interactive mode. Any absent flag is filled in by the TUI prompt. `--moddable`, `--private`, and `--contracts` are independently optional in both modes — their absence means a non-moddable, public, frontend-only deploy. **Requirement**: the `ipfs` CLI (Kubo) must be on `PATH`. `dot init` installs it; if you skipped init you can install it manually (`brew install ipfs` or follow [docs.ipfs.tech/install](https://docs.ipfs.tech/install/)). This is a temporary requirement while `bulletin-deploy`'s pure-JS merkleizer has a bug that makes the browser fallback unusable. @@ -76,18 +76,18 @@ For fully non-interactive (CI) runs, combine `--signer`, `--domain`, `--buildDir - `--suri //Alice` — required with `--signer dev` so the dev signer has a known keypair (works with any dev name or full BIP-39 mnemonic). - `--no-build` — reuse pre-built frontend assets in `--buildDir`. - `--contracts` + `--no-contract-build` — reuse pre-built contract artefacts in `out/` / `target/.release.polkavm` / `artifacts/contracts/` (skips `forge build --resolc`, `cargo-contract build`, or `npx hardhat compile`). -- `--no-modable` — explicitly skip source publishing even if `--modable` would otherwise apply. +- `--no-moddable` — explicitly skip source publishing even if `--moddable` would otherwise apply. - `--private` — publish to the playground with owner-only visibility. ### `dot mod` -Pull a modable playground app's source into a fresh local project so you can customise and re-deploy it. The interactive picker only shows apps that opted into modable at deploy time; non-modable apps surface a clear "this app is not modable" error if you target them by domain. +Pull a moddable playground app's source into a fresh local project so you can customise and re-deploy it. The interactive picker only shows apps that opted into moddable at deploy time; non-moddable apps surface a clear "this app is not moddable" error if you target them by domain. The implementation is GitHub-only and **requires no CLI tooling** — neither `git` nor `gh` is needed. Source is downloaded as a tarball over HTTPS from `codeload.github.com` (no auth needed for public repos), extracted into the target dir, then `git init`'d as a fresh empty history *if* `git` happens to be on `PATH`. No baseline commit is created, so you can stage and commit your first revision however you like. With `git` absent, the directory still works — you just don't get version control until you install git yourself. Flags: -- `[domain]` — positional; interactive picker over the registry if omitted. `.dot` suffix optional. The picker is filtered to modable apps only. +- `[domain]` — positional; interactive picker over the registry if omitted. `.dot` suffix optional. The picker is filtered to moddable apps only. - `--suri ` — dev signer secret URI (e.g. `//Alice`). The local directory name is auto-generated as `-<6 hex chars>` so repeated mods of the same starter never collide (unlike GitHub forks, which were limited to one per account per repo). diff --git a/docs/e2e-bootstrap.md b/docs/e2e-bootstrap.md index d14c9e0..ee0bb32 100644 --- a/docs/e2e-bootstrap.md +++ b/docs/e2e-bootstrap.md @@ -196,8 +196,8 @@ entries. ### Per-run rotating state (Phase 5e, not yet shipped) -When Phase 5e (`nightly-deploy-modable`) ships, each run will create: -- A `e2e-cli-modable-` registry domain +When Phase 5e (`nightly-deploy-moddable`) ships, each run will create: +- A `e2e-cli-moddable-` registry domain - A GitHub repo tagged `e2e-test-fixture` These are swept by `.github/workflows/e2e-cleanup.yml` (Sunday 04:00 UTC) diff --git a/docs/e2e-running-tests.md b/docs/e2e-running-tests.md index 0cfe63a..a3bf5f0 100644 --- a/docs/e2e-running-tests.md +++ b/docs/e2e-running-tests.md @@ -283,8 +283,8 @@ tools/e2e-local.sh -- e2e/cli/mod.test.ts **"The cleanup cron — what does it do?"** `.github/workflows/e2e-cleanup.yml` runs Sunday 04:00 UTC. It is currently a stub -— there is nothing to sweep until Phase 5e (modable deploy testing) ships. When -that lands, it will sweep `e2e-cli-modable-*` GH repos and registry domains older +— there is nothing to sweep until Phase 5e (moddable deploy testing) ships. When +that lands, it will sweep `e2e-cli-moddable-*` GH repos and registry domains older than 14 days. --- diff --git a/sentry/dashboards/2143100.json b/sentry/dashboards/2143100.json index ce38096..87bda7c 100644 --- a/sentry/dashboards/2143100.json +++ b/sentry/dashboards/2143100.json @@ -650,7 +650,7 @@ { "id": "451743", "title": "Deploy Options", - "description": "Deploy options matrix \u2014 combinations of mode/playground/modable/contracts. Useful to find a regression that only appears in one combination.", + "description": "Deploy options matrix \u2014 combinations of mode/playground/moddable/contracts. Useful to find a regression that only appears in one combination.", "displayType": "table", "thresholds": null, "interval": "5m", @@ -663,7 +663,7 @@ "fields": [ "cli.deploy.mode", "cli.deploy.playground", - "cli.deploy.modable", + "cli.deploy.moddable", "cli.deploy.contracts", "count()", "avg(span.duration)" @@ -675,7 +675,7 @@ "columns": [ "cli.deploy.mode", "cli.deploy.playground", - "cli.deploy.modable", + "cli.deploy.moddable", "cli.deploy.contracts" ], "fieldAliases": [], diff --git a/sentry/payloads/dashboard1-descriptions.json b/sentry/payloads/dashboard1-descriptions.json index b5251a2..5909fb8 100644 --- a/sentry/payloads/dashboard1-descriptions.json +++ b/sentry/payloads/dashboard1-descriptions.json @@ -23,7 +23,7 @@ {"op": "set_description", "widgetId": "451742", "value": "Phase breakdown for cli.deploy.*. Order by avg duration to find which phase is the bottleneck. Storage-and-DotNS dominates by design — chunk uploads + DotNS commit-reveal both span seconds."}, {"op": "set_description", "widgetId": "451743", - "value": "Deploy options matrix — combinations of mode/playground/modable/contracts. Useful to find a regression that only appears in one combination."}, + "value": "Deploy options matrix — combinations of mode/playground/moddable/contracts. Useful to find a regression that only appears in one combination."}, {"op": "set_description", "widgetId": "451744", "value": "Sub-phase breakdown of the playground publish — metadata upload vs registry contract publish. Registry publish has a 3-attempt retry loop; high counts here without proportional warning events suggest the retry path is not hitting captureWarning."}, {"op": "set_description", "widgetId": "451745", diff --git a/src/commands/deploy/DeployScreen.test.ts b/src/commands/deploy/DeployScreen.test.ts index 3a141e4..12feb4b 100644 --- a/src/commands/deploy/DeployScreen.test.ts +++ b/src/commands/deploy/DeployScreen.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { pickNextStage } from "./DeployScreen.js"; describe("pickNextStage", () => { - it("continues past modable preflight once a repository URL is resolved", () => { + it("continues past moddable preflight once a repository URL is resolved", () => { expect( pickNextStage( false, @@ -17,9 +17,9 @@ describe("pickNextStage", () => { ).toEqual({ kind: "confirm" }); }); - it("enters modable preflight when modable is true and no repository URL is resolved yet", () => { + it("enters moddable preflight when moddable is true and no repository URL is resolved yet", () => { expect( pickNextStage(false, "phone", "dist", "tw33d3r.dot", true, false, true, null), - ).toEqual({ kind: "modable-preflight" }); + ).toEqual({ kind: "moddable-preflight" }); }); }); diff --git a/src/commands/deploy/DeployScreen.tsx b/src/commands/deploy/DeployScreen.tsx index 04b9539..ee1e6be 100644 --- a/src/commands/deploy/DeployScreen.tsx +++ b/src/commands/deploy/DeployScreen.tsx @@ -44,7 +44,7 @@ import type { ResolvedSigner } from "../../utils/signer.js"; import type { ContractsType } from "../../utils/build/detect.js"; import { DEFAULT_BUILD_DIR } from "../../config.js"; import { VERSION_LABEL } from "../../utils/version.js"; -import { ensureGitInstalled, resolveRepositoryUrl } from "../../utils/deploy/modable.js"; +import { ensureGitInstalled, resolveRepositoryUrl } from "../../utils/deploy/moddable.js"; export interface DeployScreenInputs { projectDir: string; @@ -59,8 +59,8 @@ export interface DeployScreenInputs { contractsType: ContractsType | null; /** Whether to deploy the project's contracts. null = ask the user. */ deployContracts: boolean | null; - /** Pre-set modable from `--modable` / `--no-modable`. null = ask. */ - modable: boolean | null; + /** Pre-set moddable from `--moddable` / `--no-moddable`. null = ask. */ + moddable: boolean | null; userSigner: ResolvedSigner | null; onDone: (outcome: DeployOutcome | null) => void; } @@ -72,9 +72,9 @@ export type Stage = | { kind: "prompt-domain" } | { kind: "validate-domain"; domain: string } | { kind: "prompt-publish" } - | { kind: "prompt-modable" } - | { kind: "modable-preflight" } - | { kind: "modable-error"; message: string } + | { kind: "prompt-moddable" } + | { kind: "moddable-preflight" } + | { kind: "moddable-error"; message: string } | { kind: "prompt-contracts" } | { kind: "confirm" } | { kind: "running" } @@ -88,7 +88,7 @@ interface Resolved { publishToPlayground: boolean; skipBuild: boolean; deployContracts: boolean; - modable: boolean; + moddable: boolean; repositoryUrl: string | null; } @@ -102,7 +102,7 @@ export function DeployScreen({ skipBuild: initialSkipBuild, contractsType, deployContracts: initialDeployContracts, - modable: initialModable, + moddable: initialModdable, userSigner, onDone, }: DeployScreenInputs) { @@ -115,7 +115,7 @@ export function DeployScreen({ const [deployContracts, setDeployContracts] = useState( contractsType === null ? false : initialDeployContracts, ); - const [modable, setModable] = useState(initialModable); + const [moddable, setModdable] = useState(initialModdable); const [repositoryUrl, setRepositoryUrl] = useState(null); const [domainError, setDomainError] = useState(null); // Captured from the availability check; feeds `resolveSignerSetup` so @@ -130,7 +130,7 @@ export function DeployScreen({ initialDomain, initialPublish, contractsType === null ? false : initialDeployContracts, - initialModable, + initialModdable, null, ), ); @@ -147,7 +147,7 @@ export function DeployScreen({ nextDomain: string | null = domain, nextPublish: boolean | null = publishToPlayground, nextDeployContracts: boolean | null = deployContracts, - nextModable: boolean | null = modable, + nextModdable: boolean | null = moddable, nextRepoUrl: string | null = repositoryUrl, ) => { const s = pickNextStage( @@ -157,7 +157,7 @@ export function DeployScreen({ nextDomain, nextPublish, nextDeployContracts, - nextModable, + nextModdable, nextRepoUrl, ); setStage(s); @@ -171,7 +171,7 @@ export function DeployScreen({ publishToPlayground === null || skipBuild === null || deployContracts === null || - modable === null + moddable === null ) return null; return { @@ -181,7 +181,7 @@ export function DeployScreen({ publishToPlayground, skipBuild, deployContracts, - modable, + moddable, repositoryUrl, }; }, [ @@ -191,7 +191,7 @@ export function DeployScreen({ publishToPlayground, skipBuild, deployContracts, - modable, + moddable, repositoryUrl, ]); @@ -306,7 +306,7 @@ export function DeployScreen({ initialIndex={0} onSelect={(yes) => { setPublishToPlayground(yes); - if (!yes) setModable(false); + if (!yes) setModdable(false); advance( skipBuild, mode, @@ -314,15 +314,15 @@ export function DeployScreen({ domain, yes, deployContracts, - yes ? modable : false, + yes ? moddable : false, ); }} /> )} - {stage.kind === "prompt-modable" && ( + {stage.kind === "prompt-moddable" && ( - label="make this app modable? (anyone in the playground can dot mod it)" + label="make this app moddable? (anyone in the playground can dot mod it)" options={[ { value: false, @@ -333,9 +333,9 @@ export function DeployScreen({ ]} initialIndex={0} onSelect={(yes) => { - setModable(yes); + setModdable(yes); if (yes) { - setStage({ kind: "modable-preflight" }); + setStage({ kind: "moddable-preflight" }); } else { advance( skipBuild, @@ -351,8 +351,8 @@ export function DeployScreen({ /> )} - {stage.kind === "modable-preflight" && ( - { setRepositoryUrl(url); @@ -368,13 +368,13 @@ export function DeployScreen({ ); }} onError={(msg) => { - setStage({ kind: "modable-error", message: msg }); + setStage({ kind: "moddable-error", message: msg }); }} /> )} - {stage.kind === "modable-error" && ( - onDone(null)} /> + {stage.kind === "moddable-error" && ( + onDone(null)} /> )} {stage.kind === "prompt-contracts" && contractsType !== null && ( @@ -456,7 +456,7 @@ function pickInitialStage( domain: string | null, publish: boolean | null, deployContracts: boolean | null, - modable: boolean | null, + moddable: boolean | null, repositoryUrl: string | null, ): Stage { return pickNextStage( @@ -466,7 +466,7 @@ function pickInitialStage( domain, publish, deployContracts, - modable, + moddable, repositoryUrl, ); } @@ -478,7 +478,7 @@ export function pickNextStage( domain: string | null, publish: boolean | null, deployContracts: boolean | null, - modable: boolean | null, + moddable: boolean | null, repositoryUrl: string | null, ): Stage { if (skipBuild === null) return { kind: "prompt-build" }; @@ -486,18 +486,18 @@ export function pickNextStage( if (buildDir === null) return { kind: "prompt-buildDir" }; if (domain === null) return { kind: "prompt-domain" }; if (publish === null) return { kind: "prompt-publish" }; - if (publish && modable === null) return { kind: "prompt-modable" }; - // --modable=true via flag: skip the prompt and drive into the preflight. - if (publish && modable === true && repositoryUrl === null) { - return { kind: "modable-preflight" }; + if (publish && moddable === null) return { kind: "prompt-moddable" }; + // --moddable=true via flag: skip the prompt and drive into the preflight. + if (publish && moddable === true && repositoryUrl === null) { + return { kind: "moddable-preflight" }; } if (deployContracts === null) return { kind: "prompt-contracts" }; return { kind: "confirm" }; } -// ── Modable preflight ──────────────────────────────────────────────────────── +// ── Moddable preflight ──────────────────────────────────────────────────────── -function ModablePreflightStage({ +function ModdablePreflightStage({ projectDir, onResolved, onError, @@ -543,19 +543,19 @@ function ModablePreflightStage({ } /** - * Formal warning stage shown when the modable preflight cannot proceed — + * Formal warning stage shown when the moddable preflight cannot proceed — * almost always because the user hasn't set up a public GitHub `origin` yet. * Renders the actionable error inside a yellow Callout (matching the * "check your phone" banner) so it visually registers as a setup requirement * rather than a deploy crash. Pressing Enter or Esc exits the deploy. */ -function ModableErrorStage({ message, onExit }: { message: string; onExit: () => void }) { +function ModdableErrorStage({ message, onExit }: { message: string; onExit: () => void }) { useInput((_input, key) => { if (key.return || key.escape) onExit(); }); return ( - + {message} @@ -709,7 +709,7 @@ function ConfirmStage({ buildDir: inputs.buildDir, skipBuild: inputs.skipBuild, publishToPlayground: inputs.publishToPlayground, - modable: inputs.modable, + moddable: inputs.moddable, repositoryUrl: inputs.repositoryUrl, approvals: "approvals" in setup ? setup.approvals : [], contracts: contractsType @@ -883,7 +883,7 @@ function RunningStage({ mode: inputs.mode, publishToPlayground: inputs.publishToPlayground, playgroundPrivate, - modable: inputs.modable, + moddable: inputs.moddable, repositoryUrl: inputs.repositoryUrl, deployContracts: inputs.deployContracts, contractsFundingNeeded: diff --git a/src/commands/deploy/index.ts b/src/commands/deploy/index.ts index 6e629e6..f5eb80b 100644 --- a/src/commands/deploy/index.ts +++ b/src/commands/deploy/index.ts @@ -26,7 +26,7 @@ import { loadDetectInput } from "../../utils/build/runner.js"; import { readSessionAccount, SESSION_MIN_BALANCE } from "../../utils/deploy/session-account.js"; import { checkBalance } from "../../utils/account/funding.js"; import { DEFAULT_BUILD_DIR, type Env } from "../../config.js"; -import { ensureGitInstalled, resolveRepositoryUrl } from "../../utils/deploy/modable.js"; +import { ensureGitInstalled, resolveRepositoryUrl } from "../../utils/deploy/moddable.js"; interface DeployOpts { suri?: string; @@ -51,8 +51,8 @@ interface DeployOpts { contractBuild?: boolean; /** Deploy the project's contracts alongside the frontend. Defaults to false. */ contracts?: boolean; - /** Publish the source repo so others can `dot mod` it. Commander auto-negates: `--no-modable` ⇒ false. */ - modable?: boolean; + /** Publish the source repo so others can `dot mod` it. Commander auto-negates: `--no-moddable` ⇒ false. */ + moddable?: boolean; env?: Env; /** Project root. Hidden — defaults to cwd. */ dir?: string; @@ -83,10 +83,10 @@ export const deployCommand = new Command("deploy") "Publish to the playground with private visibility (owner-only). Requires --playground.", ) .option( - "--modable", + "--moddable", "Publish the source repo so others can `dot mod` it. Requires --playground and a public GitHub `origin`.", ) - .option("--no-modable", "Explicitly skip publishing source (the default).") + .option("--no-moddable", "Explicitly skip publishing source (the default).") .option("--suri ", "Secret URI for the user signer (e.g. //Alice for dev)") .addOption( new Option("--env ", "Target environment") @@ -311,18 +311,18 @@ async function runHeadless(ctx: { } process.stdout.write(`✔ ${formatAvailability(availability)}\n`); - const modable = ctx.opts.modable === true; + const moddable = ctx.opts.moddable === true; let repositoryUrl: string | null = null; - if (modable) { + if (moddable) { if (!publishToPlayground) { throw new Error( - "--modable requires --playground (no metadata is published without it).", + "--moddable requires --playground (no metadata is published without it).", ); } repositoryUrl = await withSpan( - "cli.deploy.modable", - "prepare modable repository", + "cli.deploy.moddable", + "prepare moddable repository", async () => { await ensureGitInstalled(); return resolveRepositoryUrl({ @@ -357,7 +357,7 @@ async function runHeadless(ctx: { buildDir, skipBuild, publishToPlayground, - modable, + moddable, repositoryUrl, approvals: setup.approvals, }); @@ -369,7 +369,7 @@ async function runHeadless(ctx: { { "cli.deploy.mode": mode, "cli.deploy.playground": publishToPlayground ? "true" : "false", - "cli.deploy.modable": modable ? "true" : "false", + "cli.deploy.moddable": moddable ? "true" : "false", "cli.deploy.contracts": deployContracts ? "true" : "false", }, () => @@ -381,7 +381,7 @@ async function runHeadless(ctx: { mode, publishToPlayground, playgroundPrivate: Boolean(ctx.opts.private), - modable, + moddable, repositoryUrl, deployContracts, skipContractBuild, @@ -450,8 +450,8 @@ function runInteractive(ctx: { skipBuild: ctx.opts.build === false ? true : null, contractsType, deployContracts: ctx.opts.contracts !== undefined ? ctx.opts.contracts : null, - modable: - ctx.opts.modable === true ? true : ctx.opts.modable === false ? false : null, + moddable: + ctx.opts.moddable === true ? true : ctx.opts.moddable === false ? false : null, userSigner: ctx.userSigner, onDone: (outcome: DeployOutcome | null) => { if (settled) return; diff --git a/src/commands/deploy/summary.ts b/src/commands/deploy/summary.ts index 58134f7..823dcd1 100644 --- a/src/commands/deploy/summary.ts +++ b/src/commands/deploy/summary.ts @@ -13,7 +13,7 @@ export interface SummaryInputs { buildDir: string; skipBuild: boolean; publishToPlayground: boolean; - modable?: boolean; + moddable?: boolean; repositoryUrl?: string | null; approvals: DeployApproval[]; /** Contract project kind + user's yes/no. Omit when no contracts were detected. */ @@ -44,8 +44,8 @@ export function buildSummaryView(input: SummaryInputs): SummaryView { ]; if (input.publishToPlayground) { rows.push({ - label: "Modable", - value: input.modable ? `yes — ${input.repositoryUrl}` : "no", + label: "Moddable", + value: input.moddable ? `yes — ${input.repositoryUrl}` : "no", }); } if (input.contracts) { diff --git a/src/commands/mod/AppBrowser.tsx b/src/commands/mod/AppBrowser.tsx index 5bbb43c..1c13212 100644 --- a/src/commands/mod/AppBrowser.tsx +++ b/src/commands/mod/AppBrowser.tsx @@ -3,14 +3,14 @@ import { Box, Text, useInput, useStdout } from "ink"; import { getGateway, fetchJson } from "@polkadot-apps/bulletin"; import { Mark, Hint, COLOR } from "../../utils/ui/theme/index.js"; -import { filterModable, type AppEntry } from "./browserFilter.js"; +import { filterModdable, type AppEntry } from "./browserFilter.js"; export type { AppEntry }; interface Props { registry: any; onSelect: (app: AppEntry) => void; onCancel?: () => void; - modableOnly?: boolean; + moddableOnly?: boolean; } const BATCH = 10; @@ -20,7 +20,7 @@ function pad(s: string, w: number): string { return s.length > w ? s.slice(0, w - 1) + "…" : s.padEnd(w); } -export function AppBrowser({ registry, onSelect, onCancel, modableOnly }: Props) { +export function AppBrowser({ registry, onSelect, onCancel, moddableOnly }: Props) { const { stdout } = useStdout(); const viewH = Math.max((stdout?.rows ?? 24) - 6, 5); @@ -115,7 +115,7 @@ export function AppBrowser({ registry, onSelect, onCancel, modableOnly }: Props) loadBatch(0); }, [loadBatch]); - const filtered = filterModable(apps, Boolean(modableOnly)); + const filtered = filterModdable(apps, Boolean(moddableOnly)); useEffect(() => { if (cursor >= filtered.length - 3 && nextStart.current !== null && !fetching) { @@ -181,13 +181,13 @@ export function AppBrowser({ registry, onSelect, onCancel, modableOnly }: Props) )} {!fetching && filtered.length === 0 && nextStart.current === null && ( - No modable apps in the registry yet. + No moddable apps in the registry yet. )} {`↑↓ navigate · ⏎ select · q quit · ${ - modableOnly - ? `(${filtered.length} modable, ${apps.length}/${total} scanned)` + moddableOnly + ? `(${filtered.length} moddable, ${apps.length}/${total} scanned)` : `(${apps.length}/${total})` }`} diff --git a/src/commands/mod/SetupScreen.tsx b/src/commands/mod/SetupScreen.tsx index 59b1b49..f350e19 100644 --- a/src/commands/mod/SetupScreen.tsx +++ b/src/commands/mod/SetupScreen.tsx @@ -65,7 +65,7 @@ export function SetupScreen({ domain, metadata: initial, registry, targetDir, on const repoUrl = meta.repository; if (!repoUrl) throw new Error( - `App "${domain}" is not modable — no source repository published.`, + `App "${domain}" is not moddable — no source repository published.`, ); const ref = parseGitHubRepoUrl(repoUrl); if (!ref) { @@ -73,7 +73,7 @@ export function SetupScreen({ domain, metadata: initial, registry, targetDir, on `Only GitHub-hosted source is supported for dot mod today (got ${repoUrl}).`, ); } - // `meta.branch` is written by `dot deploy --modable` from + // `meta.branch` is written by `dot deploy --moddable` from // `git rev-parse --abbrev-ref HEAD` at deploy time. The "main" // fallback handles the rare case of an old deploy that // pre-dates the metadata field — codeload returns 404 for a diff --git a/src/commands/mod/browserFilter.test.ts b/src/commands/mod/browserFilter.test.ts index af3ec04..d16bc1c 100644 --- a/src/commands/mod/browserFilter.test.ts +++ b/src/commands/mod/browserFilter.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { filterModable, type AppEntry } from "./browserFilter.js"; +import { filterModdable, type AppEntry } from "./browserFilter.js"; const make = (domain: string, repository: string | null): AppEntry => ({ domain, @@ -10,20 +10,20 @@ const make = (domain: string, repository: string | null): AppEntry => ({ tag: null, }); -describe("filterModable", () => { - it("hides entries without a repository when modableOnly is true", () => { +describe("filterModdable", () => { + it("hides entries without a repository when moddableOnly is true", () => { const apps = [make("a.dot", "https://github.com/x/a"), make("b.dot", null)]; - expect(filterModable(apps, true)).toEqual([apps[0]]); + expect(filterModdable(apps, true)).toEqual([apps[0]]); }); - it("returns everything when modableOnly is false", () => { + it("returns everything when moddableOnly is false", () => { const apps = [make("a.dot", "https://github.com/x/a"), make("b.dot", null)]; - expect(filterModable(apps, false)).toEqual(apps); + expect(filterModdable(apps, false)).toEqual(apps); }); - it("treats empty-string repository as non-modable", () => { + it("treats empty-string repository as non-moddable", () => { const apps = [make("a.dot", "")]; - expect(filterModable(apps, true)).toEqual([]); + expect(filterModdable(apps, true)).toEqual([]); }); it("preserves order", () => { @@ -32,6 +32,6 @@ describe("filterModable", () => { make("b.dot", null), make("c.dot", "https://github.com/x/c"), ]; - expect(filterModable(apps, true)).toEqual([apps[0], apps[2]]); + expect(filterModdable(apps, true)).toEqual([apps[0], apps[2]]); }); }); diff --git a/src/commands/mod/browserFilter.ts b/src/commands/mod/browserFilter.ts index 9257502..709024f 100644 --- a/src/commands/mod/browserFilter.ts +++ b/src/commands/mod/browserFilter.ts @@ -14,7 +14,7 @@ export interface AppEntry { tag: string | null; } -export function filterModable(apps: AppEntry[], modableOnly: boolean): AppEntry[] { - if (!modableOnly) return apps; +export function filterModdable(apps: AppEntry[], moddableOnly: boolean): AppEntry[] { + if (!moddableOnly) return apps; return apps.filter((a) => Boolean(a.repository)); } diff --git a/src/commands/mod/index.ts b/src/commands/mod/index.ts index c5b3ffe..cdc2068 100644 --- a/src/commands/mod/index.ts +++ b/src/commands/mod/index.ts @@ -10,7 +10,7 @@ import { AppBrowser, type AppEntry } from "./AppBrowser.js"; import { SetupScreen } from "./SetupScreen.js"; import { defaultRepoName } from "../../utils/git/repoName.js"; import { runCliCommand } from "../../cli-runtime.js"; -import { assertPublicGitHubRepo, ModablePreflightError } from "../../utils/deploy/modable.js"; +import { assertPublicGitHubRepo, ModdablePreflightError } from "../../utils/deploy/moddable.js"; export const modCommand = new Command("mod") .description("Mod a playground app — clone the source as a fresh project to customise") @@ -46,7 +46,7 @@ async function runModCommand( if (rawDomain) { domain = rawDomain.endsWith(".dot") ? rawDomain : `${rawDomain}.dot`; } else { - const picked = await withSpan("cli.mod.browse", "browse modable apps", () => + const picked = await withSpan("cli.mod.browse", "browse moddable apps", () => browseAndPick(registry), ); if (!picked) { @@ -79,7 +79,7 @@ async function runModCommand( () => assertPublicGitHubRepo(repoUrl), ); } catch (err) { - if (err instanceof ModablePreflightError) { + if (err instanceof ModdablePreflightError) { console.error(); console.error(` ${err.message}.`); console.error( @@ -149,7 +149,7 @@ function browseAndPick(registry: any): Promise { const app = render( React.createElement(AppBrowser, { registry, - modableOnly: true, + moddableOnly: true, onSelect: (selected: AppEntry) => { app.unmount(); resolve(selected); diff --git a/src/telemetry.test.ts b/src/telemetry.test.ts index ffadf66..c1c8209 100644 --- a/src/telemetry.test.ts +++ b/src/telemetry.test.ts @@ -36,16 +36,16 @@ describe("expected CLI errors", () => { expect(isExpectedCliError("Bulletin storage allowance is exhausted")).toBe(true); expect( isExpectedCliError( - "--modable: no GitHub origin configured. Create a public GitHub repository…", + "--moddable: no GitHub origin configured. Create a public GitHub repository…", ), ).toBe(true); expect( isExpectedCliError( - "modable apps must use a public GitHub repository (got: https://gitlab.com/foo/bar)", + "moddable apps must use a public GitHub repository (got: https://gitlab.com/foo/bar)", ), ).toBe(true); expect( - isExpectedCliError("foo/bar is private or does not exist — modable apps must use…"), + isExpectedCliError("foo/bar is private or does not exist — moddable apps must use…"), ).toBe(true); expect(isExpectedCliError('Invalid domain "bad_domain"')).toBe(true); expect(isExpectedCliError("No foundry/hardhat/cdm project was detected")).toBe(true); diff --git a/src/utils/deploy/modable.test.ts b/src/utils/deploy/moddable.test.ts similarity index 91% rename from src/utils/deploy/modable.test.ts rename to src/utils/deploy/moddable.test.ts index 9ea6f7f..457fe7e 100644 --- a/src/utils/deploy/modable.test.ts +++ b/src/utils/deploy/moddable.test.ts @@ -3,7 +3,11 @@ import { execFileSync } from "node:child_process"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { resolveRepositoryUrl, assertPublicGitHubRepo, ModablePreflightError } from "./modable.js"; +import { + resolveRepositoryUrl, + assertPublicGitHubRepo, + ModdablePreflightError, +} from "./moddable.js"; describe("assertPublicGitHubRepo", () => { // After the rate-limit-elimination work this function probes the regular @@ -73,7 +77,7 @@ describe("resolveRepositoryUrl", () => { const privateFetch: typeof fetch = async () => new Response("Not Found", { status: 404 }); it("returns the existing origin when it points to a public GitHub repo", async () => { - tmp = mkdtempSync(join(tmpdir(), "pg-modable-origin-")); + tmp = mkdtempSync(join(tmpdir(), "pg-moddable-origin-")); execFileSync("git", ["init"], { cwd: tmp, stdio: "ignore" }); execFileSync("git", ["remote", "add", "origin", "git@github.com:foo/bar.git"], { cwd: tmp, @@ -86,7 +90,7 @@ describe("resolveRepositoryUrl", () => { }); it("throws when the existing origin is a private GitHub repo", async () => { - tmp = mkdtempSync(join(tmpdir(), "pg-modable-private-")); + tmp = mkdtempSync(join(tmpdir(), "pg-moddable-private-")); execFileSync("git", ["init"], { cwd: tmp, stdio: "ignore" }); execFileSync("git", ["remote", "add", "origin", "https://github.com/org/secret.git"], { cwd: tmp, @@ -94,12 +98,12 @@ describe("resolveRepositoryUrl", () => { }); await expect(resolveRepositoryUrl({ cwd: tmp, fetch: privateFetch })).rejects.toThrow( - ModablePreflightError, + ModdablePreflightError, ); }); it("throws when the existing origin is non-GitHub", async () => { - tmp = mkdtempSync(join(tmpdir(), "pg-modable-gitlab-")); + tmp = mkdtempSync(join(tmpdir(), "pg-moddable-gitlab-")); execFileSync("git", ["init"], { cwd: tmp, stdio: "ignore" }); execFileSync("git", ["remote", "add", "origin", "https://gitlab.com/foo/bar"], { cwd: tmp, @@ -112,7 +116,7 @@ describe("resolveRepositoryUrl", () => { }); it("throws with an actionable message when no origin is set", async () => { - tmp = mkdtempSync(join(tmpdir(), "pg-modable-no-origin-")); + tmp = mkdtempSync(join(tmpdir(), "pg-moddable-no-origin-")); execFileSync("git", ["init"], { cwd: tmp, stdio: "ignore" }); await expect(resolveRepositoryUrl({ cwd: tmp, fetch: publicFetch })).rejects.toThrow( diff --git a/src/utils/deploy/modable.ts b/src/utils/deploy/moddable.ts similarity index 82% rename from src/utils/deploy/modable.ts rename to src/utils/deploy/moddable.ts index a3069a2..938bf53 100644 --- a/src/utils/deploy/modable.ts +++ b/src/utils/deploy/moddable.ts @@ -1,5 +1,5 @@ /** - * `dot deploy --modable` preflight: resolves the public GitHub URL we'll + * `dot deploy --moddable` preflight: resolves the public GitHub URL we'll * record in the Bulletin metadata. * * The contract is intentionally narrow: the user is responsible for setting @@ -13,12 +13,12 @@ import { execFileSync } from "node:child_process"; import { commandExists, TOOL_STEPS } from "../toolchain.js"; import { parseGitHubRepoUrl } from "../mod/source.js"; -export class ModablePreflightError extends Error {} +export class ModdablePreflightError extends Error {} export async function ensureGitInstalled(onLog?: (line: string) => void): Promise { if (await commandExists("git")) return; const step = TOOL_STEPS.find((s) => s.name === "git"); - if (!step) throw new ModablePreflightError("internal: git step missing from TOOL_STEPS"); + if (!step) throw new ModdablePreflightError("internal: git step missing from TOOL_STEPS"); await step.install(onLog); } @@ -42,17 +42,17 @@ export interface ResolveRepoOptions { } const NO_ORIGIN_MESSAGE = - "--modable: no GitHub origin configured. Create a public GitHub repository, " + + "--moddable: no GitHub origin configured. Create a public GitHub repository, " + "commit and push your code, set it as `origin` (e.g. `git remote add origin " + "https://github.com//` followed by `git push -u origin main`), " + - "and re-run. Pass --no-modable to skip publishing source."; + "and re-run. Pass --no-moddable to skip publishing source."; /** * Verifies that a repository URL is a publicly accessible GitHub repo. * * - Non-GitHub URLs (GitLab, Bitbucket, self-hosted, anything `parseGitHubRepoUrl` * refuses) hard-fail. `dot mod` only fetches from `codeload.github.com`, so a - * non-GitHub URL would publish a "modable" app that nobody can actually mod. + * non-GitHub URL would publish a "moddable" app that nobody can actually mod. * - For GitHub URLs we issue a `HEAD https://github.com/{owner}/{repo}` against * the regular HTML page rather than `api.github.com/repos/{owner}/{repo}` — * the HTML surface is NOT subject to the 60/hour anonymous-IP API rate limit @@ -70,8 +70,8 @@ const NO_ORIGIN_MESSAGE = export async function assertPublicGitHubRepo(url: string, f: typeof fetch = fetch): Promise { const ref = parseGitHubRepoUrl(url); if (!ref) { - throw new ModablePreflightError( - `modable apps must use a public GitHub repository (got: ${url})`, + throw new ModdablePreflightError( + `moddable apps must use a public GitHub repository (got: ${url})`, ); } @@ -85,8 +85,8 @@ export async function assertPublicGitHubRepo(url: string, f: typeof fetch = fetc if (res.ok) return; if (res.status === 404) { - throw new ModablePreflightError( - `${ref.owner}/${ref.repo} is private or does not exist — modable apps must use a public repository`, + throw new ModdablePreflightError( + `${ref.owner}/${ref.repo} is private or does not exist — moddable apps must use a public repository`, ); } // 5xx, 403 anti-abuse, etc. — skip and let the downstream codeload @@ -96,7 +96,7 @@ export async function assertPublicGitHubRepo(url: string, f: typeof fetch = fetc export async function resolveRepositoryUrl(opts: ResolveRepoOptions): Promise { const f = opts.fetch ?? fetch; const origin = readOrigin(opts.cwd); - if (!origin) throw new ModablePreflightError(NO_ORIGIN_MESSAGE); + if (!origin) throw new ModdablePreflightError(NO_ORIGIN_MESSAGE); const normalised = origin.replace(/\.git$/, ""); opts.onLog?.(`using existing origin (${normalised})…`); await assertPublicGitHubRepo(normalised, f); diff --git a/src/utils/deploy/run.ts b/src/utils/deploy/run.ts index ed6ffc1..3a73215 100644 --- a/src/utils/deploy/run.ts +++ b/src/utils/deploy/run.ts @@ -76,9 +76,9 @@ export interface RunDeployOptions { publishToPlayground: boolean; /** Publish to the playground with private visibility (owner-only). Ignored when `publishToPlayground` is false. */ playgroundPrivate?: boolean; - /** Whether the deploy should publish source as modable. */ - modable?: boolean; - /** Resolved public repository URL to record in metadata (modable=true) or `null` (modable=false). */ + /** Whether the deploy should publish source as moddable. */ + moddable?: boolean; + /** Resolved public repository URL to record in metadata (moddable=true) or `null` (moddable=false). */ repositoryUrl?: string | null; /** Compile + deploy foundry/hardhat/cdm contracts alongside the frontend. */ deployContracts?: boolean; diff --git a/src/utils/mod/git-baseline.ts b/src/utils/mod/git-baseline.ts index 0b27603..3fb3986 100644 --- a/src/utils/mod/git-baseline.ts +++ b/src/utils/mod/git-baseline.ts @@ -8,7 +8,7 @@ type Log = (line: string) => void; * user can start tracking changes immediately. We deliberately do NOT create * a baseline commit — that would require `user.name`/`user.email` to be * configured globally, and the user is going to commit + push to their own - * GitHub repo anyway as part of the `dot deploy --modable` workflow. + * GitHub repo anyway as part of the `dot deploy --moddable` workflow. * * `git init` is purely local: no network, no auth, no GitHub credentials. * If `git` is not on PATH we just log and continue — the directory still diff --git a/src/utils/version-check.test.ts b/src/utils/version-check.test.ts index 3b6fc28..601b15c 100644 --- a/src/utils/version-check.test.ts +++ b/src/utils/version-check.test.ts @@ -94,7 +94,7 @@ describe("shouldSkip", () => { it("does not skip on a normal command", () => { expect(shouldSkip(["init"], baseEnv, true)).toBe(false); - expect(shouldSkip(["deploy", "--modable"], baseEnv, true)).toBe(false); + expect(shouldSkip(["deploy", "--moddable"], baseEnv, true)).toBe(false); }); }); diff --git a/src/utils/version-check.ts b/src/utils/version-check.ts index cb58a9f..9d94f5e 100644 --- a/src/utils/version-check.ts +++ b/src/utils/version-check.ts @@ -5,7 +5,7 @@ * (`data.jsdelivr.com/v1/packages/gh///resolved`) instead of the * GitHub releases API — jsDelivr is rate-limit-effectively-unlimited for our * scale and isn't shared with the GitHub anonymous-IP quota that `dot mod` - * and `dot deploy --modable` already chip away at on hackathon WiFi. + * and `dot deploy --moddable` already chip away at on hackathon WiFi. * * No on-disk cache: the call is fire-and-forget on command start with a 1 s * `AbortSignal.timeout`, so an unreachable jsDelivr cannot delay exit. diff --git a/tools/list-registry-apps.ts b/tools/list-registry-apps.ts index 5d95a0c..84e4b19 100644 --- a/tools/list-registry-apps.ts +++ b/tools/list-registry-apps.ts @@ -40,9 +40,9 @@ async function probePublicGithub(repoUrl: string): Promise<{ ok: boolean; status const [, owner, repo] = match; // HEAD on the HTML page rather than a GET on `api.github.com` — same // 200/404 signal, but doesn't consume GitHub's 60/hour anonymous-IP - // API quota. Mirrors `src/utils/deploy/modable.ts::assertPublicGitHubRepo` + // API quota. Mirrors `src/utils/deploy/moddable.ts::assertPublicGitHubRepo` // so a maintainer running this tool burns the same kind of probe their - // CLI does at modable-preflight time. + // CLI does at moddable-preflight time. const res = await fetch(`https://github.com/${owner}/${repo}`, { method: "HEAD" }); return { ok: res.ok, status: res.status }; }