Repository navigation
LOU-I: Deploy portability & documentation - #10
Merged
Merged
Conversation
- src/deploy/types.ts: DeploymentAdapter {scaffold, build, describe} plus a
Map-backed registerAdapter/getAdapter/listAdapters registry.
- src/cli/build.ts: `loushy build --target=<name> --agent=<path> [--out=<dir>]`
using bin/loushy.js's --flag=value / --flag value convention; unknown
target -> `Error: unknown target "<t>"` on stderr + exit 1; otherwise
scaffold() -> build() -> print describe(). Registers a 'stub' adapter that
records its call order.
- bin/loushy.js: first-arg subcommand switch (dev | build).
- tsup: new cli/build entry.
- Tests: direct runBuild() tests + bin subprocess smoke test.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- src/deploy/adapters/node-server.ts: scaffold() loads + validates an AgentSpec (.yaml/.yml/.json, via LOU-H9's loadSpec) and writes agent.config.js, server.ts and package.json; build() bundles server.ts with tsup's programmatic build() into a self-contained dist/server.js; describe() -> 'node dist/server.js'. - Generated server.ts calls the static AgentExecutor.execute() directly (GET /health, POST /chat) and keeps loushy dev's security properties: 127.0.0.1 by default with --host/HOST opt-in, 1MB body cap (413). - src/deploy/runtime.ts + specExecution.ts: runtime the scaffold imports via a virtual '@loushy/build-ai-agent/deploy-runtime' specifier, resolved at build time to the SDK's own source (src/deploy/bundle.ts plugin). - Split NoopSandbox + SandboxAdapter into src/security/sandboxCore.ts so the execution core no longer imports dockerode (whose native ssh2 subtree cannot be bundled); sandbox.ts re-exports it, public API unchanged. - Export resolveSpecTool from specToAgent; explicit registerBuiltInAdapters() (a top-level side-effect registration was tree-shaken out of dist/cli/build.js). - package.json: ship src (runtime sources the adapters bundle from), tsup as an optional peer dependency (loaded lazily only by `loushy build`). - Tests: real scaffold + build + subprocess server integration test (/health, /chat, 400, 413, 127.0.0.1 default, --host opt-in) and a bin subprocess test building node-server end to end. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- src/deploy/adapters/cloudflare.ts: scaffold() writes worker.ts (module Worker exporting fetch(request, env) that wraps the static AgentExecutor.execute(): GET /health, POST /chat, 1MB body cap), agent.config.js and wrangler.toml (name/main/compatibility_date, no_bundle so the tsup output is uploaded as built); rejects tools and providers that cannot run on Workers. build() bundles with tsup (format esm, platform browser) and fails if any `node:` specifier leaks into dist/worker.js. describe() -> 'wrangler deploy'. - src/deploy/runtime.worker.ts: Node-free runtime (mock provider, env-binding API keys, current-date/day-name tools). - src/deploy/shims/sandboxCore.worker.ts + workerSandboxShimPlugin: swaps the host-exec NoopSandbox (node:child_process) for a Worker-safe stub in the Worker bundle only. - wrangler added as a devDependency. Tests: wrangler.toml/scaffold unit tests, zero-`node:` grep of the real bundle, bundle fetch() exercised in-process, plus (guarded on wrangler being installed, LOU-F6 style) `wrangler deploy --dry-run` and `wrangler dev` on local workerd. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- src/deploy/adapters/docker.ts: scaffold() calls NodeServerAdapter.scaffold() directly and adds a Dockerfile (node:20-slim, WORKDIR /app, COPY package.json, RUN npm install --omit=dev, COPY dist/, EXPOSE 3000, CMD node dist/server.js) plus .dockerignore; build() calls NodeServerAdapter.build(); describe() -> 'docker build -t loushy-agent . && docker run -p 3000:3000 loushy-agent'. The image sets HOST=0.0.0.0 as the explicit bind opt-in a container needs for -p to reach the server. - Registered as 'docker' in registerBuiltInAdapters(). - Tests: Dockerfile content, spy assertions that scaffold()/build() delegate to NodeServerAdapter; real docker build/run integration test behind LOU-F6's dockerode-ping describe.skipIf guard (skipped here: no Docker daemon running). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- typedoc added as a devDependency; typedoc.json (entryPoints src/index.ts, out docs/api, excludePrivate, excludeInternal). - "docs:build": "typedoc" script. `npm run docs:build` exits 0 (0 errors, 2 pre-existing @variablename tag warnings in src/flows/inputs.ts) and generates pages for AgentBuilder, AgentExecutor, createAgent, ToolRegistry, LLMProviderRegistry, FlowBuilder, loadSpec, specToAgent, registerAdapter, DeploymentAdapter, etc. - docs/api/ is generated output and is gitignored. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- README.md had no "## Documentation" section at all (the header nav's #documentation anchor pointed nowhere) and its "## Examples" section had no links. Added a Documentation section linking five new pages, pointed Examples at the real examples/README.md index (LOU-H11), added a Quick Start pointer to the verified docs page, and replaced the API Reference link to the gitignored ./dist/index.d.ts with docs/api-overview.md + `npm run docs:build`. - New docs, written from what the codebase actually does: installation.md, quick-start.md, configuration.md, deployment.md (LOU-I1..I4 targets), api-overview.md. - docs/docs-links.test.ts: every relative link under README's Documentation/Examples headings (and inside docs/*.md) resolves from the repo root to an existing, non-empty file; nav anchors have headings. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
No docs framework / embed mechanism (CodeSandbox, StackBlitz) exists in this repo and the ticket rules out introducing one for this ticket alone, so the deliverable is the verifiable core: every Quick Start snippet is proven to type-check and run against the locally built package. The interactive embed UI is deferred as a follow-up. - scripts/verify-docs-snippets.ts (+ `npm run docs:verify-snippets`): extracts ```ts/```typescript fences from docs/quick-start.md (or given files; `no-verify` opts a fence out), builds + `npm pack`s the SDK, installs the tarball into a throwaway temp project (LOU-H4's pattern), runs strict `tsc --noEmit` (module nodenext) over all snippets, then runs each with tsx (credential env vars stripped -> deterministic mock path). Non-zero exit on any type or runtime failure, reported per snippet with its markdown file:line. All 5 Quick Start snippets pass. - Fix found by the verifier: src/evals/defineEval.ts value-imported vitest, so tsup bundled vitest into dist/index.js/.mjs and importing the package root threw "Vitest failed to access its internal state" in any non-test process. defineEval() now resolves test/expect at call time from the running vitest worker (globalThis.__vitest_index__, falling back to vitest globals) with only a type import; its signature and behaviour under vitest are unchanged (src/evals tests pass). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…orkers free-tier limit QA found the cloudflare-worker adapter's build() never measured or reported bundle size against Cloudflare Workers' script size limit, even though it's an explicit LOU-I3 AC and wrangler dry-run only prints it to an external console loushy build never surfaces. - Add measureBundleSize() (raw fs.statSync + zlib.gzipSync) and WORKER_SIZE_LIMIT_BYTES, documented against Cloudflare's 2026-09-04 changelog (64 MiB uncompressed, unified across Free/paid plans, replacing the old 3 MB/10 MB compressed limits). - build() measures the bundle after the node: builtin check and warns (not throws) via console.warn if it's over budget. - describe(outDir) reports the bundle's raw/gzip size and a pass/WARNING verdict when a built bundle exists at outDir. - Tests: cross-check the adapter's measurement against fs.statSync ground truth, prove the WARNING path fires for a genuinely oversized on-disk bundle, and cross-check against wrangler deploy --dry-run's own "Total Upload" figure for the same build (247.12 KiB raw / 46.05 KiB gzip from wrangler vs. 247.1 KB / 46.0 KB measured). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This was referenced Oct 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements all 7 tickets from LOU-I (Phase 2-3, final epic), giving Loushy a real multi-target deployment story and closing the README's broken-doc-link credibility gap:
DeploymentAdapterinterface (scaffold()/build()/describe()) + registry,loushy build --target=<name> --agent=<path>CLI.AgentExecutorin an HTTP server (/health,/chat), carrying forward LOU-H6's localhost-only-by-default binding and request body size cap.node:imports, generatedwrangler.toml.docs/api/).scripts/verify-docs-snippets.ts— packs the SDK for real, installs it into a throwaway project, and executes every Quick Start snippet against the actual built package.A real pre-existing production bug found and fixed
While verifying LOU-I7's snippets against a real, non-test consumer, a genuine bug on
mainsurfaced: importing the package root threw at runtime ("Vitest failed to access its internal state") for any real application, becausedefineEval()value-importedvitestat module scope, bundling the test runner into the published package. Fixed by resolving vitest lazily at call time instead of at import time. Verified with a real reproduction: packed the SDK, installed the tarball into an isolated temp project, and confirmed a plainnodescript (no test runner involved) can now import@loushy/build-ai-agentcleanly.QA-caught AC gap, also fixed
QA found the Cloudflare adapter built and deployed correctly via real
wranglertooling, butloushy build's own lifecycle never measured or reported bundle size against Workers' size limit — an explicit AC. Fixed with realfs.statSync/gzip-based measurement inbuild()/describe(), cross-checked againstwrangler deploy --dry-run's own reported size (247.1 KB raw / 46.0 KB gzip on both sides, consistent) and against the current documented Workers limit (64 MiB uncompressed, verified via Cloudflare's own changelog rather than assumed from memory).Validation results
npx vitest run: 680 passing, 3 skipped (2 pre-existing Docker-daemon-unavailable skips + 1 new). 3 known pre-existing provider-suite failures (missing optional peer deps) — unrelated to this epic.npx tsc --noEmit: clean.dist/server.jsas an actual background process and curled/health+/chatfor a genuineAgentExecutorresponse; rannpx wrangler dev/wrangler deploy --dry-runfor real against the Cloudflare adapter's output (wrangler was available in this environment); Docker's daemon wasn't running here, so its integration test skips gracefully (matching the pattern already established in LOU-F6) while the generated Dockerfile content and delegation-not-duplication are verified directly.Known, documented, out-of-scope limitation
QA also surfaced a related but separate pre-existing issue: the package root eagerly imports all three provider SDKs even though they're marked optional in
peerDependenciesMeta, so importing without all three installed still throws — contradicting the "optional" framing. Not introduced by this epic and out of scope for it; worth a follow-up ticket.🤖 Generated with Claude Code