@frontman-ai/astro 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,22 +1,17 @@
1
1
  # @frontman-ai/astro
2
2
 
3
- Astro framework integration for Frontman, exposing tools and services via HTTP middleware and dev toolbar.
3
+ [![npm version](https://img.shields.io/npm/v/@frontman-ai/astro)](https://www.npmjs.com/package/@frontman-ai/astro)
4
+ [![astro ^5.0.0](https://img.shields.io/badge/astro-%5E5.0.0-blueviolet)](https://astro.build)
4
5
 
5
- ## Stack
6
+ Astro integration for [Frontman](https://frontman.sh) — AI-powered development tools that let you edit your frontend from the browser.
6
7
 
7
- - [ReScript](https://rescript-lang.org) with ES6 modules
8
- - [Astro](https://astro.build) 5.0+
9
- - HTTP middleware integration
10
- - SSE (Server-Sent Events) for streaming responses
11
-
12
- ## Features
8
+ ## Installation
13
9
 
14
- - HTTP middleware for handling tool requests
15
- - Dev toolbar integration for interactive debugging
16
- - Framework-specific tools (e.g., `GetPages`)
17
- - Streaming responses via SSE
10
+ ```bash
11
+ npx astro add @frontman-ai/astro
12
+ ```
18
13
 
19
- ## Installation
14
+ Or manually:
20
15
 
21
16
  ```bash
22
17
  npm install @frontman-ai/astro
@@ -28,26 +23,72 @@ Add the integration to your `astro.config.mjs`:
28
23
 
29
24
  ```javascript
30
25
  import { defineConfig } from 'astro/config';
31
- import { frontmanIntegration } from '@frontman-ai/astro/integration';
26
+ import frontman from '@frontman-ai/astro';
32
27
 
33
28
  export default defineConfig({
34
29
  integrations: [
35
- frontmanIntegration(),
30
+ frontman({ projectRoot: import.meta.dirname }),
36
31
  ],
37
32
  });
38
33
  ```
39
34
 
40
- ## Exports
35
+ Then start your dev server and open `http://localhost:4321/frontman/`.
36
+
37
+ ## What it does
38
+
39
+ The integration automatically (in dev mode only):
40
+
41
+ - Registers a dev toolbar app for element selection
42
+ - Captures Astro source annotations so the AI knows which `.astro` file and line each element comes from
43
+ - Serves the Frontman UI at `/<basePath>/` (default: `/frontman/`)
44
+ - Exposes tool endpoints for AI interactions (file edits, screenshots, etc.)
45
+
46
+ > **Note:** Element source detection requires `devToolbar.enabled: true` (the default). Astro only emits `data-astro-source-file` / `data-astro-source-loc` annotations when the dev toolbar is enabled. If you've disabled it, Frontman will log a warning and fall back to CSS selector-based detection.
47
+
48
+ ## Configuration
49
+
50
+ All options are optional with sensible defaults:
51
+
52
+ | Option | Default | Description |
53
+ |---|---|---|
54
+ | `projectRoot` | `PROJECT_ROOT` env var, `PWD`, or `"."` | Path to the project root directory |
55
+ | `sourceRoot` | Same as `projectRoot` | Root for source file resolution (useful in monorepos) |
56
+ | `basePath` | `"frontman"` | URL prefix for Frontman routes |
57
+ | `host` | `FRONTMAN_HOST` env var or `"api.frontman.sh"` | Frontman server host for client connections |
58
+ | `serverName` | `"frontman-astro"` | Server name included in tool responses |
59
+ | `serverVersion` | `"1.0.0"` | Server version included in tool responses |
60
+ | `clientUrl` | Auto-generated from `host` | URL to the Frontman client bundle (must include a `host` query parameter) |
61
+ | `isLightTheme` | `false` | Use a light theme for the Frontman UI |
62
+
63
+ ### Environment variables
64
+
65
+ | Variable | Description |
66
+ |---|---|
67
+ | `FRONTMAN_HOST` | Override the default server host without changing config |
68
+ | `PROJECT_ROOT` | Override the project root path |
69
+ | `FRONTMAN_CLIENT_URL` | Override the client bundle URL |
70
+
71
+ ## How it works
72
+
73
+ The integration uses two Astro hooks:
74
+
75
+ - **`astro:config:setup`** — Registers the dev toolbar app and injects the annotation capture script via `injectScript('head-inline', ...)`
76
+ - **`astro:server:setup`** — Registers Frontman API routes as Vite dev server middleware via `server.middlewares.use()`
77
+
78
+ No manual middleware file needed. No SSR adapter required. Works with static (`output: 'static'`) Astro projects.
79
+
80
+ ## Requirements
41
81
 
42
- - `.` - Main module with `createMiddleware`, `makeConfig`, `ToolRegistry`
43
- - `./integration` - Astro integration hook
44
- - `./toolbar` - Dev toolbar app component
82
+ - Astro ^5.0.0
83
+ - Node.js >= 18
45
84
 
46
- ## Dependencies
85
+ ## Links
47
86
 
48
- - `@frontman/frontman-core` - Tool registry and SSE utilities
49
- - `astro` ^5.0.0 (peer dependency)
87
+ - [Website](https://frontman.sh)
88
+ - [Documentation](https://frontman.sh/docs/astro)
89
+ - [Changelog](https://github.com/frontman-ai/frontman/blob/main/CHANGELOG.md)
90
+ - [Issues](https://github.com/frontman-ai/frontman/issues)
50
91
 
51
- ## Commands
92
+ ## License
52
93
 
53
- Run `make` or `make help` to see all available commands.
94
+ [Apache-2.0](https://github.com/frontman-ai/frontman/blob/main/LICENSE)
package/dist/index.js CHANGED
@@ -97,8 +97,12 @@ function orElse(opt, other) {
97
97
 
98
98
  // src/FrontmanAstro__Config.res.mjs
99
99
  var host = process.env["FRONTMAN_HOST"];
100
- var defaultHost = host !== void 0 ? host : "frontman.local:4000";
101
- function makeFromObject(config) {
100
+ var defaultHost = host !== void 0 ? host : "api.frontman.sh";
101
+ var ensureConfig = (function(c2) {
102
+ return c2 || {};
103
+ });
104
+ function makeFromObject(rawConfig) {
105
+ let config = ensureConfig(rawConfig);
102
106
  let projectRoot = getOr(orElse(config.projectRoot, orElse(process.env["PROJECT_ROOT"], process.env["PWD"])), ".");
103
107
  let sourceRoot = getOr(config.sourceRoot, projectRoot);
104
108
  let basePath = getOr(config.basePath, "frontman");
@@ -699,14 +703,14 @@ function withPathPrepend(b, input, path, maybeDynamicLocationVar, appendSafe, fn
699
703
  return fn(b, input, path);
700
704
  }
701
705
  try {
702
- let $$catch = (b2, errorVar2) => {
706
+ let $$catch2 = (b2, errorVar2) => {
703
707
  b2.c = errorVar2 + `.path=` + fromString(path) + `+` + (maybeDynamicLocationVar !== void 0 ? `'["'+` + maybeDynamicLocationVar + `+'"]'+` : "") + errorVar2 + `.path`;
704
708
  };
705
709
  let fn$1 = (b2) => fn(b2, input, "");
706
710
  let prevCode = b.c;
707
711
  b.c = "";
708
712
  let errorVar = varWithoutAllocation(b.g);
709
- let maybeResolveVal = $$catch(b, errorVar);
713
+ let maybeResolveVal = $$catch2(b, errorVar);
710
714
  let catchCode = `if(` + (errorVar + `&&` + errorVar + `.s===s`) + `){` + b.c;
711
715
  b.c = "";
712
716
  let bb = {
@@ -5577,41 +5581,6 @@ function make2() {
5577
5581
  }
5578
5582
 
5579
5583
  // src/FrontmanAstro__Middleware.res.mjs
5580
- var annotationCaptureScript = `
5581
- <script>
5582
- (function() {
5583
- var annotations = new Map();
5584
- document.querySelectorAll('[data-astro-source-file]').forEach(function(el) {
5585
- annotations.set(el, {
5586
- file: el.getAttribute('data-astro-source-file'),
5587
- loc: el.getAttribute('data-astro-source-loc')
5588
- });
5589
- });
5590
- window.__frontman_annotations__ = {
5591
- _map: annotations,
5592
- get: function(el) { return annotations.get(el); },
5593
- has: function(el) { return annotations.has(el); },
5594
- size: function() { return annotations.size; }
5595
- };
5596
- console.log('[Frontman] Captured ' + annotations.size + ' elements');
5597
- })();
5598
- </script>
5599
- `;
5600
- async function injectAnnotationScript(response) {
5601
- let contentType = response.headers.get("content-type");
5602
- if (contentType === null) {
5603
- return response;
5604
- }
5605
- if (!contentType.includes("text/html")) {
5606
- return response;
5607
- }
5608
- let html = await response.text();
5609
- let injectedHtml = html.replace("</body>", annotationCaptureScript + `</body>`);
5610
- return new Response(injectedHtml, {
5611
- status: response.status,
5612
- headers: some(response.headers)
5613
- });
5614
- }
5615
5584
  function uiHtml(clientUrl, isLightTheme) {
5616
5585
  let openrouterKey = flatMap(process.env["OPENROUTER_API_KEY"], (key) => {
5617
5586
  if (key !== "") {
@@ -5661,9 +5630,10 @@ function serveUI(config) {
5661
5630
  }
5662
5631
  function createMiddleware(config) {
5663
5632
  let registry = make2();
5664
- return async (context, next) => {
5665
- let pathname = context.url.pathname;
5666
- let method = context.request.method;
5633
+ return async (request) => {
5634
+ let url2 = new URL(request.url);
5635
+ let pathname = url2.pathname;
5636
+ let method = request.method;
5667
5637
  let basePath = `/` + config.basePath;
5668
5638
  if (pathname === basePath || pathname.startsWith(basePath + `/`)) {
5669
5639
  if (method === "OPTIONS") {
@@ -5673,9 +5643,9 @@ function createMiddleware(config) {
5673
5643
  } else if (pathname === basePath + `/tools` && method === "GET") {
5674
5644
  return handleGetTools(registry, config);
5675
5645
  } else if (pathname === basePath + `/tools/call` && method === "POST") {
5676
- return await handleToolCall(registry, config, context.request);
5646
+ return await handleToolCall(registry, config, request);
5677
5647
  } else if (pathname === basePath + `/resolve-source-location` && method === "POST") {
5678
- return await handleResolveSourceLocation(config, context.request);
5648
+ return await handleResolveSourceLocation(config, request);
5679
5649
  } else {
5680
5650
  return Response.json(Object.fromEntries([[
5681
5651
  "error",
@@ -5685,8 +5655,109 @@ function createMiddleware(config) {
5685
5655
  });
5686
5656
  }
5687
5657
  }
5688
- let response = await next();
5689
- return await injectAnnotationScript(response);
5658
+ };
5659
+ }
5660
+
5661
+ // ../../node_modules/@rescript/runtime/lib/es6/Stdlib_Promise.js
5662
+ function $$catch(promise, callback) {
5663
+ return promise.catch((err) => callback(internalToException(err)));
5664
+ }
5665
+
5666
+ // ../bindings/src/NodeHttp.res.mjs
5667
+ var collectRequestBody = (async function(req) {
5668
+ const chunks = [];
5669
+ for await (const chunk of req) {
5670
+ chunks.push(chunk);
5671
+ }
5672
+ const { Buffer: Buffer2 } = await import('buffer');
5673
+ return Buffer2.concat(chunks);
5674
+ });
5675
+
5676
+ // src/FrontmanAstro__ViteAdapter.res.mjs
5677
+ var copyHeaders = (function(headers2, res) {
5678
+ headers2.forEach(function(value, key) {
5679
+ res.setHeader(key, value);
5680
+ });
5681
+ });
5682
+ async function toWebRequest(req) {
5683
+ let host2 = getOr(req.headers["host"], "localhost");
5684
+ let url2 = `http://` + host2 + req.url;
5685
+ let method = req.method;
5686
+ let match = method.toUpperCase();
5687
+ let body;
5688
+ switch (match) {
5689
+ case "PATCH":
5690
+ case "POST":
5691
+ case "PUT":
5692
+ body = await collectRequestBody(req);
5693
+ break;
5694
+ default:
5695
+ body = void 0;
5696
+ }
5697
+ let headersDict = req.headers;
5698
+ let init = {
5699
+ method,
5700
+ headers: some(headersDict),
5701
+ body: map(body, (b) => b)
5702
+ };
5703
+ if (body !== void 0) {
5704
+ init["duplex"] = "half";
5705
+ }
5706
+ return new Request(url2, init);
5707
+ }
5708
+ async function writeWebResponse(webResponse, res) {
5709
+ res.statusCode = webResponse.status;
5710
+ copyHeaders(webResponse.headers, res);
5711
+ let body = webResponse.body;
5712
+ if (body !== null) {
5713
+ let reader = body.getReader();
5714
+ let decoder = new TextDecoder();
5715
+ let reading = true;
5716
+ while (reading) {
5717
+ let result = await reader.read();
5718
+ if (result.done) {
5719
+ reading = false;
5720
+ } else {
5721
+ let chunk = result.value;
5722
+ if (!(chunk == null)) {
5723
+ let text = decoder.decode(chunk, {
5724
+ stream: true
5725
+ });
5726
+ res.write(text);
5727
+ }
5728
+ }
5729
+ }
5730
+ res.end();
5731
+ return;
5732
+ }
5733
+ res.end();
5734
+ }
5735
+ function adaptToConnect(middleware, basePath) {
5736
+ let prefix = `/` + basePath;
5737
+ return (req, res, next) => {
5738
+ let reqPath = getOr(req.url.split("?")[0], req.url);
5739
+ if (!(reqPath === prefix || reqPath.startsWith(prefix + `/`))) {
5740
+ return next();
5741
+ }
5742
+ let handleRequest = async () => {
5743
+ let webRequest = await toWebRequest(req);
5744
+ let maybeResponse = await middleware(webRequest);
5745
+ if (maybeResponse !== void 0) {
5746
+ return await writeWebResponse(maybeResponse, res);
5747
+ } else {
5748
+ return next();
5749
+ }
5750
+ };
5751
+ $$catch(handleRequest(), (error) => {
5752
+ console.error("[Frontman] Middleware error:", error);
5753
+ if (res.headersSent) {
5754
+ res.end();
5755
+ } else {
5756
+ res.statusCode = 500;
5757
+ res.end("Internal Server Error");
5758
+ }
5759
+ return Promise.resolve();
5760
+ });
5690
5761
  };
5691
5762
  }
5692
5763
 
@@ -5695,33 +5766,61 @@ var icon = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewB
5695
5766
  function getToolbarAppPath() {
5696
5767
  return new URL("./toolbar.js", import.meta.url).pathname;
5697
5768
  }
5698
- function make3() {
5769
+ var annotationCaptureScript = `(function() {
5770
+ function captureAnnotations() {
5771
+ var annotations = new Map();
5772
+ document.querySelectorAll('[data-astro-source-file]').forEach(function(el) {
5773
+ annotations.set(el, {
5774
+ file: el.getAttribute('data-astro-source-file'),
5775
+ loc: el.getAttribute('data-astro-source-loc')
5776
+ });
5777
+ });
5778
+ window.__frontman_annotations__ = {
5779
+ _map: annotations,
5780
+ get: function(el) { return annotations.get(el); },
5781
+ has: function(el) { return annotations.has(el); },
5782
+ size: function() { return annotations.size; }
5783
+ };
5784
+ }
5785
+ // Capture once on initial DOM parse
5786
+ document.addEventListener('DOMContentLoaded', captureAnnotations);
5787
+ // Re-capture on View Transitions (SPA navigations) \u2014 skips the initial
5788
+ // page-load event since DOMContentLoaded already captured
5789
+ var initialLoad = true;
5790
+ document.addEventListener('astro:page-load', function() {
5791
+ if (initialLoad) { initialLoad = false; return; }
5792
+ captureAnnotations();
5793
+ });
5794
+ })();`;
5795
+ function make3(configInput) {
5796
+ let config = makeFromObject(configInput);
5699
5797
  return {
5700
5798
  name: "frontman",
5701
5799
  hooks: {
5702
5800
  "astro:config:setup": (ctx2) => {
5703
5801
  if (ctx2.command === "dev") {
5704
- return ctx2.addDevToolbarApp({
5802
+ if (!ctx2.config.devToolbar.enabled) {
5803
+ console.warn("[Frontman] Astro devToolbar is disabled \u2014 element source detection will be limited. Set `devToolbar: { enabled: true }` in your astro.config to enable full component source resolution.");
5804
+ }
5805
+ ctx2.addDevToolbarApp({
5705
5806
  id: "frontman:toolbar",
5706
5807
  name: "Frontman",
5707
5808
  icon,
5708
5809
  entrypoint: getToolbarAppPath()
5709
5810
  });
5811
+ return ctx2.injectScript("head-inline", annotationCaptureScript);
5710
5812
  }
5813
+ },
5814
+ "astro:server:setup": (param) => {
5815
+ let webMiddleware = createMiddleware(config);
5816
+ let connectMiddleware = adaptToConnect(webMiddleware, config.basePath);
5817
+ param.server.middlewares.use(connectMiddleware);
5711
5818
  }
5712
5819
  }
5713
5820
  };
5714
5821
  }
5715
5822
 
5716
5823
  // src/FrontmanAstro.res.mjs
5717
- var Config;
5718
- var Middleware;
5719
- var Server;
5720
- var ToolRegistry;
5721
- var Integration;
5722
- var SSE;
5723
- var createMiddleware2 = createMiddleware;
5724
- var makeConfig = makeFromObject;
5725
5824
  var frontmanIntegration = make3;
5726
5825
 
5727
- export { Config, Integration, Middleware, SSE, Server, ToolRegistry, createMiddleware2 as createMiddleware, frontmanIntegration, makeConfig };
5826
+ export { frontmanIntegration as default, frontmanIntegration };