isotone 1.1.2 → 1.2.0

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.
Files changed (54) hide show
  1. package/AGENTS.md +84 -6
  2. package/CHANGELOG.md +46 -0
  3. package/README.md +64 -14
  4. package/dist/{IterationPanel-phJRYtUO.d.ts → IterationPanel-Bq77d5NT.d.ts} +3 -1
  5. package/dist/{chunk-FKEKBZ3E.js → chunk-2FY2JNPG.js} +95 -9
  6. package/dist/chunk-2FY2JNPG.js.map +1 -0
  7. package/dist/chunk-BDTB3DFR.js +100 -0
  8. package/dist/chunk-BDTB3DFR.js.map +1 -0
  9. package/dist/{chunk-PUXQGUMQ.js → chunk-BTJHQXF7.js} +104 -5
  10. package/dist/chunk-BTJHQXF7.js.map +1 -0
  11. package/dist/chunk-JCTWHR7U.js +13 -0
  12. package/dist/chunk-JCTWHR7U.js.map +1 -0
  13. package/dist/{chunk-I7CZUPDO.js → chunk-KRS57POF.js} +17 -33
  14. package/dist/chunk-KRS57POF.js.map +1 -0
  15. package/dist/chunk-L6O4HYGP.js +1150 -0
  16. package/dist/chunk-L6O4HYGP.js.map +1 -0
  17. package/dist/chunk-MX4RQJFV.js +9 -0
  18. package/dist/chunk-MX4RQJFV.js.map +1 -0
  19. package/dist/chunk-YPDV3PX4.js +142 -0
  20. package/dist/chunk-YPDV3PX4.js.map +1 -0
  21. package/dist/cli.js +1079 -148
  22. package/dist/cli.js.map +1 -1
  23. package/dist/core.d.ts +135 -10
  24. package/dist/core.js +12 -3
  25. package/dist/index.d.ts +13 -28
  26. package/dist/index.js +618 -281
  27. package/dist/index.js.map +1 -1
  28. package/dist/isotone.global.js +98 -42
  29. package/dist/serve.d.ts +5 -3
  30. package/dist/serve.js +60 -23
  31. package/dist/serve.js.map +1 -1
  32. package/dist/server.d.ts +7 -0
  33. package/dist/server.js +24 -11
  34. package/dist/server.js.map +1 -1
  35. package/dist/vite-plugin.d.ts +4 -3
  36. package/dist/vite-plugin.js +48 -128
  37. package/dist/vite-plugin.js.map +1 -1
  38. package/docs/blast.md +164 -0
  39. package/docs/integrations.md +75 -15
  40. package/docs/migration.md +19 -0
  41. package/docs/updates.md +110 -4
  42. package/docs/variants.md +102 -9
  43. package/package.json +2 -1
  44. package/dist/chunk-A4DX2V4C.js +0 -7
  45. package/dist/chunk-A4DX2V4C.js.map +0 -1
  46. package/dist/chunk-FKEKBZ3E.js.map +0 -1
  47. package/dist/chunk-I7CZUPDO.js.map +0 -1
  48. package/dist/chunk-PUXQGUMQ.js.map +0 -1
  49. package/dist/chunk-RQ2YI5KS.js +0 -24
  50. package/dist/chunk-RQ2YI5KS.js.map +0 -1
  51. package/dist/chunk-TGAU3WTF.js +0 -549
  52. package/dist/chunk-TGAU3WTF.js.map +0 -1
  53. package/dist/chunk-X6V2AKJL.js +0 -375
  54. package/dist/chunk-X6V2AKJL.js.map +0 -1
package/dist/serve.d.ts CHANGED
@@ -6,9 +6,11 @@ declare function readBundle(): string;
6
6
 
7
7
  declare const DEFAULT_PORT = 4123;
8
8
  interface ServeIterationOptions {
9
- /** Background npm notice, at most once a day. false disables the check. */
10
- checkForUpdates?: boolean;
11
- /** Project directory for the suggested update command. Defaults to the notes directory. */
9
+ /** Background npm notice, at most once a day. false disables the check;
10
+ * 'note' also files an update note for your agent once per new version. */
11
+ checkForUpdates?: boolean | 'note';
12
+ /** Project directory for the suggested update command and for
13
+ * `.isotone/knobs.json`. Defaults to the notes directory. */
12
14
  projectRoot?: string;
13
15
  /** Defaults to auto (page background); system follows the OS preference. */
14
16
  theme?: IterationTheme;
package/dist/serve.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import {
2
- readBundle
3
- } from "./chunk-RQ2YI5KS.js";
2
+ autoUpdateNote,
3
+ readBundle,
4
+ updateResponse
5
+ } from "./chunk-YPDV3PX4.js";
4
6
  import {
5
7
  HttpError,
6
8
  checkOrigin,
@@ -8,20 +10,42 @@ import {
8
10
  readNodeJson,
9
11
  sendFailure
10
12
  } from "./chunk-DDMHJ23I.js";
13
+ import {
14
+ guardedScriptTag
15
+ } from "./chunk-MX4RQJFV.js";
11
16
  import {
12
17
  UPDATE_ENDPOINT,
13
18
  startUpdateCheck
14
- } from "./chunk-I7CZUPDO.js";
19
+ } from "./chunk-KRS57POF.js";
15
20
  import {
16
21
  NOTES_ENDPOINT,
17
22
  ensureFileIgnored,
18
- notesResponse
19
- } from "./chunk-X6V2AKJL.js";
20
- import "./chunk-A4DX2V4C.js";
23
+ notesResponse,
24
+ readKnobs
25
+ } from "./chunk-L6O4HYGP.js";
26
+ import "./chunk-JCTWHR7U.js";
27
+ import {
28
+ VARIANTS_ENDPOINT
29
+ } from "./chunk-BTJHQXF7.js";
21
30
 
22
31
  // src/serve/index.ts
23
32
  import { createServer } from "http";
24
33
  import { dirname, resolve } from "path";
34
+
35
+ // src/serve/mount-config.ts
36
+ function serveMountOptions(origin, projectRoot, opts) {
37
+ const knobs = readKnobs({ root: projectRoot }).length > 0;
38
+ return {
39
+ endpoint: `${origin}${NOTES_ENDPOINT}`,
40
+ ...knobs ? { variants: "knobs", variantsEndpoint: `${origin}${VARIANTS_ENDPOINT}` } : {},
41
+ updateEndpoint: `${origin}${UPDATE_ENDPOINT}`,
42
+ key: opts.key ?? "",
43
+ rule: opts.rule ?? {},
44
+ theme: opts.theme
45
+ };
46
+ }
47
+
48
+ // src/serve/index.ts
25
49
  var DEFAULT_PORT = 4123;
26
50
  var BUNDLE_PATH = "/isotone.js";
27
51
  function allowedOrigin(origin) {
@@ -44,14 +68,8 @@ function cors(req, res) {
44
68
  res.setHeader("Access-Control-Max-Age", "600");
45
69
  }
46
70
  }
47
- function bundleWithMount(origin, opts) {
48
- const config = JSON.stringify({
49
- endpoint: `${origin}${NOTES_ENDPOINT}`,
50
- updateEndpoint: `${origin}${UPDATE_ENDPOINT}`,
51
- key: opts.key ?? "",
52
- rule: opts.rule ?? {},
53
- theme: opts.theme
54
- });
71
+ function bundleWithMount(origin, opts, projectRoot) {
72
+ const config = JSON.stringify(serveMountOptions(origin, projectRoot, opts));
55
73
  return `${readBundle()}
56
74
  ;window.__isotone&&window.__isotone.mount(${config});
57
75
  `;
@@ -65,7 +83,7 @@ function escapeHtml(v) {
65
83
  return v.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
66
84
  }
67
85
  function indexPage(origin, file) {
68
- const tag = `<script src="${origin}${BUNDLE_PATH}" defer></script>`;
86
+ const tag = guardedScriptTag(`${origin}${BUNDLE_PATH}`);
69
87
  const mark = bookmarklet(origin);
70
88
  return `<!doctype html>
71
89
  <html lang="en">
@@ -89,7 +107,7 @@ function indexPage(origin, file) {
89
107
  box-shadow: 0 0 10px rgba(74,222,128,.5); }
90
108
  h1 { font-family: var(--mono); font-size: 14px; font-weight: 500; letter-spacing: .02em; }
91
109
  .sub { color: var(--muted); font-size: 13px; margin-bottom: 40px; }
92
- .sub code { font-family: var(--mono); font-size: 11.5px; color: var(--fg); }
110
+ .sub code, p.note code { font-family: var(--mono); font-size: 11.5px; color: var(--fg); }
93
111
  h2 { font-family: var(--mono); font-size: 10.5px; font-weight: 500;
94
112
  letter-spacing: .08em; text-transform: uppercase; color: var(--faint);
95
113
  margin: 32px 0 10px; }
@@ -122,12 +140,14 @@ function indexPage(origin, file) {
122
140
  <header><span class="dot"></span><h1>Isotone</h1></header>
123
141
  <p class="sub">Serving the feedback loop on <code>${escapeHtml(origin)}</code>.</p>
124
142
 
125
- <h2>Script tag for your dev page</h2>
143
+ <h2>Script for your dev pages</h2>
126
144
  <div class="card row">
127
145
  <pre>${escapeHtml(tag)}</pre>
128
146
  <button type="button" class="copy" data-c="${escapeHtml(tag)}"
129
147
  onclick="navigator.clipboard.writeText(this.dataset.c);this.textContent='copied';setTimeout(()=>this.textContent='copy',1200)">copy</button>
130
148
  </div>
149
+ <p class="note">It loads the panel only on localhost, so it stays inert if the page ships.
150
+ Behind a development flag, a plain <code>&lt;script src="${escapeHtml(origin + BUNDLE_PATH)}" defer&gt;</code> works too.</p>
131
151
 
132
152
  <h2>Bookmarklet \u2014 drag to your bookmarks bar</h2>
133
153
  <a class="bm" href="${escapeHtml(mark)}">\u270E Iteration</a>
@@ -143,11 +163,14 @@ function indexPage(origin, file) {
143
163
  }
144
164
  function serveIteration(opts) {
145
165
  const file = resolve(opts.file);
166
+ const projectRoot = resolve(opts.projectRoot ?? dirname(file));
146
167
  const port = opts.port ?? DEFAULT_PORT;
147
168
  const host = opts.host ?? "127.0.0.1";
148
169
  if (!isLoopback(host)) throw new Error("isotone serve binds only to localhost, 127.0.0.1 or ::1.");
149
170
  ensureFileIgnored(file);
150
- const updates = startUpdateCheck(opts.projectRoot ?? dirname(file), opts.checkForUpdates !== false);
171
+ const updates = startUpdateCheck(projectRoot, opts.checkForUpdates !== false);
172
+ if (opts.checkForUpdates === "note") void updates.ready.then(() => autoUpdateNote(file, updates.getSnapshot())).catch(() => {
173
+ });
151
174
  let boundPort = port;
152
175
  const server = createServer((req, res) => {
153
176
  cors(req, res);
@@ -167,16 +190,18 @@ function serveIteration(opts) {
167
190
  return;
168
191
  }
169
192
  if (path === UPDATE_ENDPOINT) {
170
- res.setHeader("content-type", "application/json; charset=utf-8");
171
- res.setHeader("cache-control", "no-store");
172
- res.statusCode = method === "GET" ? 200 : 405;
173
- res.end(JSON.stringify(method === "GET" ? updates.getSnapshot() : { error: "method not allowed" }));
193
+ (method === "POST" ? readNodeJson(req) : Promise.resolve(void 0)).then((body) => {
194
+ const out = updateResponse(file, updates.getSnapshot(), { method, body });
195
+ res.statusCode = out.status;
196
+ for (const [key, value] of Object.entries(out.headers)) res.setHeader(key, value);
197
+ res.end(JSON.stringify(out.body));
198
+ }).catch((error) => sendFailure(res, error));
174
199
  return;
175
200
  }
176
201
  if (path === BUNDLE_PATH && method === "GET") {
177
202
  let body;
178
203
  try {
179
- body = bundleWithMount(origin, opts);
204
+ body = bundleWithMount(origin, opts, projectRoot);
180
205
  } catch (err) {
181
206
  res.statusCode = 500;
182
207
  res.setHeader("content-type", "text/plain; charset=utf-8");
@@ -189,6 +214,18 @@ function serveIteration(opts) {
189
214
  res.end(body);
190
215
  return;
191
216
  }
217
+ if (path === VARIANTS_ENDPOINT || path.startsWith(`${VARIANTS_ENDPOINT}/`)) {
218
+ res.setHeader("content-type", "application/json; charset=utf-8");
219
+ res.setHeader("cache-control", "no-store");
220
+ if (path === VARIANTS_ENDPOINT && method === "GET") {
221
+ res.statusCode = 200;
222
+ res.end(JSON.stringify({ variants: [], active: [], knobs: readKnobs({ root: projectRoot }) }));
223
+ } else {
224
+ res.statusCode = 405;
225
+ res.end(JSON.stringify({ error: "isotone serve has knobs only; file variants need the Vite plugin" }));
226
+ }
227
+ return;
228
+ }
192
229
  if (path === NOTES_ENDPOINT) {
193
230
  if (method !== "GET" && method !== "POST" && method !== "DELETE") {
194
231
  res.statusCode = 405;
package/dist/serve.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/serve/index.ts"],"sourcesContent":["/**\n * isotone — the standalone dev server.\n *\n * The plug-and-play path. Run it in the repo you are reviewing and it owns the\n * notes file, serves the endpoint, and hands out the panel as a single script\n * so the host app needs no build integration at all:\n *\n * npx isotone serve\n * # then, in dev only, one line in the host's HTML:\n * <script src=\"http://127.0.0.1:4123/isotone.js\" defer></script>\n *\n * Running it inside the repo is what keeps a note unambiguous: the process that\n * owns the file is the checkout the agent is working in, so a route and a\n * selector always point somewhere. Nothing about which port served the page has\n * to be mapped back to a repo.\n *\n * The Vite plugin remains the two-line convenience for a Vite host; this is for\n * every other runtime, and for a page whose dev server you cannot configure at\n * all (paste the bookmarklet the index page prints).\n */\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport { dirname, resolve } from 'node:path';\n\nimport type { RouteGateOptions } from '../core/index';\nimport type { IterationTheme } from '../react/theme';\nimport { checkOrigin, HttpError, isLoopback, readNodeJson, sendFailure } from '../server/http';\nimport { ensureFileIgnored, notesResponse, NOTES_ENDPOINT } from '../store';\n\nimport { readBundle } from '../standalone/bundle';\nimport { startUpdateCheck, UPDATE_ENDPOINT } from '../update';\nexport { readBundle } from '../standalone/bundle';\n\nexport const DEFAULT_PORT = 4123;\nconst BUNDLE_PATH = '/isotone.js';\n\nexport interface ServeIterationOptions {\n /** Background npm notice, at most once a day. false disables the check. */\n checkForUpdates?: boolean;\n /** Project directory for the suggested update command. Defaults to the notes directory. */\n projectRoot?: string;\n /** Defaults to auto (page background); system follows the OS preference. */\n theme?: IterationTheme;\n /** Where notes persist. Git-ignored automatically via `.git/info/exclude`. */\n file: string;\n /** Defaults to 4123. */\n port?: number;\n /** Defaults to 127.0.0.1 — a dev loop has no business on 0.0.0.0. */\n host?: string;\n /** Route visibility rule handed to the panel. */\n rule?: RouteGateOptions;\n /** localStorage namespace for the panel. */\n key?: string;\n}\n\nexport interface ServedIteration {\n port: number;\n url: string;\n bundleUrl: string;\n close(): Promise<void>;\n}\n\n/**\n * Only localhost may talk to this server.\n *\n * The endpoint writes a file, and a wide-open `*` would let any page you happen\n * to visit post notes into the repo you are reviewing. Restricting the allowed\n * origin to loopback keeps a drive-by from a public site out while leaving\n * every real dev origin (any localhost port, http or https) working.\n */\nfunction allowedOrigin(origin: string | undefined): string | null {\n if (!origin) return null;\n try {\n const { hostname, protocol } = new URL(origin);\n const loopback =\n hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]' || hostname === '::1';\n return loopback && (protocol === 'http:' || protocol === 'https:') ? origin : null;\n } catch {\n return null;\n }\n}\n\nfunction cors(req: IncomingMessage, res: ServerResponse): void {\n const origin = allowedOrigin(req.headers.origin);\n res.setHeader('Vary', 'Origin');\n if (origin) {\n res.setHeader('Access-Control-Allow-Origin', origin);\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type');\n res.setHeader('Access-Control-Max-Age', '600');\n }\n}\n\n/** The bundle plus the mount call, with the config the server itself knows. */\nfunction bundleWithMount(origin: string, opts: ServeIterationOptions): string {\n const config = JSON.stringify({\n endpoint: `${origin}${NOTES_ENDPOINT}`,\n updateEndpoint: `${origin}${UPDATE_ENDPOINT}`,\n key: opts.key ?? '',\n rule: opts.rule ?? {},\n theme: opts.theme,\n });\n return `${readBundle()}\\n;window.__isotone&&window.__isotone.mount(${config});\\n`;\n}\n\nfunction bookmarklet(origin: string): string {\n return `javascript:(function(){var s=document.createElement('script');s.src=${JSON.stringify(\n `${origin}${BUNDLE_PATH}`,\n )};s.defer=true;document.body.appendChild(s);})()`;\n}\n\nfunction escapeHtml(v: string): string {\n return v\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n}\n\nfunction indexPage(origin: string, file: string): string {\n const tag = `<script src=\"${origin}${BUNDLE_PATH}\" defer></script>`;\n const mark = bookmarklet(origin);\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Isotone</title>\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<style>\n * { box-sizing: border-box; margin: 0; }\n :root {\n --bg: #0d0d10; --card: #131317; --line: rgba(255,255,255,.09);\n --fg: #ececf1; --muted: #8f8f9a; --faint: #5f5f6a;\n --sans: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;\n --mono: ui-monospace, \"SF Mono\", Menlo, monospace;\n }\n body { background: var(--bg); color: var(--fg); font: 14px/1.6 var(--sans);\n -webkit-font-smoothing: antialiased; }\n .wrap { max-width: 540px; margin: 0 auto; padding: 12vh 24px 8vh; }\n header { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }\n .dot { width: 7px; height: 7px; border-radius: 9999px; background: #4ade80;\n box-shadow: 0 0 10px rgba(74,222,128,.5); }\n h1 { font-family: var(--mono); font-size: 14px; font-weight: 500; letter-spacing: .02em; }\n .sub { color: var(--muted); font-size: 13px; margin-bottom: 40px; }\n .sub code { font-family: var(--mono); font-size: 11.5px; color: var(--fg); }\n h2 { font-family: var(--mono); font-size: 10.5px; font-weight: 500;\n letter-spacing: .08em; text-transform: uppercase; color: var(--faint);\n margin: 32px 0 10px; }\n .card { background: var(--card); border: 1px solid var(--line);\n border-radius: 12px; padding: 13px 16px; }\n .card.row { display: flex; align-items: center; gap: 14px; }\n .card pre { font-family: var(--mono); font-size: 12px; line-height: 1.7;\n color: var(--fg); overflow-x: auto; white-space: pre;\n scrollbar-width: none; min-width: 0; }\n .card.row pre { flex: 1; }\n .card pre::-webkit-scrollbar { display: none; }\n .card pre .c { color: var(--faint); }\n .copy { flex: none; font-family: var(--mono); font-size: 10px;\n color: var(--faint); background: none; padding: 4px 9px;\n border: 1px solid var(--line); border-radius: 6px; cursor: pointer; }\n .copy:hover { color: var(--fg); border-color: rgba(255,255,255,.2); }\n p.note { color: var(--muted); font-size: 12.5px; margin-top: 10px; }\n a.bm { display: inline-flex; align-items: center; gap: 8px; margin-top: 2px;\n padding: 8px 14px; border: 1px solid var(--line); border-radius: 8px;\n background: var(--card); color: var(--fg); text-decoration: none;\n font-family: var(--mono); font-size: 12px; }\n a.bm:hover { border-color: rgba(255,255,255,.2); }\n footer { margin-top: 48px; padding-top: 16px; border-top: 1px solid var(--line);\n font-family: var(--mono); font-size: 10.5px; color: var(--faint);\n line-height: 2; overflow-wrap: anywhere; }\n</style>\n</head>\n<body>\n<div class=\"wrap\">\n <header><span class=\"dot\"></span><h1>Isotone</h1></header>\n <p class=\"sub\">Serving the feedback loop on <code>${escapeHtml(origin)}</code>.</p>\n\n <h2>Script tag for your dev page</h2>\n <div class=\"card row\">\n <pre>${escapeHtml(tag)}</pre>\n <button type=\"button\" class=\"copy\" data-c=\"${escapeHtml(tag)}\"\n onclick=\"navigator.clipboard.writeText(this.dataset.c);this.textContent='copied';setTimeout(()=>this.textContent='copy',1200)\">copy</button>\n </div>\n\n <h2>Bookmarklet — drag to your bookmarks bar</h2>\n <a class=\"bm\" href=\"${escapeHtml(mark)}\">✎ Iteration</a>\n\n <footer>\n notes → ${escapeHtml(file)}<br>\n loopback origins only\n </footer>\n</div>\n</body>\n</html>\n`;\n}\n\n/** Start the server. Resolves once it is listening. */\nexport function serveIteration(\n opts: ServeIterationOptions,\n): Promise<ServedIteration> {\n const file = resolve(opts.file);\n const port = opts.port ?? DEFAULT_PORT;\n const host = opts.host ?? '127.0.0.1';\n if (!isLoopback(host)) throw new Error('isotone serve binds only to localhost, 127.0.0.1 or ::1.');\n ensureFileIgnored(file);\n const updates = startUpdateCheck(opts.projectRoot ?? dirname(file), opts.checkForUpdates !== false);\n\n // The port we were ASKED for is not necessarily the one we got: 0 means \"any\n // free port\". The panel's endpoint has to be the bound one or the bundle\n // ships a URL that answers nothing.\n let boundPort = port;\n\n const server: Server = createServer((req, res) => {\n cors(req, res);\n const path = new URL(req.url ?? '/', 'http://localhost').pathname;\n const method = req.method ?? 'GET';\n const origin = `http://${host.includes(':') && !host.startsWith('[') ? `[${host}]` : host}:${boundPort}`;\n try {\n if (!isLoopback(new URL(`http://${req.headers.host ?? ''}`).hostname)) throw new HttpError(403, 'host not allowed');\n checkOrigin(req.headers.origin, req.headers.host ?? '', true);\n } catch (error) { sendFailure(res, error); return; }\n\n if (method === 'OPTIONS') {\n res.statusCode = 204;\n res.end();\n return;\n }\n\n if (path === UPDATE_ENDPOINT) {\n res.setHeader('content-type', 'application/json; charset=utf-8');\n res.setHeader('cache-control', 'no-store');\n res.statusCode = method === 'GET' ? 200 : 405;\n res.end(JSON.stringify(method === 'GET' ? updates.getSnapshot() : { error: 'method not allowed' }));\n return;\n }\n\n if (path === BUNDLE_PATH && method === 'GET') {\n let body: string;\n try {\n body = bundleWithMount(origin, opts);\n } catch (err) {\n res.statusCode = 500;\n res.setHeader('content-type', 'text/plain; charset=utf-8');\n res.end(String(err instanceof Error ? err.message : err));\n return;\n }\n res.statusCode = 200;\n res.setHeader('content-type', 'text/javascript; charset=utf-8');\n res.setHeader('cache-control', 'no-store');\n res.end(body);\n return;\n }\n\n if (path === NOTES_ENDPOINT) {\n if (method !== 'GET' && method !== 'POST' && method !== 'DELETE') {\n res.statusCode = 405;\n res.setHeader('content-type', 'application/json; charset=utf-8');\n res.end(JSON.stringify({ error: 'method not allowed' }));\n return;\n }\n (method === 'POST' ? readNodeJson(req) : Promise.resolve(undefined)).then((body) => {\n const out = notesResponse(file, { method, url: req.url, body });\n res.statusCode = out.status;\n for (const [key, value] of Object.entries(out.headers)) res.setHeader(key, value);\n res.end(JSON.stringify(out.body));\n }).catch((error) => sendFailure(res, error));\n return;\n }\n\n if (path === '/' && method === 'GET') {\n res.statusCode = 200;\n res.setHeader('content-type', 'text/html; charset=utf-8');\n res.end(indexPage(origin, file));\n return;\n }\n\n res.statusCode = 404;\n res.setHeader('content-type', 'text/plain; charset=utf-8');\n res.end('not found');\n });\n\n server.once('close', updates.dispose);\n return new Promise((ok, fail) => {\n server.once('error', (error) => { updates.dispose(); fail(error); });\n server.listen(port, host, () => {\n const actual = server.address();\n boundPort = typeof actual === 'object' && actual ? actual.port : port;\n const url = `http://${host.includes(':') && !host.startsWith('[') ? `[${host}]` : host}:${boundPort}`;\n ok({\n port: boundPort,\n url,\n bundleUrl: `${url}${BUNDLE_PATH}`,\n close: () =>\n new Promise<void>((done) => {\n server.close(() => done());\n }),\n });\n });\n });\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/serve/index.ts","../src/serve/mount-config.ts"],"sourcesContent":["/**\n * isotone — the standalone dev server.\n *\n * The plug-and-play path. Run it in the repo you are reviewing and it owns the\n * notes file, serves the endpoint, and hands out the panel as a single script\n * so the host app needs no build integration at all:\n *\n * npx isotone serve\n * # then, in dev only, one line in the host's HTML:\n * <script src=\"http://127.0.0.1:4123/isotone.js\" defer></script>\n *\n * Running it inside the repo is what keeps a note unambiguous: the process that\n * owns the file is the checkout the agent is working in, so a route and a\n * selector always point somewhere. Nothing about which port served the page has\n * to be mapped back to a repo.\n *\n * The Vite plugin remains the two-line convenience for a Vite host; this is for\n * every other runtime, and for a page whose dev server you cannot configure at\n * all (paste the bookmarklet the index page prints).\n */\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';\nimport { dirname, resolve } from 'node:path';\n\nimport type { RouteGateOptions } from '../core/index';\nimport type { IterationTheme } from '../react/theme';\nimport { checkOrigin, HttpError, isLoopback, readNodeJson, sendFailure } from '../server/http';\nimport { ensureFileIgnored, notesResponse, NOTES_ENDPOINT } from '../store';\nimport { VARIANTS_ENDPOINT } from '../core/variants';\nimport { readKnobs } from '../variants-store';\n\nimport { readBundle } from '../standalone/bundle';\nimport { serveMountOptions } from './mount-config';\nimport { guardedScriptTag } from './tag';\nimport { startUpdateCheck, UPDATE_ENDPOINT } from '../update';\nimport { autoUpdateNote, updateResponse } from '../update-note';\nexport { readBundle } from '../standalone/bundle';\n\nexport const DEFAULT_PORT = 4123;\nconst BUNDLE_PATH = '/isotone.js';\n\nexport interface ServeIterationOptions {\n /** Background npm notice, at most once a day. false disables the check;\n * 'note' also files an update note for your agent once per new version. */\n checkForUpdates?: boolean | 'note';\n /** Project directory for the suggested update command and for\n * `.isotone/knobs.json`. Defaults to the notes directory. */\n projectRoot?: string;\n /** Defaults to auto (page background); system follows the OS preference. */\n theme?: IterationTheme;\n /** Where notes persist. Git-ignored automatically via `.git/info/exclude`. */\n file: string;\n /** Defaults to 4123. */\n port?: number;\n /** Defaults to 127.0.0.1 — a dev loop has no business on 0.0.0.0. */\n host?: string;\n /** Route visibility rule handed to the panel. */\n rule?: RouteGateOptions;\n /** localStorage namespace for the panel. */\n key?: string;\n}\n\nexport interface ServedIteration {\n port: number;\n url: string;\n bundleUrl: string;\n close(): Promise<void>;\n}\n\n/**\n * Only localhost may talk to this server.\n *\n * The endpoint writes a file, and a wide-open `*` would let any page you happen\n * to visit post notes into the repo you are reviewing. Restricting the allowed\n * origin to loopback keeps a drive-by from a public site out while leaving\n * every real dev origin (any localhost port, http or https) working.\n */\nfunction allowedOrigin(origin: string | undefined): string | null {\n if (!origin) return null;\n try {\n const { hostname, protocol } = new URL(origin);\n const loopback =\n hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]' || hostname === '::1';\n return loopback && (protocol === 'http:' || protocol === 'https:') ? origin : null;\n } catch {\n return null;\n }\n}\n\nfunction cors(req: IncomingMessage, res: ServerResponse): void {\n const origin = allowedOrigin(req.headers.origin);\n res.setHeader('Vary', 'Origin');\n if (origin) {\n res.setHeader('Access-Control-Allow-Origin', origin);\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type');\n res.setHeader('Access-Control-Max-Age', '600');\n }\n}\n\n/** The bundle plus the mount call, with the config the server itself knows. */\nfunction bundleWithMount(origin: string, opts: ServeIterationOptions, projectRoot: string): string {\n const config = JSON.stringify(serveMountOptions(origin, projectRoot, opts));\n return `${readBundle()}\\n;window.__isotone&&window.__isotone.mount(${config});\\n`;\n}\n\nfunction bookmarklet(origin: string): string {\n return `javascript:(function(){var s=document.createElement('script');s.src=${JSON.stringify(\n `${origin}${BUNDLE_PATH}`,\n )};s.defer=true;document.body.appendChild(s);})()`;\n}\n\nfunction escapeHtml(v: string): string {\n return v\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n}\n\nfunction indexPage(origin: string, file: string): string {\n const tag = guardedScriptTag(`${origin}${BUNDLE_PATH}`);\n const mark = bookmarklet(origin);\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Isotone</title>\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<style>\n * { box-sizing: border-box; margin: 0; }\n :root {\n --bg: #0d0d10; --card: #131317; --line: rgba(255,255,255,.09);\n --fg: #ececf1; --muted: #8f8f9a; --faint: #5f5f6a;\n --sans: Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;\n --mono: ui-monospace, \"SF Mono\", Menlo, monospace;\n }\n body { background: var(--bg); color: var(--fg); font: 14px/1.6 var(--sans);\n -webkit-font-smoothing: antialiased; }\n .wrap { max-width: 540px; margin: 0 auto; padding: 12vh 24px 8vh; }\n header { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }\n .dot { width: 7px; height: 7px; border-radius: 9999px; background: #4ade80;\n box-shadow: 0 0 10px rgba(74,222,128,.5); }\n h1 { font-family: var(--mono); font-size: 14px; font-weight: 500; letter-spacing: .02em; }\n .sub { color: var(--muted); font-size: 13px; margin-bottom: 40px; }\n .sub code, p.note code { font-family: var(--mono); font-size: 11.5px; color: var(--fg); }\n h2 { font-family: var(--mono); font-size: 10.5px; font-weight: 500;\n letter-spacing: .08em; text-transform: uppercase; color: var(--faint);\n margin: 32px 0 10px; }\n .card { background: var(--card); border: 1px solid var(--line);\n border-radius: 12px; padding: 13px 16px; }\n .card.row { display: flex; align-items: center; gap: 14px; }\n .card pre { font-family: var(--mono); font-size: 12px; line-height: 1.7;\n color: var(--fg); overflow-x: auto; white-space: pre;\n scrollbar-width: none; min-width: 0; }\n .card.row pre { flex: 1; }\n .card pre::-webkit-scrollbar { display: none; }\n .card pre .c { color: var(--faint); }\n .copy { flex: none; font-family: var(--mono); font-size: 10px;\n color: var(--faint); background: none; padding: 4px 9px;\n border: 1px solid var(--line); border-radius: 6px; cursor: pointer; }\n .copy:hover { color: var(--fg); border-color: rgba(255,255,255,.2); }\n p.note { color: var(--muted); font-size: 12.5px; margin-top: 10px; }\n a.bm { display: inline-flex; align-items: center; gap: 8px; margin-top: 2px;\n padding: 8px 14px; border: 1px solid var(--line); border-radius: 8px;\n background: var(--card); color: var(--fg); text-decoration: none;\n font-family: var(--mono); font-size: 12px; }\n a.bm:hover { border-color: rgba(255,255,255,.2); }\n footer { margin-top: 48px; padding-top: 16px; border-top: 1px solid var(--line);\n font-family: var(--mono); font-size: 10.5px; color: var(--faint);\n line-height: 2; overflow-wrap: anywhere; }\n</style>\n</head>\n<body>\n<div class=\"wrap\">\n <header><span class=\"dot\"></span><h1>Isotone</h1></header>\n <p class=\"sub\">Serving the feedback loop on <code>${escapeHtml(origin)}</code>.</p>\n\n <h2>Script for your dev pages</h2>\n <div class=\"card row\">\n <pre>${escapeHtml(tag)}</pre>\n <button type=\"button\" class=\"copy\" data-c=\"${escapeHtml(tag)}\"\n onclick=\"navigator.clipboard.writeText(this.dataset.c);this.textContent='copied';setTimeout(()=>this.textContent='copy',1200)\">copy</button>\n </div>\n <p class=\"note\">It loads the panel only on localhost, so it stays inert if the page ships.\n Behind a development flag, a plain <code>&lt;script src=\"${escapeHtml(origin + BUNDLE_PATH)}\" defer&gt;</code> works too.</p>\n\n <h2>Bookmarklet — drag to your bookmarks bar</h2>\n <a class=\"bm\" href=\"${escapeHtml(mark)}\">✎ Iteration</a>\n\n <footer>\n notes → ${escapeHtml(file)}<br>\n loopback origins only\n </footer>\n</div>\n</body>\n</html>\n`;\n}\n\n/** Start the server. Resolves once it is listening. */\nexport function serveIteration(\n opts: ServeIterationOptions,\n): Promise<ServedIteration> {\n const file = resolve(opts.file);\n const projectRoot = resolve(opts.projectRoot ?? dirname(file));\n const port = opts.port ?? DEFAULT_PORT;\n const host = opts.host ?? '127.0.0.1';\n if (!isLoopback(host)) throw new Error('isotone serve binds only to localhost, 127.0.0.1 or ::1.');\n ensureFileIgnored(file);\n const updates = startUpdateCheck(projectRoot, opts.checkForUpdates !== false);\n if (opts.checkForUpdates === 'note') void updates.ready.then(() => autoUpdateNote(file, updates.getSnapshot())).catch(() => {});\n\n // The port we were ASKED for is not necessarily the one we got: 0 means \"any\n // free port\". The panel's endpoint has to be the bound one or the bundle\n // ships a URL that answers nothing.\n let boundPort = port;\n\n const server: Server = createServer((req, res) => {\n cors(req, res);\n const path = new URL(req.url ?? '/', 'http://localhost').pathname;\n const method = req.method ?? 'GET';\n const origin = `http://${host.includes(':') && !host.startsWith('[') ? `[${host}]` : host}:${boundPort}`;\n try {\n if (!isLoopback(new URL(`http://${req.headers.host ?? ''}`).hostname)) throw new HttpError(403, 'host not allowed');\n checkOrigin(req.headers.origin, req.headers.host ?? '', true);\n } catch (error) { sendFailure(res, error); return; }\n\n if (method === 'OPTIONS') {\n res.statusCode = 204;\n res.end();\n return;\n }\n\n if (path === UPDATE_ENDPOINT) {\n // POST asks for the update note, built from this server's own check.\n (method === 'POST' ? readNodeJson(req) : Promise.resolve(undefined)).then((body) => {\n const out = updateResponse(file, updates.getSnapshot(), { method, body });\n res.statusCode = out.status;\n for (const [key, value] of Object.entries(out.headers)) res.setHeader(key, value);\n res.end(JSON.stringify(out.body));\n }).catch((error) => sendFailure(res, error));\n return;\n }\n\n if (path === BUNDLE_PATH && method === 'GET') {\n let body: string;\n try {\n body = bundleWithMount(origin, opts, projectRoot);\n } catch (err) {\n res.statusCode = 500;\n res.setHeader('content-type', 'text/plain; charset=utf-8');\n res.end(String(err instanceof Error ? err.message : err));\n return;\n }\n res.statusCode = 200;\n res.setHeader('content-type', 'text/javascript; charset=utf-8');\n res.setHeader('cache-control', 'no-store');\n res.end(body);\n return;\n }\n\n if (path === VARIANTS_ENDPOINT || path.startsWith(`${VARIANTS_ENDPOINT}/`)) {\n res.setHeader('content-type', 'application/json; charset=utf-8');\n res.setHeader('cache-control', 'no-store');\n if (path === VARIANTS_ENDPOINT && method === 'GET') {\n res.statusCode = 200;\n res.end(JSON.stringify({ variants: [], active: [], knobs: readKnobs({ root: projectRoot }) }));\n } else {\n res.statusCode = 405;\n res.end(JSON.stringify({ error: 'isotone serve has knobs only; file variants need the Vite plugin' }));\n }\n return;\n }\n\n if (path === NOTES_ENDPOINT) {\n if (method !== 'GET' && method !== 'POST' && method !== 'DELETE') {\n res.statusCode = 405;\n res.setHeader('content-type', 'application/json; charset=utf-8');\n res.end(JSON.stringify({ error: 'method not allowed' }));\n return;\n }\n (method === 'POST' ? readNodeJson(req) : Promise.resolve(undefined)).then((body) => {\n const out = notesResponse(file, { method, url: req.url, body });\n res.statusCode = out.status;\n for (const [key, value] of Object.entries(out.headers)) res.setHeader(key, value);\n res.end(JSON.stringify(out.body));\n }).catch((error) => sendFailure(res, error));\n return;\n }\n\n if (path === '/' && method === 'GET') {\n res.statusCode = 200;\n res.setHeader('content-type', 'text/html; charset=utf-8');\n res.end(indexPage(origin, file));\n return;\n }\n\n res.statusCode = 404;\n res.setHeader('content-type', 'text/plain; charset=utf-8');\n res.end('not found');\n });\n\n server.once('close', updates.dispose);\n return new Promise((ok, fail) => {\n server.once('error', (error) => { updates.dispose(); fail(error); });\n server.listen(port, host, () => {\n const actual = server.address();\n boundPort = typeof actual === 'object' && actual ? actual.port : port;\n const url = `http://${host.includes(':') && !host.startsWith('[') ? `[${host}]` : host}:${boundPort}`;\n ok({\n port: boundPort,\n url,\n bundleUrl: `${url}${BUNDLE_PATH}`,\n close: () =>\n new Promise<void>((done) => {\n server.close(() => done());\n }),\n });\n });\n });\n}\n","import type { RouteGateOptions } from '../core/index';\nimport { VARIANTS_ENDPOINT } from '../core/variants';\nimport type { MountIterationOptions } from '../react/IterationMount';\nimport type { IterationTheme } from '../react/theme';\nimport { NOTES_ENDPOINT } from '../store';\nimport { UPDATE_ENDPOINT } from '../update';\nimport { readKnobs } from '../variants-store';\n\n/**\n * What `isotone serve` hands `mountIteration` in its bundle.\n *\n * Knobs only: file variants need a bundler to swap modules, which a script tag\n * on another dev server does not have. So the panel gets a Variants tool only\n * when `.isotone/knobs.json` holds valid knobs, and its empty list then asks\n * for knobs rather than variants. The bundle is never cached, so reloading the\n * page after the first knob is written brings the tool in.\n */\nexport function serveMountOptions(\n origin: string,\n projectRoot: string,\n opts: { key?: string; rule?: RouteGateOptions; theme?: IterationTheme },\n): MountIterationOptions {\n const knobs = readKnobs({ root: projectRoot }).length > 0;\n return {\n endpoint: `${origin}${NOTES_ENDPOINT}`,\n ...(knobs ? { variants: 'knobs' as const, variantsEndpoint: `${origin}${VARIANTS_ENDPOINT}` } : {}),\n updateEndpoint: `${origin}${UPDATE_ENDPOINT}`,\n key: opts.key ?? '',\n rule: opts.rule ?? {},\n theme: opts.theme,\n };\n}\n"],"mappings":"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","names":[]}
package/dist/server.d.ts CHANGED
@@ -103,6 +103,13 @@ interface ServeRequestOptions {
103
103
  * Off by default: only the API's own origin is accepted.
104
104
  */
105
105
  loopbackOrigins?: boolean;
106
+ /**
107
+ * Answer even when `NODE_ENV` is `production`. Off by default: the handler
108
+ * reads and writes a file in the project, which a deployed app has no
109
+ * business exposing, so a production runtime gets a 404 instead. Set it only
110
+ * for a deliberate, access-controlled review deployment.
111
+ */
112
+ allowProduction?: boolean;
106
113
  }
107
114
  /**
108
115
  * A convenience Next-compatible adapter: given a `Request`/`NextRequest` and the
package/dist/server.js CHANGED
@@ -6,27 +6,38 @@ import {
6
6
  import {
7
7
  ACTIVE_FILE,
8
8
  DEFAULT_VARIANTS_DIR,
9
- listVariants,
10
- readActive,
11
- variantsResponse,
12
- writeActive
13
- } from "./chunk-TGAU3WTF.js";
14
- import {
15
- VARIANTS_ENDPOINT
16
- } from "./chunk-PUXQGUMQ.js";
17
- import {
18
9
  NOTES_ENDPOINT,
19
10
  PAYLOAD_MAX,
20
11
  ensureFileIgnored,
12
+ listVariants,
21
13
  notesResponse,
14
+ readActive,
22
15
  readNotes,
23
16
  updateNotes,
17
+ variantsResponse,
18
+ writeActive,
24
19
  writeNotes
25
- } from "./chunk-X6V2AKJL.js";
26
- import "./chunk-A4DX2V4C.js";
20
+ } from "./chunk-L6O4HYGP.js";
21
+ import "./chunk-JCTWHR7U.js";
22
+ import {
23
+ VARIANTS_ENDPOINT
24
+ } from "./chunk-BTJHQXF7.js";
27
25
 
28
26
  // src/server/index.ts
27
+ function refusedInProduction(options) {
28
+ if (options.allowProduction || !isProductionRuntime()) return null;
29
+ return {
30
+ status: 404,
31
+ headers: { "content-type": "application/json; charset=utf-8" },
32
+ body: { error: "isotone is development-only; this endpoint is off because NODE_ENV is production" }
33
+ };
34
+ }
35
+ function isProductionRuntime() {
36
+ return typeof process !== "undefined" && process.env?.NODE_ENV === "production";
37
+ }
29
38
  async function serveNotesRequest(file, request, options = {}) {
39
+ const refused = refusedInProduction(options);
40
+ if (refused) return refused;
30
41
  try {
31
42
  const url = new URL(request.url);
32
43
  checkOrigin(request.headers.get("origin"), url.host, options.loopbackOrigins);
@@ -41,6 +52,8 @@ async function serveNotesRequest(file, request, options = {}) {
41
52
  }
42
53
  }
43
54
  async function serveVariantsRequest(opts, request, options = {}) {
55
+ const refused = refusedInProduction(options);
56
+ if (refused) return refused;
44
57
  try {
45
58
  const url = new URL(request.url);
46
59
  checkOrigin(request.headers.get("origin"), url.host, options.loopbackOrigins);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server/index.ts"],"sourcesContent":["import { checkOrigin, httpFailure, readWebJson } from './http';\n/**\n * isotone — server.\n *\n * The transport-agnostic side of the loop: the file store and a pure request\n * handler that any dev runtime (Vite middleware, Next route handler, Bun.serve,\n * Vercel function, plain `node:http`) can render. No React and no Vite here —\n * keep this subpath framework-free.\n */\n\nexport {\n ensureFileIgnored,\n notesResponse,\n readNotes,\n writeNotes,\n updateNotes,\n NOTES_ENDPOINT,\n PAYLOAD_MAX,\n} from '../store';\nimport { notesResponse } from '../store';\nexport type {\n NotesRequest,\n NotesResponse,\n StoredNote,\n} from '../store';\n\nexport {\n ACTIVE_FILE,\n DEFAULT_VARIANTS_DIR,\n listVariants,\n readActive,\n variantsResponse,\n writeActive,\n} from '../variants-store';\nimport { variantsResponse } from '../variants-store';\nimport type { VariantsStoreOptions } from '../variants-store';\nexport type { VariantsStoreOptions } from '../variants-store';\nexport { VARIANTS_ENDPOINT } from '../core/variants';\nimport { VARIANTS_ENDPOINT } from '../core/variants';\n\nexport interface ServeRequestOptions {\n /**\n * Accept requests from any loopback page (localhost, 127.0.0.1, [::1]) on\n * any port. Needed when the app's dev server and this API listen on\n * different ports, since the page's origin then never matches the API host.\n * Off by default: only the API's own origin is accepted.\n */\n loopbackOrigins?: boolean;\n}\n\n/**\n * A convenience Next-compatible adapter: given a `Request`/`NextRequest` and the\n * `file`, return a `NextResponse`-shaped `{ status, headers, body }`. Frameworks\n * that expose a `(req, res)` pair call `notesResponse` and render it directly.\n */\nexport async function serveNotesRequest(\n file: string,\n request: Request,\n options: ServeRequestOptions = {},\n): Promise<{ status: number; headers: Record<string, string>; body: unknown }> {\n try {\n const url = new URL(request.url);\n checkOrigin(request.headers.get('origin'), url.host, options.loopbackOrigins);\n const body = request.method === 'POST' ? await readWebJson(request) : undefined;\n return notesResponse(file, {\n method: request.method,\n url: `${url.pathname}?${url.searchParams.toString()}`,\n body,\n });\n } catch (error) {\n return httpFailure(error);\n }\n}\n\n/**\n * Like {@link serveNotesRequest}, for the variants endpoint: given a\n * `Request` whose path ends with `VARIANTS_ENDPOINT` (or a sub-path of it,\n * such as `.../active`), strip the mount prefix before handing the rest to\n * `variantsResponse`, which routes on the path relative to the mount.\n */\nexport async function serveVariantsRequest(\n opts: VariantsStoreOptions,\n request: Request,\n options: ServeRequestOptions = {},\n): Promise<{ status: number; headers: Record<string, string>; body: unknown }> {\n try {\n const url = new URL(request.url);\n checkOrigin(request.headers.get('origin'), url.host, options.loopbackOrigins);\n const body = request.method === 'POST' ? await readWebJson(request) : undefined;\n const mounted = url.pathname.startsWith(VARIANTS_ENDPOINT)\n ? url.pathname.slice(VARIANTS_ENDPOINT.length)\n : url.pathname;\n return variantsResponse(opts, {\n method: request.method,\n url: `${mounted || '/'}?${url.searchParams.toString()}`,\n body,\n });\n } catch (error) {\n return httpFailure(error);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuDA,eAAsB,kBACpB,MACA,SACA,UAA+B,CAAC,GAC6C;AAC7E,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAC/B,gBAAY,QAAQ,QAAQ,IAAI,QAAQ,GAAG,IAAI,MAAM,QAAQ,eAAe;AAC5E,UAAM,OAAO,QAAQ,WAAW,SAAS,MAAM,YAAY,OAAO,IAAI;AACtE,WAAO,cAAc,MAAM;AAAA,MACzB,QAAQ,QAAQ;AAAA,MAChB,KAAK,GAAG,IAAI,QAAQ,IAAI,IAAI,aAAa,SAAS,CAAC;AAAA,MACnD;AAAA,IACF,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,YAAY,KAAK;AAAA,EAC1B;AACF;AAQA,eAAsB,qBACpB,MACA,SACA,UAA+B,CAAC,GAC6C;AAC7E,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAC/B,gBAAY,QAAQ,QAAQ,IAAI,QAAQ,GAAG,IAAI,MAAM,QAAQ,eAAe;AAC5E,UAAM,OAAO,QAAQ,WAAW,SAAS,MAAM,YAAY,OAAO,IAAI;AACtE,UAAM,UAAU,IAAI,SAAS,WAAW,iBAAiB,IACrD,IAAI,SAAS,MAAM,kBAAkB,MAAM,IAC3C,IAAI;AACR,WAAO,iBAAiB,MAAM;AAAA,MAC5B,QAAQ,QAAQ;AAAA,MAChB,KAAK,GAAG,WAAW,GAAG,IAAI,IAAI,aAAa,SAAS,CAAC;AAAA,MACrD;AAAA,IACF,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,YAAY,KAAK;AAAA,EAC1B;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/server/index.ts"],"sourcesContent":["import { checkOrigin, httpFailure, readWebJson } from './http';\n/**\n * isotone — server.\n *\n * The transport-agnostic side of the loop: the file store and a pure request\n * handler that any dev runtime (Vite middleware, Next route handler, Bun.serve,\n * Vercel function, plain `node:http`) can render. No React and no Vite here —\n * keep this subpath framework-free.\n */\n\nexport {\n ensureFileIgnored,\n notesResponse,\n readNotes,\n writeNotes,\n updateNotes,\n NOTES_ENDPOINT,\n PAYLOAD_MAX,\n} from '../store';\nimport { notesResponse } from '../store';\nexport type {\n NotesRequest,\n NotesResponse,\n StoredNote,\n} from '../store';\n\nexport {\n ACTIVE_FILE,\n DEFAULT_VARIANTS_DIR,\n listVariants,\n readActive,\n variantsResponse,\n writeActive,\n} from '../variants-store';\nimport { variantsResponse } from '../variants-store';\nimport type { VariantsStoreOptions } from '../variants-store';\nexport type { VariantsStoreOptions } from '../variants-store';\nexport { VARIANTS_ENDPOINT } from '../core/variants';\nimport { VARIANTS_ENDPOINT } from '../core/variants';\n\nexport interface ServeRequestOptions {\n /**\n * Accept requests from any loopback page (localhost, 127.0.0.1, [::1]) on\n * any port. Needed when the app's dev server and this API listen on\n * different ports, since the page's origin then never matches the API host.\n * Off by default: only the API's own origin is accepted.\n */\n loopbackOrigins?: boolean;\n /**\n * Answer even when `NODE_ENV` is `production`. Off by default: the handler\n * reads and writes a file in the project, which a deployed app has no\n * business exposing, so a production runtime gets a 404 instead. Set it only\n * for a deliberate, access-controlled review deployment.\n */\n allowProduction?: boolean;\n}\n\n/** The answer a production runtime gets unless the host opted in. */\nfunction refusedInProduction(options: ServeRequestOptions): { status: number; headers: Record<string, string>; body: unknown } | null {\n if (options.allowProduction || !isProductionRuntime()) return null;\n return {\n status: 404,\n headers: { 'content-type': 'application/json; charset=utf-8' },\n body: { error: 'isotone is development-only; this endpoint is off because NODE_ENV is production' },\n };\n}\n\n/**\n * Whether this process runs as a production build. Only an explicit\n * `NODE_ENV=production` counts: test runners and hand-started dev servers\n * often leave it unset, and every common production runtime (`next start`,\n * Vercel, Netlify, most container images) sets it.\n */\nfunction isProductionRuntime(): boolean {\n return typeof process !== 'undefined' && process.env?.NODE_ENV === 'production';\n}\n\n/**\n * A convenience Next-compatible adapter: given a `Request`/`NextRequest` and the\n * `file`, return a `NextResponse`-shaped `{ status, headers, body }`. Frameworks\n * that expose a `(req, res)` pair call `notesResponse` and render it directly.\n */\nexport async function serveNotesRequest(\n file: string,\n request: Request,\n options: ServeRequestOptions = {},\n): Promise<{ status: number; headers: Record<string, string>; body: unknown }> {\n const refused = refusedInProduction(options);\n if (refused) return refused;\n try {\n const url = new URL(request.url);\n checkOrigin(request.headers.get('origin'), url.host, options.loopbackOrigins);\n const body = request.method === 'POST' ? await readWebJson(request) : undefined;\n return notesResponse(file, {\n method: request.method,\n url: `${url.pathname}?${url.searchParams.toString()}`,\n body,\n });\n } catch (error) {\n return httpFailure(error);\n }\n}\n\n/**\n * Like {@link serveNotesRequest}, for the variants endpoint: given a\n * `Request` whose path ends with `VARIANTS_ENDPOINT` (or a sub-path of it,\n * such as `.../active`), strip the mount prefix before handing the rest to\n * `variantsResponse`, which routes on the path relative to the mount.\n */\nexport async function serveVariantsRequest(\n opts: VariantsStoreOptions,\n request: Request,\n options: ServeRequestOptions = {},\n): Promise<{ status: number; headers: Record<string, string>; body: unknown }> {\n const refused = refusedInProduction(options);\n if (refused) return refused;\n try {\n const url = new URL(request.url);\n checkOrigin(request.headers.get('origin'), url.host, options.loopbackOrigins);\n const body = request.method === 'POST' ? await readWebJson(request) : undefined;\n const mounted = url.pathname.startsWith(VARIANTS_ENDPOINT)\n ? url.pathname.slice(VARIANTS_ENDPOINT.length)\n : url.pathname;\n return variantsResponse(opts, {\n method: request.method,\n url: `${mounted || '/'}?${url.searchParams.toString()}`,\n body,\n });\n } catch (error) {\n return httpFailure(error);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA0DA,SAAS,oBAAoB,SAAyG;AACpI,MAAI,QAAQ,mBAAmB,CAAC,oBAAoB,EAAG,QAAO;AAC9D,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,kCAAkC;AAAA,IAC7D,MAAM,EAAE,OAAO,mFAAmF;AAAA,EACpG;AACF;AAQA,SAAS,sBAA+B;AACtC,SAAO,OAAO,YAAY,eAAe,QAAQ,KAAK,aAAa;AACrE;AAOA,eAAsB,kBACpB,MACA,SACA,UAA+B,CAAC,GAC6C;AAC7E,QAAM,UAAU,oBAAoB,OAAO;AAC3C,MAAI,QAAS,QAAO;AACpB,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAC/B,gBAAY,QAAQ,QAAQ,IAAI,QAAQ,GAAG,IAAI,MAAM,QAAQ,eAAe;AAC5E,UAAM,OAAO,QAAQ,WAAW,SAAS,MAAM,YAAY,OAAO,IAAI;AACtE,WAAO,cAAc,MAAM;AAAA,MACzB,QAAQ,QAAQ;AAAA,MAChB,KAAK,GAAG,IAAI,QAAQ,IAAI,IAAI,aAAa,SAAS,CAAC;AAAA,MACnD;AAAA,IACF,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,YAAY,KAAK;AAAA,EAC1B;AACF;AAQA,eAAsB,qBACpB,MACA,SACA,UAA+B,CAAC,GAC6C;AAC7E,QAAM,UAAU,oBAAoB,OAAO;AAC3C,MAAI,QAAS,QAAO;AACpB,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAC/B,gBAAY,QAAQ,QAAQ,IAAI,QAAQ,GAAG,IAAI,MAAM,QAAQ,eAAe;AAC5E,UAAM,OAAO,QAAQ,WAAW,SAAS,MAAM,YAAY,OAAO,IAAI;AACtE,UAAM,UAAU,IAAI,SAAS,WAAW,iBAAiB,IACrD,IAAI,SAAS,MAAM,kBAAkB,MAAM,IAC3C,IAAI;AACR,WAAO,iBAAiB,MAAM;AAAA,MAC5B,QAAQ,QAAQ;AAAA,MAChB,KAAK,GAAG,WAAW,GAAG,IAAI,IAAI,aAAa,SAAS,CAAC;AAAA,MACrD;AAAA,IACF,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,YAAY,KAAK;AAAA,EAC1B;AACF;","names":[]}
@@ -1,13 +1,14 @@
1
1
  import { RouteGateOptions } from './core.js';
2
- import { I as IterationShortcut } from './IterationPanel-phJRYtUO.js';
2
+ import { I as IterationShortcut } from './IterationPanel-Bq77d5NT.js';
3
3
  import { I as IterationTheme } from './theme-BWv_ZNhC.js';
4
4
  import 'react';
5
5
 
6
6
  interface IterationViteOptions {
7
7
  /** Inject the bundled panel. false keeps endpoints and variants for a manual mount. */
8
8
  panel?: boolean;
9
- /** Background npm notice, at most once a day. false disables the check. */
10
- checkForUpdates?: boolean;
9
+ /** Background npm notice, at most once a day. false disables the check;
10
+ * 'note' also files an update note for your agent once per new version. */
11
+ checkForUpdates?: boolean | 'note';
11
12
  /** Defaults to auto (page background); system follows the OS preference. */
12
13
  theme?: IterationTheme;
13
14
  /** Defaults to Cmd/Ctrl+Shift+Period; customize or disable with false. */
@@ -1,14 +1,28 @@
1
1
  import {
2
- readBundle
3
- } from "./chunk-RQ2YI5KS.js";
2
+ autoUpdateNote,
3
+ readBundle,
4
+ updateResponse
5
+ } from "./chunk-YPDV3PX4.js";
4
6
  import {
5
7
  checkOrigin,
6
8
  readNodeJson,
7
9
  sendFailure
8
10
  } from "./chunk-DDMHJ23I.js";
11
+ import {
12
+ isRouteModule,
13
+ scanExports
14
+ } from "./chunk-BDTB3DFR.js";
15
+ import {
16
+ UPDATE_ENDPOINT,
17
+ startUpdateCheck
18
+ } from "./chunk-KRS57POF.js";
9
19
  import {
10
20
  ACTIVE_FILE,
21
+ NOTES_ENDPOINT,
22
+ ensureDirIgnored,
23
+ ensureFileIgnored,
11
24
  listVariants,
25
+ notesResponse,
12
26
  overrideSourcePath,
13
27
  readActive,
14
28
  readVariant,
@@ -17,22 +31,12 @@ import {
17
31
  variantDir,
18
32
  variantsDir,
19
33
  variantsResponse
20
- } from "./chunk-TGAU3WTF.js";
34
+ } from "./chunk-L6O4HYGP.js";
35
+ import "./chunk-JCTWHR7U.js";
21
36
  import {
22
37
  VARIANTS_ENDPOINT,
23
38
  overlayGroups
24
- } from "./chunk-PUXQGUMQ.js";
25
- import {
26
- UPDATE_ENDPOINT,
27
- startUpdateCheck
28
- } from "./chunk-I7CZUPDO.js";
29
- import {
30
- NOTES_ENDPOINT,
31
- ensureDirIgnored,
32
- ensureFileIgnored,
33
- notesResponse
34
- } from "./chunk-X6V2AKJL.js";
35
- import "./chunk-A4DX2V4C.js";
39
+ } from "./chunk-BTJHQXF7.js";
36
40
 
37
41
  // src/vite/index.ts
38
42
  import { existsSync, mkdirSync, readFileSync, realpathSync } from "fs";
@@ -41,101 +45,6 @@ import {
41
45
  normalizePath
42
46
  } from "vite";
43
47
 
44
- // src/vite/exports.ts
45
- var ROUTE_EXPORTS = /* @__PURE__ */ new Set([
46
- "loader",
47
- "action",
48
- "clientLoader",
49
- "clientAction",
50
- "headers",
51
- "links",
52
- "meta",
53
- "handle",
54
- "shouldRevalidate",
55
- "middleware",
56
- "clientMiddleware",
57
- "unstable_middleware",
58
- "unstable_clientMiddleware",
59
- "HydrateFallback"
60
- ]);
61
- var IDENT = /^[A-Za-z_$][\w$]*$/;
62
- function scanExports(code) {
63
- const names = /* @__PURE__ */ new Set();
64
- const lines = code.split("\n");
65
- for (let i = 0; i < lines.length; i++) {
66
- const line = (lines[i] ?? "").replace(/^(?:\/\*.*?\*\/\s*)+/, "");
67
- if (!line.startsWith("export")) continue;
68
- const rest = line.slice("export".length);
69
- if (rest !== "" && !/^[\s{*]/.test(rest)) continue;
70
- const body = rest.trim();
71
- if (body.startsWith("{") || body.startsWith("type {")) {
72
- if (body.startsWith("type")) continue;
73
- let clause = body;
74
- while (!clause.includes("}") && i + 1 < lines.length) clause += ` ${lines[++i]}`;
75
- const inner = clause.slice(clause.indexOf("{") + 1, clause.indexOf("}"));
76
- for (const raw of inner.split(",")) {
77
- const spec = raw.replace(/\/\/.*$/, "").trim();
78
- if (!spec || spec.startsWith("type ")) continue;
79
- const as = spec.split(/\s+as\s+/);
80
- const name = (as[1] ?? as[0] ?? "").trim().replace(/^["']|["']$/g, "");
81
- if (!name) return null;
82
- names.add(name);
83
- }
84
- continue;
85
- }
86
- if (/^type\s*\*/.test(body)) continue;
87
- const star = body.match(/^\*\s+as\s+([A-Za-z_$][\w$]*)/);
88
- if (star?.[1]) {
89
- names.add(star[1]);
90
- continue;
91
- }
92
- if (body.startsWith("*")) return null;
93
- if (/^default\b/.test(body)) {
94
- names.add("default");
95
- continue;
96
- }
97
- if (/^(?:declare\s|type\s|interface\s)/.test(body)) continue;
98
- if (/^(?:=|import\s|namespace\s|module\s|as\s)/.test(body)) return null;
99
- const decl = body.match(
100
- /^(?:async\s+)?function\s*\*?\s*([A-Za-z_$][\w$]*)|^(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*)|^(?:const\s+)?enum\s+([A-Za-z_$][\w$]*)/
101
- );
102
- const declared = decl?.[1] ?? decl?.[2] ?? decl?.[3];
103
- if (declared) {
104
- names.add(declared);
105
- continue;
106
- }
107
- const variable = body.match(/^(?:const|let|var)\s+(.*)$/);
108
- if (variable) {
109
- const target = (variable[1] ?? "").trim();
110
- if (target.startsWith("{") || target.startsWith("[")) return null;
111
- const name = target.match(/^([A-Za-z_$][\w$]*)/)?.[1];
112
- if (!name) return null;
113
- if (hasSecondDeclarator(target)) return null;
114
- names.add(name);
115
- continue;
116
- }
117
- return null;
118
- }
119
- for (const name of names) if (name !== "default" && !IDENT.test(name)) return null;
120
- return [...names];
121
- }
122
- function isRouteModule(names) {
123
- return names.includes("default") && names.some((name) => ROUTE_EXPORTS.has(name));
124
- }
125
- function hasSecondDeclarator(declaration) {
126
- const text = declaration.replace(/\/\/.*$/, "").replace(/'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*`/g, '""');
127
- let depth = 0;
128
- let initialized = false;
129
- for (let i = 0; i < text.length; i++) {
130
- const ch = text[i];
131
- if (ch === "(" || ch === "[" || ch === "{" || ch === "<") depth++;
132
- else if (ch === ")" || ch === "]" || ch === "}" || ch === ">" && text[i - 1] !== "=") depth = Math.max(0, depth - 1);
133
- else if (depth === 0 && ch === "=" && text[i + 1] !== "=" && text[i + 1] !== ">" && text[i - 1] !== "=") initialized = true;
134
- else if (depth === 0 && ch === "," && initialized) return true;
135
- }
136
- return depth === 0 && /,\s*$/.test(text);
137
- }
138
-
139
48
  // src/vite/live.ts
140
49
  var VERSION_QUERY = "isotone";
141
50
  var BASE_VERSION = "_base";
@@ -747,12 +656,16 @@ function iteration(options = {}) {
747
656
  };
748
657
  const updates = startUpdateCheck(server.config.root, options.checkForUpdates !== false);
749
658
  server.httpServer?.once("close", updates.dispose);
659
+ if (options.checkForUpdates === "note") {
660
+ void updates.ready.then(() => autoUpdateNote(file, updates.getSnapshot())).catch(() => {
661
+ });
662
+ }
750
663
  server.middlewares.use(UPDATE_ENDPOINT, (req, res) => {
751
664
  if (!guardOrigin(req, res)) return;
752
- res.setHeader("content-type", "application/json; charset=utf-8");
753
- res.setHeader("cache-control", "no-store");
754
- res.statusCode = req.method === "GET" ? 200 : 405;
755
- res.end(JSON.stringify(req.method === "GET" ? updates.getSnapshot() : { error: "method not allowed" }));
665
+ const method = req.method ?? "";
666
+ (method === "POST" ? readNodeJson(req) : Promise.resolve(void 0)).then((body) => {
667
+ answer(res, () => updateResponse(file, updates.getSnapshot(), { method, body }));
668
+ }).catch((error) => sendFailure(res, error));
756
669
  });
757
670
  server.middlewares.use(NOTES_ENDPOINT, (req, res, next) => {
758
671
  if (!guardOrigin(req, res)) return;
@@ -768,20 +681,23 @@ function iteration(options = {}) {
768
681
  if (variantsOptions) {
769
682
  activeIds = readActive(vopts);
770
683
  live = (variantsOptions.live ?? hasReact) && server.environments !== void 0;
771
- const clientGraph = () => server.environments?.client?.moduleGraph ?? server.moduleGraph;
684
+ const graphs = () => server.environments ? Object.values(server.environments).map((env) => env.moduleGraph) : [server.moduleGraph];
685
+ const covered = /* @__PURE__ */ new Set();
772
686
  const signature = () => JSON.stringify(readManifests().map((v) => [v.id, v.group ?? "", Object.keys(v.files).sort()]));
773
- let listed = live ? signature() : "";
687
+ let listed = signature();
774
688
  reloadPage = () => {
775
689
  if (reloadTimer) clearTimeout(reloadTimer);
776
690
  reloadTimer = setTimeout(() => {
777
691
  reloadTimer = null;
778
- const graph = clientGraph();
779
- const paths = /* @__PURE__ */ new Set([...dispatched.keys(), ...versionsByFile().keys()]);
780
- for (const path of paths) {
781
- for (const mod of graph.getModulesByFile(path) ?? []) graph.invalidateModule(mod);
692
+ const paths = /* @__PURE__ */ new Set([...dispatched.keys(), ...versionsByFile().keys(), ...covered]);
693
+ for (const graph of graphs()) {
694
+ for (const path of paths) {
695
+ for (const mod of graph.getModulesByFile(path) ?? []) graph.invalidateModule(mod);
696
+ }
697
+ const runtime = graph.getModuleById(RESOLVED_LIVE_MODULE);
698
+ if (runtime) graph.invalidateModule(runtime);
782
699
  }
783
- const runtime = graph.getModuleById(RESOLVED_LIVE_MODULE);
784
- if (runtime) graph.invalidateModule(runtime);
700
+ covered.clear();
785
701
  dispatched.clear();
786
702
  server.ws.send({ type: "full-reload", path: "*" });
787
703
  }, 50);
@@ -881,13 +797,17 @@ function iteration(options = {}) {
881
797
  if (path === activeFile) {
882
798
  void activate(readActive(vopts));
883
799
  } else if (isInside(vDir, path)) {
800
+ for (const variant of manifests ?? []) {
801
+ for (const key of Object.keys(variant.files)) covered.add(normalizePath(join(vopts.root, key)));
802
+ }
884
803
  forgetManifests();
885
- if (live) {
886
- const now = signature();
887
- if (now !== listed) {
888
- listed = now;
889
- reloadPage();
890
- }
804
+ const stillActive = readActive(vopts);
805
+ const activeChanged = stillActive.join() !== currentActive().join();
806
+ if (activeChanged) void activate(stillActive);
807
+ const now = signature();
808
+ if (now !== listed) {
809
+ listed = now;
810
+ if (live || activeChanged || stillActive.length > 0) reloadPage();
891
811
  }
892
812
  }
893
813
  });