Skip to content

EdgeZero CLI Extensions: extensible CLI, multi-store manifest, auth/provision/config#269

Draft
aram356 wants to merge 40 commits into
chore/strict-clippyfrom
feature/extensible-cli
Draft

EdgeZero CLI Extensions: extensible CLI, multi-store manifest, auth/provision/config#269
aram356 wants to merge 40 commits into
chore/strict-clippyfrom
feature/extensible-cli

Conversation

@aram356
Copy link
Copy Markdown
Contributor

@aram356 aram356 commented May 20, 2026

Epic: #268

Summary

Turns edgezero-cli into an extensible library, rewrites the manifest store schema and runtime to a multi-store model, adds auth / provision / config validate / config push commands, replaces the dev subcommand with a contributor-only demo, and updates app-demo to exercise everything across axum / cloudflare / fastly / spin.

Delivered as one PR, eight sequential stages (one per sub-issue of #268). Each stage may span multiple commits — this branch currently carries the design spec, the implementation plan, and Stage 1.

Stages (sub-issues of #268)

# Sub-issue Stage Status
1 #260 Extensible edgezero-cli library + generator + app-demo-cli skeleton ✅ Landed
2 #261 Manifest + runtime rewrite (atomic, all four adapters) ⏳ Blocked on #253
3 #262 App-config schema, derive macro, env-overlay loader ⏸ Pending
4 #263 config validate command ⏸ Pending
5 #264 auth command + CommandRunner infrastructure ⏸ Pending
6 #265 provision command ⏸ Pending
7 #266 config push command ⏸ Pending
8 #267 app-demo integration polish + docs audit ⏸ Pending

Stages 3–8 build on the StoreRegistry / Bound*Store shapes finalized in Stage 2, so they are sequenced behind it.

Stage 1 — landed

  • edgezero-cli is now a library + binary; downstream projects build their own CLI binary reusing the built-in commands via edgezero_cli::args::* and run_build / run_deploy / run_serve / run_new.
  • The generator scaffolds a <name>-cli crate; app-demo-cli is the in-tree reference downstream CLI.
  • The dev subcommand is replaced by demo, which runs the bundled app-demo example via the axum adapter's run_app. demo is contributor-only — gated on the demo-example feature (formerly dev-example) and absent from generated / downstream CLIs. Generated projects run locally with edgezero serve --adapter axum.
  • NewArgs / BuildArgs / DeployArgs / ServeArgs derive Default and are #[non_exhaustive] for downstream stability.

Design + plan

Committed under docs/superpowers/ (force-added — that path is currently in .gitignore; maintainers can decide whether to formally track it):

  • docs/superpowers/specs/2026-05-19-cli-extensions-design.md
  • docs/superpowers/plans/2026-05-20-cli-extensions.md

Test plan

  • Stage 1: existing edgezero commands unchanged; app-demo-cli --help lists the built-ins; edgezero new scaffold builds and is lint-clean; full gate green
  • Stage 2: multi-store handlers work on all four adapters; async config reads; wasm32 spin gate green
  • Stages 3–7: per-command unit + mock-runner tests
  • Stage 8: app-demo end-to-end loop on axum in CI; docs audit passes
  • All five CI gates green (fmt, clippy -D warnings, cargo test, feature cargo check, wasm32 spin)

aram356 added 20 commits May 19, 2026 16:03
Sub-project #1 of 7 in the CLI extensions roadmap. Turns edgezero-cli into
lib + bin, exposes per-command Args structs and run_* functions for
downstream projects to compose their own CLIs via clap subcommand
flattening, and adds app-demo-cli as the canonical consumer.

Force-added because docs/superpowers/ is gitignored project-wide for plans;
this spec is shared design intent and meant to be reviewed in the repo.
Replaces the sub-project-#1-only spec with a single design document that
covers the full effort: extensible edgezero-cli library, generator updates
for <name>-cli and <name>.toml scaffolding, per-service typed app-config
schema with validator integration, four new commands (auth, provision,
config validate, config push), shell-out mocking via a private
CommandRunner trait, and the app-demo overhaul that exercises everything
end-to-end.

Implementation still ships in 7 incremental PRs but the design decisions
live in one place so reviewers see the whole picture.

Force-added because docs/superpowers/ is gitignored project-wide.
High-severity fixes:
- Add --manifest to ProvisionArgs and ConfigPushArgs (matches validate)
- Update Wrangler invocations to 3.60+ syntax (space-form, --namespace-id)
- Persist provisioned IDs in edgezero.toml [stores.*.adapters.<x>].id;
  cross-write to per-adapter manifests where deploys need them
- Mermaid diagram in §3 replacing ASCII art

Medium-severity fixes:
- config push runs strict validation as pre-flight (no separate flag)
- Move --adapter to each AuthSub variant so UX is `auth login --adapter X`
- Constrain typed config push to serde_json::to_value(C) -> Object;
  document flatten / rename / skip / Option::None handling
- Unify raw + typed serialization rules; raw drops Validate + secret skip
- Replace CommandRunner positional args with CommandSpec struct
  (program, args, cwd, stdin, env)
- "Backwards-compatible" language replacing "unchanged" for default bin
- Move walkthrough doc to docs/guide/ with explicit sidebar update

Low + open questions:
- Document consumer-facing Cargo feature names and adapter opt-outs
- Generator migration note: sub-project 1 outputs don't auto-migrate
- Deprecate [stores.config.defaults] in favor of <name>.toml [config]
- Mark Spin provision / config push as "not yet supported" with pointer
  to the in-flight Spin stores PR; clear error message until then

Secret annotation:
- New §6.6 documenting #[derive(AppConfig)] from edgezero-macros
- #[secret] field attribute marks runtime-secret-store-backed fields
- Toml value for those fields is the secret-store binding name
- config validate (typed) cross-checks the binding appears in [stores.secrets]
- config push (typed) skips SECRET_FIELDS entirely

The implementation still ships in 7 incremental PRs.
…cope

Manifest schema rewrite (new sub-projects #2 and #3):
- [stores.<kind>].ids = [...] + default declare the logical stores the
  app uses (kv, secrets, config all multi-store)
- [adapters.<X>.stores.<kind>.<id>].name = "..." maps each logical id to
  the platform-specific name on adapter X, with optional adapter-specific
  tuning fields stored as free-form extras
- Provisioned platform resource IDs (Cloudflare namespace ID, Fastly
  store ID) live in each platform's native manifest (wrangler.toml,
  fastly.toml), not in edgezero.toml. provision writes them there;
  config push reads them back.
- RequestContext store accessors become id-keyed:
  ctx.kv_store("id") / ctx.kv_store_default() (and similarly for
  config_store / secret_store). Each adapter builds a StoreRegistry<H>
  at request setup from [adapters.<self>.stores.*].
- Manifest validator enforces: ids non-empty; default in ids;
  every adapter has a name mapping for every id.

Naming:
- Field on the per-adapter block is `name` (matches the user's example),
  not `binding`. The Cloudflare wrangler.toml term `binding` is now
  called out as wrangler's terminology, not ours.

Secret references (§6.7):
- The string a #[secret] field holds is an app-defined reference; the
  spec documents both valid runtime patterns (logical store id or key
  within the default secret store). Validate just confirms the string
  is non-empty and that the app has a secret store available.

config validate (§11) explicitly covers app-config validation:
- TOML syntax, [config] table presence, type matching against C,
  serde-rejected unknown fields, validator business rules, non-empty
  secret references, and the manifest-side cross-checks.

Sub-project count: 7 → 9 (added schema rewrite + RequestContext API
rewrite as #2 and #3; existing app-config/validate/auth/provision/push/
polish become #4-#9).

This is a breaking change to the on-disk manifest schema; the in-tree
example/app-demo is migrated as part of the work, and a migration guide
ships with sub-project #2.
…cret forms

HIGH severity fixes:
- Cloudflare config store rewritten from [vars] to KV (§6.9) so
  `config push` actually reaches the runtime without redeploying.
  Lands in sub-project #3 alongside the rest of the runtime work.
- Sub-project #2 is now purely additive on the schema: no runtime
  changes, no removal of [stores.config.defaults]. The runtime bridge
  and the defaults removal move out of #2 (into #3 and #9 respectively).
- Spin completeness: validator skips adapters without an
  [adapters.<X>.stores] section. App-demo's Spin adapter omits stores
  until the in-flight Spin stores PR lands.
- Extractor design (§6.8): existing Kv / Secrets extractors keep
  working as default-store accessors; new KvNamed<const ID> /
  SecretsNamed<const ID> extractors give type-safe named access. No
  handler-facing break.
- Hooks, ConfigStoreMetadata, and app! macro added to sub-project #3
  scope; they all become id-keyed. Multi-store rewrite is now complete.

MEDIUM severity fixes:
- Validate bound is DeserializeOwned + Validate + AppConfigMeta (no
  Serialize). The serde_json::to_value object check is push-only;
  push adds Serialize.
- Secret semantics: two explicit forms via attribute. #[secret] = key
  inside the default secret store. #[secret(store_ref)] = logical store
  id in [stores.secrets].ids. Validate cross-checks the latter.
- AppConfigMeta::SECRET_FIELDS is now &'static [SecretField] carrying
  SecretKind so the CLI can apply the right validation per field.
- #[secret] constrained to non-flattened, non-renamed scalar fields;
  combinations with #[serde(flatten)] / rename / skip produce compile
  errors. Macro tests cover the constraints.
- Unknown-field rejection is no longer a validate guarantee; the
  generator template emits #[serde(deny_unknown_fields)] on the
  generated config struct so new projects opt in by default.
- Every public *Args derives Default + #[non_exhaustive]; external
  construction documented as Default + field mutation.

LOW severity fixes:
- Macro example fixed: #[proc_macro_derive(AppConfig, attributes(
  secret))] in edgezero-macros/src/lib.rs directly. No bogus _impl
  re-export.
- Cloudflare-invalid JS-identifier `name` values are errors (would
  break worker deploy), not warnings.

Sub-project ordering and risk:
- #2 risk dropped to L (purely additive).
- #3 grows to absorb Cloudflare KV swap + Hooks/macro/extractor.
- #9 now also drops [stores.config.defaults] and wires axum dev-server
  to seed from <name>.toml.
HIGH severity fixes:
- ConfigStore::get becomes async (#[async_trait(?Send)]). Cloudflare
  config moves [vars] -> KV with real async reads. Cascade (trait, 3
  adapter impls, Hooks, handlers, extractors) contained to #3.
- Drop const-generic &'static str extractors (don't compile on stable
  1.95). Kv / Secrets extractors refactored to yield a registry handle
  with default() / named(id) accessors.
- Introduce BoundKvStore / BoundConfigStore / BoundSecretStore so
  runtime accessors return a handle bound to the resolved platform
  name; callers just .get(key).await.
- Sub-project #2 models logical store declarations as
  Option<LogicalStoreConfig> so old-shape manifests (None) are
  distinguishable from new-but-incomplete ones (Some with empty ids).
  Keeps #2 genuinely additive.

MEDIUM severity fixes:
- Fastly native-manifest writeback: spec commits to a read/write-path-
  agreement contract; exact fastly.toml sections pinned in #7's plan.
- Adapter store completeness uses an explicit
  STORES_SUPPORTED_ADAPTERS allowlist (axum, cloudflare, fastly). A
  supported adapter omitting [adapters.<X>.stores] is an error; only
  non-allowlisted adapters (spin) skip.
- All "default store" prose uses the resolved default id (explicit
  default, else single ids[0]).
- AuthArgs no longer derives Default (avoids a placeholder subcommand
  leaking into a real auth path). §6.11 documents which *Args get
  Default.
- config push gains explicit "validate passes, push serialization
  fails" test scenarios (non-object typed config, compound shapes,
  skip_serializing_if, Option::None, flatten).

LOW severity:
- Ship-gate wording: existing commands stay backwards-compatible
  rather than "edgezero --help unchanged" (false once auth/provision/
  config land).

New requirement - environment-variable override resolution (§6.10):
- load_app_config overlays env vars on the toml [config] table.
- Env var format: <APP_NAME>__<SECTION>__..__<KEY>; __ separates every
  nesting level; APP_NAME is [app].name uppercased, hyphens to
  underscores.
- Type coercion against the target TOML type; --no-env escape hatch on
  validate and push.

app-demo (§15) now explicitly exercises every new capability: multi-
store, async config, named-kv extractor, nested config section, env
override, both secret forms, validate/push, auth/provision via mock.
…n, Fastly contract

HIGH severity fixes:
- Manifest old-vs-new discrimination corrected. Existing manifests
  already have [stores.kv/secrets/config] tables, so table-presence
  can't discriminate. Sub-project #2 now uses compatibility structs
  carrying legacy fields (name, legacy adapters) plus new logical
  fields (ids, default) side by side; the discriminator is
  ids.is_some(). The current app-demo edgezero.toml parses unchanged.
- Hooks cannot return bound handles. Hooks / ConfigStoreMetadata are
  static compile-time app metadata; bound handles need per-request
  adapter state. Split: Hooks/app! emit store metadata registries;
  only RequestContext returns Bound*Store handles. Adapters consume
  the metadata at request setup to build the runtime registries.
- Env overlay type coercion: with C: DeserializeOwned there is no
  pre-deserialization type reflection. Env vars now override existing
  keys only, coerced to the existing TOML value's type. Matches the
  current AxumConfigStore::from_env behavior. To make a key
  env-overridable it must appear in <name>.toml.
- Axum config push and runtime read agreed: the axum config store is
  backed by .edgezero/local-config-<id>.json; config push --adapter
  axum writes that file; edgezero dev regenerates it at startup. No
  more disagreement between push target and dev-server source.

MEDIUM severity fixes:
- Fastly writeback contract made concrete from Fastly's docs:
  [setup.<kind>_stores.<name>] + [local_server.<kind>_stores.<name>]
  keyed by resource link name (== our `name`). provision creates the
  store and ensures both fastly.toml sections exist; config push
  resolves the store id on demand via `fastly config-store list
  --json` (Fastly has no stable persisted id slot). Read/write paths
  all key off [adapters.fastly.stores.<kind>.<id>].name.
- Env key matching is deterministic and ambiguity-rejecting: keys
  transform to an env segment form (uppercase); two siblings mapping
  to the same segment is an AppConfigError. No case-insensitive fuzzy
  fallback.
- Cloudflare KV eventual consistency: §6.9 no longer claims values are
  live "on the next request"; CI does not assert immediate global
  Cloudflare visibility.

LOW severity:
- BoundSecretStore keeps the existing bytes::Bytes API (get ->
  Option<Bytes>, require_str), not Vec<u8>.
…e-PR delivery

Hard cutoff (per user directive — projects fully migrated, no compat):
- Removed all old-vs-new manifest discrimination: no compat structs,
  no ids.is_some() check, no legacy-field parsing. The store schema is
  rewritten outright. Legacy fields (name, legacy adapters overrides,
  [stores.config.defaults]) are hard load errors pointing at the
  migration guide.

Spin as a first-class store-capable adapter (PR #253 baseline):
- Removed the "Spin deferred" non-goal. Spin participates fully.
- New §6.7 Spin store semantics: KV is label-backed multi-store with a
  max_list_keys cap; config and secrets are both spin_sdk::variables —
  a single flat namespace, lowercase [a-z0-9_] keys, no dots.
- Replaced the flat STORES_SUPPORTED_ADAPTERS allowlist with an
  adapter x kind capability matrix (Multi vs Single). Validation: if
  any target adapter is Single for a kind, [stores.<kind>].ids must
  have exactly one id (you cannot have two config stores if you also
  target Spin).
- §6.4 config key model: nested config flattens to dotted keys;
  canonical handler form is dotted; Spin config store translates
  . -> __ internally; config push writes platform-native key form.
- Spin wired into commit 2 (runtime registry, async ConfigStore now
  cascades across all FOUR adapters), commit 6 (provision: spin.toml
  writeback for key_value_stores / [variables] /
  [component.<name>.variables]), commit 7 (config push: Spin variables
  in spin.toml).
- provision now has explicit axum (no-op, prints local-store note) and
  spin (manifest writeback, no CommandRunner) contracts; config push
  is split per adapter — no universal native-resource-ID assumption.

Other review fixes:
- Default resolution made strict: `default` required when ids.len() > 1.
- Docs config path corrected to docs/.vitepress/config.mts (not .ts).

Delivery: one PR with eight commits (one per sub-project), not eight
PRs. CI gates the PR head; each commit should still build for
bisectability. Sub-project count stays at 8 (manifest+runtime stay
merged as the atomic commit 2).
Nine findings against the current (f0aed20) spec, all Spin-integration
depth:

- Spin provision cannot know config/secret variable keys (manifest has
  store ids, not field keys). Fix: Spin provision does KV-label
  spin.toml writeback ONLY. Config-variable declaration moves to
  config push (which loads <name>.toml). Secret-variable declaration
  is manual.
- config push --adapter spin must write BOTH [variables] (declaration
  + default) and [component.<name>.variables] (binding) — a Spin
  variable is unreadable without the component binding. Errors rather
  than writing a half-configured manifest.
- Spin component discovery specified: parse spin.toml; single component
  resolves implicitly; multi-component requires
  [adapters.spin.adapter].component; config validate --strict surfaces
  failures early.
- Secret variables are not inferable (#[secret(store_ref)] runtime keys
  are code-local). Spin secret variables are declared manually by the
  developer; the CLI never writes them.
- Config/secret namespace collision guarantee was wrong: #[secret]
  field VALUES (not Rust field names) are the secret keys. config
  validate now computes the effective Spin variable set
  ({flattened config keys} u {#[secret] values}) and errors on
  duplicates.
- Spin KV TTL: BoundKvStore exposes put_*_with_ttl (verified in
  key_value_store.rs). On Spin these return a deterministic
  KvError::Unsupported, never silent store-without-expiry.
- Spin KV listing-cap error variant flagged as an open reconciliation
  point with PR #253 (Validation -> a limit/server error); resolved in
  commit 2, not a blocker.
- Single (adapter, kind) per-id mapping blocks are now FORBIDDEN
  (validation error), not "accepted but vestigial". Fixes the §1 vs
  §6.6 contradiction.
- Spin variable naming rule pinned as Spin's own ^[a-z][a-z0-9_]*$
  (cites spinframework.dev/manifest-reference), not an EdgeZero rule.

app-demo (§15) updated: manually declares Spin secret variables,
single-component spin.toml, asserts Spin provision writes only
key_value_stores and config push writes both spin.toml tables.
Seventh-pass review fixes (against 27a6169):
- KvError::Unsupported does not exist today — spec now states commit 2
  adds the variant with a 5xx-class EdgeError mapping (Spin TTL writes).
- Spin listing-cap error resolved in-spec, not left open: commit 2 adds
  KvError::LimitExceeded (5xx-class), and the Spin listing path returns
  it past max_list_keys, replacing PR #253's KvError::Validation.
- run_dev() -> ! corrected: the dev server may return. Now
  run_demo() -> Result<(), String>; commit 1 adjusts the dev-server
  boundary (today it returns ()).
- Commit 2 bisectability: added a config-seeding story — the axum
  config store's backing-file contract lands in commit 2, but commit-2
  tests seed the .edgezero/local-config-<id>.json fixture directly;
  config push / demo-regeneration that produce the file land in
  commits 7/8.
- Spin config/secret collision check clarified as typed-only
  (needs AppConfigMeta::SECRET_FIELDS); raw validation does the
  key-syntax and component-discovery checks but not the collision
  check, and says so in its diagnostics.
- Spin variable-name rule kept pinned to spinframework.dev docs.

dev → demo subcommand rename (per user):
- The subcommand that runs the example app locally on axum is now
  `demo`; `dev` is reserved for a future dev-workflow command.
- run_dev → run_demo, Command::Dev → Command::Demo, the CLI's
  dev_server module → demo_server. The edgezero-adapter-axum crate's
  own internal dev_server module is left as-is (not user-facing).

Documentation update step (per user):
- New §6.12 makes documentation part of every commit's
  definition-of-done, with a page→commit ownership table
  (cli-reference, configuration, kv, handlers, getting-started,
  adapters/cloudflare, adapters/overview, architecture).
- Commit 8 ends with a documentation audit: grep docs/ for stale
  references (old manifest keys, dev subcommand, old store API),
  confirm none remain, confirm the .vitepress/config.mts sidebar is
  complete, docs CI green.
- Commit 2 bisectability vs AppDemoConfig: §8 now states commit 2's
  app-demo handler migration is store-accessor-only (ctx.kv_store(id),
  config_store, the refactored extractors). AppDemoConfig and any
  typed-app-config handler work are commit 3 — commit 2 never
  references a type that lands in commit 3.
- #[secret(store_ref)] vs Single-secrets capability: §6.8 spells out
  that axum/cloudflare/spin are all Single for secrets, so any app
  including one of them has exactly one secrets id, and every
  #[secret(store_ref)] field must resolve to it. store_ref only buys
  multiple secret stores on a Fastly-only project. §15 / the
  walkthrough show this for the all-four-adapter app-demo.
- Spin variable-name rule drift guard: commit 7 gets a golden-file
  test on the generated spin.toml — asserts every variable name
  matches ^[a-z][a-z0-9_]*$ and that the generated manifest parses
  (round-trips through the same parser the runtime uses), so the rule
  cannot drift from Spin's actual manifest behaviour.

Reviewer confirms no blocking design issues remain.
- Spin manifest validation strength: the spin.toml golden test now
  specifies a strongest-first ladder — (1) the spin CLI's own manifest
  validation when present (the wasm32 spin CI job already installs it),
  (2) a spin_sdk validation entry point if exposed, (3) toml + regex
  as the weakest acceptable fallback. The regex is the floor, not the
  ceiling; real Spin validation is preferred wherever reachable.
- Generated template vs app-demo example made explicit: `edgezero new`
  scaffolds the common case — greeting, nested service section, a
  single plain #[secret] — and deliberately does NOT include
  #[secret(store_ref)] (a commented line shows how to add it).
  store_ref only helps Fastly-only projects, so it should not be the
  default in every fresh scaffold. app-demo remains the full-capability
  showcase that exercises both secret forms.
- Commit 2 flagged as the explicit review hotspot in §16: the atomic
  manifest+runtime rewrite warrants the most reviewer attention; its
  per-adapter contract tests are the primary mitigation and should be
  reviewed alongside the code.

Reviewer confirms no blocking issues; spec is implementation-ready.
- Spin config-push --dry-run never mutates: plan Task 8.1 and spec §15
  reworded — dry-run PRINTS the would-be both-table content and the
  test asserts spin.toml is unchanged on disk. (The real push writing
  both tables is covered by commit 7's non-dry-run tests.)
- Spin `component` field location: it belongs on the
  [adapters.<x>.adapter] definition struct (with `crate`/`manifest`),
  not the top-level ManifestAdapter — otherwise the accepted TOML
  would wrongly be [adapters.spin] component = ...
- load_app_config API made consistent: AppConfigLoadOptions
  { env_overlay } struct; simple load_app_config / _raw apply the
  overlay (default); load_app_config_with_options / _raw_with_options
  take the struct; --no-env calls the _with_options form with
  env_overlay: false. No hidden bool param. Updated spec §4 + §6.10
  and plan Tasks 3.1 / 3.3.
- Axum multi-KV path rule: one redb file per logical id, file stem
  from [adapters.axum.stores.kv.<id>].name -> .edgezero/kv-<name>.redb.
  Prevents multi-store collapsing into one backing file.
- Generator manual check: stop assuming the project lands in CWD or
  /tmp/throwaway; generate into an explicit mktemp dir via --dir.
- Removed references to a non-existent crates/edgezero-core/src/
  hooks.rs — Hooks + ConfigStoreMetadata both live in app.rs.
- Macro compile-fail tests: Task 3.2 now adds `trybuild = "1"` to
  edgezero-macros [dev-dependencies] explicitly (only `tempfile` was
  there), with a tests/ui/*.rs fixture + .stderr golden per rejected
  case.
- External-consumer test env guard: tests/lib_consumer.rs must restore
  EDGEZERO_MANIFEST via an RAII EnvOverride guard and stay a single
  #[test] (no in-binary parallelism); a shared Mutex guard is required
  if more env-touching tests are ever added.
- WASM contract test commands pinned: Task 2.7 step 6 names the exact
  target / features / runner per adapter (cloudflare wasm32-unknown-
  unknown + wasm-bindgen; fastly wasm32-wasip1 + Viceroy; spin
  wasm32-wasip1 + Wasmtime), deferring to test.yml as source of truth.
- app-demo e2e lifecycle: Task 8.1/8.2 now require an ephemeral port
  (no hard-coded 8787), a readiness poll (no bare sleep), and RAII
  teardown that kills the demo server even on assertion failure;
  the loop is preferably a Rust integration test, not shell-in-YAML.
…ign gate

- Default `edgezero` binary wiring (High): commits 4-7 now have explicit
  steps to add Auth / Provision / Config(Validate|Push) to the default
  edgezero-cli `Command` enum and `main.rs` dispatch (raw run_* — the
  default binary has no app struct), with `edgezero --help` / parse
  tests. Previously only the original five commands and app-demo-cli
  were wired; the spec requires the new subcommands on the default
  binary too. New Task 4.2 covers `config`; Task 5.2/6.1/7.2 extended.
- Generated `<name>-cli` template upgrade (Medium): new Task 8.2 updates
  templates/cli/src/main.rs.hbs to the full eight-command set once
  auth/provision/config exist, wiring the scaffold's config arm to the
  typed functions with the generated project's config struct. Generator
  test asserts it.
- Full-gate alignment (Medium): added a canonical "## The full gate"
  section with the exact five CI commands from CLAUDE.md / the
  workflows (cargo check uses --features "fastly cloudflare spin", not
  --all-features). Every "run the full gate" step references it; fixed
  the commit-1 and commit-8 gate steps and the Codebase-facts CI line
  that had drifted to --all-features.

Commit-8 tasks renumbered (8.2 CI wiring -> 8.3; walkthrough/audit -> 8.4).
- app-demo-cli missing app-demo-core dep (High): Task 4.3 now adds
  `app-demo-core = { path = "../app-demo-core" }` to
  app-demo-cli/Cargo.toml — it references AppDemoConfig once typed
  `config validate` / `config push` are wired, but its deps were only
  edgezero-cli/clap/log.
- Generated <name>-cli template missing core-crate dep (High): Task 8.2
  now also updates templates/cli/Cargo.toml.hbs to depend on
  `{{name}}-core` (path dep), and the generator test asserts the
  scaffold builds with that dependency and resolves the typed config
  type.
- AppConfig macro + validator availability (Medium): chosen route
  stated explicitly — `edgezero-core` re-exports the `AppConfig` derive
  (matching the existing `action`/`app` re-exports), so a config crate
  needs only `edgezero-core` for the macro, no direct edgezero-macros
  dep. Task 3.4 updates templates/core/Cargo.toml.hbs to add
  `validator` (with derive); Task 3.5 verifies app-demo-core already
  carries edgezero-core + validator + serde. Generator test checks the
  scaffolded core crate builds.

Task 3.4 / 4.3 / 8.2 steps renumbered to fit the inserted dependency
steps.
- Generated config type placeholder (Medium): Task 3.4 step 1 now
  explicitly adds a `NameUpperCamel` key to the generator Handlebars
  context (derived from `name`: split on -/_, upper-case each segment,
  join — `my-app` -> `MyApp`), with a unit test. Templates reference
  `{{NameUpperCamel}}Config`; the key was previously unset (generator
  data only had name/proj_core/proj_core_mod/proj_mod).
- validator workspace-dep plumbing (Medium/Low): Task 3.4 step 3 now
  names the generator change explicitly — `templates/core/Cargo.toml.hbs`
  uses `validator = { workspace = true }`, so `validator` must also be
  added to the generator's workspace-dependency seed
  (`seed_workspace_dependencies` in generator.rs), which omits it today.
- Duplicate Step 4 in Task 3.4 (Low): Task 3.4 renumbered cleanly to
  Steps 1-6.
…ident

- Generated CLI import (Medium): the cli template's `use` must
  reference the core crate's Rust module name, not the package name.
  `use {{name}}_core::...` renders `my-app_core` for `my-app` (invalid
  Rust). Task 8.2 now uses `{{proj_core_mod}}` — the hyphen-to-
  underscore module form the generator already exposes.
- NameUpperCamel validity (Medium/Low): Task 3.4 step 1 derivation now
  guarantees a valid Rust type identifier — derive from the sanitized
  crate name, drop empty segments (absorbs a leading `_`), and prefix
  with `App` when the result would start with a non-letter (digit-
  leading project names). Unit test covers `123-app` -> `App123App`,
  `_foo` -> `Foo`, etc.
- **Refactored `Kv` / `Secrets` / `Config` extractors** resolving the
default store or a named one (§6.8).
- Platform credential and resource management (`auth`, `provision`)
shelling out to each platform's native CLI, wrapped in a mockable
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little skeptical if shelling out to the platform cli is the right choice or not.

I'm sure initial implementation is slightly simpler, but there are costs.

  • The cli user will also have to download the platform cli, not a big deal but worth noting.

  • output from clis may not be easily parseable if there's no json option. where as apis will always be parseable.

may be okay for a first pass and review if it's worth a tight api based implementation after.

aram356 added 9 commits May 20, 2026 14:12
Turn edgezero-cli into lib + bin so downstream projects can build their
own CLI binary reusing any subset of the built-in commands.

- Promote Command variant fields into standalone #[derive(clap::Args)]
  structs (BuildArgs / DeployArgs / ServeArgs; NewArgs already standalone),
  each #[non_exhaustive] + Default for external construction.
- Add src/lib.rs exposing the public API: run_build / run_deploy /
  run_serve / run_new / run_demo, init_cli_logger, and the args module
  (pub mod, not pub use — restriction lint). main.rs becomes a thin
  wrapper over the library.
- Rename the `dev` subcommand to `demo` (dev is reserved for a future
  dev-workflow command): dev_server.rs -> demo_server.rs, run_dev ->
  run_demo (now Result<(), String>), Command::Dev -> Command::Demo.
- Extend the generator to scaffold a crates/<name>-cli crate from new
  templates/cli/ Handlebars templates; seed clap + edgezero-cli as
  workspace dependencies; add crates/<name>-cli to the workspace members.
- Add the handwritten examples/app-demo/crates/app-demo-cli crate as the
  canonical downstream consumer, with a --help smoke test.
- Add crates/edgezero-cli/tests/lib_consumer.rs: external-consumer
  integration test proving the public API is usable from outside.
- Docs: cli-reference.md (demo rename + "Building Your Own CLI"),
  getting-started.md, CLAUDE.md.

All gates green: fmt, clippy -D warnings, cargo test --workspace,
feature cargo check, spin wasm32; app-demo workspace fmt/clippy/test.
After the dev->demo rename, `demo` should mean "run the bundled example",
not "run the project's axum adapter". Drop `try_run_manifest_axum` (and
its `load_manifest_optional` helper) from `demo_server`: `edgezero demo`
now always starts the built-in example server on 127.0.0.1:8787 and never
reads `edgezero.toml`.

`edgezero serve --adapter axum` is now the single, unambiguous way to run
a project's axum adapter (it runs `[adapters.axum.commands].serve`). This
removes the demo / serve --adapter axum behavioral overlap. Docs updated.
…mit 2

- Commit 1 marked DONE (landed 1d582dd + follow-up 06f4b72) with a
  Status section, so workers don't redo already-landed work.
- Working-branch reference corrected: feature/extensible-cli (was the
  stale docs/extensible-cli-library-spec).
- app-demo edgezero-cli dep path fixed to ../../crates/edgezero-cli
  (relative to the workspace manifest; the four-up path was wrong and
  would break the demo workspace).
- Task 2.7 Fastly step expanded from one line to explicit per-kind
  registry steps + contract tests: Fastly is Multi for KV/config/
  secrets, two logical stores per kind, per-id name resolution,
  id-keyed contract coverage under Viceroy — parity with the
  cloudflare/spin acceptance criteria.
Commit 1 shipped `pub mod args` rather than crate-root re-exports: a
root `pub use args::{...}` trips clippy::pub_use (the restriction group
is -D-denied workspace-wide). §4 now documents the supported API as
edgezero_cli::args::BuildArgs etc., with run_* staying at the crate
root, and updates every run_* signature to &args::<T>. Matches what
1d582dd actually exposes and what lib_consumer.rs / cli-reference.md
already use.

(Reviewer's second finding — demo overlapping serve --adapter axum in
1d582dd — was already resolved by 06f4b72; no action.)
…e/extensible-cli

# Conflicts:
#	crates/edgezero-cli/src/dev_server.rs
`edgezero demo` now delegates to `edgezero_adapter_axum::dev_server::run_app`,
running the bundled app-demo example the same way its own axum adapter does.
This wires the complete manifest setup (routing, KV/config/secret stores,
logging, host/port) instead of a hand-rolled echo router.

The demo path requires the `dev-example` feature; without it `run_demo`
returns an actionable error.
The eight numbered work units are now "stages" rather than "commits" —
each stage may span multiple git commits. Literal git-commit actions
(commit steps, `git commit -m`, the PR head commit) keep the "commit"
wording.
Addresses review findings on the demo subcommand:

- demo is exposed only when built with the new `demo-example` feature.
  Generated CLIs and app-demo-cli no longer expose `Demo` at all — a
  downstream project has no bundled app-demo to run. The default
  `edgezero` binary gates `Command::Demo` on `demo-example`, so the
  advertised `--help` surface matches what actually works.
- `demo-example` (renamed from `dev-example`) now also pulls in
  `edgezero-adapter-axum`, making the feature self-contained.
- getting-started.md points generated projects at
  `edgezero serve --adapter axum`; cli-reference.md documents `demo`
  as contributor-only.
- NewArgs now derives Default and is #[non_exhaustive], matching the
  other public *Args structs.
- Generated handler tests serialize API_BASE_URL access behind a
  mutex + RAII env guard.
- Refreshed README, CLAUDE.md, architecture docs, and agent docs for
  the dev->demo / dev-example->demo-example rename.
aram356 added 2 commits May 20, 2026 22:31
Addresses review findings on the Stage 1 surface:

- Add a `[[bin]] name = "edgezero"` target so `cargo build` produces
  `target/debug/edgezero` — the name every doc and the clap `about`
  already use.
- Remove the inert `--local-core` flag from `NewArgs`; it was never
  read by the generator.
- Warn when `edgezero new` falls back to a Git dependency for
  `edgezero-cli`: the generated CLI crate needs `edgezero-cli` as a
  published library, so an out-of-repo scaffold only builds once that
  is available on the referenced remote. In-repo generation uses a
  path dependency and is unaffected.
- Replace removed `edgezero dev` references with
  `edgezero serve --adapter axum` in the root README, architecture,
  and axum adapter docs.
- Drop `run_demo` from the "build your own CLI" surface (it is
  contributor-only), and add the generated `*-cli` and Spin adapter
  crates to the scaffold structure docs.
Fixes fresh `edgezero new` projects failing to build outside the repo.

The generated CLI crate imports `edgezero_cli`, but dependency
resolution fell back to a Git dependency whenever the output directory
was outside the repo root — and the published `edgezero-cli` has no
library target, so every `edgezero_cli::...` import failed.

- Locate the edgezero checkout via `CARGO_MANIFEST_DIR` (baked in at
  build time) instead of the current directory, so generation finds the
  checkout regardless of where the project is created or where the
  command runs.
- When the output directory is outside the checkout, emit an absolute
  path dependency rather than the Git fallback. The Git fallback now
  only applies to a binary detached from its source tree.
- Assert in the generator test that the scaffold resolves edgezero
  crates to path dependencies, so a regression to the Git fallback is
  caught by `cargo test -p edgezero-cli`.
- Add an opt-in (`#[ignore]`) integration test that runs `cargo check`
  on the generated CLI crate, proving it compiles against the local
  `edgezero-cli` library.
- Drop the stale `--local-core` option from the CLI reference docs.
@aram356 aram356 requested a review from prk-Jr May 21, 2026 15:57
aram356 added 9 commits May 21, 2026 10:34
Broaden the opt-in scaffold test to `cargo check --workspace` and drop
`--offline`: a freshly generated project has no lockfile, so offline
resolution of transitive registry crates is unreliable (true of any
scaffolded project). Online, the full generated workspace compiles.
- Adapter README `dev_steps` snippets advised `edgezero-cli serve
  --adapter ...`, but the binary is `edgezero` (the `edgezero-cli`
  package builds `target/debug/edgezero`). Corrected all four adapters
  (axum, cloudflare, fastly, spin) so generated-project READMEs show a
  working command.
- Updated the plan and spec acceptance notes: generated and app-demo
  CLIs expose the four downstream built-ins (build/deploy/new/serve),
  not five — `demo` is contributor-only and absent from downstream
  CLIs. Also corrected the Stage 8 generated-CLI command count.
- Add a CI step that runs the `generated_project_builds` test
  (`-- --ignored`), so the Stage 1 scaffold regression — a fresh
  `edgezero new` project failing to compile — is caught by CI rather
  than only by manual runs.
- Correct two stale Stage 1 plan steps: a default `cargo build
  -p edgezero-cli` exposes four subcommands, not five; `demo` is gated
  behind the `demo-example` feature.
Stage 1 review findings on generated projects:

- Cloudflare adapter template called `run_app(req, env, ctx)` but the
  API takes `manifest_src` first — generated Cloudflare crates failed
  to compile for wasm32. Aligned the template with the other three
  adapters and the handwritten app-demo crate.
- The Spin `#[http_component]` macro expands to an unsafe wasm export,
  which trips the generated workspace's `unsafe_code = "deny"` gate.
  Added a narrow wasm-only `#[allow(unsafe_code)]` with a reason to the
  Spin entrypoint, in the template and in app-demo.
- `sanitize_crate_name` mangled uppercase letters to `-`, so
  `edgezero new MyApp` produced the invalid package name `-y-pp-core`.
  It now lower-cases ASCII letters, keeps `-`/`_`, collapses other
  characters, and trims leading/trailing separators; added unit tests.
- The opt-in `generated_project_builds` test only checked the host
  target. It now also runs `cargo check` for each adapter's wasm
  target (skipping a target that is not installed), which is where the
  two failures above lived.

Plan: marked PR #253 merged, and recorded two post-review Stage 2
design inputs — downstream binaries must build without an
`edgezero.toml`, and the manifest holds only non-adapter-specific
config.
- Status block no longer calls Stage 2 "gated on PR #253" — the
  precondition is met (PR #253 merged).
- Task 8.2 now says Stage 1 created the generated CLI template with
  four downstream built-ins, not five (demo is contributor-only).
Reworks spec §6.6/§8 and the plan's Stage 2 tasks for the design agreed
in review:

- edgezero.toml is portable and non-adapter-specific — [app], routes,
  [environment], and [stores.<kind>] logical ids/default only. No
  [adapters.*] table.
- The manifest is never compiled into the binary; the app! macro bakes
  the portable config into the App/Hooks type at compile time, and
  run_app::<A>() drops its manifest_src parameter (no include_str!).
- Adapter-specific runtime config — store platform names, tuning,
  host/port, logging — comes from EDGEZERO__* environment variables at
  runtime, with defaults when absent.
- An adapter binary builds and runs with no edgezero.toml and zero env
  vars.

Plan Task 2.1–2.9 rewritten accordingly (adds the EDGEZERO__ env-config
layer task; drops the in-manifest per-adapter mapping).
Rewrites the manifest store model to the §6.6 portable schema:

- `[stores.<kind>]` now carries only logical `ids` (non-empty) and an
  optional `default` (required when >1 id, must be a declared id). The
  five per-adapter store config types collapse into one reusable
  `StoreDeclaration`.
- The pre-rewrite store schema (`[stores.<kind>] name`,
  `[stores.config.defaults]`, `[stores.<kind>.adapters.*]`, `enabled`)
  is a hard load error whose message points at the migration guide.
- Store helper methods resolve a store's name to its logical default
  id (interim — `EDGEZERO__*` env overrides arrive in Task 2.2).
- `[stores.config.defaults]` and its axum dev-server seeding are gone.
- Migrated `examples/app-demo/edgezero.toml` and the generated
  `edgezero.toml.hbs` template to the new schema.

Scoped to store types only; `[adapters.*]`, the env layer, and adapter
store registries are later Stage 2 tasks.
New `edgezero-core::env_config` module: parses `EDGEZERO__`-prefixed
environment variables (`__` = key-path separator, segments lower-cased)
into an `EnvConfig` value with accessors for store platform names +
tuning, bind host/port, and logging level.

- `from_env()` reads the process environment; `from_vars()` lets the
  Cloudflare adapter supply its `Env` binding (no `std::env` there).
- `store_name(kind, id)` falls back to the logical id when unset.

Additive only — wired into the runtime in later Stage 2 tasks.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants