Compare commits
7 Commits
cli-v0.1.4
...
de684c44bb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de684c44bb | ||
|
|
66b9696b2d | ||
|
|
09c5d759fa | ||
|
|
a1c6c6dc6a | ||
|
|
00b5ba8190 | ||
|
|
ccff802163 | ||
|
|
231618c595 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -67,3 +67,8 @@ dist/
|
|||||||
|
|
||||||
# Auto Claude data directory
|
# Auto Claude data directory
|
||||||
.auto-claude/
|
.auto-claude/
|
||||||
|
|
||||||
|
# Payload CMS
|
||||||
|
apps/web/payload.db
|
||||||
|
apps/web/public/media/*
|
||||||
|
!apps/web/public/media/.gitkeep
|
||||||
|
|||||||
42
apps/cli/src/__tests__/crypto-roundtrip.test.ts
Normal file
42
apps/cli/src/__tests__/crypto-roundtrip.test.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { encryptDirect, decryptDirect } from "../crypto/envelope";
|
||||||
|
import { generateKeypair } from "../crypto/keypair";
|
||||||
|
|
||||||
|
describe("crypto roundtrip", () => {
|
||||||
|
it("Alice encrypts for Bob, Bob decrypts successfully", async () => {
|
||||||
|
const alice = await generateKeypair();
|
||||||
|
const bob = await generateKeypair();
|
||||||
|
|
||||||
|
const plaintext = "hello world";
|
||||||
|
const envelope = await encryptDirect(plaintext, bob.publicKey, alice.secretKey);
|
||||||
|
|
||||||
|
const decrypted = await decryptDirect(envelope, alice.publicKey, bob.secretKey);
|
||||||
|
expect(decrypted).toBe(plaintext);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Carol cannot decrypt a message encrypted for Bob", async () => {
|
||||||
|
const alice = await generateKeypair();
|
||||||
|
const bob = await generateKeypair();
|
||||||
|
const carol = await generateKeypair();
|
||||||
|
|
||||||
|
const envelope = await encryptDirect("hello world", bob.publicKey, alice.secretKey);
|
||||||
|
|
||||||
|
const decrypted = await decryptDirect(envelope, alice.publicKey, carol.secretKey);
|
||||||
|
expect(decrypted).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("tampered ciphertext returns null on decrypt", async () => {
|
||||||
|
const alice = await generateKeypair();
|
||||||
|
const bob = await generateKeypair();
|
||||||
|
|
||||||
|
const envelope = await encryptDirect("hello world", bob.publicKey, alice.secretKey);
|
||||||
|
|
||||||
|
// Flip a byte in the ciphertext
|
||||||
|
const raw = Buffer.from(envelope.ciphertext, "base64");
|
||||||
|
raw[0] = raw[0]! ^ 0xff;
|
||||||
|
const tampered = { nonce: envelope.nonce, ciphertext: raw.toString("base64") };
|
||||||
|
|
||||||
|
const decrypted = await decryptDirect(tampered, alice.publicKey, bob.secretKey);
|
||||||
|
expect(decrypted).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
67
apps/cli/src/__tests__/invite-parse.test.ts
Normal file
67
apps/cli/src/__tests__/invite-parse.test.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import {
|
||||||
|
parseInviteLink,
|
||||||
|
buildSignedInvite,
|
||||||
|
extractInviteToken,
|
||||||
|
} from "../invite/parse";
|
||||||
|
import { generateKeypair } from "../crypto/keypair";
|
||||||
|
|
||||||
|
describe("invite parse", () => {
|
||||||
|
it("round-trips a signed invite through encode and parse", async () => {
|
||||||
|
const owner = await generateKeypair();
|
||||||
|
const expiresAt = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now
|
||||||
|
|
||||||
|
const { link, payload } = await buildSignedInvite({
|
||||||
|
v: 1,
|
||||||
|
mesh_id: "mesh-abc-123",
|
||||||
|
mesh_slug: "test-mesh",
|
||||||
|
broker_url: "wss://broker.example.com",
|
||||||
|
expires_at: expiresAt,
|
||||||
|
mesh_root_key: "deadbeefcafebabe",
|
||||||
|
role: "member",
|
||||||
|
owner_pubkey: owner.publicKey,
|
||||||
|
owner_secret_key: owner.secretKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsed = await parseInviteLink(link);
|
||||||
|
expect(parsed.payload.mesh_id).toBe("mesh-abc-123");
|
||||||
|
expect(parsed.payload.mesh_slug).toBe("test-mesh");
|
||||||
|
expect(parsed.payload.broker_url).toBe("wss://broker.example.com");
|
||||||
|
expect(parsed.payload.expires_at).toBe(expiresAt);
|
||||||
|
expect(parsed.payload.role).toBe("member");
|
||||||
|
expect(parsed.payload.owner_pubkey).toBe(owner.publicKey);
|
||||||
|
expect(parsed.payload.signature).toBe(payload.signature);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects an expired invite", async () => {
|
||||||
|
const owner = await generateKeypair();
|
||||||
|
const expiredAt = Math.floor(Date.now() / 1000) - 60; // 1 minute ago
|
||||||
|
|
||||||
|
const { link } = await buildSignedInvite({
|
||||||
|
v: 1,
|
||||||
|
mesh_id: "mesh-expired",
|
||||||
|
mesh_slug: "expired-mesh",
|
||||||
|
broker_url: "wss://broker.example.com",
|
||||||
|
expires_at: expiredAt,
|
||||||
|
mesh_root_key: "deadbeef",
|
||||||
|
role: "member",
|
||||||
|
owner_pubkey: owner.publicKey,
|
||||||
|
owner_secret_key: owner.secretKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(parseInviteLink(link)).rejects.toThrow("invite expired");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects malformed base64 in invite URL", async () => {
|
||||||
|
// Empty payload after ic://join/ should throw.
|
||||||
|
expect(() => extractInviteToken("ic://join/")).toThrow("invite link has no payload");
|
||||||
|
|
||||||
|
// Short garbage that doesn't match any format should throw.
|
||||||
|
expect(() => extractInviteToken("!!!not-valid!!!")).toThrow("invalid invite format");
|
||||||
|
|
||||||
|
// A sufficiently long but garbage base64url token that decodes to
|
||||||
|
// invalid JSON should throw at the JSON parse stage.
|
||||||
|
const garbage = "A".repeat(30); // valid base64url chars, decodes to binary
|
||||||
|
await expect(parseInviteLink(`ic://join/${garbage}`)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -93,7 +93,7 @@ export class BrokerClient {
|
|||||||
/** Open WS, send hello, resolve when hello_ack received. */
|
/** Open WS, send hello, resolve when hello_ack received. */
|
||||||
async connect(): Promise<void> {
|
async connect(): Promise<void> {
|
||||||
if (this.closed) throw new Error("client is closed");
|
if (this.closed) throw new Error("client is closed");
|
||||||
this.setStatus("connecting");
|
this.setConnStatus("connecting");
|
||||||
const ws = new WebSocket(this.mesh.brokerUrl);
|
const ws = new WebSocket(this.mesh.brokerUrl);
|
||||||
this.ws = ws;
|
this.ws = ws;
|
||||||
|
|
||||||
@@ -146,7 +146,7 @@ export class BrokerClient {
|
|||||||
if (msg.type === "hello_ack") {
|
if (msg.type === "hello_ack") {
|
||||||
if (this.helloTimer) clearTimeout(this.helloTimer);
|
if (this.helloTimer) clearTimeout(this.helloTimer);
|
||||||
this.helloTimer = null;
|
this.helloTimer = null;
|
||||||
this.setStatus("open");
|
this.setConnStatus("open");
|
||||||
this.reconnectAttempt = 0;
|
this.reconnectAttempt = 0;
|
||||||
this.flushOutbound();
|
this.flushOutbound();
|
||||||
resolve();
|
resolve();
|
||||||
@@ -163,7 +163,7 @@ export class BrokerClient {
|
|||||||
reject(new Error("ws closed before hello_ack"));
|
reject(new Error("ws closed before hello_ack"));
|
||||||
}
|
}
|
||||||
if (!this.closed) this.scheduleReconnect();
|
if (!this.closed) this.scheduleReconnect();
|
||||||
else this.setStatus("closed");
|
else this.setConnStatus("closed");
|
||||||
};
|
};
|
||||||
|
|
||||||
const onError = (err: Error): void => {
|
const onError = (err: Error): void => {
|
||||||
@@ -277,7 +277,7 @@ export class BrokerClient {
|
|||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.setStatus("closed");
|
this.setConnStatus("closed");
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Internals ---
|
// --- Internals ---
|
||||||
@@ -373,7 +373,7 @@ export class BrokerClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private scheduleReconnect(): void {
|
private scheduleReconnect(): void {
|
||||||
this.setStatus("reconnecting");
|
this.setConnStatus("reconnecting");
|
||||||
const delay =
|
const delay =
|
||||||
BACKOFF_CAPS[Math.min(this.reconnectAttempt, BACKOFF_CAPS.length - 1)]!;
|
BACKOFF_CAPS[Math.min(this.reconnectAttempt, BACKOFF_CAPS.length - 1)]!;
|
||||||
this.reconnectAttempt += 1;
|
this.reconnectAttempt += 1;
|
||||||
@@ -388,7 +388,7 @@ export class BrokerClient {
|
|||||||
}, delay);
|
}, delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
private setStatus(s: ConnStatus): void {
|
private setConnStatus(s: ConnStatus): void {
|
||||||
if (this._status === s) return;
|
if (this._status === s) return;
|
||||||
this._status = s;
|
this._status = s;
|
||||||
this.opts.onStatusChange?.(s);
|
this.opts.onStatusChange?.(s);
|
||||||
|
|||||||
7
apps/cli/vitest.config.ts
Normal file
7
apps/cli/vitest.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
include: ["src/__tests__/**/*.test.ts"],
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
import { withPayload } from "@payloadcms/next/withPayload";
|
||||||
|
|
||||||
import env from "./env.config";
|
import env from "./env.config";
|
||||||
|
|
||||||
@@ -124,4 +125,4 @@ const withBundleAnalyzer = require("@next/bundle-analyzer")({
|
|||||||
enabled: env.ANALYZE,
|
enabled: env.ANALYZE,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default withBundleAnalyzer(config);
|
export default withPayload(withBundleAnalyzer(config));
|
||||||
|
|||||||
@@ -20,6 +20,9 @@
|
|||||||
"@hookform/resolvers": "5.2.2",
|
"@hookform/resolvers": "5.2.2",
|
||||||
"@next/bundle-analyzer": "16.0.10",
|
"@next/bundle-analyzer": "16.0.10",
|
||||||
"@number-flow/react": "0.5.10",
|
"@number-flow/react": "0.5.10",
|
||||||
|
"@payloadcms/db-sqlite": "^3.81.0",
|
||||||
|
"@payloadcms/next": "^3.81.0",
|
||||||
|
"@payloadcms/richtext-lexical": "^3.81.0",
|
||||||
"@tanstack/react-query": "catalog:",
|
"@tanstack/react-query": "catalog:",
|
||||||
"@tanstack/react-query-devtools": "catalog:",
|
"@tanstack/react-query-devtools": "catalog:",
|
||||||
"@tanstack/react-table": "catalog:",
|
"@tanstack/react-table": "catalog:",
|
||||||
@@ -44,6 +47,7 @@
|
|||||||
"next-i18n-router": "5.5.5",
|
"next-i18n-router": "5.5.5",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"nuqs": "2.7.2",
|
"nuqs": "2.7.2",
|
||||||
|
"payload": "^3.81.0",
|
||||||
"pdfjs-dist": "5.4.530",
|
"pdfjs-dist": "5.4.530",
|
||||||
"qrcode": "1.5.4",
|
"qrcode": "1.5.4",
|
||||||
"react": "catalog:react19",
|
"react": "catalog:react19",
|
||||||
@@ -57,6 +61,7 @@
|
|||||||
"rehype-raw": "7.0.0",
|
"rehype-raw": "7.0.0",
|
||||||
"remark-gfm": "4.0.1",
|
"remark-gfm": "4.0.1",
|
||||||
"remark-math": "6.0.0",
|
"remark-math": "6.0.0",
|
||||||
|
"sharp": "0.34.5",
|
||||||
"sonner": "2.0.7",
|
"sonner": "2.0.7",
|
||||||
"zod": "catalog:",
|
"zod": "catalog:",
|
||||||
"zustand": "5.0.8"
|
"zustand": "5.0.8"
|
||||||
|
|||||||
199
apps/web/payload.config.ts
Normal file
199
apps/web/payload.config.ts
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import { buildConfig } from "payload";
|
||||||
|
import { sqliteAdapter } from "@payloadcms/db-sqlite";
|
||||||
|
import { lexicalEditor } from "@payloadcms/richtext-lexical";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import sharp from "sharp";
|
||||||
|
|
||||||
|
const filename = fileURLToPath(import.meta.url);
|
||||||
|
const dirname = path.dirname(filename);
|
||||||
|
|
||||||
|
export default buildConfig({
|
||||||
|
secret: process.env.PAYLOAD_SECRET || "claudemesh-dev-secret-change-in-production",
|
||||||
|
|
||||||
|
admin: {
|
||||||
|
user: "users",
|
||||||
|
meta: {
|
||||||
|
titleSuffix: "— claudemesh",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
editor: lexicalEditor(),
|
||||||
|
|
||||||
|
db: sqliteAdapter({
|
||||||
|
client: {
|
||||||
|
url: process.env.PAYLOAD_DATABASE_URI || path.resolve(dirname, "payload.db"),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
sharp,
|
||||||
|
|
||||||
|
collections: [
|
||||||
|
// --- Users (admin panel) ---
|
||||||
|
{
|
||||||
|
slug: "users",
|
||||||
|
auth: true,
|
||||||
|
admin: { useAsTitle: "email" },
|
||||||
|
fields: [
|
||||||
|
{ name: "name", type: "text" },
|
||||||
|
{ name: "role", type: "select", options: ["admin", "editor"], defaultValue: "editor" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Media ---
|
||||||
|
{
|
||||||
|
slug: "media",
|
||||||
|
upload: {
|
||||||
|
staticDir: path.resolve(dirname, "public/media"),
|
||||||
|
mimeTypes: ["image/*"],
|
||||||
|
},
|
||||||
|
admin: { useAsTitle: "alt" },
|
||||||
|
fields: [
|
||||||
|
{ name: "alt", type: "text", required: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Authors ---
|
||||||
|
{
|
||||||
|
slug: "authors",
|
||||||
|
admin: { useAsTitle: "name" },
|
||||||
|
fields: [
|
||||||
|
{ name: "name", type: "text", required: true },
|
||||||
|
{ name: "slug", type: "text", required: true, unique: true },
|
||||||
|
{ name: "bio", type: "textarea" },
|
||||||
|
{ name: "role", type: "text" },
|
||||||
|
{
|
||||||
|
name: "avatar",
|
||||||
|
type: "upload",
|
||||||
|
relationTo: "media",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "links",
|
||||||
|
type: "group",
|
||||||
|
fields: [
|
||||||
|
{ name: "github", type: "text" },
|
||||||
|
{ name: "twitter", type: "text" },
|
||||||
|
{ name: "website", type: "text" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Categories ---
|
||||||
|
{
|
||||||
|
slug: "categories",
|
||||||
|
admin: { useAsTitle: "name" },
|
||||||
|
fields: [
|
||||||
|
{ name: "name", type: "text", required: true },
|
||||||
|
{ name: "slug", type: "text", required: true, unique: true },
|
||||||
|
{ name: "description", type: "textarea" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Blog Posts ---
|
||||||
|
{
|
||||||
|
slug: "posts",
|
||||||
|
admin: {
|
||||||
|
useAsTitle: "title",
|
||||||
|
defaultColumns: ["title", "status", "publishedAt", "author"],
|
||||||
|
},
|
||||||
|
versions: { drafts: true },
|
||||||
|
fields: [
|
||||||
|
{ name: "title", type: "text", required: true },
|
||||||
|
{
|
||||||
|
name: "slug",
|
||||||
|
type: "text",
|
||||||
|
required: true,
|
||||||
|
unique: true,
|
||||||
|
admin: {
|
||||||
|
position: "sidebar",
|
||||||
|
description: "URL-friendly identifier. Auto-generated from title if left blank.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "excerpt",
|
||||||
|
type: "textarea",
|
||||||
|
admin: { description: "1-2 sentence summary for cards and meta descriptions." },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "content",
|
||||||
|
type: "richText",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "coverImage",
|
||||||
|
type: "upload",
|
||||||
|
relationTo: "media",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "author",
|
||||||
|
type: "relationship",
|
||||||
|
relationTo: "authors",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "categories",
|
||||||
|
type: "relationship",
|
||||||
|
relationTo: "categories",
|
||||||
|
hasMany: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "publishedAt",
|
||||||
|
type: "date",
|
||||||
|
admin: { position: "sidebar", date: { pickerAppearance: "dayOnly" } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "status",
|
||||||
|
type: "select",
|
||||||
|
options: [
|
||||||
|
{ label: "Draft", value: "draft" },
|
||||||
|
{ label: "Published", value: "published" },
|
||||||
|
],
|
||||||
|
defaultValue: "draft",
|
||||||
|
admin: { position: "sidebar" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "seo",
|
||||||
|
type: "group",
|
||||||
|
fields: [
|
||||||
|
{ name: "metaTitle", type: "text" },
|
||||||
|
{ name: "metaDescription", type: "textarea" },
|
||||||
|
{ name: "ogImage", type: "upload", relationTo: "media" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Changelog ---
|
||||||
|
{
|
||||||
|
slug: "changelog",
|
||||||
|
admin: {
|
||||||
|
useAsTitle: "version",
|
||||||
|
defaultColumns: ["version", "date", "type"],
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{ name: "version", type: "text", required: true },
|
||||||
|
{ name: "date", type: "date", required: true },
|
||||||
|
{
|
||||||
|
name: "type",
|
||||||
|
type: "select",
|
||||||
|
options: [
|
||||||
|
{ label: "Feature", value: "feat" },
|
||||||
|
{ label: "Fix", value: "fix" },
|
||||||
|
{ label: "Docs", value: "docs" },
|
||||||
|
{ label: "Breaking", value: "breaking" },
|
||||||
|
],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{ name: "summary", type: "text", required: true },
|
||||||
|
{ name: "body", type: "richText" },
|
||||||
|
{ name: "npmUrl", type: "text" },
|
||||||
|
{ name: "githubUrl", type: "text" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
typescript: {
|
||||||
|
outputFile: path.resolve(dirname, "src/payload-types.ts"),
|
||||||
|
},
|
||||||
|
});
|
||||||
0
apps/web/public/media/.gitkeep
Normal file
0
apps/web/public/media/.gitkeep
Normal file
14
apps/web/src/app/(payload)/admin/[[...segments]]/page.tsx
Normal file
14
apps/web/src/app/(payload)/admin/[[...segments]]/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck — Payload generates these types at build time
|
||||||
|
import { RootPage, generatePageMetadata } from "@payloadcms/next/views";
|
||||||
|
import { importMap } from "../importMap";
|
||||||
|
import config from "@payload-config";
|
||||||
|
|
||||||
|
type Args = { params: Promise<{ segments: string[] }> };
|
||||||
|
|
||||||
|
export const generateMetadata = ({ params }: Args) =>
|
||||||
|
generatePageMetadata({ config, params });
|
||||||
|
|
||||||
|
export default function Page({ params }: Args) {
|
||||||
|
return <RootPage config={config} params={params} importMap={importMap} />;
|
||||||
|
}
|
||||||
2
apps/web/src/app/(payload)/admin/importMap.js
Normal file
2
apps/web/src/app/(payload)/admin/importMap.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// Auto-generated by Payload — placeholder until first build
|
||||||
|
export const importMap = {};
|
||||||
11
apps/web/src/app/(payload)/api/[...slug]/route.ts
Normal file
11
apps/web/src/app/(payload)/api/[...slug]/route.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST, REST_PUT } from "@payloadcms/next/routes";
|
||||||
|
import config from "@payload-config";
|
||||||
|
|
||||||
|
export const GET = REST_GET(config);
|
||||||
|
export const POST = REST_POST(config);
|
||||||
|
export const DELETE = REST_DELETE(config);
|
||||||
|
export const PATCH = REST_PATCH(config);
|
||||||
|
export const PUT = REST_PUT(config);
|
||||||
|
export const OPTIONS = REST_OPTIONS(config);
|
||||||
14
apps/web/src/app/(payload)/layout.tsx
Normal file
14
apps/web/src/app/(payload)/layout.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import "@payloadcms/next/css";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: "Admin — claudemesh",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PayloadLayout({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
80
apps/web/src/app/[locale]/(marketing)/blog/[slug]/page.tsx
Normal file
80
apps/web/src/app/[locale]/(marketing)/blog/[slug]/page.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { getPayload } from "payload";
|
||||||
|
import config from "@payload-config";
|
||||||
|
import { RichText } from "@payloadcms/richtext-lexical/react";
|
||||||
|
|
||||||
|
type Props = { params: Promise<{ slug: string }> };
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }: Props) {
|
||||||
|
const { slug } = await params;
|
||||||
|
const payload = await getPayload({ config });
|
||||||
|
const { docs } = await payload.find({
|
||||||
|
collection: "posts",
|
||||||
|
where: { slug: { equals: slug }, status: { equals: "published" } },
|
||||||
|
limit: 1,
|
||||||
|
depth: 1,
|
||||||
|
});
|
||||||
|
const post = docs[0];
|
||||||
|
if (!post) return { title: "Not found — claudemesh" };
|
||||||
|
return {
|
||||||
|
title: `${post.title} — claudemesh`,
|
||||||
|
description: post.excerpt || post.seo?.metaDescription || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function BlogPost({ params }: Props) {
|
||||||
|
const { slug } = await params;
|
||||||
|
const payload = await getPayload({ config });
|
||||||
|
const { docs } = await payload.find({
|
||||||
|
collection: "posts",
|
||||||
|
where: { slug: { equals: slug }, status: { equals: "published" } },
|
||||||
|
limit: 1,
|
||||||
|
depth: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = docs[0] as any;
|
||||||
|
if (!post) notFound();
|
||||||
|
|
||||||
|
const author = typeof post.author === "object" ? post.author : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className="mx-auto max-w-3xl px-6 py-24 md:py-32">
|
||||||
|
<header className="mb-12">
|
||||||
|
<time
|
||||||
|
dateTime={post.publishedAt}
|
||||||
|
className="text-[11px] uppercase tracking-wider text-[var(--cm-fg-tertiary)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||||
|
>
|
||||||
|
{post.publishedAt
|
||||||
|
? new Date(post.publishedAt).toLocaleDateString("en-US", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
})
|
||||||
|
: "Draft"}
|
||||||
|
</time>
|
||||||
|
<h1
|
||||||
|
className="mt-3 text-[clamp(2rem,4.5vw,3rem)] font-medium leading-[1.1] text-[var(--cm-fg)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||||
|
>
|
||||||
|
{post.title}
|
||||||
|
</h1>
|
||||||
|
{author && (
|
||||||
|
<p
|
||||||
|
className="mt-4 text-sm text-[var(--cm-fg-secondary)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||||
|
>
|
||||||
|
by {author.name}{author.role ? ` · ${author.role}` : ""}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="prose prose-invert max-w-none prose-headings:font-medium prose-a:text-[var(--cm-clay)] prose-a:no-underline hover:prose-a:underline prose-code:text-[var(--cm-fg-secondary)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||||
|
>
|
||||||
|
{post.content && <RichText data={post.content} />}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
78
apps/web/src/app/[locale]/(marketing)/blog/page.tsx
Normal file
78
apps/web/src/app/[locale]/(marketing)/blog/page.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { getPayload } from "payload";
|
||||||
|
import config from "@payload-config";
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: "Blog — claudemesh",
|
||||||
|
description: "Engineering notes on peer messaging, protocol design, and multi-agent security.",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function BlogIndex() {
|
||||||
|
const payload = await getPayload({ config });
|
||||||
|
const { docs: posts } = await payload.find({
|
||||||
|
collection: "posts",
|
||||||
|
where: { status: { equals: "published" } },
|
||||||
|
sort: "-publishedAt",
|
||||||
|
limit: 20,
|
||||||
|
depth: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="mx-auto max-w-3xl px-6 py-24 md:py-32">
|
||||||
|
<h1
|
||||||
|
className="text-[clamp(2rem,4.5vw,3rem)] font-medium leading-[1.1] text-[var(--cm-fg)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||||
|
>
|
||||||
|
Blog
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
className="mt-4 text-[15px] text-[var(--cm-fg-secondary)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||||
|
>
|
||||||
|
Engineering notes on protocol design, security, and multi-agent UX.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-12 space-y-10">
|
||||||
|
{posts.length === 0 && (
|
||||||
|
<p className="text-sm text-[var(--cm-fg-tertiary)]" style={{ fontFamily: "var(--cm-font-mono)" }}>
|
||||||
|
No posts yet. First one ships soon.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{posts.map((post: any) => (
|
||||||
|
<article key={post.id} className="border-b border-[var(--cm-border)] pb-8">
|
||||||
|
<time
|
||||||
|
dateTime={post.publishedAt}
|
||||||
|
className="text-[11px] uppercase tracking-wider text-[var(--cm-fg-tertiary)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||||
|
>
|
||||||
|
{post.publishedAt
|
||||||
|
? new Date(post.publishedAt).toLocaleDateString("en-US", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
})
|
||||||
|
: "Draft"}
|
||||||
|
</time>
|
||||||
|
<h2 className="mt-2">
|
||||||
|
<Link
|
||||||
|
href={`/blog/${post.slug}`}
|
||||||
|
className="text-[22px] font-medium leading-tight text-[var(--cm-fg)] transition-colors hover:text-[var(--cm-clay)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||||
|
>
|
||||||
|
{post.title}
|
||||||
|
</Link>
|
||||||
|
</h2>
|
||||||
|
{post.excerpt && (
|
||||||
|
<p
|
||||||
|
className="mt-3 text-[14px] leading-[1.6] text-[var(--cm-fg-secondary)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||||
|
>
|
||||||
|
{post.excerpt}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
107
apps/web/src/app/[locale]/(marketing)/changelog/page.tsx
Normal file
107
apps/web/src/app/[locale]/(marketing)/changelog/page.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { getPayload } from "payload";
|
||||||
|
import config from "@payload-config";
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: "Changelog — claudemesh",
|
||||||
|
description: "Release history for claudemesh-cli.",
|
||||||
|
};
|
||||||
|
|
||||||
|
const TYPE_LABELS: Record<string, string> = {
|
||||||
|
feat: "Feature",
|
||||||
|
fix: "Fix",
|
||||||
|
docs: "Docs",
|
||||||
|
breaking: "Breaking",
|
||||||
|
};
|
||||||
|
|
||||||
|
const TYPE_COLORS: Record<string, string> = {
|
||||||
|
feat: "bg-[var(--cm-clay)]",
|
||||||
|
fix: "bg-[var(--cm-cactus)]",
|
||||||
|
docs: "bg-[var(--cm-oat)]",
|
||||||
|
breaking: "bg-red-500",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function ChangelogPage() {
|
||||||
|
const payload = await getPayload({ config });
|
||||||
|
const { docs: entries } = await payload.find({
|
||||||
|
collection: "changelog",
|
||||||
|
sort: "-date",
|
||||||
|
limit: 50,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="mx-auto max-w-3xl px-6 py-24 md:py-32">
|
||||||
|
<h1
|
||||||
|
className="text-[clamp(2rem,4.5vw,3rem)] font-medium leading-[1.1] text-[var(--cm-fg)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||||
|
>
|
||||||
|
Changelog
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
className="mt-4 text-[15px] text-[var(--cm-fg-secondary)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||||
|
>
|
||||||
|
Every shipped version of claudemesh-cli.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-12 space-y-8">
|
||||||
|
{entries.length === 0 && (
|
||||||
|
<p className="text-sm text-[var(--cm-fg-tertiary)]" style={{ fontFamily: "var(--cm-font-mono)" }}>
|
||||||
|
No entries yet.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{entries.map((entry: any) => (
|
||||||
|
<article
|
||||||
|
key={entry.id}
|
||||||
|
className="border-b border-[var(--cm-border)] pb-6"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span
|
||||||
|
className={`rounded-[4px] px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider text-[var(--cm-bg)] ${TYPE_COLORS[entry.type] || "bg-[var(--cm-fg-tertiary)]"}`}
|
||||||
|
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||||
|
>
|
||||||
|
{TYPE_LABELS[entry.type] || entry.type}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="text-[18px] font-medium text-[var(--cm-fg)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||||
|
>
|
||||||
|
v{entry.version}
|
||||||
|
</span>
|
||||||
|
<time
|
||||||
|
dateTime={entry.date}
|
||||||
|
className="text-[11px] text-[var(--cm-fg-tertiary)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||||
|
>
|
||||||
|
{new Date(entry.date).toLocaleDateString("en-US", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
})}
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
className="mt-2 text-[14px] leading-[1.6] text-[var(--cm-fg-secondary)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||||
|
>
|
||||||
|
{entry.summary}
|
||||||
|
</p>
|
||||||
|
{(entry.npmUrl || entry.githubUrl) && (
|
||||||
|
<div className="mt-3 flex gap-4 text-[12px]" style={{ fontFamily: "var(--cm-font-mono)" }}>
|
||||||
|
{entry.npmUrl && (
|
||||||
|
<a href={entry.npmUrl} className="text-[var(--cm-clay)] hover:underline">
|
||||||
|
npm →
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
{entry.githubUrl && (
|
||||||
|
<a href={entry.githubUrl} className="text-[var(--cm-clay)] hover:underline">
|
||||||
|
github →
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
100
apps/web/src/app/install/route.ts
Normal file
100
apps/web/src/app/install/route.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
/**
|
||||||
|
* GET /install — serves a shell installer for claudemesh-cli.
|
||||||
|
*
|
||||||
|
* Intended to be piped into bash:
|
||||||
|
* curl -fsSL https://claudemesh.com/install | bash
|
||||||
|
*
|
||||||
|
* The script is kept short + auditable. It does not try to install
|
||||||
|
* Node for the user — it checks for a compatible Node + npm and
|
||||||
|
* directs them to install Node themselves if missing. Running `bash`
|
||||||
|
* against a domain you do not fully trust is always a risk; publishing
|
||||||
|
* the script this way (rather than obfuscating it behind a binary
|
||||||
|
* blob) lets security-conscious users inspect before executing.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const SCRIPT = `#!/usr/bin/env bash
|
||||||
|
# claudemesh-cli installer
|
||||||
|
# Source: https://claudemesh.com/install
|
||||||
|
# Audit: curl -fsSL https://claudemesh.com/install | less
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
RED=$'\\033[31m'; GREEN=$'\\033[32m'; DIM=$'\\033[2m'; BOLD=$'\\033[1m'; RESET=$'\\033[0m'
|
||||||
|
|
||||||
|
say() { printf "%s\\n" "$*"; }
|
||||||
|
ok() { printf "%s✓%s %s\\n" "\${GREEN}" "\${RESET}" "$*"; }
|
||||||
|
err() { printf "%s✗%s %s\\n" "\${RED}" "\${RESET}" "$*" >&2; }
|
||||||
|
|
||||||
|
say ""
|
||||||
|
say "\${BOLD}claudemesh-cli installer\${RESET}"
|
||||||
|
say "$(printf '%.0s─' {1..40})"
|
||||||
|
|
||||||
|
# --- preflight ------------------------------------------------------
|
||||||
|
|
||||||
|
if ! command -v node >/dev/null 2>&1; then
|
||||||
|
err "Node.js is not installed."
|
||||||
|
say " Install Node.js 20 or newer: \${BOLD}https://nodejs.org\${RESET}"
|
||||||
|
say " Or via nvm: \${DIM}curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash\${RESET}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
NODE_MAJOR=$(node -p "process.versions.node.split('.')[0]")
|
||||||
|
if [ "$NODE_MAJOR" -lt 20 ]; then
|
||||||
|
err "Node.js $(node -v) is too old — claudemesh-cli needs >= 20."
|
||||||
|
say " Upgrade: \${BOLD}https://nodejs.org\${RESET}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ok "Node.js $(node -v)"
|
||||||
|
|
||||||
|
if ! command -v npm >/dev/null 2>&1; then
|
||||||
|
err "npm is not installed (usually ships with Node)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ok "npm $(npm -v)"
|
||||||
|
|
||||||
|
# --- install --------------------------------------------------------
|
||||||
|
|
||||||
|
say ""
|
||||||
|
say "Installing \${BOLD}claudemesh-cli\${RESET} from npm…"
|
||||||
|
if ! npm install -g claudemesh-cli; then
|
||||||
|
err "npm install failed."
|
||||||
|
say " If this is a permissions error on macOS/Linux, try:"
|
||||||
|
say " \${DIM}sudo npm install -g claudemesh-cli\${RESET}"
|
||||||
|
say " or configure npm to use a user-owned prefix:"
|
||||||
|
say " \${DIM}https://docs.npmjs.com/resolving-eacces-permissions-errors\${RESET}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ok "claudemesh-cli installed ($(claudemesh --version))"
|
||||||
|
|
||||||
|
# --- register MCP + hooks ------------------------------------------
|
||||||
|
|
||||||
|
say ""
|
||||||
|
say "Registering Claude Code MCP server + status hooks…"
|
||||||
|
if ! claudemesh install; then
|
||||||
|
err "claudemesh install failed — run it manually to see the error."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- done -----------------------------------------------------------
|
||||||
|
|
||||||
|
say ""
|
||||||
|
say "\${GREEN}\${BOLD}Done.\${RESET}"
|
||||||
|
say ""
|
||||||
|
say "Next steps:"
|
||||||
|
say " 1. Restart Claude Code so the MCP tools appear."
|
||||||
|
say " 2. Join a mesh: \${BOLD}claudemesh join <invite-url>\${RESET}"
|
||||||
|
say " 3. Launch with push: \${BOLD}claudemesh launch\${RESET}"
|
||||||
|
say ""
|
||||||
|
say "Need an invite? Visit \${BOLD}https://claudemesh.com\${RESET}"
|
||||||
|
say ""
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function GET(): Response {
|
||||||
|
return new Response(SCRIPT, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/x-shellscript; charset=utf-8",
|
||||||
|
"Cache-Control": "public, max-age=300, s-maxage=600",
|
||||||
|
"X-Content-Type-Options": "nosniff",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -49,7 +49,7 @@ export const CallToAction = () => {
|
|||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="#docs"
|
href="https://github.com/alezmad/claudemesh-cli#readme"
|
||||||
className="inline-flex items-center justify-center gap-2 rounded-[var(--cm-radius-xs)] border border-[var(--cm-fg-tertiary)] px-6 py-3.5 text-[15px] font-medium text-[var(--cm-fg)] transition-colors duration-300 hover:border-[var(--cm-fg)] hover:bg-[var(--cm-bg-elevated)]"
|
className="inline-flex items-center justify-center gap-2 rounded-[var(--cm-radius-xs)] border border-[var(--cm-fg-tertiary)] px-6 py-3.5 text-[15px] font-medium text-[var(--cm-fg)] transition-colors duration-300 hover:border-[var(--cm-fg)] hover:bg-[var(--cm-bg-elevated)]"
|
||||||
style={{ fontFamily: "var(--cm-font-sans)" }}
|
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import { Reveal } from "./_reveal";
|
|||||||
const ITEMS = [
|
const ITEMS = [
|
||||||
{
|
{
|
||||||
q: "Is claudemesh free?",
|
q: "Is claudemesh free?",
|
||||||
a: "Yes — the broker, CLI, dashboard, and SDK are MIT-licensed and free forever. Solo developers and small teams can self-host at no cost. Paid tiers add hosted brokers, SSO, audit retention, and support.",
|
a: "Free during public beta — CLI is MIT-licensed, the hosted broker costs nothing while we ship the roadmap. Paid tiers launch when the dashboard ships. Beta users keep the free plan for life.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
q: "How do I get started?",
|
q: "How do I get started?",
|
||||||
a: "Install the broker with one curl command. Add one env var to your Claude Code config. Your session joins the mesh. `npx claudemesh init` does both in 60 seconds.",
|
a: "One command: `curl -fsSL claudemesh.com/install | bash`. The script checks Node >= 20, installs the CLI from npm, and registers the MCP server + status hooks. Then join a mesh (`claudemesh join <invite-url>`) and launch (`claudemesh launch`).",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
q: "Does claudemesh send my code or prompts to the cloud?",
|
q: "Does claudemesh send my code or prompts to the cloud?",
|
||||||
@@ -29,7 +29,7 @@ const ITEMS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
q: "Which Claude Code versions work with claudemesh?",
|
q: "Which Claude Code versions work with claudemesh?",
|
||||||
a: "Claude Code 2.0 and above. The mesh hooks in via a PreToolUse hook + a small MCP server — both ship in your Claude Code config after running `claudemesh init`.",
|
a: "Claude Code 2.0 and above. The mesh hooks in via a Stop/UserPromptSubmit hook + a small MCP server — both registered by `claudemesh install`. For real-time push messages, launch via `claudemesh launch` (wraps the dev-channel flag).",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
q: "How is this different from MCP?",
|
q: "How is this different from MCP?",
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const Features = () => {
|
|||||||
style={{ fontFamily: "var(--cm-font-mono)" }}
|
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||||
>
|
>
|
||||||
<span className="text-[var(--cm-clay)]">$</span>
|
<span className="text-[var(--cm-clay)]">$</span>
|
||||||
<span>curl -fsSL claudemesh.sh/install | bash</span>
|
<span>curl -fsSL claudemesh.com/install | bash</span>
|
||||||
<button
|
<button
|
||||||
className="ml-2 rounded border border-[var(--cm-border)] px-1.5 py-0.5 text-[10px] text-[var(--cm-fg-tertiary)] transition-colors hover:border-[var(--cm-fg)] hover:text-[var(--cm-fg)]"
|
className="ml-2 rounded border border-[var(--cm-border)] px-1.5 py-0.5 text-[10px] text-[var(--cm-fg-tertiary)] transition-colors hover:border-[var(--cm-fg)] hover:text-[var(--cm-fg)]"
|
||||||
aria-label="Copy"
|
aria-label="Copy"
|
||||||
@@ -61,7 +61,7 @@ export const Features = () => {
|
|||||||
>
|
>
|
||||||
Free forever for solo developers · Or read the{" "}
|
Free forever for solo developers · Or read the{" "}
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="https://github.com/alezmad/claudemesh-cli#readme"
|
||||||
className="underline decoration-[var(--cm-fg-tertiary)] underline-offset-4 transition-colors hover:text-[var(--cm-fg)] hover:decoration-[var(--cm-clay)]"
|
className="underline decoration-[var(--cm-fg-tertiary)] underline-offset-4 transition-colors hover:text-[var(--cm-fg)] hover:decoration-[var(--cm-clay)]"
|
||||||
>
|
>
|
||||||
documentation
|
documentation
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import Link from "next/link";
|
|||||||
import { Reveal, SectionIcon } from "./_reveal";
|
import { Reveal, SectionIcon } from "./_reveal";
|
||||||
|
|
||||||
const LOGOS = [
|
const LOGOS = [
|
||||||
"Vercel",
|
"Claude Code",
|
||||||
"Linear",
|
"MCP",
|
||||||
"Stripe",
|
"libsodium",
|
||||||
"Supabase",
|
"Bun",
|
||||||
"Shopify",
|
"TypeScript",
|
||||||
"Figma",
|
"MIT",
|
||||||
];
|
];
|
||||||
|
|
||||||
export const Hero = () => {
|
export const Hero = () => {
|
||||||
@@ -55,11 +55,12 @@ export const Hero = () => {
|
|||||||
className="mx-auto mt-6 max-w-2xl text-center text-lg leading-[1.65] text-[var(--cm-fg-secondary)] md:text-xl"
|
className="mx-auto mt-6 max-w-2xl text-center text-lg leading-[1.65] text-[var(--cm-fg-secondary)] md:text-xl"
|
||||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||||
>
|
>
|
||||||
Peer mesh for Claude — reachable from anywhere you are. Connect
|
Peer mesh for Claude Code. Connect your sessions across repos and
|
||||||
every Claude Code session on your team, then bridge the mesh to
|
machines. Messages are end-to-end encrypted, delivered mid-turn
|
||||||
WhatsApp, Slack, your phone. Terminal is one client, not THE client.
|
as {"`<channel>`"} reminders. Your Claudes talk to each other; the
|
||||||
|
broker never sees plaintext.
|
||||||
<span className="block pt-2 text-[var(--cm-clay)]">
|
<span className="block pt-2 text-[var(--cm-clay)]">
|
||||||
Free and open-source. Forever.
|
Open-source CLI. Free during public beta.
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
@@ -81,7 +82,7 @@ export const Hero = () => {
|
|||||||
style={{ fontFamily: "var(--cm-font-mono)" }}
|
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||||
>
|
>
|
||||||
<span className="text-[var(--cm-clay)]">$</span>
|
<span className="text-[var(--cm-clay)]">$</span>
|
||||||
<span>curl -fsSL claudemesh.sh/install | bash</span>
|
<span>curl -fsSL claudemesh.com/install | bash</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
@@ -93,7 +94,7 @@ export const Hero = () => {
|
|||||||
>
|
>
|
||||||
Or{" "}
|
Or{" "}
|
||||||
<Link
|
<Link
|
||||||
href="#docs"
|
href="https://github.com/alezmad/claudemesh-cli#readme"
|
||||||
className="underline decoration-[var(--cm-fg-tertiary)] underline-offset-4 transition-colors hover:text-[var(--cm-fg)] hover:decoration-[var(--cm-clay)]"
|
className="underline decoration-[var(--cm-fg-tertiary)] underline-offset-4 transition-colors hover:text-[var(--cm-fg)] hover:decoration-[var(--cm-clay)]"
|
||||||
>
|
>
|
||||||
read the documentation
|
read the documentation
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export const LaptopToLaptop = () => {
|
|||||||
</Reveal>
|
</Reveal>
|
||||||
<Reveal delay={3} className="mt-10 flex justify-center">
|
<Reveal delay={3} className="mt-10 flex justify-center">
|
||||||
<Link
|
<Link
|
||||||
href="#"
|
href="/auth/register"
|
||||||
className="inline-flex items-center justify-center gap-2 rounded-[var(--cm-radius-xs)] border border-[var(--cm-fg-tertiary)] px-5 py-3 text-sm font-medium text-[var(--cm-fg)] transition-colors hover:border-[var(--cm-fg)] hover:bg-[var(--cm-bg)]"
|
className="inline-flex items-center justify-center gap-2 rounded-[var(--cm-radius-xs)] border border-[var(--cm-fg-tertiary)] px-5 py-3 text-sm font-medium text-[var(--cm-fg)] transition-colors hover:border-[var(--cm-fg)] hover:bg-[var(--cm-bg)]"
|
||||||
style={{ fontFamily: "var(--cm-font-sans)" }}
|
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const CARDS = [
|
|||||||
accent: "clay",
|
accent: "clay",
|
||||||
title: "Start in your terminal",
|
title: "Start in your terminal",
|
||||||
body: "Drop the broker next to Claude Code. One env var. Your session joins the mesh.",
|
body: "Drop the broker next to Claude Code. One env var. Your session joins the mesh.",
|
||||||
cta: { label: "Install", href: "#" },
|
cta: { label: "Install", href: "https://github.com/alezmad/claudemesh-cli#install" },
|
||||||
mock: (
|
mock: (
|
||||||
<div
|
<div
|
||||||
className="rounded-[8px] bg-[#D97757] p-6 font-mono text-[11px] leading-[1.6] text-[#141413]"
|
className="rounded-[8px] bg-[#D97757] p-6 font-mono text-[11px] leading-[1.6] text-[#141413]"
|
||||||
@@ -26,8 +26,8 @@ const CARDS = [
|
|||||||
accent: "oat",
|
accent: "oat",
|
||||||
title: "Bridge to your editor",
|
title: "Bridge to your editor",
|
||||||
body: "VS Code, Cursor, JetBrains — the mesh exposes an MCP server your editor's agent can call.",
|
body: "VS Code, Cursor, JetBrains — the mesh exposes an MCP server your editor's agent can call.",
|
||||||
cta: { label: "VS Code", href: "#" },
|
cta: { label: "VS Code", href: "https://github.com/alezmad/claudemesh-cli#readme" },
|
||||||
cta2: { label: "JetBrains", href: "#" },
|
cta2: { label: "JetBrains", href: "https://github.com/alezmad/claudemesh-cli#readme" },
|
||||||
mock: (
|
mock: (
|
||||||
<div
|
<div
|
||||||
className="rounded-[8px] border border-[var(--cm-border)] bg-[var(--cm-bg)] p-4"
|
className="rounded-[8px] border border-[var(--cm-border)] bg-[var(--cm-bg)] p-4"
|
||||||
@@ -52,7 +52,7 @@ const CARDS = [
|
|||||||
accent: "cactus",
|
accent: "cactus",
|
||||||
title: "Reach across machines",
|
title: "Reach across machines",
|
||||||
body: "Tailscale, WireGuard, or plain WS over your LAN. The broker is one binary, anywhere.",
|
body: "Tailscale, WireGuard, or plain WS over your LAN. The broker is one binary, anywhere.",
|
||||||
cta: { label: "Open the dashboard", href: "#" },
|
cta: { label: "Open the dashboard", href: "/dashboard" },
|
||||||
mock: (
|
mock: (
|
||||||
<div
|
<div
|
||||||
className="rounded-[8px] border border-[var(--cm-border)] bg-[var(--cm-bg)] p-4"
|
className="rounded-[8px] border border-[var(--cm-border)] bg-[var(--cm-bg)] p-4"
|
||||||
|
|||||||
@@ -1,64 +1,25 @@
|
|||||||
"use client";
|
|
||||||
import { useState } from "react";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Reveal, SectionIcon } from "./_reveal";
|
import { Reveal, SectionIcon } from "./_reveal";
|
||||||
|
|
||||||
const TIERS = {
|
const SHIPPING = [
|
||||||
individual: [
|
"CLI + MCP server (Claude Code integration)",
|
||||||
{
|
"Hosted broker on claudemesh.com",
|
||||||
name: "Solo",
|
"End-to-end encrypted direct messages (crypto_box)",
|
||||||
desc: "Run the broker on your laptop. Pair your Claude Code sessions across repos.",
|
"Priority routing (now / next / low)",
|
||||||
price: "Free",
|
"Mesh invites + membership",
|
||||||
cta: "Start free",
|
"Windows, macOS, Linux support",
|
||||||
href: "/auth/register",
|
];
|
||||||
},
|
|
||||||
{
|
const ROADMAP = [
|
||||||
name: "Pro",
|
"Mesh dashboard (browser UI)",
|
||||||
desc: "Mesh dashboard, peer registry, message history, priority routing.",
|
"Message history + retention controls",
|
||||||
price: "$12",
|
"Audit log",
|
||||||
note: "per month",
|
"Slack / WhatsApp / Telegram gateways",
|
||||||
cta: "Start free trial",
|
"Self-host broker + SSO",
|
||||||
href: "/auth/register",
|
"Cross-broker federation",
|
||||||
},
|
];
|
||||||
{
|
|
||||||
name: "Plus",
|
|
||||||
desc: "Cross-machine mesh via Tailscale / WireGuard, MCP bridge, audit log.",
|
|
||||||
price: "$24",
|
|
||||||
note: "per month",
|
|
||||||
cta: "Start free trial",
|
|
||||||
href: "/auth/register",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
team: [
|
|
||||||
{
|
|
||||||
name: "Team",
|
|
||||||
desc: "Self-hosted broker. SSO, shared presence, team audit log, 25 peers.",
|
|
||||||
price: "$99",
|
|
||||||
note: "per month · unlimited peers",
|
|
||||||
cta: "Start free",
|
|
||||||
href: "/auth/register",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Business",
|
|
||||||
desc: "Multi-region brokers, retention controls, Slack/Linear bridges.",
|
|
||||||
price: "$499",
|
|
||||||
note: "per month",
|
|
||||||
cta: "Start free",
|
|
||||||
href: "/auth/register",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Enterprise",
|
|
||||||
desc: "Air-gapped deploy, custom SAML, dedicated support, SOC 2 pack.",
|
|
||||||
price: "Contact",
|
|
||||||
cta: "Contact sales",
|
|
||||||
href: "/contact",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Pricing = () => {
|
export const Pricing = () => {
|
||||||
const [tab, setTab] = useState<"individual" | "team">("individual");
|
|
||||||
const tiers = TIERS[tab];
|
|
||||||
return (
|
return (
|
||||||
<section className="border-b border-[var(--cm-border)] bg-[var(--cm-bg)] px-6 py-24 md:px-12 md:py-32">
|
<section className="border-b border-[var(--cm-border)] bg-[var(--cm-bg)] px-6 py-24 md:px-12 md:py-32">
|
||||||
<div className="mx-auto max-w-[var(--cm-max-w)]">
|
<div className="mx-auto max-w-[var(--cm-max-w)]">
|
||||||
@@ -73,72 +34,104 @@ export const Pricing = () => {
|
|||||||
Get started with claudemesh
|
Get started with claudemesh
|
||||||
</h2>
|
</h2>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
<Reveal delay={2} className="mt-10 flex justify-center">
|
<Reveal delay={2}>
|
||||||
<div className="inline-flex rounded-[var(--cm-radius-xs)] border border-[var(--cm-border)] bg-[var(--cm-bg-elevated)] p-1">
|
<p
|
||||||
{(["individual", "team"] as const).map((k) => (
|
className="mx-auto mt-4 max-w-[520px] text-center text-[15px] leading-[1.6] text-[var(--cm-fg-secondary)]"
|
||||||
<button
|
|
||||||
key={k}
|
|
||||||
onClick={() => setTab(k)}
|
|
||||||
className={
|
|
||||||
"rounded-[calc(var(--cm-radius-xs)-2px)] px-4 py-2 text-[13px] font-medium transition-colors " +
|
|
||||||
(tab === k
|
|
||||||
? "bg-[var(--cm-fg)] text-[var(--cm-bg)]"
|
|
||||||
: "text-[var(--cm-fg-secondary)] hover:text-[var(--cm-fg)]")
|
|
||||||
}
|
|
||||||
style={{ fontFamily: "var(--cm-font-sans)" }}
|
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||||
>
|
>
|
||||||
{k === "individual" ? "Individual" : "Team & Enterprise"}
|
Free during public beta. The CLI is MIT-licensed. The hosted
|
||||||
</button>
|
broker stays free while the roadmap ships. No billing today.
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Reveal>
|
|
||||||
<Reveal delay={3}>
|
|
||||||
<div className="mt-16 grid gap-6 md:grid-cols-3">
|
|
||||||
{tiers.map((tier) => (
|
|
||||||
<article
|
|
||||||
key={tier.name}
|
|
||||||
className="flex flex-col rounded-[var(--cm-radius-md)] border border-[var(--cm-border)] bg-[var(--cm-bg-elevated)] p-8 transition-colors hover:border-[var(--cm-clay)]"
|
|
||||||
>
|
|
||||||
<div className="mb-5">
|
|
||||||
<SectionIcon glyph="leaf" />
|
|
||||||
</div>
|
|
||||||
<h3
|
|
||||||
className="mb-2 text-[28px] font-medium leading-tight text-[var(--cm-fg)]"
|
|
||||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
|
||||||
>
|
|
||||||
{tier.name}
|
|
||||||
</h3>
|
|
||||||
<p
|
|
||||||
className="mb-6 text-[14px] leading-[1.6] text-[var(--cm-fg-secondary)]"
|
|
||||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
|
||||||
>
|
|
||||||
{tier.desc}
|
|
||||||
</p>
|
</p>
|
||||||
<div className="mb-6 mt-auto">
|
</Reveal>
|
||||||
|
|
||||||
|
<Reveal delay={3}>
|
||||||
|
<div className="mx-auto mt-16 max-w-[720px] rounded-[var(--cm-radius-md)] border border-[var(--cm-border)] bg-[var(--cm-bg-elevated)] p-8 md:p-10">
|
||||||
|
<div className="mb-6 flex items-baseline justify-between gap-4">
|
||||||
|
<h3
|
||||||
|
className="text-[28px] font-medium leading-tight text-[var(--cm-fg)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||||
|
>
|
||||||
|
Public beta
|
||||||
|
</h3>
|
||||||
|
<div className="text-right">
|
||||||
<div
|
<div
|
||||||
className="text-[32px] font-medium text-[var(--cm-fg)]"
|
className="text-[32px] font-medium text-[var(--cm-fg)]"
|
||||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||||
>
|
>
|
||||||
{tier.price}
|
Free
|
||||||
</div>
|
</div>
|
||||||
{tier.note && (
|
|
||||||
<div
|
<div
|
||||||
className="text-xs text-[var(--cm-fg-tertiary)]"
|
className="text-xs text-[var(--cm-fg-tertiary)]"
|
||||||
style={{ fontFamily: "var(--cm-font-mono)" }}
|
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||||
>
|
>
|
||||||
{tier.note}
|
no card required
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<Link
|
</div>
|
||||||
href={tier.href}
|
|
||||||
className="inline-flex items-center justify-center gap-2 rounded-[var(--cm-radius-xs)] border border-[var(--cm-fg-tertiary)] px-5 py-2.5 text-sm font-medium text-[var(--cm-fg)] transition-colors hover:border-[var(--cm-fg)] hover:bg-[var(--cm-bg)]"
|
<div className="grid gap-8 md:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="mb-3 text-[10px] uppercase tracking-wider text-[var(--cm-fg-tertiary)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||||
|
>
|
||||||
|
Shipping today
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{SHIPPING.map((item) => (
|
||||||
|
<li
|
||||||
|
key={item}
|
||||||
|
className="flex items-start gap-2 text-[13px] leading-[1.6] text-[var(--cm-fg-secondary)]"
|
||||||
style={{ fontFamily: "var(--cm-font-sans)" }}
|
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||||
>
|
>
|
||||||
{tier.cta}
|
<span className="mt-[6px] block h-[6px] w-[6px] shrink-0 rounded-full bg-[var(--cm-clay)]" />
|
||||||
</Link>
|
<span>{item}</span>
|
||||||
</article>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className="mb-3 text-[10px] uppercase tracking-wider text-[var(--cm-fg-tertiary)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||||
|
>
|
||||||
|
Roadmap · v0.2–v0.3
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{ROADMAP.map((item) => (
|
||||||
|
<li
|
||||||
|
key={item}
|
||||||
|
className="flex items-start gap-2 text-[13px] leading-[1.6] text-[var(--cm-fg-tertiary)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||||
|
>
|
||||||
|
<span className="mt-[6px] block h-[6px] w-[6px] shrink-0 rounded-full border border-[var(--cm-fg-tertiary)]" />
|
||||||
|
<span>{item}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 flex flex-col items-start gap-3 border-t border-[var(--cm-border)] pt-6 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<p
|
||||||
|
className="text-[12px] leading-[1.5] text-[var(--cm-fg-tertiary)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||||
|
>
|
||||||
|
Paid tiers launch when the dashboard ships. Beta users keep
|
||||||
|
the free plan for life.
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/auth/register"
|
||||||
|
className="inline-flex shrink-0 items-center gap-2 rounded-[var(--cm-radius-xs)] bg-[var(--cm-fg)] px-5 py-2.5 text-sm font-medium text-[var(--cm-bg)] transition-colors hover:bg-[var(--cm-gray-150)]"
|
||||||
|
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||||
|
>
|
||||||
|
Start free
|
||||||
|
<span className="transition-transform duration-300 group-hover:translate-x-0.5">
|
||||||
|
→
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ export const Surfaces = () => {
|
|||||||
name, by repo, by priority.
|
name, by repo, by priority.
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
href="#"
|
href="/dashboard"
|
||||||
className="inline-flex items-center gap-2 rounded-[var(--cm-radius-xs)] border border-[var(--cm-fg-tertiary)] px-5 py-2.5 text-sm font-medium text-[var(--cm-fg)] transition-colors hover:border-[var(--cm-fg)] hover:bg-[var(--cm-bg)]"
|
className="inline-flex items-center gap-2 rounded-[var(--cm-radius-xs)] border border-[var(--cm-fg-tertiary)] px-5 py-2.5 text-sm font-medium text-[var(--cm-fg)] transition-colors hover:border-[var(--cm-fg)] hover:bg-[var(--cm-bg)]"
|
||||||
style={{ fontFamily: "var(--cm-font-sans)" }}
|
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ const USE_CASES: UseCase[] = [
|
|||||||
title: "Bug Alice fixed, Bob rediscovers",
|
title: "Bug Alice fixed, Bob rediscovers",
|
||||||
before:
|
before:
|
||||||
"Alice in payments-api fixes a Stripe signature bug. Two weeks later, Bob in checkout-frontend hits the same thing. Alice's fix is buried in a PR thread. Bob re-solves it for three hours.",
|
"Alice in payments-api fixes a Stripe signature bug. Two weeks later, Bob in checkout-frontend hits the same thing. Alice's fix is buried in a PR thread. Bob re-solves it for three hours.",
|
||||||
now: "Bob's Claude asks the mesh: who's seen this? Alice's Claude self-nominates with context. Bob solves in ten minutes. Alice isn't interrupted — her Claude surfaces the history on its own.",
|
now: "Bob's Claude asks the mesh: who's seen this? Alice's Claude volunteers with context. Bob solves in ten minutes. Alice isn't interrupted — her Claude shares the history on its own.",
|
||||||
limits:
|
limits:
|
||||||
"Each Claude stays inside its own repo. Nobody's reading anyone else's files. Information flows at the agent layer, with a human still on the PR.",
|
"Each Claude stays inside its own repo. Nobody's reading anyone else's files. Information flows at the agent layer, with a human still on the PR.",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ export const proxy = (request: NextRequest) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: "/((?!api|static|.*\\..*|_next).*)",
|
matcher: "/((?!api|static|install|admin|.*\\..*|_next).*)",
|
||||||
unstable_allowDynamic: ["**/node_modules/lodash*/**/*.js"],
|
unstable_allowDynamic: ["**/node_modules/lodash*/**/*.js"],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"~/*": ["./src/*"]
|
"~/*": ["./src/*"],
|
||||||
|
"@payload-config": ["./payload.config.ts"]
|
||||||
},
|
},
|
||||||
"plugins": [{ "name": "next" }],
|
"plugins": [{ "name": "next" }],
|
||||||
"module": "esnext"
|
"module": "esnext"
|
||||||
|
|||||||
@@ -44,7 +44,9 @@
|
|||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"esbuild",
|
"esbuild",
|
||||||
"duckdb"
|
"duckdb",
|
||||||
|
"better-sqlite3",
|
||||||
|
"sharp"
|
||||||
],
|
],
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"csstype": "3.1.3",
|
"csstype": "3.1.3",
|
||||||
|
|||||||
2635
pnpm-lock.yaml
generated
2635
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
|||||||
esbuild@0.23.1
|
esbuild@0.23.1
|
||||||
esbuild@0.25.10
|
esbuild@0.25.10
|
||||||
esbuild@0.27.2
|
esbuild@0.27.2
|
||||||
|
better-sqlite3@12.4.1
|
||||||
|
sharp@0.34.5
|
||||||
|
|||||||
Reference in New Issue
Block a user