proto-plugin 0.1.8 → 0.1.9

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
@@ -56,7 +56,17 @@ export default page.default;
56
56
  export const generateStaticParams = page.generateStaticParams;
57
57
  ```
58
58
 
59
- 4. **Next.js config** — transpile the package (use a relative import so CSS is not loaded during config evaluation):
59
+ 4. **Component library** — `src/app/component-library/page.tsx` (created automatically on `pnpm install` if missing; or run `pnpm --dir node_modules/proto-plugin setup-host`):
60
+
61
+ ```tsx
62
+ import { createPrototypeComponentLibraryPage } from "proto-plugin";
63
+
64
+ export default createPrototypeComponentLibraryPage();
65
+ ```
66
+
67
+ Pass `children` once you've synced UI from source. Until then, the page shows an empty state with a copyable agent prompt.
68
+
69
+ 5. **Next.js config** — transpile the package (use a relative import so CSS is not loaded during config evaluation):
60
70
 
61
71
  ```ts
62
72
  import { withPrototype } from "./packages/prototype/src/config/with-prototype";
@@ -64,7 +74,7 @@ import { withPrototype } from "./packages/prototype/src/config/with-prototype";
64
74
  export default withPrototype({ nextConfig: { /* … */ } });
65
75
  ```
66
76
 
67
- 5. **Tailwind** — scan prototype package components from the host `globals.css`. Register semantic `--color-*` tokens as self-referential vars so utilities like `text-muted-foreground` resolve from `[data-prototype-root]` at runtime — **do not** map `--color-foreground` / `--color-background` to product `:root` tokens (that breaks gallery contrast):
77
+ 6. **Tailwind** — scan prototype package components from the host `globals.css`. Register semantic `--color-*` tokens as self-referential vars so utilities like `text-muted-foreground` resolve from `[data-prototype-root]` at runtime — **do not** map `--color-foreground` / `--color-background` to product `:root` tokens (that breaks gallery contrast):
68
78
 
69
79
  ```css
70
80
  @import "tailwindcss";
@@ -79,7 +89,7 @@ export default withPrototype({ nextConfig: { /* … */ } });
79
89
 
80
90
  Product tokens use separate names (e.g. `--product-background`) scoped to `[data-prototype-screenshot]`.
81
91
 
82
- 6. **Root layout + tool themes** — wrap routes with `createPrototypeSiteLayout()` and import package chrome CSS (dark/light themes live here, not in host `globals.css`):
92
+ 7. **Root layout + tool themes** — wrap routes with `createPrototypeSiteLayout()` and import package chrome CSS (dark/light themes live here, not in host `globals.css`):
83
93
 
84
94
  ```tsx
85
95
  import { createPrototypeSiteLayout } from "proto-plugin";
@@ -89,7 +99,7 @@ import "./globals.css";
89
99
  const PrototypeSiteLayout = createPrototypeSiteLayout();
90
100
  ```
91
101
 
92
- 7. **tsconfig paths** (optional, for tool internal `@prototype/*` alias when developing the package):
102
+ 8. **tsconfig paths** (optional, for tool internal `@prototype/*` alias when developing the package):
93
103
 
94
104
  ```json
95
105
  {
@@ -102,7 +112,7 @@ const PrototypeSiteLayout = createPrototypeSiteLayout();
102
112
  }
103
113
  ```
104
114
 
105
- 8. **Dev server port** — run the host on port **1985** so it stays separate from a typical source app on 3000. In the host `package.json`:
115
+ 9. **Dev server port** — run the host on port **1985** so it stays separate from a typical source app on 3000. In the host `package.json`:
106
116
 
107
117
  ```json
108
118
  {
@@ -120,6 +130,7 @@ Open [http://localhost:1985](http://localhost:1985) when developing.
120
130
  | --- | --- |
121
131
  | `definePrototypeConfig` | Register prototypes (sets global config as a side effect) |
122
132
  | `createPrototypePage` | Factory for `/prototypes/[slug]` page + `generateStaticParams` |
133
+ | `createPrototypeComponentLibraryPage` | Factory for `/component-library` (empty state until host passes `children`) |
123
134
  | `createPrototypeRegistry` | Server helpers: `getAllPrototypes`, `getPrototype`, … |
124
135
  | `PrototypeComponent`, `PrototypeControl` | Comment-anchorable UI wrappers |
125
136
  | `PrototypeShell` | Review chrome (comments sidebar, screenshot capture) |
@@ -261,16 +272,17 @@ Follow the package README (`node_modules/proto-plugin/README.md`) and implement
261
272
  1. **`prototype.config.ts`** at the repo root — start with an empty `prototypes: []` array using `definePrototypeConfig` from `proto-plugin`.
262
273
  2. **Gallery home** — `src/app/page.tsx` via `createPrototypeGalleryPage(prototypeConfig)`.
263
274
  3. **Prototype route** — `src/app/prototypes/[slug]/page.tsx` via `createPrototypePage(prototypeConfig)` (export `default` + `generateStaticParams`).
264
- 4. **Root layout** — wrap children with `createPrototypeSiteLayout()` from `proto-plugin` in `src/app/layout.tsx`, and import the tool chrome theme:
275
+ 4. **Component library** — `src/app/component-library/page.tsx` via `createPrototypeComponentLibraryPage()` (auto-scaffolded on `pnpm install` if missing; shows empty state + copyable populate prompt until synced UI is wired as `children`).
276
+ 5. **Root layout** — wrap children with `createPrototypeSiteLayout()` from `proto-plugin` in `src/app/layout.tsx`, and import the tool chrome theme:
265
277
 
266
278
  ```tsx
267
279
  import { createPrototypeSiteLayout } from "proto-plugin";
268
280
  import "proto-plugin/styles/globals.css";
269
281
  import "./globals.css";
270
282
  ```
271
- 5. **API catch-all** — `src/app/api/[...path]/route.ts` using `createPrototypeApiRoute(prototypeConfig)` from `proto-plugin/server`; export `GET`, `POST`, and `PUT` handlers that call `dispatch`.
272
- 6. **`next.config.ts`** — wrap with `withPrototype` from `proto-plugin/with-prototype` and ensure `proto-plugin` is transpiled.
273
- 7. **`src/app/globals.css`** — Tailwind v4 + product tokens synced from source. **Do not** put tool chrome dark/light mode here (no `prefers-color-scheme`, no hardcoded gallery colors). Scan package components:
283
+ 6. **API catch-all** — `src/app/api/[...path]/route.ts` using `createPrototypeApiRoute(prototypeConfig)` from `proto-plugin/server`; export `GET`, `POST`, and `PUT` handlers that call `dispatch`.
284
+ 7. **`next.config.ts`** — wrap with `withPrototype` from `proto-plugin/with-prototype` and ensure `proto-plugin` is transpiled.
285
+ 8. **`src/app/globals.css`** — Tailwind v4 + product tokens synced from source. **Do not** put tool chrome dark/light mode here (no `prefers-color-scheme`, no hardcoded gallery colors). Scan package components:
274
286
 
275
287
  ```css
276
288
  @import "tailwindcss";
@@ -279,7 +291,7 @@ Follow the package README (`node_modules/proto-plugin/README.md`) and implement
279
291
 
280
292
  Tool themes live in `proto-plugin/styles/globals.css` (import in layout). The package toggles dark/light on `[data-prototype-root]` via `data-prototype-comment-theme`.
281
293
 
282
- 8. **`package.json` scripts** — set dev to port 1985 and forward CLI commands to the installed package:
294
+ 9. **`package.json` scripts** — set dev to port 1985 and forward CLI commands to the installed package:
283
295
 
284
296
  ```json
285
297
  {
@@ -292,9 +304,9 @@ Follow the package README (`node_modules/proto-plugin/README.md`) and implement
292
304
  }
293
305
  ```
294
306
 
295
- 9. **`.env.example` and `.env.local`** — document and set `SOURCE_PATH` to the source app path I provided. Add comment/screenshot storage vars only if I asked for them in step 5 — the app should run without them, but comments won't persist.
307
+ 10. **`.env.example` and `.env.local`** — document and set `SOURCE_PATH` to the source app path I provided. Add comment/screenshot storage vars only if I asked for them in step 5 — the app should run without them, but comments won't persist.
296
308
 
297
- Run `pnpm dev` and confirm [http://localhost:1985](http://localhost:1985) shows the empty gallery and the app builds cleanly before continuing.
309
+ Run `pnpm dev` and confirm [http://localhost:1985](http://localhost:1985) shows the empty gallery, `/component-library` renders (empty state is fine), and the app builds cleanly before continuing.
298
310
 
299
311
  ## 3. Link the source app
300
312
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "proto-plugin",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org"
@@ -31,6 +31,8 @@
31
31
  "./styles/globals.css": "./src/styles/globals.css"
32
32
  },
33
33
  "scripts": {
34
+ "postinstall": "node scripts/ensure-component-library-page.mjs",
35
+ "setup-host": "node scripts/ensure-component-library-page.mjs",
34
36
  "verify:prototype-ids": "node scripts/verify-prototype-component-ids.mjs",
35
37
  "verify:prototype-preview-states": "node scripts/verify-prototype-preview-states.mjs",
36
38
  "link-source": "./scripts/link-source.sh",
@@ -0,0 +1,89 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import { resolveHostRoot } from "./lib/resolve-host-root.mjs";
6
+
7
+ const PAGE_CONTENT = `import { createPrototypeComponentLibraryPage } from "proto-plugin";
8
+
9
+ // Scaffolded by proto-plugin — pass \`children\` once you've synced UI from source.
10
+ export default createPrototypeComponentLibraryPage();
11
+ `;
12
+
13
+ function resolveAppDir(hostRoot) {
14
+ const srcApp = path.join(hostRoot, "src", "app");
15
+ if (fs.existsSync(srcApp)) {
16
+ return srcApp;
17
+ }
18
+
19
+ const app = path.join(hostRoot, "app");
20
+ if (fs.existsSync(app)) {
21
+ return app;
22
+ }
23
+
24
+ return null;
25
+ }
26
+
27
+ function isPrototypeHost(hostRoot) {
28
+ if (fs.existsSync(path.join(hostRoot, "prototype.config.ts"))) {
29
+ return true;
30
+ }
31
+
32
+ const packageJsonPath = path.join(hostRoot, "package.json");
33
+ if (!fs.existsSync(packageJsonPath)) {
34
+ return false;
35
+ }
36
+
37
+ try {
38
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
39
+ const deps = {
40
+ ...packageJson.dependencies,
41
+ ...packageJson.devDependencies,
42
+ };
43
+ return Boolean(deps["proto-plugin"]);
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ export function ensureComponentLibraryPage(hostRoot) {
50
+ if (!isPrototypeHost(hostRoot)) {
51
+ return { created: false, reason: "not-a-prototype-host" };
52
+ }
53
+
54
+ const appDir = resolveAppDir(hostRoot);
55
+ if (!appDir) {
56
+ return { created: false, reason: "missing-app-dir" };
57
+ }
58
+
59
+ const pageDir = path.join(appDir, "component-library");
60
+ const pagePath = path.join(pageDir, "page.tsx");
61
+
62
+ if (fs.existsSync(pagePath)) {
63
+ return { created: false, reason: "already-exists", pagePath };
64
+ }
65
+
66
+ fs.mkdirSync(pageDir, { recursive: true });
67
+ fs.writeFileSync(pagePath, PAGE_CONTENT, "utf8");
68
+
69
+ return { created: true, pagePath };
70
+ }
71
+
72
+ function main() {
73
+ const toolRoot = path.resolve(
74
+ path.dirname(fileURLToPath(import.meta.url)),
75
+ "..",
76
+ );
77
+ const hostRoot = resolveHostRoot(toolRoot);
78
+ const result = ensureComponentLibraryPage(hostRoot);
79
+
80
+ if (result.created) {
81
+ console.log(
82
+ `proto-plugin: created component library route at ${path.relative(hostRoot, result.pagePath)}`,
83
+ );
84
+ }
85
+ }
86
+
87
+ if (import.meta.url === new URL(process.argv[1], "file:").href) {
88
+ main();
89
+ }
@@ -38,3 +38,5 @@ fi
38
38
 
39
39
  ln -s "$TARGET" "$LINK"
40
40
  echo "Linked source/ -> $TARGET"
41
+
42
+ node "$TOOL_ROOT/scripts/ensure-component-library-page.mjs"
package/src/app.ts CHANGED
@@ -2,3 +2,4 @@ import "server-only";
2
2
 
3
3
  export { createPrototypeGalleryPage } from "./config/create-prototype-gallery-page";
4
4
  export { createPrototypeCatchAllPage } from "./config/create-prototype-host-app";
5
+ export { createPrototypeComponentLibraryPage } from "./config/create-prototype-component-library-page";
@@ -16,17 +16,17 @@ export function PrototypePluginUpdatePopup() {
16
16
  return null;
17
17
  }
18
18
 
19
- const handleCopy = async () => {
19
+ const handleCopyPrompt = async () => {
20
20
  try {
21
21
  await copy(
22
22
  buildUpgradeProtoPluginCopyText({
23
- installed: status.installed!,
24
- latest: status.latest!,
23
+ installed: status.installed,
24
+ latest: status.latest,
25
25
  }),
26
26
  );
27
27
  } catch {
28
28
  toast.error("Copy error", {
29
- description: "Failed to copy upgrade prompt",
29
+ description: "Failed to copy update prompt",
30
30
  });
31
31
  }
32
32
  };
@@ -36,37 +36,33 @@ export function PrototypePluginUpdatePopup() {
36
36
  role="status"
37
37
  aria-live="polite"
38
38
  className={cn(
39
- "border-border bg-card ring-border pointer-events-auto fixed bottom-4 left-4 z-[1050] flex w-[min(20rem,calc(100vw-2rem))] flex-col gap-3 rounded-xl border p-4 shadow-lg ring-[0.5px]",
39
+ "pointer-events-auto fixed bottom-4 left-4 z-[1050] relative isolate flex w-fit max-w-[min(18rem,calc(100vw-2rem))] flex-col gap-2 rounded-lg bg-[var(--tool-chrome-surface)] p-2.5 pr-7 shadow-[var(--tool-review-card-shadow)] ring-[0.5px] ring-[var(--tool-chrome-border)]",
40
40
  )}
41
41
  >
42
- <div className="flex items-start justify-between gap-2">
43
- <div className="min-w-0 flex-1">
44
- <p className="text-sm font-medium text-foreground">Update proto-plugin</p>
45
- <p className="mt-1 text-sm text-muted-foreground">
46
- v{status.latest} is available. You&apos;re on v{status.installed}.
47
- </p>
48
- </div>
49
- <button
50
- type="button"
51
- aria-label="Dismiss update notification"
52
- className="text-muted-foreground hover:text-foreground shrink-0 rounded p-1 transition-colors duration-200 ease"
53
- onClick={dismiss}
54
- >
55
- <X size={14} />
56
- </button>
57
- </div>
42
+ <button
43
+ type="button"
44
+ aria-label="Dismiss update notification"
45
+ className="absolute right-1.5 top-1.5 rounded p-0.5 text-[var(--tool-chrome-text-muted)] transition-colors duration-200 ease hover:text-[var(--tool-chrome-text-heading)]"
46
+ onClick={dismiss}
47
+ >
48
+ <X size={12} />
49
+ </button>
50
+
51
+ <p className="text-xs leading-relaxed text-[var(--tool-chrome-text-muted)]">
52
+ A new version of Proto is available, copy the prompt below into your agent to update
53
+ </p>
58
54
 
59
55
  <Button
60
56
  type="button"
61
- variant="outline"
57
+ variant="chrome"
62
58
  size="sm"
63
- className="h-8 w-fit cursor-pointer gap-1.5"
59
+ className="h-7 w-fit cursor-pointer gap-1 px-2 text-[11px]"
64
60
  onClick={() => {
65
- void handleCopy();
61
+ void handleCopyPrompt();
66
62
  }}
67
- aria-label="Copy upgrade prompt"
63
+ aria-label="Copy update prompt"
68
64
  >
69
- {isCopied ? "Copied prompt" : "Copy prompt"}
65
+ {isCopied ? "Copied update prompt" : "Copy update prompt"}
70
66
  {icon}
71
67
  </Button>
72
68
  </div>
@@ -10,9 +10,12 @@ import type {
10
10
  PrototypeExtraRoute,
11
11
  } from "@prototype/lib/prototypes/prototype-config-types";
12
12
 
13
+ import { createPrototypeComponentLibraryPage } from "./create-prototype-component-library-page";
13
14
  import { createPrototypePage } from "./create-prototype-page";
14
15
  import { createPrototypeStateMapPage } from "./create-prototype-state-map-page";
15
16
 
17
+ const ComponentLibraryPage = createPrototypeComponentLibraryPage();
18
+
16
19
  function matchExtraRoute(
17
20
  path: string[],
18
21
  pattern: string[],
@@ -61,7 +64,11 @@ export function createPrototypeCatchAllPage(config: PrototypeConfig) {
61
64
  path: ["prototypes", slug, "states"],
62
65
  }));
63
66
 
64
- return [...prototypePaths, ...stateMapPaths];
67
+ return [
68
+ { path: ["component-library"] },
69
+ ...prototypePaths,
70
+ ...stateMapPaths,
71
+ ];
65
72
  }
66
73
 
67
74
  async function CatchAllPage({
@@ -71,6 +78,10 @@ export function createPrototypeCatchAllPage(config: PrototypeConfig) {
71
78
  }) {
72
79
  const { path = [] } = await params;
73
80
 
81
+ if (path.length === 1 && path[0] === "component-library") {
82
+ return <ComponentLibraryPage />;
83
+ }
84
+
74
85
  if (path.length === 2 && path[0] === "prototypes") {
75
86
  const slug = path[1]!;
76
87
  return prototypePage.default({ params: Promise.resolve({ slug }) });
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { PrototypeProvider } from "@prototype/components/prototype-provider";
3
4
  import { PrototypePluginUpdatePopup } from "@prototype/components/shell/prototype-plugin-update-popup";
4
5
  import { usePathname } from "next/navigation";
5
6
  import type { ComponentType, ReactNode } from "react";
@@ -29,7 +30,7 @@ export function PrototypeSiteLayoutClient({
29
30
 
30
31
  if (isGalleryRoute) {
31
32
  return (
32
- <div className="bg-[var(--bg-main)] flex h-svh min-h-svh w-full flex-col overflow-hidden">
33
+ <PrototypeProvider className="bg-[var(--bg-main)] flex h-svh min-h-svh w-full flex-col overflow-hidden">
33
34
  <GalleryShell
34
35
  sourceDirectoryName={sourceDirectoryName}
35
36
  sourcePath={sourcePath}
@@ -38,15 +39,15 @@ export function PrototypeSiteLayoutClient({
38
39
  </GalleryShell>
39
40
  <PrototypePluginUpdatePopup />
40
41
  {toaster}
41
- </div>
42
+ </PrototypeProvider>
42
43
  );
43
44
  }
44
45
 
45
46
  return (
46
- <div className="bg-[var(--bg-ground)] flex h-svh overflow-hidden">
47
+ <PrototypeProvider className="bg-[var(--bg-ground)] flex h-svh overflow-hidden">
47
48
  {children}
48
49
  <PrototypePluginUpdatePopup />
49
50
  {toaster}
50
- </div>
51
+ </PrototypeProvider>
51
52
  );
52
53
  }
@@ -38,3 +38,9 @@ export function buildUpgradeProtoPluginCopyText(
38
38
  ): string {
39
39
  return buildUpgradeProtoPluginPrompt(options);
40
40
  }
41
+
42
+ export function buildUpgradeProtoPluginCommand({
43
+ latest,
44
+ }: Pick<UpgradeProtoPluginPromptOptions, "latest">): string {
45
+ return `pnpm add proto-plugin@${latest}`;
46
+ }
@@ -8,12 +8,20 @@ import { usePersistedLocalString } from "@prototype/lib/prototypes/use-persisted
8
8
  import { useCallback, useEffect, useMemo, useState } from "react";
9
9
 
10
10
  const DISMISSED_VERSION_KEY = "prototype-review:global:dismissed-plugin-version";
11
+ const PREVIEW_PLUGIN_UPDATE_PARAM = "previewPluginUpdate";
12
+
13
+ function readPreviewPluginUpdateParam(): boolean {
14
+ if (process.env.NODE_ENV !== "development") return false;
15
+ if (typeof window === "undefined") return false;
16
+ return new URLSearchParams(window.location.search).has(PREVIEW_PLUGIN_UPDATE_PARAM);
17
+ }
11
18
 
12
19
  export function useProtoPluginVersionCheck() {
13
20
  const [status, setStatus] = useState<ProtoPluginVersionStatus | null>(null);
14
21
  const [isLoading, setIsLoading] = useState(true);
15
22
  const { value: dismissedVersion, updateValue: setDismissedVersion } =
16
23
  usePersistedLocalString(DISMISSED_VERSION_KEY, "");
24
+ const forcePreview = readPreviewPluginUpdateParam();
17
25
 
18
26
  useEffect(() => {
19
27
  let cancelled = false;
@@ -46,7 +54,35 @@ export function useProtoPluginVersionCheck() {
46
54
  }
47
55
  }, [setDismissedVersion, status?.latest]);
48
56
 
57
+ const previewStatus = useMemo<ProtoPluginVersionStatus | null>(() => {
58
+ if (!forcePreview) return null;
59
+
60
+ const installed = status?.installed ?? "0.1.7";
61
+ let latest = status?.latest ?? "0.1.9";
62
+
63
+ if (latest === installed) {
64
+ const match = /^(\d+)\.(\d+)\.(\d+)/.exec(installed);
65
+ latest = match
66
+ ? `${match[1]}.${match[2]}.${Number(match[3]) + 1}`
67
+ : "0.1.9";
68
+ }
69
+
70
+ return {
71
+ installed,
72
+ latest,
73
+ updateAvailable: true,
74
+ isWorkspaceLink: false,
75
+ repositoryUrl: status?.repositoryUrl,
76
+ };
77
+ }, [forcePreview, status]);
78
+
79
+ const effectiveStatus = previewStatus ?? status;
80
+
49
81
  const shouldShowPopup = useMemo(() => {
82
+ if (forcePreview) {
83
+ return Boolean(effectiveStatus?.installed && effectiveStatus.latest);
84
+ }
85
+
50
86
  if (isLoading || !status?.updateAvailable || !status.latest || !status.installed) {
51
87
  return false;
52
88
  }
@@ -56,10 +92,10 @@ export function useProtoPluginVersionCheck() {
56
92
  }
57
93
 
58
94
  return dismissedVersion !== status.latest;
59
- }, [dismissedVersion, isLoading, status]);
95
+ }, [dismissedVersion, effectiveStatus, forcePreview, isLoading, status]);
60
96
 
61
97
  return {
62
- status,
98
+ status: effectiveStatus,
63
99
  shouldShowPopup,
64
100
  dismiss,
65
101
  isLoading,
@@ -5,7 +5,6 @@ import { NextResponse } from "next/server";
5
5
 
6
6
  const PACKAGE_NAME = "proto-plugin";
7
7
  const NPM_LATEST_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
8
- const CACHE_TTL_MS = 60 * 60 * 1000;
9
8
 
10
9
  export type ProtoPluginVersionStatus = {
11
10
  installed: string | null;
@@ -20,9 +19,6 @@ type NpmLatestResponse = {
20
19
  repository?: { url?: string };
21
20
  };
22
21
 
23
- let cachedLatest: { version: string; repositoryUrl?: string; fetchedAt: number } | null =
24
- null;
25
-
26
22
  function parseSemver(version: string): [number, number, number] | null {
27
23
  const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version.trim());
28
24
  if (!match) return null;
@@ -76,18 +72,9 @@ async function fetchLatestFromNpm(): Promise<{
76
72
  version: string | null;
77
73
  repositoryUrl?: string;
78
74
  }> {
79
- const now = Date.now();
80
-
81
- if (cachedLatest && now - cachedLatest.fetchedAt < CACHE_TTL_MS) {
82
- return {
83
- version: cachedLatest.version,
84
- repositoryUrl: cachedLatest.repositoryUrl,
85
- };
86
- }
87
-
88
75
  try {
89
76
  const response = await fetch(NPM_LATEST_URL, {
90
- next: { revalidate: 3600 },
77
+ cache: "no-store",
91
78
  });
92
79
 
93
80
  if (!response.ok) {
@@ -98,10 +85,6 @@ async function fetchLatestFromNpm(): Promise<{
98
85
  const version = data.version ?? null;
99
86
  const repositoryUrl = data.repository?.url?.replace(/^git\+/, "").replace(/\.git$/, "");
100
87
 
101
- if (version) {
102
- cachedLatest = { version, repositoryUrl, fetchedAt: now };
103
- }
104
-
105
88
  return { version, repositoryUrl };
106
89
  } catch {
107
90
  return { version: null };
@@ -119,7 +102,9 @@ export async function GET() {
119
102
  updateAvailable: false,
120
103
  isWorkspaceLink: true,
121
104
  };
122
- return NextResponse.json(status);
105
+ return NextResponse.json(status, {
106
+ headers: { "Cache-Control": "no-store, max-age=0" },
107
+ });
123
108
  }
124
109
 
125
110
  const { version: latest, repositoryUrl } = await fetchLatestFromNpm();
@@ -135,5 +120,7 @@ export async function GET() {
135
120
  repositoryUrl,
136
121
  };
137
122
 
138
- return NextResponse.json(status);
123
+ return NextResponse.json(status, {
124
+ headers: { "Cache-Control": "no-store, max-age=0" },
125
+ });
139
126
  }