@frontman-ai/astro 0.1.4 → 0.1.6

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
@@ -24,11 +24,6 @@ var require_lib = __commonJS({
24
24
  }
25
25
  });
26
26
 
27
- // ../../node_modules/@rescript/runtime/lib/es6/Stdlib_JsError.js
28
- function throwWithMessage(str) {
29
- throw new Error(str);
30
- }
31
-
32
27
  // ../../node_modules/@rescript/runtime/lib/es6/Primitive_option.js
33
28
  function some(x) {
34
29
  if (x === void 0) {
@@ -95,25 +90,39 @@ function orElse(opt, other) {
95
90
  }
96
91
  }
97
92
 
93
+ // ../frontman-core/src/FrontmanCore__Hosts.res.mjs
94
+ var apiHost = "api.frontman.sh";
95
+ var clientJs = "https://app.frontman.sh/frontman.es.js";
96
+ var clientCss = "https://app.frontman.sh/frontman.css";
97
+ var devClientJs = "http://localhost:5173/src/Main.res.mjs";
98
+
98
99
  // src/FrontmanAstro__Config.res.mjs
99
100
  var host = process.env["FRONTMAN_HOST"];
100
- var defaultHost = host !== void 0 ? host : "frontman.local:4000";
101
- function makeFromObject(config) {
101
+ var defaultHost = host !== void 0 ? host : apiHost;
102
+ var ensureConfig = (function(c2) {
103
+ return c2 || {};
104
+ });
105
+ function makeFromObject(rawConfig) {
106
+ let config = ensureConfig(rawConfig);
107
+ let host2 = getOr(config.host, defaultHost);
108
+ let isDev = host2 !== apiHost;
102
109
  let projectRoot = getOr(orElse(config.projectRoot, orElse(process.env["PROJECT_ROOT"], process.env["PWD"])), ".");
103
110
  let sourceRoot = getOr(config.sourceRoot, projectRoot);
104
111
  let basePath = getOr(config.basePath, "frontman");
105
112
  let serverName = getOr(config.serverName, "frontman-astro");
106
113
  let serverVersion = getOr(config.serverVersion, "1.0.0");
107
114
  let isLightTheme = getOr(config.isLightTheme, false);
108
- let host2 = getOr(config.host, defaultHost);
109
- let baseUrl = getOr(process.env["FRONTMAN_CLIENT_URL"], "http://localhost:5173/src/Main.res.mjs");
115
+ let baseUrl = getOr(config.clientUrl, getOr(process.env["FRONTMAN_CLIENT_URL"], isDev ? devClientJs : clientJs));
110
116
  let url2 = new URL(baseUrl);
111
- let clientUrl = getOr(config.clientUrl, (url2.searchParams.set("clientName", "astro"), url2.searchParams.set("host", host2), url2.href));
112
- let parsedUrl = new URL(clientUrl);
113
- if (!parsedUrl.searchParams.has("host")) {
114
- throwWithMessage(`[frontman-astro] clientUrl must include a "host" query parameter. Got: ` + clientUrl);
117
+ if (url2.searchParams.has("clientName")) ; else {
118
+ url2.searchParams.set("clientName", "astro");
119
+ }
120
+ if (url2.searchParams.has("host")) ; else {
121
+ url2.searchParams.set("host", host2);
115
122
  }
123
+ let clientUrl = url2.href;
116
124
  return {
125
+ isDev,
117
126
  projectRoot,
118
127
  sourceRoot,
119
128
  basePath,
@@ -121,6 +130,7 @@ function makeFromObject(config) {
121
130
  serverVersion,
122
131
  host: host2,
123
132
  clientUrl,
133
+ clientCssUrl: orElse(config.clientCssUrl, isDev ? void 0 : clientCss),
124
134
  isLightTheme
125
135
  };
126
136
  }
@@ -699,14 +709,14 @@ function withPathPrepend(b, input, path, maybeDynamicLocationVar, appendSafe, fn
699
709
  return fn(b, input, path);
700
710
  }
701
711
  try {
702
- let $$catch = (b2, errorVar2) => {
712
+ let $$catch2 = (b2, errorVar2) => {
703
713
  b2.c = errorVar2 + `.path=` + fromString(path) + `+` + (maybeDynamicLocationVar !== void 0 ? `'["'+` + maybeDynamicLocationVar + `+'"]'+` : "") + errorVar2 + `.path`;
704
714
  };
705
715
  let fn$1 = (b2) => fn(b2, input, "");
706
716
  let prevCode = b.c;
707
717
  b.c = "";
708
718
  let errorVar = varWithoutAllocation(b.g);
709
- let maybeResolveVal = $$catch(b, errorVar);
719
+ let maybeResolveVal = $$catch2(b, errorVar);
710
720
  let catchCode = `if(` + (errorVar + `&&` + errorVar + `.s===s`) + `){` + b.c;
711
721
  b.c = "";
712
722
  let bb = {
@@ -5577,42 +5587,7 @@ function make2() {
5577
5587
  }
5578
5588
 
5579
5589
  // 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
- function uiHtml(clientUrl, isLightTheme) {
5590
+ function uiHtml(clientUrl, clientCssUrl, isLightTheme) {
5616
5591
  let openrouterKey = flatMap(process.env["OPENROUTER_API_KEY"], (key) => {
5617
5592
  if (key !== "") {
5618
5593
  return key;
@@ -5627,12 +5602,14 @@ function uiHtml(clientUrl, isLightTheme) {
5627
5602
  });
5628
5603
  let runtimeConfig = JSON.stringify(configObj);
5629
5604
  let themeClass = isLightTheme ? "" : "dark";
5605
+ let cssLink = clientCssUrl !== void 0 ? `<link rel="stylesheet" href="` + clientCssUrl + `">` : "";
5630
5606
  return `<!DOCTYPE html>
5631
5607
  <html lang="en" class="` + themeClass + `">
5632
5608
  <head>
5633
5609
  <meta charset="UTF-8">
5634
5610
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
5635
5611
  <title>Frontman</title>
5612
+ ` + cssLink + `
5636
5613
  <style>
5637
5614
  html, body, #root {
5638
5615
  margin: 0;
@@ -5650,7 +5627,7 @@ function uiHtml(clientUrl, isLightTheme) {
5650
5627
  </html>`;
5651
5628
  }
5652
5629
  function serveUI(config) {
5653
- let html = uiHtml(config.clientUrl, config.isLightTheme);
5630
+ let html = uiHtml(config.clientUrl, config.clientCssUrl, config.isLightTheme);
5654
5631
  let headers2 = Object.fromEntries([[
5655
5632
  "Content-Type",
5656
5633
  "text/html"
@@ -5661,9 +5638,10 @@ function serveUI(config) {
5661
5638
  }
5662
5639
  function createMiddleware(config) {
5663
5640
  let registry = make2();
5664
- return async (context, next) => {
5665
- let pathname = context.url.pathname;
5666
- let method = context.request.method;
5641
+ return async (request) => {
5642
+ let url2 = new URL(request.url);
5643
+ let pathname = url2.pathname;
5644
+ let method = request.method;
5667
5645
  let basePath = `/` + config.basePath;
5668
5646
  if (pathname === basePath || pathname.startsWith(basePath + `/`)) {
5669
5647
  if (method === "OPTIONS") {
@@ -5673,9 +5651,9 @@ function createMiddleware(config) {
5673
5651
  } else if (pathname === basePath + `/tools` && method === "GET") {
5674
5652
  return handleGetTools(registry, config);
5675
5653
  } else if (pathname === basePath + `/tools/call` && method === "POST") {
5676
- return await handleToolCall(registry, config, context.request);
5654
+ return await handleToolCall(registry, config, request);
5677
5655
  } else if (pathname === basePath + `/resolve-source-location` && method === "POST") {
5678
- return await handleResolveSourceLocation(config, context.request);
5656
+ return await handleResolveSourceLocation(config, request);
5679
5657
  } else {
5680
5658
  return Response.json(Object.fromEntries([[
5681
5659
  "error",
@@ -5685,8 +5663,109 @@ function createMiddleware(config) {
5685
5663
  });
5686
5664
  }
5687
5665
  }
5688
- let response = await next();
5689
- return await injectAnnotationScript(response);
5666
+ };
5667
+ }
5668
+
5669
+ // ../../node_modules/@rescript/runtime/lib/es6/Stdlib_Promise.js
5670
+ function $$catch(promise, callback) {
5671
+ return promise.catch((err) => callback(internalToException(err)));
5672
+ }
5673
+
5674
+ // ../bindings/src/NodeHttp.res.mjs
5675
+ var collectRequestBody = (async function(req) {
5676
+ const chunks = [];
5677
+ for await (const chunk of req) {
5678
+ chunks.push(chunk);
5679
+ }
5680
+ const { Buffer: Buffer2 } = await import('buffer');
5681
+ return Buffer2.concat(chunks);
5682
+ });
5683
+
5684
+ // src/FrontmanAstro__ViteAdapter.res.mjs
5685
+ var copyHeaders = (function(headers2, res) {
5686
+ headers2.forEach(function(value, key) {
5687
+ res.setHeader(key, value);
5688
+ });
5689
+ });
5690
+ async function toWebRequest(req) {
5691
+ let host2 = getOr(req.headers["host"], "localhost");
5692
+ let url2 = `http://` + host2 + req.url;
5693
+ let method = req.method;
5694
+ let match = method.toUpperCase();
5695
+ let body;
5696
+ switch (match) {
5697
+ case "PATCH":
5698
+ case "POST":
5699
+ case "PUT":
5700
+ body = await collectRequestBody(req);
5701
+ break;
5702
+ default:
5703
+ body = void 0;
5704
+ }
5705
+ let headersDict = req.headers;
5706
+ let init = {
5707
+ method,
5708
+ headers: some(headersDict),
5709
+ body: map(body, (b) => b)
5710
+ };
5711
+ if (body !== void 0) {
5712
+ init["duplex"] = "half";
5713
+ }
5714
+ return new Request(url2, init);
5715
+ }
5716
+ async function writeWebResponse(webResponse, res) {
5717
+ res.statusCode = webResponse.status;
5718
+ copyHeaders(webResponse.headers, res);
5719
+ let body = webResponse.body;
5720
+ if (body !== null) {
5721
+ let reader = body.getReader();
5722
+ let decoder = new TextDecoder();
5723
+ let reading = true;
5724
+ while (reading) {
5725
+ let result = await reader.read();
5726
+ if (result.done) {
5727
+ reading = false;
5728
+ } else {
5729
+ let chunk = result.value;
5730
+ if (!(chunk == null)) {
5731
+ let text = decoder.decode(chunk, {
5732
+ stream: true
5733
+ });
5734
+ res.write(text);
5735
+ }
5736
+ }
5737
+ }
5738
+ res.end();
5739
+ return;
5740
+ }
5741
+ res.end();
5742
+ }
5743
+ function adaptToConnect(middleware, basePath) {
5744
+ let prefix = `/` + basePath;
5745
+ return (req, res, next) => {
5746
+ let reqPath = getOr(req.url.split("?")[0], req.url);
5747
+ if (!(reqPath === prefix || reqPath.startsWith(prefix + `/`))) {
5748
+ return next();
5749
+ }
5750
+ let handleRequest = async () => {
5751
+ let webRequest = await toWebRequest(req);
5752
+ let maybeResponse = await middleware(webRequest);
5753
+ if (maybeResponse !== void 0) {
5754
+ return await writeWebResponse(maybeResponse, res);
5755
+ } else {
5756
+ return next();
5757
+ }
5758
+ };
5759
+ $$catch(handleRequest(), (error) => {
5760
+ console.error("[Frontman] Middleware error:", error);
5761
+ if (res.headersSent) {
5762
+ res.end();
5763
+ } else {
5764
+ res.statusCode = 500;
5765
+ res.end("Internal Server Error");
5766
+ }
5767
+ return Promise.resolve();
5768
+ });
5690
5769
  };
5691
5770
  }
5692
5771
 
@@ -5695,33 +5774,61 @@ var icon = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewB
5695
5774
  function getToolbarAppPath() {
5696
5775
  return new URL("./toolbar.js", import.meta.url).pathname;
5697
5776
  }
5698
- function make3() {
5777
+ var annotationCaptureScript = `(function() {
5778
+ function captureAnnotations() {
5779
+ var annotations = new Map();
5780
+ document.querySelectorAll('[data-astro-source-file]').forEach(function(el) {
5781
+ annotations.set(el, {
5782
+ file: el.getAttribute('data-astro-source-file'),
5783
+ loc: el.getAttribute('data-astro-source-loc')
5784
+ });
5785
+ });
5786
+ window.__frontman_annotations__ = {
5787
+ _map: annotations,
5788
+ get: function(el) { return annotations.get(el); },
5789
+ has: function(el) { return annotations.has(el); },
5790
+ size: function() { return annotations.size; }
5791
+ };
5792
+ }
5793
+ // Capture once on initial DOM parse
5794
+ document.addEventListener('DOMContentLoaded', captureAnnotations);
5795
+ // Re-capture on View Transitions (SPA navigations) \u2014 skips the initial
5796
+ // page-load event since DOMContentLoaded already captured
5797
+ var initialLoad = true;
5798
+ document.addEventListener('astro:page-load', function() {
5799
+ if (initialLoad) { initialLoad = false; return; }
5800
+ captureAnnotations();
5801
+ });
5802
+ })();`;
5803
+ function make3(configInput) {
5804
+ let config = makeFromObject(configInput);
5699
5805
  return {
5700
5806
  name: "frontman",
5701
5807
  hooks: {
5702
5808
  "astro:config:setup": (ctx2) => {
5703
5809
  if (ctx2.command === "dev") {
5704
- return ctx2.addDevToolbarApp({
5810
+ if (!ctx2.config.devToolbar.enabled) {
5811
+ 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.");
5812
+ }
5813
+ ctx2.addDevToolbarApp({
5705
5814
  id: "frontman:toolbar",
5706
5815
  name: "Frontman",
5707
5816
  icon,
5708
5817
  entrypoint: getToolbarAppPath()
5709
5818
  });
5819
+ return ctx2.injectScript("head-inline", annotationCaptureScript);
5710
5820
  }
5821
+ },
5822
+ "astro:server:setup": (param) => {
5823
+ let webMiddleware = createMiddleware(config);
5824
+ let connectMiddleware = adaptToConnect(webMiddleware, config.basePath);
5825
+ param.server.middlewares.use(connectMiddleware);
5711
5826
  }
5712
5827
  }
5713
5828
  };
5714
5829
  }
5715
5830
 
5716
5831
  // 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
5832
  var frontmanIntegration = make3;
5726
5833
 
5727
- export { Config, Integration, Middleware, SSE, Server, ToolRegistry, createMiddleware2 as createMiddleware, frontmanIntegration, makeConfig };
5834
+ export { frontmanIntegration as default, frontmanIntegration };