diff --git a/.agents/skills/convex-performance-audit/references/hot-path-rules.md b/.agents/skills/convex-performance-audit/references/hot-path-rules.md index e003e052..7c914cf2 100644 --- a/.agents/skills/convex-performance-audit/references/hot-path-rules.md +++ b/.agents/skills/convex-performance-audit/references/hot-path-rules.md @@ -126,10 +126,10 @@ defineTable({ team: v.id("teams"), user: v.id("users") }) ```ts // Good: single compound index serves both query patterns -defineTable({ team: v.id("teams"), user: v.id("users") }).index( - "by_team_and_user", - ["team", "user"], -); +defineTable({ team: v.id("teams"), user: v.id("users") }).index("by_team_and_user", [ + "team", + "user", +]); ``` Exception: `.index("by_foo", ["foo"])` is really an index on `foo` + `_creationTime`, while `.index("by_foo_and_bar", ["foo", "bar"])` is on `foo` + `bar` + `_creationTime`. If you need results sorted by `foo` then `_creationTime`, you need the single-field index because the compound one would sort by `bar` first. @@ -171,8 +171,7 @@ const ownerName = project.ownerName ?? "Unknown owner"; ```ts // Good: denormalized data is an optimization, not the only source of truth -const ownerName = - project.ownerName ?? (await ctx.db.get(project.ownerId))?.name ?? null; +const ownerName = project.ownerName ?? (await ctx.db.get(project.ownerId))?.name ?? null; ``` Bad lookup map pattern: diff --git a/.agents/skills/convex-performance-audit/references/subscription-cost.md b/.agents/skills/convex-performance-audit/references/subscription-cost.md index ae7d1adb..d699b3f8 100644 --- a/.agents/skills/convex-performance-audit/references/subscription-cost.md +++ b/.agents/skills/convex-performance-audit/references/subscription-cost.md @@ -134,10 +134,7 @@ const profile = useQuery(api.users.getProfile, { userId: selectedId! }); ```ts // Good: skip when there is nothing to fetch -const profile = useQuery( - api.users.getProfile, - selectedId ? { userId: selectedId } : "skip", -); +const profile = useQuery(api.users.getProfile, selectedId ? { userId: selectedId } : "skip"); ``` ### 4. Isolate frequently-updated fields into separate documents diff --git a/.agents/skills/convex-quickstart/SKILL.md b/.agents/skills/convex-quickstart/SKILL.md index f506b3e4..08dad523 100644 --- a/.agents/skills/convex-quickstart/SKILL.md +++ b/.agents/skills/convex-quickstart/SKILL.md @@ -143,9 +143,7 @@ Create the `ConvexReactClient` at module scope, not inside a component: ```tsx // Bad: re-creates the client on every render function App() { - const convex = new ConvexReactClient( - import.meta.env.VITE_CONVEX_URL as string, - ); + const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string); return ...; } @@ -196,11 +194,7 @@ export function ConvexClientProvider({ children }: { children: ReactNode }) { // app/layout.tsx import { ConvexClientProvider } from "./ConvexClientProvider"; -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { +export default function RootLayout({ children }: { children: React.ReactNode }) { return ( diff --git a/.agents/skills/convex-setup-auth/SKILL.md b/.agents/skills/convex-setup-auth/SKILL.md index 59a92285..9bb8381e 100644 --- a/.agents/skills/convex-setup-auth/SKILL.md +++ b/.agents/skills/convex-setup-auth/SKILL.md @@ -101,9 +101,7 @@ export const getMyProfile = query({ return await ctx.db .query("users") - .withIndex("by_tokenIdentifier", (q) => - q.eq("tokenIdentifier", identity.tokenIdentifier), - ) + .withIndex("by_tokenIdentifier", (q) => q.eq("tokenIdentifier", identity.tokenIdentifier)) .unique(); }, }); diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf90561b..47c8d439 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,9 @@ jobs: - name: Audit dependencies run: bun audit + - name: Format + run: bun run format:check + - name: Lint run: bun run lint diff --git a/.oxlintrc.json b/.oxlintrc.json index 2d56052e..6ca8799e 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,38 +1,38 @@ { - "$schema": "./node_modules/oxlint/configuration_schema.json", - "plugins": ["unicorn", "typescript", "oxc"], - "categories": { - "correctness": "error", - "perf": "error", - "suspicious": "error" - }, - "rules": { - "curly": "off", - "eslint-plugin-unicorn/prefer-array-find": "off", - "eslint-plugin-unicorn/no-array-sort": "off", - "eslint/no-await-in-loop": "off", - "eslint/no-underscore-dangle": "off", - "eslint/no-new": "off", - "oxc/no-accumulating-spread": "off", - "oxc/no-async-endpoint-handlers": "off", - "oxc/no-map-spread": "off", - "typescript/no-explicit-any": "error", - "typescript/no-extraneous-class": "off", - "typescript/no-unnecessary-boolean-literal-compare": "off", - "typescript/no-unnecessary-type-assertion": "off", - "typescript/no-unsafe-type-assertion": "off", - "unicorn/consistent-function-scoping": "off", - "unicorn/require-post-message-target-origin": "off" - }, - "ignorePatterns": [ - ".output/", - ".tanstack/", - "convex/_generated/", - "coverage/", - "dist/", - "node_modules/", - "public/", - "src/routeTree.gen.ts", - "test-results/" - ] + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["unicorn", "typescript", "oxc"], + "categories": { + "correctness": "error", + "perf": "error", + "suspicious": "error" + }, + "rules": { + "curly": "off", + "eslint-plugin-unicorn/prefer-array-find": "off", + "eslint-plugin-unicorn/no-array-sort": "off", + "eslint/no-await-in-loop": "off", + "eslint/no-underscore-dangle": "off", + "eslint/no-new": "off", + "oxc/no-accumulating-spread": "off", + "oxc/no-async-endpoint-handlers": "off", + "oxc/no-map-spread": "off", + "typescript/no-explicit-any": "error", + "typescript/no-extraneous-class": "off", + "typescript/no-unnecessary-boolean-literal-compare": "off", + "typescript/no-unnecessary-type-assertion": "off", + "typescript/no-unsafe-type-assertion": "off", + "unicorn/consistent-function-scoping": "off", + "unicorn/require-post-message-target-origin": "off" + }, + "ignorePatterns": [ + ".output/", + ".tanstack/", + "convex/_generated/", + "coverage/", + "dist/", + "node_modules/", + "public/", + "src/routeTree.gen.ts", + "test-results/" + ] } diff --git a/CLAUDE.md b/CLAUDE.md index 55bac110..e8c466b7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,9 +47,11 @@ - Mock `db` objects MUST include `normalizeId: vi.fn()` for trigger wrapper compatibility. + This project uses [Convex](https://convex.dev) as its backend. When working on Convex code, **always read `convex/_generated/ai/guidelines.md` first** for important guidelines on how to correctly use Convex APIs and patterns. The file contains rules that override what you may have learned about Convex from training data. Convex agent skills for common tasks can be installed by running `npx convex ai-files install`. + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3f1854ef..c438033a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -145,7 +145,9 @@ clawhub publish ## Before Submitting a PR ```bash +bun run format:check # oxfmt bun run lint # oxlint +bun run deadcode:ci # Knip files/deps/exports bun run test # Vitest (80% coverage threshold) bun run build # Vite + Nitro bun run --cwd packages/clawhub verify diff --git a/DESIGN.md b/DESIGN.md index 32a8ac92..42b49a5e 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -10,14 +10,14 @@ This document outlines the design rules, patterns, and guidelines for the ClawHu ClawHub uses a strict **3-5 color palette** based on the OpenClaw brand: -| Token | Light Mode | Dark Mode | Usage | -|-------|------------|-----------|-------| -| `--accent` | `#dc2626` | `#dc2626` | Primary actions, interactive elements, emphasis | -| `--accent-deep` | `#b91c1c` | `#ef4444` | Hover states, secondary emphasis | -| `--ink` | `#0a0a0a` | `#fafafa` | Primary text | -| `--ink-soft` | `#525252` | `#a1a1a1` | Secondary text, descriptions | -| `--surface` | `#ffffff` | `#121212` | Card backgrounds, elevated surfaces | -| `--bg` | `#fafafa` | `#0a0a0a` | Page background | +| Token | Light Mode | Dark Mode | Usage | +| --------------- | ---------- | --------- | ----------------------------------------------- | +| `--accent` | `#dc2626` | `#dc2626` | Primary actions, interactive elements, emphasis | +| `--accent-deep` | `#b91c1c` | `#ef4444` | Hover states, secondary emphasis | +| `--ink` | `#0a0a0a` | `#fafafa` | Primary text | +| `--ink-soft` | `#525252` | `#a1a1a1` | Secondary text, descriptions | +| `--surface` | `#ffffff` | `#121212` | Card backgrounds, elevated surfaces | +| `--bg` | `#fafafa` | `#0a0a0a` | Page background | ### Rules @@ -33,21 +33,21 @@ ClawHub uses a strict **3-5 color palette** based on the OpenClaw brand: ### Font Stack ```css ---font-sans: 'Geist', system-ui, sans-serif; ---font-mono: 'Geist Mono', monospace; ---font-display: 'Geist', system-ui, sans-serif; +--font-sans: "Geist", system-ui, sans-serif; +--font-mono: "Geist Mono", monospace; +--font-display: "Geist", system-ui, sans-serif; ``` ### Scale -| Token | Size | Usage | -|-------|------|-------| -| `--fs-xs` | 0.75rem (12px) | Labels, badges, metadata | -| `--fs-sm` | 0.875rem (14px) | Body text, descriptions | -| `--fs-base` | 1rem (16px) | Default body text | -| `--fs-md` | 1.125rem (18px) | Subheadings | -| `--fs-lg` | 1.25rem (20px) | Section titles | -| `--fs-xl` | 1.5rem (24px) | Page headings | +| Token | Size | Usage | +| ----------- | --------------- | ------------------------ | +| `--fs-xs` | 0.75rem (12px) | Labels, badges, metadata | +| `--fs-sm` | 0.875rem (14px) | Body text, descriptions | +| `--fs-base` | 1rem (16px) | Default body text | +| `--fs-md` | 1.125rem (18px) | Subheadings | +| `--fs-lg` | 1.25rem (20px) | Section titles | +| `--fs-xl` | 1.5rem (24px) | Page headings | ### Rules @@ -72,25 +72,24 @@ Use this hierarchy for layout decisions: ### Spacing Scale ```css ---space-1: 0.25rem /* 4px */ ---space-2: 0.5rem /* 8px */ ---space-3: 0.75rem /* 12px */ ---space-4: 1rem /* 16px */ ---space-5: 1.5rem /* 24px */ ---space-6: 2rem /* 32px */ +--space-1: 0.25rem /* 4px */ --space-2: 0.5rem /* 8px */ --space-3: 0.75rem /* 12px */ + --space-4: 1rem /* 16px */ --space-5: 1.5rem /* 24px */ --space-6: 2rem /* 32px */; ``` ### Grid Patterns #### Auto-fit Grid (Recommended for Cards) + ```css grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); ``` + - Automatically adjusts columns based on container width - Prevents orphan items on partial rows - Maintains consistent card widths #### Fixed Grid (When exact columns needed) + ```css /* 3-column at desktop, 2 at tablet, 1 at mobile */ grid-template-columns: repeat(3, minmax(0, 1fr)); @@ -106,11 +105,11 @@ grid-template-columns: repeat(3, minmax(0, 1fr)); ### Container Widths -| Size | Max Width | Usage | -|------|-----------|-------| -| Default | `--page-max` (1200px) | Standard pages | -| Narrow | `--page-narrow` (720px) | Reading content, forms | -| Wide | Full width | Dashboards, data tables | +| Size | Max Width | Usage | +| ------- | ----------------------- | ----------------------- | +| Default | `--page-max` (1200px) | Standard pages | +| Narrow | `--page-narrow` (720px) | Reading content, forms | +| Wide | Full width | Dashboards, data tables | --- @@ -128,20 +127,22 @@ grid-template-columns: repeat(3, minmax(0, 1fr)); ``` **Rules:** + - Always use `display: flex; flex-direction: column;` for consistent height - Add `flex: 1` to content area for equal-height cards in grids - Include hover state with `border-color` and subtle `box-shadow` ### Buttons -| Variant | Usage | -|---------|-------| -| `primary` | Main actions (Submit, Save, Download) | -| `secondary` | Alternative actions | -| `ghost` | Tertiary actions, navigation | -| `destructive` | Delete, remove, dangerous actions | +| Variant | Usage | +| ------------- | ------------------------------------- | +| `primary` | Main actions (Submit, Save, Download) | +| `secondary` | Alternative actions | +| `ghost` | Tertiary actions, navigation | +| `destructive` | Delete, remove, dangerous actions | **Rules:** + - Always include visible focus state - Minimum touch target: 44x44px on mobile - Include `aria-label` when icon-only @@ -314,17 +315,22 @@ grid-template-columns: repeat(3, minmax(0, 1fr)); ```css /* Component */ -.component-name { } +.component-name { +} /* Component modifier */ -.component-name.variant { } +.component-name.variant { +} /* Component child */ -.component-name-child { } +.component-name-child { +} /* State */ -.component-name.is-active { } -.component-name[data-state="open"] { } +.component-name.is-active { +} +.component-name[data-state="open"] { +} ``` ### File Organization diff --git a/convex/httpApiV1/shared.ts b/convex/httpApiV1/shared.ts index 411a73e6..87f042ce 100644 --- a/convex/httpApiV1/shared.ts +++ b/convex/httpApiV1/shared.ts @@ -35,9 +35,7 @@ export function safeTextFileResponse(params: { const headers = mergeHeaders( params.headers, { - "Content-Type": contentType - ? `${contentType}; charset=utf-8` - : "text/plain; charset=utf-8", + "Content-Type": contentType ? `${contentType}; charset=utf-8` : "text/plain; charset=utf-8", "Cache-Control": "private, max-age=60", ETag: params.sha256, "X-Content-SHA256": params.sha256, diff --git a/convex/lib/moderation.test.ts b/convex/lib/moderation.test.ts index 29dc8a64..9605ecba 100644 --- a/convex/lib/moderation.test.ts +++ b/convex/lib/moderation.test.ts @@ -207,8 +207,7 @@ describe("deriveModerationFlags", () => { skill: { slug: "test", displayName: "Test", - summary: - "Malware stealer that posts to discord.gg/hook via curl | bash from bit.ly", + summary: "Malware stealer that posts to discord.gg/hook via curl | bash from bit.ly", }, parsed: { frontmatter: {} }, files: [], diff --git a/convex/lib/moderation.ts b/convex/lib/moderation.ts index f4f49d4f..05ad6e63 100644 --- a/convex/lib/moderation.ts +++ b/convex/lib/moderation.ts @@ -15,7 +15,8 @@ const FLAG_RULES: Array<{ flag: string; pattern: RegExp }> = [ // not legitimate integrations that mention generic webhook support. { flag: "suspicious.webhook", - pattern: /(discord\.gg\/|discord\.com\/api\/webhooks|discordapp\.com\/api\/webhooks|hooks\.slack)/i, + pattern: + /(discord\.gg\/|discord\.com\/api\/webhooks|discordapp\.com\/api\/webhooks|hooks\.slack)/i, }, // Arbitrary code execution - curl | bash is dangerous diff --git a/convex/lib/soulPublish.ts b/convex/lib/soulPublish.ts index bb014e13..69fea2a3 100644 --- a/convex/lib/soulPublish.ts +++ b/convex/lib/soulPublish.ts @@ -1,5 +1,5 @@ -import { ConvexError } from "convex/values"; import { normalizeTextContentType } from "clawhub-schema"; +import { ConvexError } from "convex/values"; import semver from "semver"; import { internal } from "../_generated/api"; import type { Doc, Id } from "../_generated/dataModel"; diff --git a/convex/lib/userSkillStats.ts b/convex/lib/userSkillStats.ts index 122aea6b..602cb9fa 100644 --- a/convex/lib/userSkillStats.ts +++ b/convex/lib/userSkillStats.ts @@ -42,9 +42,11 @@ export async function adjustUserSkillStatsForSkillChange( if (prevOwnerId && prevOwnerId === nextOwnerId) { await patchUserStats(ctx, prevOwnerId, { - publishedSkills: (nextContribution?.publishedSkills ?? 0) - (prevContribution?.publishedSkills ?? 0), + publishedSkills: + (nextContribution?.publishedSkills ?? 0) - (prevContribution?.publishedSkills ?? 0), totalStars: (nextContribution?.totalStars ?? 0) - (prevContribution?.totalStars ?? 0), - totalDownloads: (nextContribution?.totalDownloads ?? 0) - (prevContribution?.totalDownloads ?? 0), + totalDownloads: + (nextContribution?.totalDownloads ?? 0) - (prevContribution?.totalDownloads ?? 0), }); return; } diff --git a/convex/maintenance.test.ts b/convex/maintenance.test.ts index 94da900e..92a7806f 100644 --- a/convex/maintenance.test.ts +++ b/convex/maintenance.test.ts @@ -285,10 +285,11 @@ describe("maintenance backfill", () => { }); const runMutation = vi.fn().mockResolvedValue({ ok: true }); - const result = await backfillUserStatsInternalHandler( - { runQuery, runMutation } as never, - { batchSize: 10, skillBatchSize: 50, maxBatches: 1 }, - ); + const result = await backfillUserStatsInternalHandler({ runQuery, runMutation } as never, { + batchSize: 10, + skillBatchSize: 50, + maxBatches: 1, + }); expect(result).toEqual({ ok: true, @@ -299,10 +300,14 @@ describe("maintenance backfill", () => { isDone: true, cursor: null, }); - expect(runQuery).toHaveBeenNthCalledWith(1, internal.maintenance.getUserStatsBackfillPageInternal, { - cursor: undefined, - batchSize: 10, - }); + expect(runQuery).toHaveBeenNthCalledWith( + 1, + internal.maintenance.getUserStatsBackfillPageInternal, + { + cursor: undefined, + batchSize: 10, + }, + ); expect(runQuery).toHaveBeenNthCalledWith( 2, internal.maintenance.getUserOwnedSkillsBackfillPageInternal, diff --git a/convex/model/rescans/policy.ts b/convex/model/rescans/policy.ts index d6830b6a..461747b3 100644 --- a/convex/model/rescans/policy.ts +++ b/convex/model/rescans/policy.ts @@ -145,8 +145,7 @@ export async function buildRescanState( maxRequests: MAX_OWNER_RESCAN_REQUESTS_PER_RELEASE, requestCount, remainingRequests: Math.max(0, MAX_OWNER_RESCAN_REQUESTS_PER_RELEASE - requestCount), - canRequest: - requestCount < MAX_OWNER_RESCAN_REQUESTS_PER_RELEASE && inProgressRequest === null, + canRequest: requestCount < MAX_OWNER_RESCAN_REQUESTS_PER_RELEASE && inProgressRequest === null, inProgressRequest: serializeRescanRequest(inProgressRequest), latestRequest: serializeRescanRequest(requests[0] ?? null), }; diff --git a/convex/packages.public.test.ts b/convex/packages.public.test.ts index 3b13a144..1c682b71 100644 --- a/convex/packages.public.test.ts +++ b/convex/packages.public.test.ts @@ -583,33 +583,35 @@ function makeInsertReleaseCtx( indexName: string, buildQuery?: (q: { eq: (field: string, value: unknown) => unknown }) => unknown, ) => { - if (indexName === "by_package") { + if (indexName === "by_package") { + return { + collect: vi.fn().mockResolvedValue(priorReleases), + }; + } + if (indexName === "by_package_version") { + const filters = new Map(); + const query = { + eq(field: string, value: unknown) { + filters.set(field, value); + return query; + }, + }; + buildQuery?.(query); + return { + unique: vi + .fn() + .mockResolvedValue( + priorReleases.find( + (release) => + release.packageId === filters.get("packageId") && + release.version === filters.get("version"), + ) ?? null, + ), + }; + } return { - collect: vi.fn().mockResolvedValue(priorReleases), + unique: vi.fn().mockResolvedValue(null), }; - } - if (indexName === "by_package_version") { - const filters = new Map(); - const query = { - eq(field: string, value: unknown) { - filters.set(field, value); - return query; - }, - }; - buildQuery?.(query); - return { - unique: vi.fn().mockResolvedValue( - priorReleases.find( - (release) => - release.packageId === filters.get("packageId") && - release.version === filters.get("version"), - ) ?? null, - ), - }; - } - return { - unique: vi.fn().mockResolvedValue(null), - }; }, ), }; diff --git a/convex/rescans.domain.test.ts b/convex/rescans.domain.test.ts index 2348659a..72deda13 100644 --- a/convex/rescans.domain.test.ts +++ b/convex/rescans.domain.test.ts @@ -1,18 +1,15 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { - dispatchPackageRescanInternal, - requestRescan as requestPackageRescan, -} from "./packages"; -import { - dispatchSkillRescanInternal, - getRescanState as getSkillRescanState, - requestRescan as requestSkillRescan, -} from "./skills"; import { requireUser } from "./lib/access"; import { finalizeInProgressRescanRequestsForTarget, MAX_OWNER_RESCAN_REQUESTS_PER_RELEASE, } from "./model/rescans/policy"; +import { dispatchPackageRescanInternal, requestRescan as requestPackageRescan } from "./packages"; +import { + dispatchSkillRescanInternal, + getRescanState as getSkillRescanState, + requestRescan as requestSkillRescan, +} from "./skills"; vi.mock("./lib/access", () => ({ requireUser: vi.fn(), @@ -172,7 +169,9 @@ function createDb(options?: { const constraints: Record = {}; build(chainEq(constraints)); const matched = requests - .filter((request) => matches(request as unknown as Record, constraints)) + .filter((request) => + matches(request as unknown as Record, constraints), + ) .sort((a, b) => b.createdAt - a.createdAt); return { order: () => ({ diff --git a/convex/skills.backportLatest.test.ts b/convex/skills.backportLatest.test.ts index 5db66874..924a3903 100644 --- a/convex/skills.backportLatest.test.ts +++ b/convex/skills.backportLatest.test.ts @@ -257,9 +257,7 @@ function buildDb(skill: SkillDoc, captured: Captured) { return { withIndex: ( name: string, - build: - | ((q: { eq: (field: string, value: string) => unknown }) => unknown) - | undefined, + build: ((q: { eq: (field: string, value: string) => unknown }) => unknown) | undefined, ) => { if (name !== "by_version") { throw new Error(`unexpected skillEmbeddings index ${name}`); @@ -328,8 +326,7 @@ function buildDb(skill: SkillDoc, captured: Captured) { // convex-helpers `triggers` calls innerDb.patch(tableName, id, value) // for tables with registered triggers (e.g. "skills"); otherwise it // falls back to innerDb.patch(id, value). - const [id, value] = - arg2 !== undefined ? [arg1 as string, arg2] : [arg0 as string, arg1]; + const [id, value] = arg2 !== undefined ? [arg1 as string, arg2] : [arg0 as string, arg1]; captured.allPatches.push({ id: id, @@ -474,9 +471,7 @@ describe("skills.insertVersion latest-tag protection", () => { expect(finalPatch.capabilityTags).toEqual(["cap-v2"]); // `tags.latest` still points to the previous version. - expect(finalPatch.tags).toEqual( - expect.objectContaining({ latest: PREV_LATEST_VERSION_ID }), - ); + expect(finalPatch.tags).toEqual(expect.objectContaining({ latest: PREV_LATEST_VERSION_ID })); // versions counter still increments on every publish, regardless of version order. expect(finalPatch.stats).toMatchObject({ versions: 2 }); @@ -486,10 +481,7 @@ describe("skills.insertVersion latest-tag protection", () => { const skill = buildExistingSkill(); const { ctx, captured } = buildCtx(skill); - await insertVersionHandler( - ctx as never, - buildPublishArgs({ version: "1.0.1" }) as never, - ); + await insertVersionHandler(ctx as never, buildPublishArgs({ version: "1.0.1" }) as never); // New version embedding is NOT marked latest. expect(captured.embeddingInserts).toHaveLength(1); @@ -566,9 +558,7 @@ describe("skills.insertVersion latest-tag protection", () => { const finalPatch = captured.skillPatches.at(-1) as Record; expect(finalPatch.latestVersionId).toBe(PREV_LATEST_VERSION_ID); - expect(finalPatch.tags).toEqual( - expect.objectContaining({ latest: PREV_LATEST_VERSION_ID }), - ); + expect(finalPatch.tags).toEqual(expect.objectContaining({ latest: PREV_LATEST_VERSION_ID })); // The case-variant tag must not leak into the stored tag map either. const tags = finalPatch.tags as Record; expect(tags.LaTeSt).toBeUndefined(); @@ -685,9 +675,7 @@ describe("skills.insertVersion latest-tag protection", () => { const finalPatch = captured.skillPatches.at(-1) as Record; expect(finalPatch.latestVersionId).toBe(NEW_VERSION_ID); expect(finalPatch.latestVersionSummary).toMatchObject({ version: "1.0.0" }); - expect(finalPatch.tags).toEqual( - expect.objectContaining({ latest: NEW_VERSION_ID }), - ); + expect(finalPatch.tags).toEqual(expect.objectContaining({ latest: NEW_VERSION_ID })); expect(captured.embeddingInserts[0]).toMatchObject({ isLatest: true }); }); diff --git a/convex/skills.pendingScanQueue.test.ts b/convex/skills.pendingScanQueue.test.ts index 812b83d0..b60abc99 100644 --- a/convex/skills.pendingScanQueue.test.ts +++ b/convex/skills.pendingScanQueue.test.ts @@ -5,11 +5,11 @@ vi.mock("@convex-dev/auth/server", () => ({ authTables: {}, })); +import { MODERATION_ENGINE_VERSION } from "./lib/moderationReasonCodes"; import { getActiveSkillBatchForStaticScanBackfillInternal, getPendingScanSkillsInternal, } from "./skills"; -import { MODERATION_ENGINE_VERSION } from "./lib/moderationReasonCodes"; type PendingScanResult = Array<{ skillId: string; diff --git a/convex/souls.test.ts b/convex/souls.test.ts index 94a2a7be..b83f8001 100644 --- a/convex/souls.test.ts +++ b/convex/souls.test.ts @@ -10,7 +10,8 @@ const insertVersionHandler = (insertVersion as unknown as WrappedHandler )._handler; -const listHandler = (list as unknown as WrappedHandler<{ ownerUserId?: string; limit?: number }>)._handler; +const listHandler = (list as unknown as WrappedHandler<{ ownerUserId?: string; limit?: number }>) + ._handler; describe("souls.insertVersion", () => { it("throws a soul-specific ownership error for non-owners", async () => { diff --git a/convex/stars.test.ts b/convex/stars.test.ts index 03e0a9ad..e73d26e5 100644 --- a/convex/stars.test.ts +++ b/convex/stars.test.ts @@ -2,8 +2,8 @@ import { getAuthUserId } from "@convex-dev/auth/server"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { isStarred } from "./stars"; import { isStarred as isSoulStarred } from "./soulStars"; +import { isStarred } from "./stars"; vi.mock("@convex-dev/auth/server", () => ({ getAuthUserId: vi.fn(), diff --git a/convex/statsMaintenance.test.ts b/convex/statsMaintenance.test.ts index aa8208d5..74c6c5cd 100644 --- a/convex/statsMaintenance.test.ts +++ b/convex/statsMaintenance.test.ts @@ -215,7 +215,7 @@ describe("reconcileSkillStarCounts", () => { // it should NOT trigger a patch based on the star count alone. const skill = { _id: "skills:1", - statsStars: 5, // canonical value — correct + statsStars: 5, // canonical value — correct stats: { stars: 99, comments: 0 }, // legacy value — stale, but not reconcile's concern }; @@ -249,7 +249,7 @@ describe("reconcileSkillStarCounts", () => { it("patches both statsStars and stats.stars when canonical value drifts from actual count", async () => { const skill = { _id: "skills:1", - statsStars: 10, // canonical value — out of sync with actual + statsStars: 10, // canonical value — out of sync with actual stats: { stars: 10, comments: 0 }, }; @@ -259,10 +259,13 @@ describe("reconcileSkillStarCounts", () => { expect(result.scanned).toBe(1); expect(result.patched).toBe(1); - expect(patch).toHaveBeenCalledWith("skills:1", expect.objectContaining({ - statsStars: 7, - stats: expect.objectContaining({ stars: 7 }), - })); + expect(patch).toHaveBeenCalledWith( + "skills:1", + expect.objectContaining({ + statsStars: 7, + stats: expect.objectContaining({ stars: 7 }), + }), + ); }); it("patches when comment count drifts even if star count is correct", async () => { @@ -278,9 +281,12 @@ describe("reconcileSkillStarCounts", () => { expect(result.scanned).toBe(1); expect(result.patched).toBe(1); - expect(patch).toHaveBeenCalledWith("skills:1", expect.objectContaining({ - stats: expect.objectContaining({ comments: 3 }), - })); + expect(patch).toHaveBeenCalledWith( + "skills:1", + expect.objectContaining({ + stats: expect.objectContaining({ comments: 3 }), + }), + ); }); it("skips soft-deleted skills", async () => { diff --git a/e2e/mobile-skills.pw.test.ts b/e2e/mobile-skills.pw.test.ts index 651e8f84..40ddb286 100644 --- a/e2e/mobile-skills.pw.test.ts +++ b/e2e/mobile-skills.pw.test.ts @@ -3,10 +3,7 @@ import { expectHealthyPage, trackRuntimeErrors } from "./helpers/runtimeErrors"; // Only run in mobile projects — skip on desktop test.beforeEach(({}, testInfo) => { - test.skip( - !testInfo.project.name.includes("mobile"), - "mobile-only test", - ); + test.skip(!testInfo.project.name.includes("mobile"), "mobile-only test"); }); test("browse page has no horizontal overflow on mobile", async ({ page }) => { @@ -75,12 +72,13 @@ test("skill detail page has no horizontal overflow on mobile", async ({ page, re }; const ownerHandle = payload.owner?.handle?.trim(); const slug = payload.skill?.slug?.trim(); - test.skip(!ownerHandle || !slug || !payload.skill?.displayName, "fixture missing owner handle, slug, or displayName"); + test.skip( + !ownerHandle || !slug || !payload.skill?.displayName, + "fixture missing owner handle, slug, or displayName", + ); await page.goto(`/${ownerHandle}/${slug}`, { waitUntil: "domcontentloaded" }); - await expect( - page.getByRole("heading", { name: payload.skill!.displayName! }), - ).toBeVisible(); + await expect(page.getByRole("heading", { name: payload.skill!.displayName! })).toBeVisible(); const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth); const clientWidth = await page.evaluate(() => document.documentElement.clientWidth); diff --git a/packages/clawhub/package.json b/packages/clawhub/package.json index 07d0e3ef..6746b991 100644 --- a/packages/clawhub/package.json +++ b/packages/clawhub/package.json @@ -1,59 +1,59 @@ { - "name": "clawhub", - "version": "0.12.0", - "description": "ClawHub CLI \\u2014 install, update, search, and publish skills plus OpenClaw packages.", - "homepage": "https://clawhub.ai", - "bugs": { - "url": "https://github.com/openclaw/clawhub/issues" - }, - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/openclaw/clawhub.git", - "directory": "packages/clawhub" - }, - "bin": { - "clawdhub": "bin/clawdhub.js", - "clawhub": "bin/clawdhub.js" - }, - "files": [ - "bin", - "dist", - "README.md", - "LICENSE" - ], - "type": "module", - "publishConfig": { - "access": "public" - }, - "scripts": { - "build": "node ./scripts/build.mjs", - "dev": "node --enable-source-maps dist/cli.js", - "prepublishOnly": "npm run build", - "test": "bun run test:src", - "test:artifact": "bun run build && vitest run -c vitest.artifact.config.ts", - "test:src": "vitest run -c vitest.config.ts", - "verify": "bun run test:src && bun run verify:build && bun run test:artifact", - "verify:build": "tsc -p tsconfig.json --noEmit" - }, - "dependencies": { - "@clack/prompts": "^1.3.0", - "arktype": "^2.2.0", - "commander": "^14.0.3", - "fflate": "^0.8.2", - "ignore": "^7.0.5", - "json5": "^2.2.3", - "mime": "^4.1.0", - "ora": "^9.4.0", - "p-retry": "8.0.0", - "semver": "^7.7.4", - "undici": "7.25.0" - }, - "devDependencies": { - "@types/node": "^25.5.0", - "typescript": "6.0.3" - }, - "engines": { - "node": ">=20" - } + "name": "clawhub", + "version": "0.12.0", + "description": "ClawHub CLI \\u2014 install, update, search, and publish skills plus OpenClaw packages.", + "homepage": "https://clawhub.ai", + "bugs": { + "url": "https://github.com/openclaw/clawhub/issues" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/openclaw/clawhub.git", + "directory": "packages/clawhub" + }, + "bin": { + "clawdhub": "bin/clawdhub.js", + "clawhub": "bin/clawdhub.js" + }, + "files": [ + "bin", + "dist", + "README.md", + "LICENSE" + ], + "type": "module", + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "node ./scripts/build.mjs", + "dev": "node --enable-source-maps dist/cli.js", + "prepublishOnly": "npm run build", + "test": "bun run test:src", + "test:artifact": "bun run build && vitest run -c vitest.artifact.config.ts", + "test:src": "vitest run -c vitest.config.ts", + "verify": "bun run test:src && bun run verify:build && bun run test:artifact", + "verify:build": "tsc -p tsconfig.json --noEmit" + }, + "dependencies": { + "@clack/prompts": "^1.3.0", + "arktype": "^2.2.0", + "commander": "^14.0.3", + "fflate": "^0.8.2", + "ignore": "^7.0.5", + "json5": "^2.2.3", + "mime": "^4.1.0", + "ora": "^9.4.0", + "p-retry": "8.0.0", + "semver": "^7.7.4", + "undici": "7.25.0" + }, + "devDependencies": { + "@types/node": "^25.5.0", + "typescript": "6.0.3" + }, + "engines": { + "node": ">=20" + } } diff --git a/packages/schema/package.json b/packages/schema/package.json index 1cc47c16..21cc042c 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -1,38 +1,38 @@ { - "name": "clawhub-schema", - "version": "0.0.2", - "private": true, - "files": [ - "dist", - "README.md" - ], - "type": "module", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - }, - "./licenseConstants": { - "types": "./dist/licenseConstants.d.ts", - "default": "./dist/licenseConstants.js" - }, - "./routes": { - "types": "./dist/routes.d.ts", - "default": "./dist/routes.js" - }, - "./textFiles": { - "types": "./dist/textFiles.d.ts", - "default": "./dist/textFiles.js" - } - }, - "scripts": { - "build": "tsc -p tsconfig.json", - "prepublishOnly": "npm run build" - }, - "dependencies": { - "arktype": "^2.2.0" - }, - "devDependencies": { - "typescript": "6.0.3" - } + "name": "clawhub-schema", + "version": "0.0.2", + "private": true, + "files": [ + "dist", + "README.md" + ], + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./licenseConstants": { + "types": "./dist/licenseConstants.d.ts", + "default": "./dist/licenseConstants.js" + }, + "./routes": { + "types": "./dist/routes.d.ts", + "default": "./dist/routes.js" + }, + "./textFiles": { + "types": "./dist/textFiles.d.ts", + "default": "./dist/textFiles.js" + } + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "prepublishOnly": "npm run build" + }, + "dependencies": { + "arktype": "^2.2.0" + }, + "devDependencies": { + "typescript": "6.0.3" + } } diff --git a/scripts/check-staged-secrets.mjs b/scripts/check-staged-secrets.mjs index 0a4ecb33..be509442 100644 --- a/scripts/check-staged-secrets.mjs +++ b/scripts/check-staged-secrets.mjs @@ -35,13 +35,14 @@ const SECRET_PATTERNS = [ ]; function getStagedPaths() { - const output = execFileSync("git", ["diff", "--cached", "--name-only", "--diff-filter=ACMR", "-z"], { - encoding: "buffer", - }); - return output - .toString("utf8") - .split("\0") - .filter(Boolean); + const output = execFileSync( + "git", + ["diff", "--cached", "--name-only", "--diff-filter=ACMR", "-z"], + { + encoding: "buffer", + }, + ); + return output.toString("utf8").split("\0").filter(Boolean); } function isAllowedExamplePath(path) { @@ -49,7 +50,9 @@ function isAllowedExamplePath(path) { } function isDisallowedPath(path) { - return DISALLOWED_PATH_PATTERNS.some((pattern) => pattern.test(path)) && !isAllowedExamplePath(path); + return ( + DISALLOWED_PATH_PATTERNS.some((pattern) => pattern.test(path)) && !isAllowedExamplePath(path) + ); } function getStagedFileContent(path) { @@ -100,7 +103,9 @@ if (findings.length === 0) { } console.error("Secret scan blocked this commit."); -console.error("Remove the secret, move it to local env/config, or add `secret-scan: allow` next to an intentional test fixture."); +console.error( + "Remove the secret, move it to local env/config, or add `secret-scan: allow` next to an intentional test fixture.", +); console.error(""); for (const finding of findings) { console.error(`- ${finding.path}: ${finding.reason}`); diff --git a/scripts/copy-og-assets.ts b/scripts/copy-og-assets.ts index b2a09ac1..3109e207 100644 --- a/scripts/copy-og-assets.ts +++ b/scripts/copy-og-assets.ts @@ -25,10 +25,14 @@ const resvgWasmSource = await resolveExistingPath( nodeModuleCandidates("@resvg/resvg-wasm/index_bg.wasm"), ); const bricolage800Source = await resolveExistingPath( - nodeModuleCandidates("@fontsource/bricolage-grotesque/files/bricolage-grotesque-latin-800-normal.woff2"), + nodeModuleCandidates( + "@fontsource/bricolage-grotesque/files/bricolage-grotesque-latin-800-normal.woff2", + ), ); const bricolage500Source = await resolveExistingPath( - nodeModuleCandidates("@fontsource/bricolage-grotesque/files/bricolage-grotesque-latin-500-normal.woff2"), + nodeModuleCandidates( + "@fontsource/bricolage-grotesque/files/bricolage-grotesque-latin-500-normal.woff2", + ), ); const ibmPlex500Source = await resolveExistingPath( nodeModuleCandidates("@fontsource/ibm-plex-mono/files/ibm-plex-mono-latin-500-normal.woff2"), diff --git a/scripts/github/clawhub-rescan-auto-response.test.mjs b/scripts/github/clawhub-rescan-auto-response.test.mjs index 08587726..aa1f7e3c 100644 --- a/scripts/github/clawhub-rescan-auto-response.test.mjs +++ b/scripts/github/clawhub-rescan-auto-response.test.mjs @@ -62,11 +62,7 @@ describe("clawhub rescan auto-response classifier", () => { "Skill flagged as suspicious", "This skill should be clean now. Please tell me how to clear the flag.", ], - [ - 1903, - "supicious flag on plugin", - "The plugin is incorrectly flagged and needs a fresh scan.", - ], + [1903, "supicious flag on plugin", "The plugin is incorrectly flagged and needs a fresh scan."], ])("matches explicit rescan/re-evaluation request #%s", (number, title, body) => { const result = classifyRescanRequest(issue({ number, title, body })); diff --git a/scripts/install-git-hooks.mjs b/scripts/install-git-hooks.mjs index 1e3d2674..ac532f12 100644 --- a/scripts/install-git-hooks.mjs +++ b/scripts/install-git-hooks.mjs @@ -33,9 +33,13 @@ const hookSnippet = ` `; if (!existsSync(preCommitPath)) { - writeFileSync(preCommitPath, `#!/bin/sh + writeFileSync( + preCommitPath, + `#!/bin/sh set -eu${hookSnippet} -`, "utf8"); +`, + "utf8", + ); chmodSync(preCommitPath, 0o755); process.exit(0); } @@ -45,6 +49,10 @@ if (existing.includes("pre-commit-secret-scan")) { process.exit(0); } -writeFileSync(preCommitPath, `${existing.trimEnd()}${hookSnippet} -`, "utf8"); +writeFileSync( + preCommitPath, + `${existing.trimEnd()}${hookSnippet} +`, + "utf8", +); chmodSync(preCommitPath, 0o755); diff --git a/scripts/security-dataset/convexOutput.test.ts b/scripts/security-dataset/convexOutput.test.ts index da60f562..2cfc3332 100644 --- a/scripts/security-dataset/convexOutput.test.ts +++ b/scripts/security-dataset/convexOutput.test.ts @@ -2,45 +2,45 @@ import { describe, expect, it } from "vitest"; import { parseConvexJson, parseConvexJsonMatching } from "./convexOutput"; describe("Convex CLI output parsing", () => { - it("parses a JSON object with nested braces and trailing CLI text", () => { - expect( - parseConvexJson( - [ - "Running function...", - '{ "page": [{ "text": "brace } inside string", "items": [1, 2] }], "isDone": true }', - "Function ran successfully.", - ].join("\n"), - ), - ).toEqual({ - page: [{ text: "brace } inside string", items: [1, 2] }], - isDone: true, - }); - }); + it("parses a JSON object with nested braces and trailing CLI text", () => { + expect( + parseConvexJson( + [ + "Running function...", + '{ "page": [{ "text": "brace } inside string", "items": [1, 2] }], "isDone": true }', + "Function ran successfully.", + ].join("\n"), + ), + ).toEqual({ + page: [{ text: "brace } inside string", items: [1, 2] }], + isDone: true, + }); + }); - it("skips incomplete JSON-looking prefixes", () => { - expect(parseConvexJson("partial { nope\n[1, 2, 3]\n")).toEqual([1, 2, 3]); - }); + it("skips incomplete JSON-looking prefixes", () => { + expect(parseConvexJson("partial { nope\n[1, 2, 3]\n")).toEqual([1, 2, 3]); + }); - it("can require the expected response envelope", () => { - const output = [ - '{"artifactSha256":"nested-artifact"}', - '{"continueCursor":"cursor","exportMode":"public","isDone":false,"page":[]}', - ].join("\n"); + it("can require the expected response envelope", () => { + const output = [ + '{"artifactSha256":"nested-artifact"}', + '{"continueCursor":"cursor","exportMode":"public","isDone":false,"page":[]}', + ].join("\n"); - expect( - parseConvexJsonMatching( - output, - (value): value is { continueCursor: string } => - typeof value === "object" && - value !== null && - "continueCursor" in value && - "page" in value, - ), - ).toEqual({ - continueCursor: "cursor", - exportMode: "public", - isDone: false, - page: [], - }); - }); + expect( + parseConvexJsonMatching( + output, + (value): value is { continueCursor: string } => + typeof value === "object" && + value !== null && + "continueCursor" in value && + "page" in value, + ), + ).toEqual({ + continueCursor: "cursor", + exportMode: "public", + isDone: false, + page: [], + }); + }); }); diff --git a/scripts/security-dataset/convexOutput.ts b/scripts/security-dataset/convexOutput.ts index 01edf976..d4d0e01d 100644 --- a/scripts/security-dataset/convexOutput.ts +++ b/scripts/security-dataset/convexOutput.ts @@ -1,65 +1,65 @@ export function parseConvexJson(output: string): unknown { - return parseConvexJsonMatching(output, isJsonValue); + return parseConvexJsonMatching(output, isJsonValue); } export function parseConvexJsonMatching( - output: string, - validate: (value: unknown) => value is T, + output: string, + validate: (value: unknown) => value is T, ): T { - for (let index = 0; index < output.length; index += 1) { - const char = output[index]; - if (char !== "{" && char !== "[") continue; + for (let index = 0; index < output.length; index += 1) { + const char = output[index]; + if (char !== "{" && char !== "[") continue; - const end = findJsonValueEnd(output, index); - if (end === null) continue; + const end = findJsonValueEnd(output, index); + if (end === null) continue; - try { - const parsed: unknown = JSON.parse(output.slice(index, end)); - if (validate(parsed)) return parsed; - } catch { - continue; - } - } + try { + const parsed: unknown = JSON.parse(output.slice(index, end)); + if (validate(parsed)) return parsed; + } catch { + continue; + } + } - throw new Error(`Unable to parse matching Convex JSON output (${output.length} bytes)`); + throw new Error(`Unable to parse matching Convex JSON output (${output.length} bytes)`); } function isJsonValue(value: unknown): value is unknown { - return value !== undefined; + return value !== undefined; } function findJsonValueEnd(output: string, start: number) { - const first = output[start]; - const stack = first === "{" ? ["}"] : first === "[" ? ["]"] : []; - let inString = false; - let escaped = false; + const first = output[start]; + const stack = first === "{" ? ["}"] : first === "[" ? ["]"] : []; + let inString = false; + let escaped = false; - for (let index = start + 1; index < output.length; index += 1) { - const char = output[index]; + for (let index = start + 1; index < output.length; index += 1) { + const char = output[index]; - if (inString) { - if (escaped) { - escaped = false; - } else if (char === "\\") { - escaped = true; - } else if (char === '"') { - inString = false; - } - continue; - } + if (inString) { + if (escaped) { + escaped = false; + } else if (char === "\\") { + escaped = true; + } else if (char === '"') { + inString = false; + } + continue; + } - if (char === '"') { - inString = true; - } else if (char === "{") { - stack.push("}"); - } else if (char === "[") { - stack.push("]"); - } else if (char === "}" || char === "]") { - if (stack.at(-1) !== char) return null; - stack.pop(); - if (stack.length === 0) return index + 1; - } - } + if (char === '"') { + inString = true; + } else if (char === "{") { + stack.push("}"); + } else if (char === "[") { + stack.push("]"); + } else if (char === "}" || char === "]") { + if (stack.at(-1) !== char) return null; + stack.pop(); + if (stack.length === 0) return index + 1; + } + } - return null; + return null; } diff --git a/scripts/security-dataset/export-snapshot.ts b/scripts/security-dataset/export-snapshot.ts index d3dd8fcf..a5ffa935 100644 --- a/scripts/security-dataset/export-snapshot.ts +++ b/scripts/security-dataset/export-snapshot.ts @@ -10,82 +10,82 @@ import { parseConvexJsonMatching } from "./convexOutput"; import { reserveExportInputs } from "./exportLimit"; import { buildSecurityDatasetManifest } from "./manifest"; import { - normalizeArtifactExport, - type ArtifactExportInput, - type NormalizedDatasetRows, - type SourceKind, + normalizeArtifactExport, + type ArtifactExportInput, + type NormalizedDatasetRows, + type SourceKind, } from "./normalize"; import { - assertCreatedTimeWindow, - clampCreatedBounds, - emptyCreatedTimeWindow, - parseCreatedTimestamp, - type CreatedTimeWindow, + assertCreatedTimeWindow, + clampCreatedBounds, + emptyCreatedTimeWindow, + parseCreatedTimestamp, + type CreatedTimeWindow, } from "./timeWindow"; const execFileAsync = promisify(execFile); type ConvexPage = { - page: ArtifactExportInput[]; - isDone: boolean; - continueCursor: string; - exportMode: "public"; + page: ArtifactExportInput[]; + isDone: boolean; + continueCursor: string; + exportMode: "public"; }; type ConvexBounds = { - sourceKind: SourceKind; - minCreatedAt: number | null; - maxCreatedAt: number | null; + sourceKind: SourceKind; + minCreatedAt: number | null; + maxCreatedAt: number | null; }; type CompressedConvexPage = { - encoding: "gzip-base64-json"; - payload: string; + encoding: "gzip-base64-json"; + payload: string; }; type Options = { - deployment: string | null; - prod: boolean; - push: boolean; - dryRun: boolean; - mode: "public"; - limit: number | null; - pageSize: number; - batchPages: number; - concurrency: number; - shards: number; - outDir: string; - sourceKind: SourceKind | "all"; - timeWindow: CreatedTimeWindow; - convexExportZip: string | null; + deployment: string | null; + prod: boolean; + push: boolean; + dryRun: boolean; + mode: "public"; + limit: number | null; + pageSize: number; + batchPages: number; + concurrency: number; + shards: number; + outDir: string; + sourceKind: SourceKind | "all"; + timeWindow: CreatedTimeWindow; + convexExportZip: string | null; }; type ExportShard = { - sourceKind: SourceKind; - createdAtGte?: number; - createdAtLt?: number; - label: string; + sourceKind: SourceKind; + createdAtGte?: number; + createdAtLt?: number; + label: string; }; type SnapshotState = { - sourceArtifacts: number; - rowCounts: { - artifacts: number; - scanResults: number; - staticFindings: number; - labels: number; - splits: number; - }; - scannerVersions: Set; - modelNames: Set; + sourceArtifacts: number; + rowCounts: { + artifacts: number; + scanResults: number; + staticFindings: number; + labels: number; + splits: number; + }; + scannerVersions: Set; + modelNames: Set; }; type SnapshotWriters = { - artifacts: WriteStream; - scanResults: WriteStream; - staticFindings: WriteStream; - labels: WriteStream; - splits: WriteStream; + artifacts: WriteStream; + scanResults: WriteStream; + staticFindings: WriteStream; + labels: WriteStream; + splits: WriteStream; }; const DEFAULT_PAGE_SIZE = 50; @@ -98,567 +98,567 @@ const CONVEX_RUN_MAX_BUFFER_BYTES = 128 * 1024 * 1024; const SOURCE_KINDS: SourceKind[] = ["skill", "package"]; async function main() { - const options = parseArgs(process.argv.slice(2)); - const snapshotId = buildSnapshotId(options); - const snapshotDir = resolve(options.outDir, snapshotId); - const writers = options.dryRun ? null : await openSnapshotWriters(snapshotDir); - let writersClosed = false; - const state = createSnapshotState(); + const options = parseArgs(process.argv.slice(2)); + const snapshotId = buildSnapshotId(options); + const snapshotDir = resolve(options.outDir, snapshotId); + const writers = options.dryRun ? null : await openSnapshotWriters(snapshotDir); + let writersClosed = false; + const state = createSnapshotState(); - try { - const shardCount = options.convexExportZip - ? await exportConvexExportZip({ options, state, writers }) - : await exportRemoteShards({ options, state, writers }); - const manifest = buildManifest({ options, snapshotId, state, shardCount }); + try { + const shardCount = options.convexExportZip + ? await exportConvexExportZip({ options, state, writers }) + : await exportRemoteShards({ options, state, writers }); + const manifest = buildManifest({ options, snapshotId, state, shardCount }); - if (options.dryRun) { - console.log(JSON.stringify({ snapshotId, dryRun: true, manifest }, null, 2)); - return; - } + if (options.dryRun) { + console.log(JSON.stringify({ snapshotId, dryRun: true, manifest }, null, 2)); + return; + } - if (!writers) throw new Error("Snapshot writers were not opened."); - await closeSnapshotWriters(writers); - writersClosed = true; - await writeFile(join(snapshotDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`); + if (!writers) throw new Error("Snapshot writers were not opened."); + await closeSnapshotWriters(writers); + writersClosed = true; + await writeFile(join(snapshotDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`); - console.log(JSON.stringify({ snapshotId, snapshotDir, manifest }, null, 2)); - } catch (error) { - if (writers && !writersClosed) await closeSnapshotWriters(writers).catch(() => {}); - throw error; - } + console.log(JSON.stringify({ snapshotId, snapshotDir, manifest }, null, 2)); + } catch (error) { + if (writers && !writersClosed) await closeSnapshotWriters(writers).catch(() => {}); + throw error; + } } async function exportRemoteShards(input: { - options: Options; - state: SnapshotState; - writers: SnapshotWriters | null; + options: Options; + state: SnapshotState; + writers: SnapshotWriters | null; }) { - const { options, state, writers } = input; - const shards = await buildExportShards(options); - await exportShards({ options, shards, state, writers }); - return shards.length; + const { options, state, writers } = input; + const shards = await buildExportShards(options); + await exportShards({ options, shards, state, writers }); + return shards.length; } async function exportConvexExportZip(input: { - options: Options; - state: SnapshotState; - writers: SnapshotWriters | null; + options: Options; + state: SnapshotState; + writers: SnapshotWriters | null; }) { - const { options, state, writers } = input; - if (!options.convexExportZip) throw new Error("Missing Convex export ZIP path."); - const inputs = await artifactInputsFromConvexExportZip(options.convexExportZip); - const reserved = reserveExportInputs( - filterExportInputs(inputs, options.sourceKind, options.timeWindow), - state, - options.limit, - ); - await processArtifactInputs({ inputs: reserved, state, writers }); - console.error( - `[snapshot] convex-export +${reserved.length} artifacts (${state.sourceArtifacts} total)`, - ); - return 0; + const { options, state, writers } = input; + if (!options.convexExportZip) throw new Error("Missing Convex export ZIP path."); + const inputs = await artifactInputsFromConvexExportZip(options.convexExportZip); + const reserved = reserveExportInputs( + filterExportInputs(inputs, options.sourceKind, options.timeWindow), + state, + options.limit, + ); + await processArtifactInputs({ inputs: reserved, state, writers }); + console.error( + `[snapshot] convex-export +${reserved.length} artifacts (${state.sourceArtifacts} total)`, + ); + return 0; } function filterExportInputs( - inputs: ArtifactExportInput[], - sourceKind: SourceKind | "all", - timeWindow: CreatedTimeWindow, + inputs: ArtifactExportInput[], + sourceKind: SourceKind | "all", + timeWindow: CreatedTimeWindow, ) { - return inputs.filter((input) => { - if (sourceKind !== "all" && input.sourceKind !== sourceKind) return false; - if (timeWindow.createdAtGte !== null && input.createdAt < timeWindow.createdAtGte) return false; - if (timeWindow.createdAtLt !== null && input.createdAt >= timeWindow.createdAtLt) return false; - return true; - }); + return inputs.filter((input) => { + if (sourceKind !== "all" && input.sourceKind !== sourceKind) return false; + if (timeWindow.createdAtGte !== null && input.createdAt < timeWindow.createdAtGte) return false; + if (timeWindow.createdAtLt !== null && input.createdAt >= timeWindow.createdAtLt) return false; + return true; + }); } async function buildExportShards(options: Options) { - const sourceKinds = options.sourceKind === "all" ? SOURCE_KINDS : [options.sourceKind]; - const shards: ExportShard[] = []; + const sourceKinds = options.sourceKind === "all" ? SOURCE_KINDS : [options.sourceKind]; + const shards: ExportShard[] = []; - for (const sourceKind of sourceKinds) { - const bounds = await runConvexBounds(options, sourceKind); - shards.push(...boundsToShards(clampCreatedBounds(bounds, options.timeWindow), options.shards)); - } + for (const sourceKind of sourceKinds) { + const bounds = await runConvexBounds(options, sourceKind); + shards.push(...boundsToShards(clampCreatedBounds(bounds, options.timeWindow), options.shards)); + } - return shards; + return shards; } async function exportShards(input: { - options: Options; - shards: ExportShard[]; - state: SnapshotState; - writers: SnapshotWriters | null; + options: Options; + shards: ExportShard[]; + state: SnapshotState; + writers: SnapshotWriters | null; }) { - const { options, shards, state, writers } = input; - await runWithConcurrency(shards, options.concurrency, async (shard) => { - await exportShard({ options, shard, state, writers }); - }); + const { options, shards, state, writers } = input; + await runWithConcurrency(shards, options.concurrency, async (shard) => { + await exportShard({ options, shard, state, writers }); + }); } async function exportShard(input: { - options: Options; - shard: ExportShard; - state: SnapshotState; - writers: SnapshotWriters | null; + options: Options; + shard: ExportShard; + state: SnapshotState; + writers: SnapshotWriters | null; }) { - const { options, shard, state, writers } = input; - let cursor: string | null = null; - let batchPages = options.batchPages; - while (!isLimitReached(options, state)) { - const result = await runConvexPage(options, shard, cursor, options.pageSize, batchPages); - batchPages = result.batchPages; - const page = result.page; - const inputs = reserveExportInputs(page.page, state, options.limit); - if (inputs.length > 0) { - await processArtifactInputs({ inputs, state, writers }); - console.error( - `[snapshot] ${shard.label} +${inputs.length} artifacts (${state.sourceArtifacts} total)`, - ); - } - if (page.isDone || inputs.length < page.page.length) return; - cursor = page.continueCursor; - } + const { options, shard, state, writers } = input; + let cursor: string | null = null; + let batchPages = options.batchPages; + while (!isLimitReached(options, state)) { + const result = await runConvexPage(options, shard, cursor, options.pageSize, batchPages); + batchPages = result.batchPages; + const page = result.page; + const inputs = reserveExportInputs(page.page, state, options.limit); + if (inputs.length > 0) { + await processArtifactInputs({ inputs, state, writers }); + console.error( + `[snapshot] ${shard.label} +${inputs.length} artifacts (${state.sourceArtifacts} total)`, + ); + } + if (page.isDone || inputs.length < page.page.length) return; + cursor = page.continueCursor; + } } async function runConvexPage( - options: Options, - shard: ExportShard, - cursor: string | null, - numItems: number, - batchPages: number, + options: Options, + shard: ExportShard, + cursor: string | null, + numItems: number, + batchPages: number, ): Promise<{ page: ConvexPage; batchPages: number }> { - const functionName = "securityDatasetNode:listArtifactExportBatchCompressedInternal"; - let pageCount = batchPages; + const functionName = "securityDatasetNode:listArtifactExportBatchCompressedInternal"; + let pageCount = batchPages; - while (true) { - const args = { - sourceKind: shard.sourceKind, - mode: options.mode, - createdAtGte: shard.createdAtGte, - createdAtLt: shard.createdAtLt, - paginationOpts: { cursor, numItems }, - pageCount, - }; + while (true) { + const args = { + sourceKind: shard.sourceKind, + mode: options.mode, + createdAtGte: shard.createdAtGte, + createdAtLt: shard.createdAtLt, + paginationOpts: { cursor, numItems }, + pageCount, + }; - let lastError: unknown = null; - for (let attempt = 1; attempt <= DEFAULT_MAX_CONVEX_ATTEMPTS; attempt += 1) { - try { - const compressed = await runConvexJsonOnce( - options, - functionName, - args, - isCompressedConvexPage, - ); - return { page: decodeCompressedConvexPage(compressed), batchPages: pageCount }; - } catch (error) { - lastError = error; - if (isLikelyTruncatedConvexOutput(error) && pageCount > 1) break; - if (attempt === DEFAULT_MAX_CONVEX_ATTEMPTS) break; - console.error( - `[snapshot] retrying ${functionName} batch-pages=${pageCount} after attempt ${attempt}: ${errorMessage(error)}`, - ); - await delay(attempt * 500); - } - } + let lastError: unknown = null; + for (let attempt = 1; attempt <= DEFAULT_MAX_CONVEX_ATTEMPTS; attempt += 1) { + try { + const compressed = await runConvexJsonOnce( + options, + functionName, + args, + isCompressedConvexPage, + ); + return { page: decodeCompressedConvexPage(compressed), batchPages: pageCount }; + } catch (error) { + lastError = error; + if (isLikelyTruncatedConvexOutput(error) && pageCount > 1) break; + if (attempt === DEFAULT_MAX_CONVEX_ATTEMPTS) break; + console.error( + `[snapshot] retrying ${functionName} batch-pages=${pageCount} after attempt ${attempt}: ${errorMessage(error)}`, + ); + await delay(attempt * 500); + } + } - if (isLikelyTruncatedConvexOutput(lastError) && pageCount > 1) { - const nextPageCount = Math.max(1, Math.floor(pageCount / 2)); - console.error( - `[snapshot] ${shard.label} reducing batch-pages ${pageCount}->${nextPageCount}: ${errorMessage(lastError)}`, - ); - pageCount = nextPageCount; - continue; - } + if (isLikelyTruncatedConvexOutput(lastError) && pageCount > 1) { + const nextPageCount = Math.max(1, Math.floor(pageCount / 2)); + console.error( + `[snapshot] ${shard.label} reducing batch-pages ${pageCount}->${nextPageCount}: ${errorMessage(lastError)}`, + ); + pageCount = nextPageCount; + continue; + } - writeCommandErrorOutput(lastError); - throw lastError; - } + writeCommandErrorOutput(lastError); + throw lastError; + } } async function runConvexBounds(options: Options, sourceKind: SourceKind): Promise { - return runConvexJson( - options, - "securityDataset:getArtifactExportBoundsInternal", - { sourceKind }, - isConvexBounds, - ); + return runConvexJson( + options, + "securityDataset:getArtifactExportBoundsInternal", + { sourceKind }, + isConvexBounds, + ); } async function runConvexJson( - options: Options, - functionName: string, - args: unknown, - validate: (value: unknown) => value is T, + options: Options, + functionName: string, + args: unknown, + validate: (value: unknown) => value is T, ): Promise { - let lastError: unknown = null; - for (let attempt = 1; attempt <= DEFAULT_MAX_CONVEX_ATTEMPTS; attempt += 1) { - try { - return await runConvexJsonOnce(options, functionName, args, validate); - } catch (error) { - lastError = error; - if (attempt === DEFAULT_MAX_CONVEX_ATTEMPTS) break; - console.error( - `[snapshot] retrying ${functionName} after attempt ${attempt}: ${errorMessage(error)}`, - ); - await delay(attempt * 500); - } - } + let lastError: unknown = null; + for (let attempt = 1; attempt <= DEFAULT_MAX_CONVEX_ATTEMPTS; attempt += 1) { + try { + return await runConvexJsonOnce(options, functionName, args, validate); + } catch (error) { + lastError = error; + if (attempt === DEFAULT_MAX_CONVEX_ATTEMPTS) break; + console.error( + `[snapshot] retrying ${functionName} after attempt ${attempt}: ${errorMessage(error)}`, + ); + await delay(attempt * 500); + } + } - writeCommandErrorOutput(lastError); - throw lastError; + writeCommandErrorOutput(lastError); + throw lastError; } async function runConvexJsonOnce( - options: Options, - functionName: string, - args: unknown, - validate: (value: unknown) => value is T, + options: Options, + functionName: string, + args: unknown, + validate: (value: unknown) => value is T, ): Promise { - const commandArgs = buildConvexRunArgs(options, functionName, args); - const result = await execFileAsync("bunx", commandArgs, { - cwd: process.cwd(), - encoding: "utf8", - env: convexRunEnv(), - maxBuffer: CONVEX_RUN_MAX_BUFFER_BYTES, - }); - try { - return parseConvexJsonMatching(result.stdout, validate); - } catch (parseError) { - await writeDebugConvexOutput(functionName, result.stdout); - throw parseError; - } + const commandArgs = buildConvexRunArgs(options, functionName, args); + const result = await execFileAsync("bunx", commandArgs, { + cwd: process.cwd(), + encoding: "utf8", + env: convexRunEnv(), + maxBuffer: CONVEX_RUN_MAX_BUFFER_BYTES, + }); + try { + return parseConvexJsonMatching(result.stdout, validate); + } catch (parseError) { + await writeDebugConvexOutput(functionName, result.stdout); + throw parseError; + } } function convexRunEnv() { - const { FORCE_COLOR: _forceColor, ...env } = process.env; - return { ...env, NO_COLOR: "1" }; + const { FORCE_COLOR: _forceColor, ...env } = process.env; + return { ...env, NO_COLOR: "1" }; } async function writeDebugConvexOutput(functionName: string, stdout: string) { - const debugDir = process.env.SECURITY_DATASET_DEBUG_CONVEX_OUTPUT_DIR; - if (!debugDir) return; - await mkdir(debugDir, { recursive: true }); - const safeFunctionName = functionName.replace(/[^a-zA-Z0-9_-]/g, "-"); - const path = join(debugDir, `${Date.now()}-${process.pid}-${safeFunctionName}.stdout`); - await writeFile(path, stdout); - console.error(`[snapshot] wrote debug Convex stdout to ${path}`); + const debugDir = process.env.SECURITY_DATASET_DEBUG_CONVEX_OUTPUT_DIR; + if (!debugDir) return; + await mkdir(debugDir, { recursive: true }); + const safeFunctionName = functionName.replace(/[^a-zA-Z0-9_-]/g, "-"); + const path = join(debugDir, `${Date.now()}-${process.pid}-${safeFunctionName}.stdout`); + await writeFile(path, stdout); + console.error(`[snapshot] wrote debug Convex stdout to ${path}`); } function buildManifest(input: { - options: Options; - snapshotId: string; - state: SnapshotState; - shardCount: number; + options: Options; + snapshotId: string; + state: SnapshotState; + shardCount: number; }) { - const { options, snapshotId, state, shardCount } = input; - const repoGitSha = gitSha(); - return buildSecurityDatasetManifest({ - snapshotId, - createdAt: new Date().toISOString(), - repoGitSha, - convexDeployment: options.deployment ?? (options.prod ? "prod" : "configured-dev"), - exportMode: options.mode, - pageSize: options.pageSize, - concurrency: options.concurrency, - shards: options.shards, - shardCount, - rowCounts: { - sourceArtifacts: state.sourceArtifacts, - artifacts: state.rowCounts.artifacts, - scanResults: state.rowCounts.scanResults, - staticFindings: state.rowCounts.staticFindings, - labels: state.rowCounts.labels, - splits: state.rowCounts.splits, - }, - scannerVersions: Array.from(state.scannerVersions).sort(), - modelNames: Array.from(state.modelNames).sort(), - redactionPolicyVersion: "public-signals-v1", - sourceTables: ["skillVersions", "packageReleases"], - timeWindow: options.timeWindow, - }); + const { options, snapshotId, state, shardCount } = input; + const repoGitSha = gitSha(); + return buildSecurityDatasetManifest({ + snapshotId, + createdAt: new Date().toISOString(), + repoGitSha, + convexDeployment: options.deployment ?? (options.prod ? "prod" : "configured-dev"), + exportMode: options.mode, + pageSize: options.pageSize, + concurrency: options.concurrency, + shards: options.shards, + shardCount, + rowCounts: { + sourceArtifacts: state.sourceArtifacts, + artifacts: state.rowCounts.artifacts, + scanResults: state.rowCounts.scanResults, + staticFindings: state.rowCounts.staticFindings, + labels: state.rowCounts.labels, + splits: state.rowCounts.splits, + }, + scannerVersions: Array.from(state.scannerVersions).sort(), + modelNames: Array.from(state.modelNames).sort(), + redactionPolicyVersion: "public-signals-v1", + sourceTables: ["skillVersions", "packageReleases"], + timeWindow: options.timeWindow, + }); } function buildSnapshotId(options: Options) { - const timestamp = new Date() - .toISOString() - .replace(/[-:]/g, "") - .replace(/\.\d{3}Z$/, "Z"); - const deployment = - options.deployment?.replace(/[^a-zA-Z0-9]+/g, "-") ?? (options.prod ? "prod" : "dev"); - return `clawhub-${deployment}-${timestamp}-${gitSha().slice(0, 8)}`; + const timestamp = new Date() + .toISOString() + .replace(/[-:]/g, "") + .replace(/\.\d{3}Z$/, "Z"); + const deployment = + options.deployment?.replace(/[^a-zA-Z0-9]+/g, "-") ?? (options.prod ? "prod" : "dev"); + return `clawhub-${deployment}-${timestamp}-${gitSha().slice(0, 8)}`; } function createSnapshotState(): SnapshotState { - return { - sourceArtifacts: 0, - rowCounts: { - artifacts: 0, - scanResults: 0, - staticFindings: 0, - labels: 0, - splits: 0, - }, - scannerVersions: new Set(), - modelNames: new Set(), - }; + return { + sourceArtifacts: 0, + rowCounts: { + artifacts: 0, + scanResults: 0, + staticFindings: 0, + labels: 0, + splits: 0, + }, + scannerVersions: new Set(), + modelNames: new Set(), + }; } async function processArtifactInputs(input: { - inputs: ArtifactExportInput[]; - state: SnapshotState; - writers: SnapshotWriters | null; + inputs: ArtifactExportInput[]; + state: SnapshotState; + writers: SnapshotWriters | null; }) { - const { inputs, state, writers } = input; - const rows = normalizeArtifactExport(inputs); - state.rowCounts.artifacts += rows.artifacts.length; - state.rowCounts.scanResults += rows.scanResults.length; - state.rowCounts.staticFindings += rows.staticFindings.length; - state.rowCounts.labels += rows.labels.length; - state.rowCounts.splits += rows.splits.length; - for (const row of rows.scanResults) { - if (row.scanner_version) state.scannerVersions.add(row.scanner_version); - if (row.model) state.modelNames.add(row.model); - } + const { inputs, state, writers } = input; + const rows = normalizeArtifactExport(inputs); + state.rowCounts.artifacts += rows.artifacts.length; + state.rowCounts.scanResults += rows.scanResults.length; + state.rowCounts.staticFindings += rows.staticFindings.length; + state.rowCounts.labels += rows.labels.length; + state.rowCounts.splits += rows.splits.length; + for (const row of rows.scanResults) { + if (row.scanner_version) state.scannerVersions.add(row.scanner_version); + if (row.model) state.modelNames.add(row.model); + } - if (!writers) return; - await writeNormalizedRows(writers, rows); + if (!writers) return; + await writeNormalizedRows(writers, rows); } async function openSnapshotWriters(snapshotDir: string): Promise { - await mkdir(snapshotDir, { recursive: true }); - return { - artifacts: createWriteStream(join(snapshotDir, "artifacts.jsonl"), { encoding: "utf8" }), - scanResults: createWriteStream(join(snapshotDir, "scan_results.jsonl"), { encoding: "utf8" }), - staticFindings: createWriteStream(join(snapshotDir, "static_findings.jsonl"), { - encoding: "utf8", - }), - labels: createWriteStream(join(snapshotDir, "labels.jsonl"), { encoding: "utf8" }), - splits: createWriteStream(join(snapshotDir, "splits.jsonl"), { encoding: "utf8" }), - }; + await mkdir(snapshotDir, { recursive: true }); + return { + artifacts: createWriteStream(join(snapshotDir, "artifacts.jsonl"), { encoding: "utf8" }), + scanResults: createWriteStream(join(snapshotDir, "scan_results.jsonl"), { encoding: "utf8" }), + staticFindings: createWriteStream(join(snapshotDir, "static_findings.jsonl"), { + encoding: "utf8", + }), + labels: createWriteStream(join(snapshotDir, "labels.jsonl"), { encoding: "utf8" }), + splits: createWriteStream(join(snapshotDir, "splits.jsonl"), { encoding: "utf8" }), + }; } async function closeSnapshotWriters(writers: SnapshotWriters) { - await Promise.all(Object.values(writers).map((stream) => endStream(stream))); + await Promise.all(Object.values(writers).map((stream) => endStream(stream))); } async function endStream(stream: WriteStream) { - stream.end(); - await once(stream, "finish"); + stream.end(); + await once(stream, "finish"); } async function writeNormalizedRows(writers: SnapshotWriters, rows: NormalizedDatasetRows) { - await writeJsonlRows(writers.artifacts, rows.artifacts); - await writeJsonlRows(writers.scanResults, rows.scanResults); - await writeJsonlRows(writers.staticFindings, rows.staticFindings); - await writeJsonlRows(writers.labels, rows.labels); - await writeJsonlRows(writers.splits, rows.splits); + await writeJsonlRows(writers.artifacts, rows.artifacts); + await writeJsonlRows(writers.scanResults, rows.scanResults); + await writeJsonlRows(writers.staticFindings, rows.staticFindings); + await writeJsonlRows(writers.labels, rows.labels); + await writeJsonlRows(writers.splits, rows.splits); } async function writeJsonlRows(stream: WriteStream, rows: unknown[]) { - if (rows.length === 0) return; - const chunk = `${rows.map((row) => JSON.stringify(row)).join("\n")}\n`; - if (!stream.write(chunk)) await once(stream, "drain"); + if (rows.length === 0) return; + const chunk = `${rows.map((row) => JSON.stringify(row)).join("\n")}\n`; + if (!stream.write(chunk)) await once(stream, "drain"); } function boundsToShards(bounds: ConvexBounds, shardCount: number): ExportShard[] { - if (bounds.minCreatedAt === null || bounds.maxCreatedAt === null) return []; - const start = bounds.minCreatedAt; - const end = bounds.maxCreatedAt + 1; - const span = Math.max(1, end - start); - const width = Math.ceil(span / shardCount); - const shards: ExportShard[] = []; - for (let shardIndex = 0; shardIndex < shardCount; shardIndex += 1) { - const createdAtGte = start + shardIndex * width; - const createdAtLt = Math.min(end, createdAtGte + width); - if (createdAtGte >= end) break; - shards.push({ - sourceKind: bounds.sourceKind, - createdAtGte, - createdAtLt, - label: `${bounds.sourceKind}:${shardIndex + 1}/${shardCount}`, - }); - } - return shards; + if (bounds.minCreatedAt === null || bounds.maxCreatedAt === null) return []; + const start = bounds.minCreatedAt; + const end = bounds.maxCreatedAt + 1; + const span = Math.max(1, end - start); + const width = Math.ceil(span / shardCount); + const shards: ExportShard[] = []; + for (let shardIndex = 0; shardIndex < shardCount; shardIndex += 1) { + const createdAtGte = start + shardIndex * width; + const createdAtLt = Math.min(end, createdAtGte + width); + if (createdAtGte >= end) break; + shards.push({ + sourceKind: bounds.sourceKind, + createdAtGte, + createdAtLt, + label: `${bounds.sourceKind}:${shardIndex + 1}/${shardCount}`, + }); + } + return shards; } function buildConvexRunArgs(options: Options, functionName: string, args: unknown) { - const commandArgs = ["convex", "run"]; - if (options.prod) commandArgs.push("--prod"); - if (options.deployment) commandArgs.push("--deployment", options.deployment); - if (options.push) commandArgs.push("--push", "--typecheck=disable"); - commandArgs.push(functionName, JSON.stringify(args)); - return commandArgs; + const commandArgs = ["convex", "run"]; + if (options.prod) commandArgs.push("--prod"); + if (options.deployment) commandArgs.push("--deployment", options.deployment); + if (options.push) commandArgs.push("--push", "--typecheck=disable"); + commandArgs.push(functionName, JSON.stringify(args)); + return commandArgs; } async function runWithConcurrency( - items: T[], - concurrency: number, - worker: (item: T) => Promise, + items: T[], + concurrency: number, + worker: (item: T) => Promise, ) { - let next = 0; - await Promise.all( - Array.from({ length: Math.min(concurrency, items.length) }, async () => { - while (true) { - const index = next; - next += 1; - if (index >= items.length) return; - await worker(items[index]!); - } - }), - ); + let next = 0; + await Promise.all( + Array.from({ length: Math.min(concurrency, items.length) }, async () => { + while (true) { + const index = next; + next += 1; + if (index >= items.length) return; + await worker(items[index]!); + } + }), + ); } function isLimitReached(options: Options, state: SnapshotState) { - return options.limit !== null && state.sourceArtifacts >= options.limit; + return options.limit !== null && state.sourceArtifacts >= options.limit; } function isConvexPage(value: unknown): value is ConvexPage { - return ( - isRecord(value) && - Array.isArray(value.page) && - typeof value.isDone === "boolean" && - typeof value.continueCursor === "string" && - value.exportMode === "public" - ); + return ( + isRecord(value) && + Array.isArray(value.page) && + typeof value.isDone === "boolean" && + typeof value.continueCursor === "string" && + value.exportMode === "public" + ); } function isConvexBounds(value: unknown): value is ConvexBounds { - return ( - isRecord(value) && - (value.sourceKind === "skill" || value.sourceKind === "package") && - (typeof value.minCreatedAt === "number" || value.minCreatedAt === null) && - (typeof value.maxCreatedAt === "number" || value.maxCreatedAt === null) - ); + return ( + isRecord(value) && + (value.sourceKind === "skill" || value.sourceKind === "package") && + (typeof value.minCreatedAt === "number" || value.minCreatedAt === null) && + (typeof value.maxCreatedAt === "number" || value.maxCreatedAt === null) + ); } function isCompressedConvexPage(value: unknown): value is CompressedConvexPage { - return ( - isRecord(value) && value.encoding === "gzip-base64-json" && typeof value.payload === "string" - ); + return ( + isRecord(value) && value.encoding === "gzip-base64-json" && typeof value.payload === "string" + ); } function decodeCompressedConvexPage(value: CompressedConvexPage) { - const json = gunzipSync(Buffer.from(value.payload, "base64")).toString("utf8"); - const parsed: unknown = JSON.parse(json); - if (isConvexPage(parsed)) return parsed; - throw new Error("Invalid compressed Convex page response."); + const json = gunzipSync(Buffer.from(value.payload, "base64")).toString("utf8"); + const parsed: unknown = JSON.parse(json); + if (isConvexPage(parsed)) return parsed; + throw new Error("Invalid compressed Convex page response."); } function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null; + return typeof value === "object" && value !== null; } function delay(ms: number) { - return new Promise((done) => setTimeout(done, ms)); + return new Promise((done) => setTimeout(done, ms)); } function errorMessage(error: unknown) { - return error instanceof Error ? error.message : String(error); + return error instanceof Error ? error.message : String(error); } function isLikelyTruncatedConvexOutput(error: unknown) { - return /Convex JSON output \(524288 bytes\)/.test(errorMessage(error)); + return /Convex JSON output \(524288 bytes\)/.test(errorMessage(error)); } function writeCommandErrorOutput(error: unknown) { - if (!isRecord(error)) return; - if (typeof error.stderr === "string" && error.stderr.length > 0) { - process.stderr.write(error.stderr); - } else if (typeof error.stdout === "string" && error.stdout.length > 0) { - process.stderr.write(error.stdout); - } + if (!isRecord(error)) return; + if (typeof error.stderr === "string" && error.stderr.length > 0) { + process.stderr.write(error.stderr); + } else if (typeof error.stdout === "string" && error.stdout.length > 0) { + process.stderr.write(error.stdout); + } } function gitSha() { - const result = spawnSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }); - if (result.status !== 0) return "unknown"; - return result.stdout.trim(); + const result = spawnSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }); + if (result.status !== 0) return "unknown"; + return result.stdout.trim(); } function parseArgs(args: string[]): Options { - const options: Options = { - deployment: null, - prod: false, - push: false, - dryRun: false, - mode: "public", - limit: null, - pageSize: DEFAULT_PAGE_SIZE, - batchPages: DEFAULT_BATCH_PAGES, - concurrency: DEFAULT_CONCURRENCY, - shards: DEFAULT_SHARDS, - outDir: DEFAULT_OUT_DIR, - sourceKind: "all", - timeWindow: emptyCreatedTimeWindow(), - convexExportZip: null, - }; + const options: Options = { + deployment: null, + prod: false, + push: false, + dryRun: false, + mode: "public", + limit: null, + pageSize: DEFAULT_PAGE_SIZE, + batchPages: DEFAULT_BATCH_PAGES, + concurrency: DEFAULT_CONCURRENCY, + shards: DEFAULT_SHARDS, + outDir: DEFAULT_OUT_DIR, + sourceKind: "all", + timeWindow: emptyCreatedTimeWindow(), + convexExportZip: null, + }; - for (let index = 0; index < args.length; index += 1) { - const arg = args[index]; - if (arg === "--prod") { - options.prod = true; - } else if (arg === "--push") { - options.push = true; - } else if (arg === "--dry-run") { - options.dryRun = true; - } else if (arg === "--deployment") { - options.deployment = readValue(args, ++index, arg); - } else if (arg === "--limit") { - options.limit = readPositiveInt(readValue(args, ++index, arg), arg); - } else if (arg === "--page-size") { - options.pageSize = readPositiveInt(readValue(args, ++index, arg), arg); - } else if (arg === "--batch-pages") { - options.batchPages = readPositiveInt(readValue(args, ++index, arg), arg); - } else if (arg === "--concurrency") { - options.concurrency = readPositiveInt(readValue(args, ++index, arg), arg); - } else if (arg === "--shards") { - options.shards = readPositiveInt(readValue(args, ++index, arg), arg); - } else if (arg === "--out-dir") { - options.outDir = readValue(args, ++index, arg); - } else if (arg === "--source-kind") { - options.sourceKind = readSourceKind(readValue(args, ++index, arg)); - } else if (arg === "--created-after") { - options.timeWindow.createdAtGte = parseCreatedTimestamp(readValue(args, ++index, arg), arg); - } else if (arg === "--created-before") { - options.timeWindow.createdAtLt = parseCreatedTimestamp(readValue(args, ++index, arg), arg); - } else if (arg === "--convex-export-zip" || arg === "--from-convex-export") { - options.convexExportZip = readValue(args, ++index, arg); - } else if (arg === "--mode") { - const mode = readValue(args, ++index, arg); - if (mode !== "public") throw new Error(`Unsupported mode: ${mode}`); - options.mode = mode; - } else { - throw new Error(`Unknown argument: ${arg}`); - } - } + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + if (arg === "--prod") { + options.prod = true; + } else if (arg === "--push") { + options.push = true; + } else if (arg === "--dry-run") { + options.dryRun = true; + } else if (arg === "--deployment") { + options.deployment = readValue(args, ++index, arg); + } else if (arg === "--limit") { + options.limit = readPositiveInt(readValue(args, ++index, arg), arg); + } else if (arg === "--page-size") { + options.pageSize = readPositiveInt(readValue(args, ++index, arg), arg); + } else if (arg === "--batch-pages") { + options.batchPages = readPositiveInt(readValue(args, ++index, arg), arg); + } else if (arg === "--concurrency") { + options.concurrency = readPositiveInt(readValue(args, ++index, arg), arg); + } else if (arg === "--shards") { + options.shards = readPositiveInt(readValue(args, ++index, arg), arg); + } else if (arg === "--out-dir") { + options.outDir = readValue(args, ++index, arg); + } else if (arg === "--source-kind") { + options.sourceKind = readSourceKind(readValue(args, ++index, arg)); + } else if (arg === "--created-after") { + options.timeWindow.createdAtGte = parseCreatedTimestamp(readValue(args, ++index, arg), arg); + } else if (arg === "--created-before") { + options.timeWindow.createdAtLt = parseCreatedTimestamp(readValue(args, ++index, arg), arg); + } else if (arg === "--convex-export-zip" || arg === "--from-convex-export") { + options.convexExportZip = readValue(args, ++index, arg); + } else if (arg === "--mode") { + const mode = readValue(args, ++index, arg); + if (mode !== "public") throw new Error(`Unsupported mode: ${mode}`); + options.mode = mode; + } else { + throw new Error(`Unknown argument: ${arg}`); + } + } - if (options.prod && options.deployment) { - throw new Error("Use either --prod or --deployment, not both."); - } - assertCreatedTimeWindow(options.timeWindow); - return options; + if (options.prod && options.deployment) { + throw new Error("Use either --prod or --deployment, not both."); + } + assertCreatedTimeWindow(options.timeWindow); + return options; } function readValue(args: string[], index: number, flag: string) { - const value = args[index]; - if (!value) throw new Error(`Missing value for ${flag}`); - return value; + const value = args[index]; + if (!value) throw new Error(`Missing value for ${flag}`); + return value; } function readPositiveInt(value: string, flag: string) { - const parsed = Number.parseInt(value, 10); - if (!Number.isFinite(parsed) || parsed <= 0) - throw new Error(`Expected positive integer for ${flag}`); - return parsed; + const parsed = Number.parseInt(value, 10); + if (!Number.isFinite(parsed) || parsed <= 0) + throw new Error(`Expected positive integer for ${flag}`); + return parsed; } function readSourceKind(value: string): SourceKind | "all" { - if (value === "all" || value === "skill" || value === "package") return value; - throw new Error(`Unsupported source kind: ${value}`); + if (value === "all" || value === "skill" || value === "package") return value; + throw new Error(`Unsupported source kind: ${value}`); } main().catch((error: unknown) => { - console.error(error instanceof Error ? error.message : error); - process.exit(1); + console.error(error instanceof Error ? error.message : error); + process.exit(1); }); diff --git a/scripts/security-dataset/exportLimit.test.ts b/scripts/security-dataset/exportLimit.test.ts index df92e691..df48d17e 100644 --- a/scripts/security-dataset/exportLimit.test.ts +++ b/scripts/security-dataset/exportLimit.test.ts @@ -2,19 +2,19 @@ import { describe, expect, it } from "vitest"; import { reserveExportInputs } from "./exportLimit"; describe("security dataset export limits", () => { - it("reserves rows synchronously so concurrent shards cannot exceed the global limit", () => { - const state = { sourceArtifacts: 0 }; + it("reserves rows synchronously so concurrent shards cannot exceed the global limit", () => { + const state = { sourceArtifacts: 0 }; - expect(reserveExportInputs(["a", "b", "c"], state, 5)).toEqual(["a", "b", "c"]); - expect(reserveExportInputs(["d", "e", "f"], state, 5)).toEqual(["d", "e"]); - expect(reserveExportInputs(["g"], state, 5)).toEqual([]); - expect(state.sourceArtifacts).toBe(5); - }); + expect(reserveExportInputs(["a", "b", "c"], state, 5)).toEqual(["a", "b", "c"]); + expect(reserveExportInputs(["d", "e", "f"], state, 5)).toEqual(["d", "e"]); + expect(reserveExportInputs(["g"], state, 5)).toEqual([]); + expect(state.sourceArtifacts).toBe(5); + }); - it("counts all rows when no limit is set", () => { - const state = { sourceArtifacts: 2 }; + it("counts all rows when no limit is set", () => { + const state = { sourceArtifacts: 2 }; - expect(reserveExportInputs(["a", "b"], state, null)).toEqual(["a", "b"]); - expect(state.sourceArtifacts).toBe(4); - }); + expect(reserveExportInputs(["a", "b"], state, null)).toEqual(["a", "b"]); + expect(state.sourceArtifacts).toBe(4); + }); }); diff --git a/scripts/security-dataset/exportLimit.ts b/scripts/security-dataset/exportLimit.ts index e67e9fc4..14b59693 100644 --- a/scripts/security-dataset/exportLimit.ts +++ b/scripts/security-dataset/exportLimit.ts @@ -1,10 +1,10 @@ export type ExportLimitState = { - sourceArtifacts: number; + sourceArtifacts: number; }; export function reserveExportInputs(inputs: T[], state: ExportLimitState, limit: number | null) { - const remaining = limit === null ? inputs.length : Math.max(0, limit - state.sourceArtifacts); - const reserved = inputs.slice(0, remaining); - state.sourceArtifacts += reserved.length; - return reserved; + const remaining = limit === null ? inputs.length : Math.max(0, limit - state.sourceArtifacts); + const reserved = inputs.slice(0, remaining); + state.sourceArtifacts += reserved.length; + return reserved; } diff --git a/scripts/security-dataset/manifest.test.ts b/scripts/security-dataset/manifest.test.ts index c535a377..3309a29d 100644 --- a/scripts/security-dataset/manifest.test.ts +++ b/scripts/security-dataset/manifest.test.ts @@ -2,45 +2,45 @@ import { describe, expect, it } from "vitest"; import { buildSecurityDatasetManifest, inferConvexProject } from "./manifest"; describe("security dataset manifest", () => { - it("includes dataset lineage fields from the spec", () => { - const manifest = buildSecurityDatasetManifest({ - snapshotId: "clawhub-prod-20260430T000000Z-abcdef12", - createdAt: "2026-04-30T00:00:00.000Z", - repoGitSha: "abcdef123456", - convexDeployment: "amantus:clawdhub:prod", - exportMode: "public", - pageSize: 50, - concurrency: 6, - shards: 12, - shardCount: 24, - rowCounts: { - sourceArtifacts: 1, - artifacts: 1, - scanResults: 2, - staticFindings: 3, - labels: 4, - splits: 1, - }, - scannerVersions: ["v2.4.2"], - modelNames: ["gpt-5-mini"], - redactionPolicyVersion: "public-signals-v1", - sourceTables: ["skillVersions", "packageReleases"], - timeWindow: { createdAtGte: 1777507200000, createdAtLt: 1780185600000 }, - }); + it("includes dataset lineage fields from the spec", () => { + const manifest = buildSecurityDatasetManifest({ + snapshotId: "clawhub-prod-20260430T000000Z-abcdef12", + createdAt: "2026-04-30T00:00:00.000Z", + repoGitSha: "abcdef123456", + convexDeployment: "amantus:clawdhub:prod", + exportMode: "public", + pageSize: 50, + concurrency: 6, + shards: 12, + shardCount: 24, + rowCounts: { + sourceArtifacts: 1, + artifacts: 1, + scanResults: 2, + staticFindings: 3, + labels: 4, + splits: 1, + }, + scannerVersions: ["v2.4.2"], + modelNames: ["gpt-5-mini"], + redactionPolicyVersion: "public-signals-v1", + sourceTables: ["skillVersions", "packageReleases"], + timeWindow: { createdAtGte: 1777507200000, createdAtLt: 1780185600000 }, + }); - expect(manifest).toMatchObject({ - repo_git_sha: "abcdef123456", - source_commit: "abcdef123456", - convex_deployment: "amantus:clawdhub:prod", - convex_project: "clawdhub", - created_time_window: { - created_at_gte: 1777507200000, - created_at_lt: 1780185600000, - }, - }); - }); + expect(manifest).toMatchObject({ + repo_git_sha: "abcdef123456", + source_commit: "abcdef123456", + convex_deployment: "amantus:clawdhub:prod", + convex_project: "clawdhub", + created_time_window: { + created_at_gte: 1777507200000, + created_at_lt: 1780185600000, + }, + }); + }); - it("leaves the project null when Convex only provides a deployment name", () => { - expect(inferConvexProject("wry-manatee-359")).toBeNull(); - }); + it("leaves the project null when Convex only provides a deployment name", () => { + expect(inferConvexProject("wry-manatee-359")).toBeNull(); + }); }); diff --git a/scripts/security-dataset/manifest.ts b/scripts/security-dataset/manifest.ts index 55225588..1a61ccb2 100644 --- a/scripts/security-dataset/manifest.ts +++ b/scripts/security-dataset/manifest.ts @@ -1,65 +1,65 @@ export type SnapshotManifestInput = { - snapshotId: string; - createdAt: string; - repoGitSha: string; - convexDeployment: string; - exportMode: "public"; - pageSize: number; - concurrency: number; - shards: number; - shardCount: number; - rowCounts: { - sourceArtifacts: number; - artifacts: number; - scanResults: number; - staticFindings: number; - labels: number; - splits: number; - }; - scannerVersions: string[]; - modelNames: string[]; - redactionPolicyVersion: string; - sourceTables: string[]; - timeWindow?: { - createdAtGte: number | null; - createdAtLt: number | null; - }; + snapshotId: string; + createdAt: string; + repoGitSha: string; + convexDeployment: string; + exportMode: "public"; + pageSize: number; + concurrency: number; + shards: number; + shardCount: number; + rowCounts: { + sourceArtifacts: number; + artifacts: number; + scanResults: number; + staticFindings: number; + labels: number; + splits: number; + }; + scannerVersions: string[]; + modelNames: string[]; + redactionPolicyVersion: string; + sourceTables: string[]; + timeWindow?: { + createdAtGte: number | null; + createdAtLt: number | null; + }; }; export function buildSecurityDatasetManifest(input: SnapshotManifestInput) { - return { - snapshot_id: input.snapshotId, - created_at: input.createdAt, - repo_git_sha: input.repoGitSha, - convex_deployment: input.convexDeployment, - convex_project: inferConvexProject(input.convexDeployment), - export_mode: input.exportMode, - page_size: input.pageSize, - concurrency: input.concurrency, - shards: input.shards, - shard_count: input.shardCount, - row_counts: { - source_artifacts: input.rowCounts.sourceArtifacts, - artifacts: input.rowCounts.artifacts, - scan_results: input.rowCounts.scanResults, - static_findings: input.rowCounts.staticFindings, - labels: input.rowCounts.labels, - splits: input.rowCounts.splits, - }, - scanner_versions: input.scannerVersions, - model_names: input.modelNames, - redaction_policy_version: input.redactionPolicyVersion, - source_tables: input.sourceTables, - source_commit: input.repoGitSha, - created_time_window: { - created_at_gte: input.timeWindow?.createdAtGte ?? null, - created_at_lt: input.timeWindow?.createdAtLt ?? null, - }, - }; + return { + snapshot_id: input.snapshotId, + created_at: input.createdAt, + repo_git_sha: input.repoGitSha, + convex_deployment: input.convexDeployment, + convex_project: inferConvexProject(input.convexDeployment), + export_mode: input.exportMode, + page_size: input.pageSize, + concurrency: input.concurrency, + shards: input.shards, + shard_count: input.shardCount, + row_counts: { + source_artifacts: input.rowCounts.sourceArtifacts, + artifacts: input.rowCounts.artifacts, + scan_results: input.rowCounts.scanResults, + static_findings: input.rowCounts.staticFindings, + labels: input.rowCounts.labels, + splits: input.rowCounts.splits, + }, + scanner_versions: input.scannerVersions, + model_names: input.modelNames, + redaction_policy_version: input.redactionPolicyVersion, + source_tables: input.sourceTables, + source_commit: input.repoGitSha, + created_time_window: { + created_at_gte: input.timeWindow?.createdAtGte ?? null, + created_at_lt: input.timeWindow?.createdAtLt ?? null, + }, + }; } export function inferConvexProject(convexDeployment: string) { - const parts = convexDeployment.split(":"); - if (parts.length >= 3 && parts[1]) return parts[1]; - return null; + const parts = convexDeployment.split(":"); + if (parts.length >= 3 && parts[1]) return parts[1]; + return null; } diff --git a/scripts/security-dataset/timeWindow.test.ts b/scripts/security-dataset/timeWindow.test.ts index 05027540..9b91d922 100644 --- a/scripts/security-dataset/timeWindow.test.ts +++ b/scripts/security-dataset/timeWindow.test.ts @@ -2,34 +2,34 @@ import { describe, expect, it } from "vitest"; import { assertCreatedTimeWindow, clampCreatedBounds, parseCreatedTimestamp } from "./timeWindow"; describe("security dataset time windows", () => { - it("parses millisecond timestamps and ISO dates", () => { - expect(parseCreatedTimestamp("1777507200000", "--created-after")).toBe(1777507200000); - expect(parseCreatedTimestamp("2026-04-30T00:00:00.000Z", "--created-before")).toBe( - 1777507200000, - ); - }); + it("parses millisecond timestamps and ISO dates", () => { + expect(parseCreatedTimestamp("1777507200000", "--created-after")).toBe(1777507200000); + expect(parseCreatedTimestamp("2026-04-30T00:00:00.000Z", "--created-before")).toBe( + 1777507200000, + ); + }); - it("rejects inverted windows", () => { - expect(() => assertCreatedTimeWindow({ createdAtGte: 20, createdAtLt: 10 })).toThrowError( - "--created-after must be earlier than --created-before.", - ); - }); + it("rejects inverted windows", () => { + expect(() => assertCreatedTimeWindow({ createdAtGte: 20, createdAtLt: 10 })).toThrowError( + "--created-after must be earlier than --created-before.", + ); + }); - it("clamps source bounds to the requested window", () => { - expect( - clampCreatedBounds( - { sourceKind: "skill", minCreatedAt: 10, maxCreatedAt: 30 }, - { createdAtGte: 15, createdAtLt: 25 }, - ), - ).toEqual({ sourceKind: "skill", minCreatedAt: 15, maxCreatedAt: 24 }); - }); + it("clamps source bounds to the requested window", () => { + expect( + clampCreatedBounds( + { sourceKind: "skill", minCreatedAt: 10, maxCreatedAt: 30 }, + { createdAtGte: 15, createdAtLt: 25 }, + ), + ).toEqual({ sourceKind: "skill", minCreatedAt: 15, maxCreatedAt: 24 }); + }); - it("returns empty bounds when the requested window does not overlap", () => { - expect( - clampCreatedBounds( - { sourceKind: "package", minCreatedAt: 10, maxCreatedAt: 30 }, - { createdAtGte: 40, createdAtLt: null }, - ), - ).toEqual({ sourceKind: "package", minCreatedAt: null, maxCreatedAt: null }); - }); + it("returns empty bounds when the requested window does not overlap", () => { + expect( + clampCreatedBounds( + { sourceKind: "package", minCreatedAt: 10, maxCreatedAt: 30 }, + { createdAtGte: 40, createdAtLt: null }, + ), + ).toEqual({ sourceKind: "package", minCreatedAt: null, maxCreatedAt: null }); + }); }); diff --git a/scripts/security-dataset/timeWindow.ts b/scripts/security-dataset/timeWindow.ts index b66a8aec..f5430f36 100644 --- a/scripts/security-dataset/timeWindow.ts +++ b/scripts/security-dataset/timeWindow.ts @@ -1,57 +1,57 @@ export type CreatedTimeWindow = { - createdAtGte: number | null; - createdAtLt: number | null; + createdAtGte: number | null; + createdAtLt: number | null; }; export type CreatedBounds = { - sourceKind: TSourceKind; - minCreatedAt: number | null; - maxCreatedAt: number | null; + sourceKind: TSourceKind; + minCreatedAt: number | null; + maxCreatedAt: number | null; }; export function emptyCreatedTimeWindow(): CreatedTimeWindow { - return { createdAtGte: null, createdAtLt: null }; + return { createdAtGte: null, createdAtLt: null }; } export function parseCreatedTimestamp(value: string, flag: string) { - if (/^\d+$/.test(value)) { - const parsed = Number.parseInt(value, 10); - if (Number.isSafeInteger(parsed)) return parsed; - } + if (/^\d+$/.test(value)) { + const parsed = Number.parseInt(value, 10); + if (Number.isSafeInteger(parsed)) return parsed; + } - const parsed = Date.parse(value); - if (Number.isFinite(parsed)) return parsed; - throw new Error(`Expected ${flag} to be a millisecond timestamp or ISO date.`); + const parsed = Date.parse(value); + if (Number.isFinite(parsed)) return parsed; + throw new Error(`Expected ${flag} to be a millisecond timestamp or ISO date.`); } export function assertCreatedTimeWindow(window: CreatedTimeWindow) { - if ( - window.createdAtGte !== null && - window.createdAtLt !== null && - window.createdAtGte >= window.createdAtLt - ) { - throw new Error("--created-after must be earlier than --created-before."); - } + if ( + window.createdAtGte !== null && + window.createdAtLt !== null && + window.createdAtGte >= window.createdAtLt + ) { + throw new Error("--created-after must be earlier than --created-before."); + } } export function clampCreatedBounds( - bounds: CreatedBounds, - window: CreatedTimeWindow, + bounds: CreatedBounds, + window: CreatedTimeWindow, ): CreatedBounds { - if (bounds.minCreatedAt === null || bounds.maxCreatedAt === null) return bounds; + if (bounds.minCreatedAt === null || bounds.maxCreatedAt === null) return bounds; - const minCreatedAt = - window.createdAtGte === null - ? bounds.minCreatedAt - : Math.max(bounds.minCreatedAt, window.createdAtGte); - const maxCreatedExclusive = - window.createdAtLt === null - ? bounds.maxCreatedAt + 1 - : Math.min(bounds.maxCreatedAt + 1, window.createdAtLt); + const minCreatedAt = + window.createdAtGte === null + ? bounds.minCreatedAt + : Math.max(bounds.minCreatedAt, window.createdAtGte); + const maxCreatedExclusive = + window.createdAtLt === null + ? bounds.maxCreatedAt + 1 + : Math.min(bounds.maxCreatedAt + 1, window.createdAtLt); - if (minCreatedAt >= maxCreatedExclusive) { - return { ...bounds, minCreatedAt: null, maxCreatedAt: null }; - } + if (minCreatedAt >= maxCreatedExclusive) { + return { ...bounds, minCreatedAt: null, maxCreatedAt: null }; + } - return { ...bounds, minCreatedAt, maxCreatedAt: maxCreatedExclusive - 1 }; + return { ...bounds, minCreatedAt, maxCreatedAt: maxCreatedExclusive - 1 }; } diff --git a/src/__tests__/about-inline-code.test.tsx b/src/__tests__/about-inline-code.test.tsx index 3e932324..669056a6 100644 --- a/src/__tests__/about-inline-code.test.tsx +++ b/src/__tests__/about-inline-code.test.tsx @@ -26,9 +26,7 @@ describe("renderWithInlineCode", () => { }); it("handles multiple code spans in a single string", () => { - const el = renderToContainer( - "Use `curl | sh` or `npx @latest` for setup." - ); + const el = renderToContainer("Use `curl | sh` or `npx @latest` for setup."); const codes = el.querySelectorAll("code"); expect(codes).toHaveLength(2); expect(codes[0].textContent).toBe("curl | sh"); diff --git a/src/__tests__/skill-route-loader.test.ts b/src/__tests__/skill-route-loader.test.ts index 5aac513a..ee48ce9a 100644 --- a/src/__tests__/skill-route-loader.test.ts +++ b/src/__tests__/skill-route-loader.test.ts @@ -49,9 +49,9 @@ async function loadRoute() { async function runBeforeLoad(params: { owner: string; slug: string }) { const route = await loadRoute(); - const beforeLoad = route.__config.beforeLoad as ((args: { - params: { owner: string; slug: string }; - }) => unknown) | undefined; + const beforeLoad = route.__config.beforeLoad as + | ((args: { params: { owner: string; slug: string } }) => unknown) + | undefined; return beforeLoad?.({ params }); } diff --git a/src/__tests__/skills-index.test.tsx b/src/__tests__/skills-index.test.tsx index f99f2f65..cc3b69f8 100644 --- a/src/__tests__/skills-index.test.tsx +++ b/src/__tests__/skills-index.test.tsx @@ -252,9 +252,9 @@ describe("SkillsIndex", () => { await vi.runAllTimersAsync(); }); - const titles = Array.from( - document.querySelectorAll(".skill-list-item-name"), - ).map((node) => node.textContent); + const titles = Array.from(document.querySelectorAll(".skill-list-item-name")).map( + (node) => node.textContent, + ); expect(titles[0]).toBe("Older High Score"); expect(titles[1]).toBe("Newer Low Score"); diff --git a/src/__tests__/skills-toolbar.test.tsx b/src/__tests__/skills-toolbar.test.tsx index 566ccae0..f41563c1 100644 --- a/src/__tests__/skills-toolbar.test.tsx +++ b/src/__tests__/skills-toolbar.test.tsx @@ -1,8 +1,8 @@ /* @vitest-environment jsdom */ -import { render, screen } from '@testing-library/react'; -import { createRef, type ComponentProps } from 'react'; -import { describe, expect, it, vi } from 'vitest'; -import { SkillsToolbar } from '../routes/skills/-SkillsToolbar'; +import { render, screen } from "@testing-library/react"; +import { createRef, type ComponentProps } from "react"; +import { describe, expect, it, vi } from "vitest"; +import { SkillsToolbar } from "../routes/skills/-SkillsToolbar"; function renderToolbar(overrides?: Partial>) { return render( @@ -28,23 +28,23 @@ function renderToolbar(overrides?: Partial> ); } -describe('SkillsToolbar', () => { - it('keeps filter chips on a dark-mode surface', () => { +describe("SkillsToolbar", () => { + it("keeps filter chips on a dark-mode surface", () => { renderToolbar(); - const staffPicksButton = screen.getByRole('button', { name: 'Staff Picks' }); + const staffPicksButton = screen.getByRole("button", { name: "Staff Picks" }); - expect(staffPicksButton.className).toContain('dark:bg-[rgba(14,28,37,0.84)]'); - expect(staffPicksButton.className).toContain('dark:text-[rgba(245,238,232,0.88)]'); + expect(staffPicksButton.className).toContain("dark:bg-[rgba(14,28,37,0.84)]"); + expect(staffPicksButton.className).toContain("dark:text-[rgba(245,238,232,0.88)]"); }); - it('uses a readable active color treatment in dark mode', () => { + it("uses a readable active color treatment in dark mode", () => { renderToolbar({ highlightedOnly: true }); - const staffPicksButton = screen.getByRole('button', { name: 'Staff Picks' }); + const staffPicksButton = screen.getByRole("button", { name: "Staff Picks" }); - expect(staffPicksButton.getAttribute('aria-pressed')).toBe('true'); - expect(staffPicksButton.className).toContain('dark:bg-[rgba(255,131,95,0.14)]'); - expect(staffPicksButton.className).toContain('dark:text-[#ffd5c9]'); + expect(staffPicksButton.getAttribute("aria-pressed")).toBe("true"); + expect(staffPicksButton.className).toContain("dark:bg-[rgba(255,131,95,0.14)]"); + expect(staffPicksButton.className).toContain("dark:text-[#ffd5c9]"); }); }); diff --git a/src/components/DetailSecuritySummary.test.tsx b/src/components/DetailSecuritySummary.test.tsx index 7915dff7..7b96fefe 100644 --- a/src/components/DetailSecuritySummary.test.tsx +++ b/src/components/DetailSecuritySummary.test.tsx @@ -31,9 +31,7 @@ describe("DetailSecuritySummary", () => { const button = screen.getByRole("button", { name: "Scanning" }); expect((button as HTMLButtonElement).disabled).toBe(true); expect(button.getAttribute("title")).toBe("A rescan is already in progress."); - expect(button.querySelector(".animate-spin")?.className).toContain( - "[animation-duration:2.4s]", - ); + expect(button.querySelector(".animate-spin")?.className).toContain("[animation-duration:2.4s]"); }); it("shows staff-cleared public scan summaries as cleared", () => { diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 2d355fcf..f151c013 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,25 +1,25 @@ import { useAuthActions } from "@convex-dev/auth/react"; import { Link, useLocation, useNavigate } from "@tanstack/react-router"; import { - ArrowRight, - Ghost, - GitPullRequest, - Menu, - Monitor, - Moon, - Plug, - Search, - Sun, - Wrench, + ArrowRight, + Ghost, + GitPullRequest, + Menu, + Monitor, + Moon, + Plug, + Search, + Sun, + Wrench, } from "lucide-react"; import { type ComponentType, useEffect, useMemo, useRef, useState } from "react"; import { getUserFacingAuthError } from "../lib/authErrorMessage"; import { gravatarUrl } from "../lib/gravatar"; import { - filterNavItems, - type NavIconName, - PRIMARY_NAV_ITEMS, - SECONDARY_NAV_ITEMS, + filterNavItems, + type NavIconName, + PRIMARY_NAV_ITEMS, + SECONDARY_NAV_ITEMS, } from "../lib/nav-items"; import { isModerator } from "../lib/roles"; import { getClawHubSiteUrl, getSiteMode, getSiteName } from "../lib/site"; @@ -27,717 +27,717 @@ import { applyTheme, useThemeMode } from "../lib/theme"; import { setAuthError, useAuthError } from "../lib/useAuthError"; import { useAuthStatus } from "../lib/useAuthStatus"; import { - useUnifiedSearch, - type UnifiedPluginResult, - type UnifiedSkillResult, + useUnifiedSearch, + type UnifiedPluginResult, + type UnifiedSkillResult, } from "../lib/useUnifiedSearch"; import { Button } from "./ui/button"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, } from "./ui/dropdown-menu"; import { - Sheet, - SheetClose, - SheetContent, - SheetDescription, - SheetHeader, - SheetTitle, + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, } from "./ui/sheet"; import { ToggleGroup, ToggleGroupItem } from "./ui/toggle-group"; const NAV_ICONS: Record> = { - wrench: Wrench, - plug: Plug, - ghost: Ghost, + wrench: Wrench, + plug: Plug, + ghost: Ghost, }; const THEME_MODE_SEQUENCE: Array<"system" | "light" | "dark"> = ["system", "light", "dark"]; type TypeaheadItem = - | { - kind: "skill"; - key: string; - result: UnifiedSkillResult; - } - | { - kind: "plugin"; - key: string; - result: UnifiedPluginResult; - } - | { - kind: "footer"; - key: string; - section: "skills" | "plugins"; - label: string; - }; + | { + kind: "skill"; + key: string; + result: UnifiedSkillResult; + } + | { + kind: "plugin"; + key: string; + result: UnifiedPluginResult; + } + | { + kind: "footer"; + key: string; + section: "skills" | "plugins"; + label: string; + }; export default function Header() { - const { isAuthenticated, isLoading, me } = useAuthStatus(); - const { signIn, signOut } = useAuthActions(); - const { theme, mode, setMode } = useThemeMode(); - const siteMode = getSiteMode(); - const siteName = useMemo(() => getSiteName(siteMode), [siteMode]); - const isSoulMode = siteMode === "souls"; - const clawHubUrl = getClawHubSiteUrl(); - const navigate = useNavigate(); - const location = useLocation(); + const { isAuthenticated, isLoading, me } = useAuthStatus(); + const { signIn, signOut } = useAuthActions(); + const { theme, mode, setMode } = useThemeMode(); + const siteMode = getSiteMode(); + const siteName = useMemo(() => getSiteName(siteMode), [siteMode]); + const isSoulMode = siteMode === "souls"; + const clawHubUrl = getClawHubSiteUrl(); + const navigate = useNavigate(); + const location = useLocation(); - const avatar = me?.image ?? (me?.email ? gravatarUrl(me.email) : undefined); - const handle = me?.handle ?? me?.displayName ?? "user"; - const initial = (me?.displayName ?? me?.name ?? handle).charAt(0).toUpperCase(); - const isStaff = isModerator(me); - const hasResolvedUser = Boolean(me); - const navCtx = useMemo( - () => ({ isSoulMode, isAuthenticated: hasResolvedUser, isStaff }), - [hasResolvedUser, isSoulMode, isStaff], - ); - const primaryItems = useMemo(() => filterNavItems(PRIMARY_NAV_ITEMS, navCtx), [navCtx]); - const secondaryItems = useMemo(() => filterNavItems(SECONDARY_NAV_ITEMS, navCtx), [navCtx]); - const { error: authError, clear: clearAuthError } = useAuthError(); - const signInRedirectTo = getCurrentRelativeUrl(); + const avatar = me?.image ?? (me?.email ? gravatarUrl(me.email) : undefined); + const handle = me?.handle ?? me?.displayName ?? "user"; + const initial = (me?.displayName ?? me?.name ?? handle).charAt(0).toUpperCase(); + const isStaff = isModerator(me); + const hasResolvedUser = Boolean(me); + const navCtx = useMemo( + () => ({ isSoulMode, isAuthenticated: hasResolvedUser, isStaff }), + [hasResolvedUser, isSoulMode, isStaff], + ); + const primaryItems = useMemo(() => filterNavItems(PRIMARY_NAV_ITEMS, navCtx), [navCtx]); + const secondaryItems = useMemo(() => filterNavItems(SECONDARY_NAV_ITEMS, navCtx), [navCtx]); + const { error: authError, clear: clearAuthError } = useAuthError(); + const signInRedirectTo = getCurrentRelativeUrl(); - const [navSearchQuery, setNavSearchQuery] = useState(""); - const [typeaheadOpen, setTypeaheadOpen] = useState(false); - const [typeaheadActiveIndex, setTypeaheadActiveIndex] = useState(0); - const [mobileSearchOpen, setMobileSearchOpen] = useState(false); - const [mobileMenuOpen, setMobileMenuOpen] = useState(false); - const searchWrapRef = useRef(null); - const ThemeModeIcon = getThemeModeIcon(mode); - const trimmedNavSearchQuery = navSearchQuery.trim(); - const showTypeahead = !isSoulMode && typeaheadOpen && trimmedNavSearchQuery.length > 0; - const { - skillResults, - skillCount, - pluginResults, - pluginCount, - isSearching: typeaheadSearching, - } = useUnifiedSearch(navSearchQuery, "all", { - debounceMs: 180, - enabled: showTypeahead, - limits: { skills: 4, plugins: 4 }, - }); - const typeaheadItems = useMemo(() => { - if (!showTypeahead) return []; - const items: TypeaheadItem[] = []; - for (const result of skillResults) { - items.push({ kind: "skill", key: `skill-${result.skill._id}`, result }); - } - if (skillCount > 0) { - items.push({ - kind: "footer", - key: "footer-skills", - section: "skills", - label: `See skill results for "${trimmedNavSearchQuery}"`, - }); - } - for (const result of pluginResults) { - items.push({ kind: "plugin", key: `plugin-${result.plugin.name}`, result }); - } - if (pluginCount > 0) { - items.push({ - kind: "footer", - key: "footer-plugins", - section: "plugins", - label: `See plugin results for "${trimmedNavSearchQuery}"`, - }); - } - return items; - }, [pluginCount, pluginResults, showTypeahead, skillCount, skillResults, trimmedNavSearchQuery]); + const [navSearchQuery, setNavSearchQuery] = useState(""); + const [typeaheadOpen, setTypeaheadOpen] = useState(false); + const [typeaheadActiveIndex, setTypeaheadActiveIndex] = useState(0); + const [mobileSearchOpen, setMobileSearchOpen] = useState(false); + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + const searchWrapRef = useRef(null); + const ThemeModeIcon = getThemeModeIcon(mode); + const trimmedNavSearchQuery = navSearchQuery.trim(); + const showTypeahead = !isSoulMode && typeaheadOpen && trimmedNavSearchQuery.length > 0; + const { + skillResults, + skillCount, + pluginResults, + pluginCount, + isSearching: typeaheadSearching, + } = useUnifiedSearch(navSearchQuery, "all", { + debounceMs: 180, + enabled: showTypeahead, + limits: { skills: 4, plugins: 4 }, + }); + const typeaheadItems = useMemo(() => { + if (!showTypeahead) return []; + const items: TypeaheadItem[] = []; + for (const result of skillResults) { + items.push({ kind: "skill", key: `skill-${result.skill._id}`, result }); + } + if (skillCount > 0) { + items.push({ + kind: "footer", + key: "footer-skills", + section: "skills", + label: `See skill results for "${trimmedNavSearchQuery}"`, + }); + } + for (const result of pluginResults) { + items.push({ kind: "plugin", key: `plugin-${result.plugin.name}`, result }); + } + if (pluginCount > 0) { + items.push({ + kind: "footer", + key: "footer-plugins", + section: "plugins", + label: `See plugin results for "${trimmedNavSearchQuery}"`, + }); + } + return items; + }, [pluginCount, pluginResults, showTypeahead, skillCount, skillResults, trimmedNavSearchQuery]); - useEffect(() => { - setTypeaheadActiveIndex(0); - }, [trimmedNavSearchQuery]); + useEffect(() => { + setTypeaheadActiveIndex(0); + }, [trimmedNavSearchQuery]); - useEffect(() => { - if (!typeaheadOpen) return () => {}; - const handlePointerDown = (event: PointerEvent) => { - if (searchWrapRef.current?.contains(event.target as Node)) return; - setTypeaheadOpen(false); - }; - document.addEventListener("pointerdown", handlePointerDown); - return () => document.removeEventListener("pointerdown", handlePointerDown); - }, [typeaheadOpen]); + useEffect(() => { + if (!typeaheadOpen) return () => {}; + const handlePointerDown = (event: PointerEvent) => { + if (searchWrapRef.current?.contains(event.target as Node)) return; + setTypeaheadOpen(false); + }; + document.addEventListener("pointerdown", handlePointerDown); + return () => document.removeEventListener("pointerdown", handlePointerDown); + }, [typeaheadOpen]); - const setThemeMode = (next: "system" | "light" | "dark") => { - applyTheme(next, theme); - setMode(next); - }; + const setThemeMode = (next: "system" | "light" | "dark") => { + applyTheme(next, theme); + setMode(next); + }; - const cycleThemeMode = () => { - const currentIndex = Math.max(0, THEME_MODE_SEQUENCE.indexOf(mode)); - setThemeMode(THEME_MODE_SEQUENCE[(currentIndex + 1) % THEME_MODE_SEQUENCE.length] ?? "system"); - }; + const cycleThemeMode = () => { + const currentIndex = Math.max(0, THEME_MODE_SEQUENCE.indexOf(mode)); + setThemeMode(THEME_MODE_SEQUENCE[(currentIndex + 1) % THEME_MODE_SEQUENCE.length] ?? "system"); + }; - const handleNavSearch = (e: React.FormEvent) => { - e.preventDefault(); - const q = navSearchQuery.trim(); - if (!q) return; - void navigate({ - to: isSoulMode ? "/souls" : "/search", - search: isSoulMode - ? { - q, - sort: undefined, - dir: undefined, - view: undefined, - focus: undefined, - } - : { q, type: undefined }, - }); - setNavSearchQuery(""); - setTypeaheadOpen(false); - setMobileSearchOpen(false); - }; + const handleNavSearch = (e: React.FormEvent) => { + e.preventDefault(); + const q = navSearchQuery.trim(); + if (!q) return; + void navigate({ + to: isSoulMode ? "/souls" : "/search", + search: isSoulMode + ? { + q, + sort: undefined, + dir: undefined, + view: undefined, + focus: undefined, + } + : { q, type: undefined }, + }); + setNavSearchQuery(""); + setTypeaheadOpen(false); + setMobileSearchOpen(false); + }; - const navigateToTypeaheadItem = (item: TypeaheadItem) => { - if (item.kind === "skill") { - const resultOwnerHandle = item.result.ownerHandle?.trim(); - if (!resultOwnerHandle) { - void navigate({ - to: "/search", - search: { q: trimmedNavSearchQuery, type: "skills" }, - }); - setNavSearchQuery(""); - setTypeaheadOpen(false); - setMobileSearchOpen(false); - return; - } - void navigate({ - to: `/${encodeURIComponent(resultOwnerHandle)}/${encodeURIComponent(item.result.skill.slug)}`, - }); - } else if (item.kind === "plugin") { - void navigate({ - to: "/plugins/$name", - params: { name: item.result.plugin.name }, - }); - } else { - void navigate({ - to: "/search", - search: { q: trimmedNavSearchQuery, type: item.section }, - }); - } - setNavSearchQuery(""); - setTypeaheadOpen(false); - setMobileSearchOpen(false); - }; + const navigateToTypeaheadItem = (item: TypeaheadItem) => { + if (item.kind === "skill") { + const resultOwnerHandle = item.result.ownerHandle?.trim(); + if (!resultOwnerHandle) { + void navigate({ + to: "/search", + search: { q: trimmedNavSearchQuery, type: "skills" }, + }); + setNavSearchQuery(""); + setTypeaheadOpen(false); + setMobileSearchOpen(false); + return; + } + void navigate({ + to: `/${encodeURIComponent(resultOwnerHandle)}/${encodeURIComponent(item.result.skill.slug)}`, + }); + } else if (item.kind === "plugin") { + void navigate({ + to: "/plugins/$name", + params: { name: item.result.plugin.name }, + }); + } else { + void navigate({ + to: "/search", + search: { q: trimmedNavSearchQuery, type: item.section }, + }); + } + setNavSearchQuery(""); + setTypeaheadOpen(false); + setMobileSearchOpen(false); + }; - const handleSearchKeyDown = (event: React.KeyboardEvent) => { - if (isSoulMode) return; - if (event.key === "Escape") { - setTypeaheadOpen(false); - return; - } - if (event.key !== "ArrowDown" && event.key !== "ArrowUp" && event.key !== "Enter") return; - if (!showTypeahead || typeaheadItems.length === 0) { - if (event.key === "ArrowDown" && trimmedNavSearchQuery) { - setTypeaheadOpen(true); - event.preventDefault(); - } - return; - } - if (event.key === "ArrowDown") { - event.preventDefault(); - setTypeaheadActiveIndex((index) => (index + 1) % typeaheadItems.length); - } else if (event.key === "ArrowUp") { - event.preventDefault(); - setTypeaheadActiveIndex( - (index) => (index - 1 + typeaheadItems.length) % typeaheadItems.length, - ); - } else if (event.key === "Enter") { - const activeItem = typeaheadItems[typeaheadActiveIndex]; - if (!activeItem) return; - event.preventDefault(); - navigateToTypeaheadItem(activeItem); - } - }; + const handleSearchKeyDown = (event: React.KeyboardEvent) => { + if (isSoulMode) return; + if (event.key === "Escape") { + setTypeaheadOpen(false); + return; + } + if (event.key !== "ArrowDown" && event.key !== "ArrowUp" && event.key !== "Enter") return; + if (!showTypeahead || typeaheadItems.length === 0) { + if (event.key === "ArrowDown" && trimmedNavSearchQuery) { + setTypeaheadOpen(true); + event.preventDefault(); + } + return; + } + if (event.key === "ArrowDown") { + event.preventDefault(); + setTypeaheadActiveIndex((index) => (index + 1) % typeaheadItems.length); + } else if (event.key === "ArrowUp") { + event.preventDefault(); + setTypeaheadActiveIndex( + (index) => (index - 1 + typeaheadItems.length) % typeaheadItems.length, + ); + } else if (event.key === "Enter") { + const activeItem = typeaheadItems[typeaheadActiveIndex]; + if (!activeItem) return; + event.preventDefault(); + navigateToTypeaheadItem(activeItem); + } + }; - return ( -
-
- {/* Row 1: Brand + Search + Actions */} -
-
- - - - - - - - {siteName} - - - - Browse sections, switch theme, and access account actions. - - -
- - - Home - - - {isSoulMode ? ( - - - ClawHub - - - ) : null} - {primaryItems.map((item) => ( - - - {item.label} - - - ))} - {secondaryItems.map((item) => ( - - - {item.label} - - - ))} -
-
-
Theme
- -
-
-
-
+ return ( +
+
+ {/* Row 1: Brand + Search + Actions */} +
+
+ + + + + + + + {siteName} + + + + Browse sections, switch theme, and access account actions. + + +
+ + + Home + + + {isSoulMode ? ( + + + ClawHub + + + ) : null} + {primaryItems.map((item) => ( + + + {item.label} + + + ))} + {secondaryItems.map((item) => ( + + + {item.label} + + + ))} +
+
+
Theme
+ +
+
+
+
- - - - - {siteName} - + + + + + {siteName} + -
-
-
+
+
+
-
- -
-
- -
- { - if (!value) return; - setThemeMode(value as "system" | "light" | "dark"); - }} - aria-label="Theme mode" - > - - - - - - - -
- {isAuthenticated && me ? ( - - - - - - - Dashboard - - - Settings - - - void signOut()}>Sign out - - - ) : ( - <> - {authError ? ( -
- {authError}{" "} - -
- ) : null} - - - )} -
-
+
+ +
+
+ +
+ { + if (!value) return; + setThemeMode(value as "system" | "light" | "dark"); + }} + aria-label="Theme mode" + > + + + + + + + +
+ {isAuthenticated && me ? ( + + + + + + + Dashboard + + + Settings + + + void signOut()}>Sign out + + + ) : ( + <> + {authError ? ( +
+ {authError}{" "} + +
+ ) : null} + + + )} +
+
- {/* Mobile search bar (expandable) */} - {mobileSearchOpen ? ( -
-
-
- ); + + + + ); } function SearchTypeahead({ - activeIndex, - items, - loading, - onHoverItem, - onSelectItem, - query, + activeIndex, + items, + loading, + onHoverItem, + onSelectItem, + query, }: { - activeIndex: number; - items: TypeaheadItem[]; - loading: boolean; - onHoverItem: (index: number) => void; - onSelectItem: (item: TypeaheadItem) => void; - query: string; + activeIndex: number; + items: TypeaheadItem[]; + loading: boolean; + onHoverItem: (index: number) => void; + onSelectItem: (item: TypeaheadItem) => void; + query: string; }) { - const skillItems = items.filter((item) => item.kind === "skill"); - const pluginItems = items.filter((item) => item.kind === "plugin"); - const footerItems = items.filter((item) => item.kind === "footer"); - const skillsFooter = footerItems.find( - (item) => item.kind === "footer" && item.section === "skills", - ); - const pluginsFooter = footerItems.find( - (item) => item.kind === "footer" && item.section === "plugins", - ); - const hasMatches = skillItems.length > 0 || pluginItems.length > 0; + const skillItems = items.filter((item) => item.kind === "skill"); + const pluginItems = items.filter((item) => item.kind === "plugin"); + const footerItems = items.filter((item) => item.kind === "footer"); + const skillsFooter = footerItems.find( + (item) => item.kind === "footer" && item.section === "skills", + ); + const pluginsFooter = footerItems.find( + (item) => item.kind === "footer" && item.section === "plugins", + ); + const hasMatches = skillItems.length > 0 || pluginItems.length > 0; - return ( - - ); + return ( + + ); } function TypeaheadSection({ - activeIndex, - footer, - items, - label, - onHoverItem, - onSelectItem, - sectionItems, + activeIndex, + footer, + items, + label, + onHoverItem, + onSelectItem, + sectionItems, }: { - activeIndex: number; - footer: TypeaheadItem | undefined; - items: TypeaheadItem[]; - label: string; - onHoverItem: (index: number) => void; - onSelectItem: (item: TypeaheadItem) => void; - sectionItems: TypeaheadItem[]; + activeIndex: number; + footer: TypeaheadItem | undefined; + items: TypeaheadItem[]; + label: string; + onHoverItem: (index: number) => void; + onSelectItem: (item: TypeaheadItem) => void; + sectionItems: TypeaheadItem[]; }) { - if (sectionItems.length === 0 && !footer) return null; - return ( -
-
{label}
- {sectionItems.map((item) => ( - candidate.key === item.key)} - onHoverItem={onHoverItem} - onSelectItem={onSelectItem} - /> - ))} - {footer ? ( - candidate.key === footer.key)} - onHoverItem={onHoverItem} - onSelectItem={onSelectItem} - /> - ) : null} -
- ); + if (sectionItems.length === 0 && !footer) return null; + return ( +
+
{label}
+ {sectionItems.map((item) => ( + candidate.key === item.key)} + onHoverItem={onHoverItem} + onSelectItem={onSelectItem} + /> + ))} + {footer ? ( + candidate.key === footer.key)} + onHoverItem={onHoverItem} + onSelectItem={onSelectItem} + /> + ) : null} +
+ ); } function TypeaheadRow({ - active, - index, - item, - onHoverItem, - onSelectItem, + active, + index, + item, + onHoverItem, + onSelectItem, }: { - active: boolean; - index: number; - item: TypeaheadItem; - onHoverItem: (index: number) => void; - onSelectItem: (item: TypeaheadItem) => void; + active: boolean; + index: number; + item: TypeaheadItem; + onHoverItem: (index: number) => void; + onSelectItem: (item: TypeaheadItem) => void; }) { - const body = getTypeaheadRowBody(item); - return ( - - ); + const body = getTypeaheadRowBody(item); + return ( + + ); } function getTypeaheadRowBody(item: TypeaheadItem) { - if (item.kind === "skill") { - const owner = item.result.ownerHandle ? `@${item.result.ownerHandle}` : "Skill"; - return { - icon: "S", - title: item.result.skill.displayName, - meta: `${owner} / ${item.result.skill.slug}`, - }; - } - if (item.kind === "plugin") { - return { - icon: "P", - title: item.result.plugin.displayName, - meta: item.result.plugin.ownerHandle - ? `@${item.result.plugin.ownerHandle} / ${item.result.plugin.name}` - : item.result.plugin.name, - }; - } - return { - icon: null, - title: item.label, - meta: null, - }; + if (item.kind === "skill") { + const owner = item.result.ownerHandle ? `@${item.result.ownerHandle}` : "Skill"; + return { + icon: "S", + title: item.result.skill.displayName, + meta: `${owner} / ${item.result.skill.slug}`, + }; + } + if (item.kind === "plugin") { + return { + icon: "P", + title: item.result.plugin.displayName, + meta: item.result.plugin.ownerHandle + ? `@${item.result.plugin.ownerHandle} / ${item.result.plugin.name}` + : item.result.plugin.name, + }; + } + return { + icon: null, + title: item.label, + meta: null, + }; } function getCurrentRelativeUrl() { - if (typeof window === "undefined") return "/"; - return `${window.location.pathname}${window.location.search}${window.location.hash}`; + if (typeof window === "undefined") return "/"; + return `${window.location.pathname}${window.location.search}${window.location.hash}`; } function getThemeModeIcon(mode: "system" | "light" | "dark") { - switch (mode) { - case "light": - return Sun; - case "dark": - return Moon; - case "system": - default: - return Monitor; - } + switch (mode) { + case "light": + return Sun; + case "dark": + return Moon; + case "system": + default: + return Monitor; + } } diff --git a/src/components/MarkdownPreview.test.tsx b/src/components/MarkdownPreview.test.tsx index 30629727..5a9c149a 100644 --- a/src/components/MarkdownPreview.test.tsx +++ b/src/components/MarkdownPreview.test.tsx @@ -5,72 +5,72 @@ import { describe, expect, it } from "vitest"; import { MarkdownPreview } from "./MarkdownPreview"; function renderMarkdown(source: string) { - // Disable Shiki highlighting to keep the tree synchronous for assertions. - const { container } = render({source}); - return container; + // Disable Shiki highlighting to keep the tree synchronous for assertions. + const { container } = render({source}); + return container; } describe("MarkdownPreview — raw HTML passthrough", () => { - it('renders an

block as a real

', () => { - const container = renderMarkdown(`

Hello logo

`); - const h1 = container.querySelector("h1"); - expect(h1).not.toBeNull(); - expect(h1?.textContent).toBe("Hello logo"); - }); + it('renders an

block as a real

', () => { + const container = renderMarkdown(`

Hello logo

`); + const h1 = container.querySelector("h1"); + expect(h1).not.toBeNull(); + expect(h1?.textContent).toBe("Hello logo"); + }); - it('renders a
block as a real
', () => { - const container = renderMarkdown(`
centered
`); - const div = container.querySelector('div[align="center"]'); - expect(div).not.toBeNull(); - expect(div?.textContent).toBe("centered"); - }); + it('renders a
block as a real
', () => { + const container = renderMarkdown(`
centered
`); + const div = container.querySelector('div[align="center"]'); + expect(div).not.toBeNull(); + expect(div?.textContent).toBe("centered"); + }); - it("renders with + fallback", () => { - const container = renderMarkdown( - `Logo`, - ); - expect(container.querySelector("picture")).not.toBeNull(); - expect(container.querySelector("picture source")).not.toBeNull(); - const img = container.querySelector("picture img"); - expect(img).not.toBeNull(); - expect(img?.getAttribute("alt")).toBe("Logo"); - expect(img?.getAttribute("src")).toBe("light.png"); - }); + it("renders with + fallback", () => { + const container = renderMarkdown( + `Logo`, + ); + expect(container.querySelector("picture")).not.toBeNull(); + expect(container.querySelector("picture source")).not.toBeNull(); + const img = container.querySelector("picture img"); + expect(img).not.toBeNull(); + expect(img?.getAttribute("alt")).toBe("Logo"); + expect(img?.getAttribute("src")).toBe("light.png"); + }); - it("renders standalone tags with src and alt", () => { - const container = renderMarkdown(`Demo screenshot`); - const img = container.querySelector("img"); - expect(img).not.toBeNull(); - // Relative paths render as-is — only external http(s) URLs get proxied. - expect(img?.getAttribute("src")).toBe("screenshot.png"); - expect(img?.getAttribute("alt")).toBe("Demo screenshot"); - }); + it("renders standalone tags with src and alt", () => { + const container = renderMarkdown(`Demo screenshot`); + const img = container.querySelector("img"); + expect(img).not.toBeNull(); + // Relative paths render as-is — only external http(s) URLs get proxied. + expect(img?.getAttribute("src")).toBe("screenshot.png"); + expect(img?.getAttribute("alt")).toBe("Demo screenshot"); + }); - it("routes external https URLs through /_vercel/image", () => { - const container = renderMarkdown( - `logo`, - ); - const img = container.querySelector("img"); - expect(img?.getAttribute("src")).toBe( - "/_vercel/image?url=https%3A%2F%2Fraw.githubusercontent.com%2Ffoo%2Fbar%2Fmain%2Flogo.png&w=1024&q=75", - ); - }); + it("routes external https URLs through /_vercel/image", () => { + const container = renderMarkdown( + `logo`, + ); + const img = container.querySelector("img"); + expect(img?.getAttribute("src")).toBe( + "/_vercel/image?url=https%3A%2F%2Fraw.githubusercontent.com%2Ffoo%2Fbar%2Fmain%2Flogo.png&w=1024&q=75", + ); + }); - it("routes external markdown ![](url) images through /_vercel/image", () => { - const container = renderMarkdown(`![logo](https://img.shields.io/badge/x-y-blue.svg)`); - const img = container.querySelector("img"); - expect(img?.getAttribute("src")).toBe( - "/_vercel/image?url=https%3A%2F%2Fimg.shields.io%2Fbadge%2Fx-y-blue.svg&w=1024&q=75", - ); - }); + it("routes external markdown ![](url) images through /_vercel/image", () => { + const container = renderMarkdown(`![logo](https://img.shields.io/badge/x-y-blue.svg)`); + const img = container.querySelector("img"); + expect(img?.getAttribute("src")).toBe( + "/_vercel/image?url=https%3A%2F%2Fimg.shields.io%2Fbadge%2Fx-y-blue.svg&w=1024&q=75", + ); + }); - it("renders
as a real line break", () => { - const container = renderMarkdown(`line one
line two`); - expect(container.querySelector("br")).not.toBeNull(); - }); + it("renders
as a real line break", () => { + const container = renderMarkdown(`line one
line two`); + expect(container.querySelector("br")).not.toBeNull(); + }); - it("renders the Opik README banner (centered h1 + picture + img)", () => { - const opikBanner = `

+ it("renders the Opik README banner (centered h1 + picture + img)", () => { + const opikBanner = `

@@ -79,117 +79,117 @@ describe("MarkdownPreview — raw HTML passthrough", () => {
OpenClaw Opik Observability Plugin

`; - const container = renderMarkdown(opikBanner); - expect(container.querySelector("h1")).not.toBeNull(); - expect(container.querySelector("picture")).not.toBeNull(); - const img = container.querySelector("img"); - expect(img?.getAttribute("alt")).toBe("Comet Opik logo"); - // And the escaped tag must NOT be present as literal text anywhere. - expect(container.textContent ?? "").not.toContain(""); - }); + const container = renderMarkdown(opikBanner); + expect(container.querySelector("h1")).not.toBeNull(); + expect(container.querySelector("picture")).not.toBeNull(); + const img = container.querySelector("img"); + expect(img?.getAttribute("alt")).toBe("Comet Opik logo"); + // And the escaped tag must NOT be present as literal text anywhere. + expect(container.textContent ?? "").not.toContain(""); + }); }); describe("MarkdownPreview — standard markdown still renders", () => { - it("renders ATX headings", () => { - const container = renderMarkdown(`## Why This Plugin`); - const h2 = container.querySelector("h2"); - expect(h2?.textContent).toBe("Why This Plugin"); - }); + it("renders ATX headings", () => { + const container = renderMarkdown(`## Why This Plugin`); + const h2 = container.querySelector("h2"); + expect(h2?.textContent).toBe("Why This Plugin"); + }); - it("renders markdown links", () => { - const container = renderMarkdown(`[Opik](https://example.com/opik)`); - const a = container.querySelector("a"); - expect(a?.getAttribute("href")).toBe("https://example.com/opik"); - expect(a?.textContent).toBe("Opik"); - }); + it("renders markdown links", () => { + const container = renderMarkdown(`[Opik](https://example.com/opik)`); + const a = container.querySelector("a"); + expect(a?.getAttribute("href")).toBe("https://example.com/opik"); + expect(a?.textContent).toBe("Opik"); + }); - it("renders inline code", () => { - const container = renderMarkdown("Use `@opik/opik-openclaw` now."); - const code = container.querySelector("code"); - expect(code?.textContent).toBe("@opik/opik-openclaw"); - }); + it("renders inline code", () => { + const container = renderMarkdown("Use `@opik/opik-openclaw` now."); + const code = container.querySelector("code"); + expect(code?.textContent).toBe("@opik/opik-openclaw"); + }); - it("renders unordered lists", () => { - const container = renderMarkdown(`- one\n- two\n- three`); - const items = container.querySelectorAll("li"); - expect(items.length).toBe(3); - expect(items[0].textContent).toBe("one"); - }); + it("renders unordered lists", () => { + const container = renderMarkdown(`- one\n- two\n- three`); + const items = container.querySelectorAll("li"); + expect(items.length).toBe(3); + expect(items[0].textContent).toBe("one"); + }); - it("renders GFM tables", () => { - const container = renderMarkdown( - ["| Key | Value |", "| --- | ----- |", "| a | 1 |", "| b | 2 |"].join("\n"), - ); - expect(container.querySelector("table")).not.toBeNull(); - expect(container.querySelectorAll("tbody tr").length).toBe(2); - }); + it("renders GFM tables", () => { + const container = renderMarkdown( + ["| Key | Value |", "| --- | ----- |", "| a | 1 |", "| b | 2 |"].join("\n"), + ); + expect(container.querySelector("table")).not.toBeNull(); + expect(container.querySelectorAll("tbody tr").length).toBe(2); + }); - it("renders fenced code blocks as
", () => {
-		const container = renderMarkdown("```ts\nconst x = 1;\n```");
-		const code = container.querySelector("pre code");
-		expect(code).not.toBeNull();
-		expect(code?.textContent).toContain("const x = 1;");
-	});
+  it("renders fenced code blocks as 
", () => {
+    const container = renderMarkdown("```ts\nconst x = 1;\n```");
+    const code = container.querySelector("pre code");
+    expect(code).not.toBeNull();
+    expect(code?.textContent).toContain("const x = 1;");
+  });
 });
 
 describe("MarkdownPreview — syntax highlighting", () => {
-	it("shiki-highlights fenced code blocks (produces colored  tokens)", async () => {
-		const { container } = render(
-			{"```ts\nconst x: number = 1;\n```"},
-		);
+  it("shiki-highlights fenced code blocks (produces colored  tokens)", async () => {
+    const { container } = render(
+      {"```ts\nconst x: number = 1;\n```"},
+    );
 
-		await waitFor(
-			() => {
-				const pre = container.querySelector("pre");
-				// Shiki wraps the output in 
 and tokens are
-				// .
-				expect(pre?.className ?? "").toMatch(/shiki/);
-				const coloredSpans = container.querySelectorAll("pre span[style*='color']");
-				expect(coloredSpans.length).toBeGreaterThan(0);
-			},
-			{ timeout: 8000 },
-		);
+    await waitFor(
+      () => {
+        const pre = container.querySelector("pre");
+        // Shiki wraps the output in 
 and tokens are
+        // .
+        expect(pre?.className ?? "").toMatch(/shiki/);
+        const coloredSpans = container.querySelectorAll("pre span[style*='color']");
+        expect(coloredSpans.length).toBeGreaterThan(0);
+      },
+      { timeout: 8000 },
+    );
 
-		// Raw code text must still be present after highlighting
-		expect(container.querySelector("pre")?.textContent).toContain("const x");
-	});
+    // Raw code text must still be present after highlighting
+    expect(container.querySelector("pre")?.textContent).toContain("const x");
+  });
 
-	it("leaves the highlight prop honored — highlight={false} renders plain 
", () => {
-		const { container } = render(
-			{"```ts\nconst x = 1;\n```"},
-		);
-		const pre = container.querySelector("pre");
-		// No shiki class, no colored spans
-		expect(pre?.className ?? "").not.toMatch(/shiki/);
-		expect(container.querySelectorAll("pre span[style*='color']").length).toBe(0);
-		expect(pre?.textContent).toContain("const x = 1;");
-	});
+  it("leaves the highlight prop honored — highlight={false} renders plain 
", () => {
+    const { container } = render(
+      {"```ts\nconst x = 1;\n```"},
+    );
+    const pre = container.querySelector("pre");
+    // No shiki class, no colored spans
+    expect(pre?.className ?? "").not.toMatch(/shiki/);
+    expect(container.querySelectorAll("pre span[style*='color']").length).toBe(0);
+    expect(pre?.textContent).toContain("const x = 1;");
+  });
 });
 
 describe("MarkdownPreview — sanitization of malicious HTML", () => {
-	it("strips world`);
-		expect(container.querySelector("script")).toBeNull();
-		expect(container.textContent ?? "").not.toContain("window.__pwn");
-	});
+  it("strips world`);
+    expect(container.querySelector("script")).toBeNull();
+    expect(container.textContent ?? "").not.toContain("window.__pwn");
+  });
 
-	it("strips onerror handlers on ", () => {
-		const container = renderMarkdown(`x`);
-		const img = container.querySelector("img");
-		// The img itself can render; the handler must be gone.
-		expect(img?.getAttribute("onerror")).toBeNull();
-	});
+  it("strips onerror handlers on ", () => {
+    const container = renderMarkdown(`x`);
+    const img = container.querySelector("img");
+    // The img itself can render; the handler must be gone.
+    expect(img?.getAttribute("onerror")).toBeNull();
+  });
 
-	it.each([
-		"javascript:alert(1)",
-		"JaVaScRiPt:alert(1)",
-		"data:text/html,",
-		"vbscript:msgbox(1)",
-	])("strips unsafe hrefs on anchors: %s", (unsafeHref) => {
-		const container = renderMarkdown(`click`);
-		const a = container.querySelector("a");
-		// Either the href is removed entirely or rewritten; it must not keep an executable scheme.
-		const href = a?.getAttribute("href") ?? "";
-		expect(href).not.toMatch(/^\s*(javascript|data|vbscript):/i);
-	});
+  it.each([
+    "javascript:alert(1)",
+    "JaVaScRiPt:alert(1)",
+    "data:text/html,",
+    "vbscript:msgbox(1)",
+  ])("strips unsafe hrefs on anchors: %s", (unsafeHref) => {
+    const container = renderMarkdown(`click`);
+    const a = container.querySelector("a");
+    // Either the href is removed entirely or rewritten; it must not keep an executable scheme.
+    const href = a?.getAttribute("href") ?? "";
+    expect(href).not.toMatch(/^\s*(javascript|data|vbscript):/i);
+  });
 });
diff --git a/src/components/MarketplaceIcon.tsx b/src/components/MarketplaceIcon.tsx
index 3e3d6a24..e4bed699 100644
--- a/src/components/MarketplaceIcon.tsx
+++ b/src/components/MarketplaceIcon.tsx
@@ -33,12 +33,7 @@ function getIcon(kind: MarketplaceIconProps["kind"]) {
   }
 }
 
-export function MarketplaceIcon({
-  kind,
-  label,
-  imageUrl,
-  size = "sm",
-}: MarketplaceIconProps) {
+export function MarketplaceIcon({ kind, label, imageUrl, size = "sm" }: MarketplaceIconProps) {
   const Icon = getIcon(kind);
   const tone = hashTone(label);
 
diff --git a/src/components/PackageSourceChooser.tsx b/src/components/PackageSourceChooser.tsx
index f4f8875d..91deac00 100644
--- a/src/components/PackageSourceChooser.tsx
+++ b/src/components/PackageSourceChooser.tsx
@@ -9,7 +9,7 @@ import { Button } from "./ui/button";
 import { Card } from "./ui/card";
 
 const OPENCLAW_PLUGIN_PACKAGE_METADATA_DOCS_URL =
-  'https://docs.openclaw.ai/plugins/sdk-setup#package-metadata';
+  "https://docs.openclaw.ai/plugins/sdk-setup#package-metadata";
 
 export function PackageSourceChooser(props: {
   files: File[];
diff --git a/src/components/PluginListItem.tsx b/src/components/PluginListItem.tsx
index adad956c..c6f8caa1 100644
--- a/src/components/PluginListItem.tsx
+++ b/src/components/PluginListItem.tsx
@@ -1,8 +1,8 @@
 import { Link } from "@tanstack/react-router";
+import type { PackageListItem } from "../lib/packageApi";
+import { familyLabel } from "../lib/packageLabels";
 import { MarketplaceIcon } from "./MarketplaceIcon";
 import { Badge } from "./ui/badge";
-import { familyLabel } from "../lib/packageLabels";
-import type { PackageListItem } from "../lib/packageApi";
 
 type PluginListItemProps = {
   item: PackageListItem;
@@ -10,7 +10,12 @@ type PluginListItemProps = {
 
 export function PluginListItem({ item }: PluginListItemProps) {
   return (
-    
+    
       
       
@@ -24,7 +29,9 @@ export function PluginListItem({ item }: PluginListItemProps) { {familyLabel(item.family)} {item.isOfficial ? Verified : null}
-

{item.summary ?? "Plugin package for agent workflows."}

+

+ {item.summary ?? "Plugin package for agent workflows."} +

Plugin {item.latestVersion ? ( diff --git a/src/components/SignInButton.tsx b/src/components/SignInButton.tsx index 4d067dd6..0fcb10b2 100644 --- a/src/components/SignInButton.tsx +++ b/src/components/SignInButton.tsx @@ -10,11 +10,7 @@ type SignInButtonProps = Omit & { redirectTo?: string; }; -export function SignInButton({ - redirectTo, - children = "Sign In", - ...props -}: SignInButtonProps) { +export function SignInButton({ redirectTo, children = "Sign In", ...props }: SignInButtonProps) { const { signIn } = useAuthActions(); return ( @@ -32,9 +28,7 @@ export function SignInButton({ } }) .catch((error) => { - setAuthError( - getUserFacingAuthError(error, "Sign in failed. Please try again."), - ); + setAuthError(getUserFacingAuthError(error, "Sign in failed. Please try again.")); }); }} > diff --git a/src/components/SkillCard.tsx b/src/components/SkillCard.tsx index 60aa5fe4..7076ff69 100644 --- a/src/components/SkillCard.tsx +++ b/src/components/SkillCard.tsx @@ -1,8 +1,8 @@ import { Link } from "@tanstack/react-router"; import type { ReactNode } from "react"; +import type { PublicSkill } from "../lib/publicUser"; import { MarketplaceIcon } from "./MarketplaceIcon"; import { Badge } from "./ui/badge"; -import type { PublicSkill } from "../lib/publicUser"; type SkillCardProps = { skill: PublicSkill; @@ -33,9 +33,7 @@ export function SkillCard({ {hasTags ? (
{badges.map((label) => ( - - {label} - + {label} ))} {chip ? {chip} : null} {platformLabels?.map((label) => ( diff --git a/src/components/SkillDiffCard.test.tsx b/src/components/SkillDiffCard.test.tsx index 5c71dac3..8aa1e9e9 100644 --- a/src/components/SkillDiffCard.test.tsx +++ b/src/components/SkillDiffCard.test.tsx @@ -20,12 +20,7 @@ vi.mock("@monaco-editor/react", () => ({ }: { className?: string; options?: { renderSideBySide?: boolean; useInlineViewWhenSpaceIsLimited?: boolean }; - }) => ( - - ), + }) => , useMonaco: () => null, })); diff --git a/src/components/SkillDiffCard.tsx b/src/components/SkillDiffCard.tsx index 2ee6bcc0..98cd5640 100644 --- a/src/components/SkillDiffCard.tsx +++ b/src/components/SkillDiffCard.tsx @@ -14,8 +14,8 @@ import { sortVersionsBySemver, } from "../lib/diffing"; import { isDarkThemeResolved, onThemeChange } from "../lib/theme"; -import { Button } from "./ui/button"; import { ClientOnly } from "./ClientOnly"; +import { Button } from "./ui/button"; type SkillDiffCardProps = { skill: Doc<"skills">; @@ -283,12 +283,8 @@ export function SkillDiffCard({ skill, versions, variant = "card" }: SkillDiffCa
-

- Compare versions -

-

- Inline or side-by-side diff for any file. -

+

Compare versions

+

Inline or side-by-side diff for any file.

{!diffUnavailable ? (
diff --git a/src/components/SkillFilesPanel.tsx b/src/components/SkillFilesPanel.tsx index 0ee8d100..7f006b5e 100644 --- a/src/components/SkillFilesPanel.tsx +++ b/src/components/SkillFilesPanel.tsx @@ -11,10 +11,7 @@ type SkillFilesPanelProps = { latestFiles: SkillFile[]; }; -export function SkillFilesPanel({ - versionId, - latestFiles, -}: SkillFilesPanelProps) { +export function SkillFilesPanel({ versionId, latestFiles }: SkillFilesPanelProps) { const getFileText = useAction(api.skills.getFileText); const [selectedPath, setSelectedPath] = useState(null); const [fileContent, setFileContent] = useState(null); @@ -89,12 +86,8 @@ export function SkillFilesPanel({
-

- Files -

- - {latestFiles.length} total - +

Files

+ {latestFiles.length} total
{latestFiles.length === 0 ? ( diff --git a/src/components/SkillInstallSurface.test.tsx b/src/components/SkillInstallSurface.test.tsx index ce1ee94b..c083d877 100644 --- a/src/components/SkillInstallSurface.test.tsx +++ b/src/components/SkillInstallSurface.test.tsx @@ -11,13 +11,7 @@ vi.mock("./ui/dropdown-menu", () => ({ DropdownMenu: ({ children }: { children: ReactNode }) =>
{children}
, DropdownMenuTrigger: ({ children }: { children: ReactNode }) =>
{children}
, DropdownMenuContent: ({ children }: { children: ReactNode }) =>
{children}
, - DropdownMenuItem: ({ - children, - onSelect, - }: { - children: ReactNode; - onSelect?: () => void; - }) => ( + DropdownMenuItem: ({ children, onSelect }: { children: ReactNode; onSelect?: () => void }) => ( @@ -85,9 +79,7 @@ describe("SkillInstallSurface", () => { expect(screen.getByText("openclaw skills install weather")).toBeTruthy(); expect(screen.queryByText("npx clawhub@latest install weather")).toBeNull(); expect(screen.getByRole("tab", { name: "CLI" }).getAttribute("aria-selected")).toBe("true"); - expect(screen.getByRole("tab", { name: "Prompt" }).getAttribute("aria-selected")).toBe( - "false", - ); + expect(screen.getByRole("tab", { name: "Prompt" }).getAttribute("aria-selected")).toBe("false"); fireEvent.click(screen.getByRole("button", { name: "Copy OpenClaw CLI command" })); @@ -98,9 +90,7 @@ describe("SkillInstallSurface", () => { fireEvent.click(screen.getByRole("tab", { name: "Prompt" })); expect(screen.getByText(/Install the skill "Weather"/i)).toBeTruthy(); - expect(screen.getByRole("tab", { name: "Prompt" }).getAttribute("aria-selected")).toBe( - "true", - ); + expect(screen.getByRole("tab", { name: "Prompt" }).getAttribute("aria-selected")).toBe("true"); fireEvent.click(screen.getByRole("button", { name: "Copy OpenClaw prompt" })); diff --git a/src/components/SkillInstallSurface.tsx b/src/components/SkillInstallSurface.tsx index c41925e6..13bd855c 100644 --- a/src/components/SkillInstallSurface.tsx +++ b/src/components/SkillInstallSurface.tsx @@ -129,8 +129,8 @@ export function SkillInstallSurface({

OpenClaw Prompt Flow

Install with OpenClaw

- Best for remote or guided setup. Copy the exact prompt, then paste it into OpenClaw - for {installTarget}. + Best for remote or guided setup. Copy the exact prompt, then paste it into OpenClaw for{" "} + {installTarget}.

@@ -207,9 +207,7 @@ export function SkillCommandLineCard({ type="button" role="tab" aria-selected={activeInstallTab === "prompt"} - className={`install-switcher-pill${ - activeInstallTab === "prompt" ? " is-active" : "" - }`} + className={`install-switcher-pill${activeInstallTab === "prompt" ? " is-active" : ""}`} onClick={() => setActiveInstallTab("prompt")} > Prompt @@ -230,9 +228,7 @@ export function SkillCommandLineCard({ @@ -128,24 +127,31 @@ function UserStatsTooltipContent({ {displayName} )} - {handle && ( - @{handle} - )} + {handle && @{handle}}
{stats === null ? ( Loading... ) : ( <> - + {formatCompactStat(stats.publishedSkills)} - + {formatCompactStat(stats.totalStars)} - + {formatCompactStat(stats.totalDownloads)} diff --git a/src/components/UserListItem.tsx b/src/components/UserListItem.tsx index cd3a3f93..f1ea0c58 100644 --- a/src/components/UserListItem.tsx +++ b/src/components/UserListItem.tsx @@ -1,6 +1,6 @@ import { Link } from "@tanstack/react-router"; -import { MarketplaceIcon } from "./MarketplaceIcon"; import type { PublicUser } from "../lib/publicUser"; +import { MarketplaceIcon } from "./MarketplaceIcon"; type UserListItemProps = { user: PublicUser; @@ -13,7 +13,12 @@ export function UserListItem({ user }: UserListItemProps) { const displayName = user.displayName ?? user.name ?? handle; return ( - +
diff --git a/src/components/layout/Container.tsx b/src/components/layout/Container.tsx index f3c8e68e..b28f3767 100644 --- a/src/components/layout/Container.tsx +++ b/src/components/layout/Container.tsx @@ -2,23 +2,23 @@ import * as React from "react"; import { cn } from "../../lib/utils"; interface ContainerProps extends React.HTMLAttributes { - size?: "default" | "narrow" | "wide"; + size?: "default" | "narrow" | "wide"; } const Container = React.forwardRef( - ({ className, size = "default", ...props }, ref) => ( -
- ), + ({ className, size = "default", ...props }, ref) => ( +
+ ), ); Container.displayName = "Container"; diff --git a/src/components/skeletons/SkillDetailSkeleton.tsx b/src/components/skeletons/SkillDetailSkeleton.tsx index 07243573..272d9ed5 100644 --- a/src/components/skeletons/SkillDetailSkeleton.tsx +++ b/src/components/skeletons/SkillDetailSkeleton.tsx @@ -59,7 +59,6 @@ export function SkillDetailSkeleton() {
-
); diff --git a/src/components/skillDetailUtils.test.ts b/src/components/skillDetailUtils.test.ts index c3a25167..a41a457d 100644 --- a/src/components/skillDetailUtils.test.ts +++ b/src/components/skillDetailUtils.test.ts @@ -13,7 +13,9 @@ describe("skill detail install helpers", () => { const ownerPublisherId = "publishers:1" as Id<"publishers">; it("prefers the owner handle for install targets", () => { - expect(buildSkillInstallTarget("steipete", ownerPublisherId, "weather")).toBe("steipete/weather"); + expect(buildSkillInstallTarget("steipete", ownerPublisherId, "weather")).toBe( + "steipete/weather", + ); }); it("falls back to owner id and then plain slug", () => { @@ -23,11 +25,15 @@ describe("skill detail install helpers", () => { it("formats the OpenClaw and ClawHub commands", () => { expect(formatOpenClawInstallCommand("weather")).toBe("openclaw skills install weather"); - expect(formatClawHubInstallCommand("weather", "npm")).toBe("npx clawhub@latest install weather"); + expect(formatClawHubInstallCommand("weather", "npm")).toBe( + "npx clawhub@latest install weather", + ); expect(formatClawHubInstallCommand("weather", "pnpm")).toBe( "pnpm dlx clawhub@latest install weather", ); - expect(formatClawHubInstallCommand("weather", "bun")).toBe("bunx clawhub@latest install weather"); + expect(formatClawHubInstallCommand("weather", "bun")).toBe( + "bunx clawhub@latest install weather", + ); }); it("builds the install-and-setup prompt from known metadata only", () => { diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index 583a3a5a..d1893f20 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -15,11 +15,16 @@ const Badge = React.forwardRef( // Variant styles — all token-driven, no dark: overrides needed variant === "default" && "bg-hover-bg px-3 py-1 text-ink-soft border border-line", variant === "accent" && "bg-active-bg px-3 py-1 text-accent-deep border border-line", - variant === "compact" && "bg-hover-bg px-2.5 py-0.5 text-fs-xs text-ink-soft border border-line", - variant === "pending" && "bg-status-warning-bg px-3 py-1 text-status-warning-fg border border-line", - variant === "success" && "bg-status-success-bg px-3 py-1 text-status-success-fg border border-line", - variant === "warning" && "bg-status-warning-bg px-3 py-1 text-status-warning-fg border border-line", - variant === "destructive" && "bg-status-error-bg px-3 py-1 text-status-error-fg border border-line", + variant === "compact" && + "bg-hover-bg px-2.5 py-0.5 text-fs-xs text-ink-soft border border-line", + variant === "pending" && + "bg-status-warning-bg px-3 py-1 text-status-warning-fg border border-line", + variant === "success" && + "bg-status-success-bg px-3 py-1 text-status-success-fg border border-line", + variant === "warning" && + "bg-status-warning-bg px-3 py-1 text-status-warning-fg border border-line", + variant === "destructive" && + "bg-status-error-bg px-3 py-1 text-status-error-fg border border-line", className, )} {...props} diff --git a/src/lib/categories.ts b/src/lib/categories.ts index fb538ceb..5305b774 100644 --- a/src/lib/categories.ts +++ b/src/lib/categories.ts @@ -7,12 +7,42 @@ export type SkillCategory = { export const SKILL_CATEGORIES: SkillCategory[] = [ { slug: "mcp-tools", label: "MCP Tools", icon: "plug", keywords: ["mcp", "tool", "server"] }, - { slug: "prompts", label: "Prompts", icon: "message-square", keywords: ["prompt", "template", "system"] }, - { slug: "workflows", label: "Workflows", icon: "git-branch", keywords: ["workflow", "pipeline", "chain"] }, - { slug: "dev-tools", label: "Dev Tools", icon: "wrench", keywords: ["dev", "debug", "lint", "test", "build"] }, - { slug: "data", label: "Data & APIs", icon: "database", keywords: ["api", "data", "fetch", "http", "rest", "graphql"] }, - { slug: "security", label: "Security", icon: "shield", keywords: ["security", "scan", "auth", "encrypt"] }, - { slug: "automation", label: "Automation", icon: "zap", keywords: ["auto", "cron", "schedule", "bot"] }, + { + slug: "prompts", + label: "Prompts", + icon: "message-square", + keywords: ["prompt", "template", "system"], + }, + { + slug: "workflows", + label: "Workflows", + icon: "git-branch", + keywords: ["workflow", "pipeline", "chain"], + }, + { + slug: "dev-tools", + label: "Dev Tools", + icon: "wrench", + keywords: ["dev", "debug", "lint", "test", "build"], + }, + { + slug: "data", + label: "Data & APIs", + icon: "database", + keywords: ["api", "data", "fetch", "http", "rest", "graphql"], + }, + { + slug: "security", + label: "Security", + icon: "shield", + keywords: ["security", "scan", "auth", "encrypt"], + }, + { + slug: "automation", + label: "Automation", + icon: "zap", + keywords: ["auto", "cron", "schedule", "bot"], + }, { slug: "other", label: "Other", icon: "package", keywords: [] }, ]; diff --git a/src/lib/convexError.ts b/src/lib/convexError.ts index 259449f1..82e8a113 100644 --- a/src/lib/convexError.ts +++ b/src/lib/convexError.ts @@ -28,10 +28,7 @@ export function getUserFacingConvexError(error: unknown, fallback: string) { if (hasOwnProperty(maybe, "data")) { if (typeof maybe.data === "string") candidates.push(maybe.data); - if ( - hasOwnProperty(maybe.data, "message") && - typeof maybe.data.message === "string" - ) { + if (hasOwnProperty(maybe.data, "message") && typeof maybe.data.message === "string") { candidates.push(maybe.data.message); } } diff --git a/src/lib/hasOwnProperty.ts b/src/lib/hasOwnProperty.ts index 65262f9d..ca842daf 100644 --- a/src/lib/hasOwnProperty.ts +++ b/src/lib/hasOwnProperty.ts @@ -2,5 +2,7 @@ export function hasOwnProperty( value: unknown, key: K, ): value is Record { - return typeof value === "object" && value !== null && Object.prototype.hasOwnProperty.call(value, key); + return ( + typeof value === "object" && value !== null && Object.prototype.hasOwnProperty.call(value, key) + ); } diff --git a/src/lib/useAuthError.ts b/src/lib/useAuthError.ts index 9a7fc850..2ecc03c7 100644 --- a/src/lib/useAuthError.ts +++ b/src/lib/useAuthError.ts @@ -6,33 +6,29 @@ let authError: string | null = null; const listeners = new Set<() => void>(); function emitChange() { - for (const listener of listeners) listener(); + for (const listener of listeners) listener(); } function subscribe(listener: () => void) { - listeners.add(listener); - return () => listeners.delete(listener); + listeners.add(listener); + return () => listeners.delete(listener); } export function getAuthErrorSnapshot() { - return authError; + return authError; } export function setAuthError(error: string | null) { - if (authError === error) return; - authError = error; - emitChange(); + if (authError === error) return; + authError = error; + emitChange(); } export function clearAuthError() { - setAuthError(null); + setAuthError(null); } export function useAuthError() { - const error = useSyncExternalStore( - subscribe, - getAuthErrorSnapshot, - getAuthErrorSnapshot, - ); - return { error, clear: clearAuthError }; + const error = useSyncExternalStore(subscribe, getAuthErrorSnapshot, getAuthErrorSnapshot); + return { error, clear: clearAuthError }; } diff --git a/src/routes/$owner/$slug/security/$scanner.tsx b/src/routes/$owner/$slug/security/$scanner.tsx index 7743e8e5..2ed6ae8b 100644 --- a/src/routes/$owner/$slug/security/$scanner.tsx +++ b/src/routes/$owner/$slug/security/$scanner.tsx @@ -1,10 +1,7 @@ import { createFileRoute, notFound, redirect } from "@tanstack/react-router"; import { useQuery } from "convex/react"; import { api } from "../../../../../convex/_generated/api"; -import { - SecurityScannerPage, - type ScannerSlug, -} from "../../../../components/SecurityScannerPage"; +import { SecurityScannerPage, type ScannerSlug } from "../../../../components/SecurityScannerPage"; import { buildSkillMeta } from "../../../../lib/og"; import { fetchSkillPageData } from "../../../../lib/skillPage"; diff --git a/src/routes/dashboard.test.tsx b/src/routes/-dashboard.test.tsx similarity index 98% rename from src/routes/dashboard.test.tsx rename to src/routes/-dashboard.test.tsx index b1fd84c9..acc7259d 100644 --- a/src/routes/dashboard.test.tsx +++ b/src/routes/-dashboard.test.tsx @@ -242,7 +242,9 @@ describe("Dashboard minimal rows", () => { expect(screen.getByText("Blocked")).toBeTruthy(); expect(screen.getByRole("button", { name: "Suspicious status reason" })).toBeTruthy(); expect(screen.getByRole("button", { name: "Blocked status reason" })).toBeTruthy(); - expect(screen.getByRole("button", { name: "Open actions for Local Flagged Skill" })).toBeTruthy(); + expect( + screen.getByRole("button", { name: "Open actions for Local Flagged Skill" }), + ).toBeTruthy(); expect( screen.getByRole("button", { name: "Open actions for Local Flagged Runtime Plugin" }), ).toBeTruthy(); @@ -289,5 +291,4 @@ describe("Dashboard minimal rows", () => { expect(screen.queryByText("2/3 rescans left")).toBeNull(); expect(screen.queryByText("Limit reached (3/3)")).toBeNull(); }); - }); diff --git a/src/routes/about.tsx b/src/routes/about.tsx index de605955..e44d48a1 100644 --- a/src/routes/about.tsx +++ b/src/routes/about.tsx @@ -1,23 +1,15 @@ -import { createFileRoute, Link } from '@tanstack/react-router'; -import type { LucideIcon } from 'lucide-react'; -import { - Banknote, - Drama, - Eye, - EyeOff, - ImageOff, - ShieldOff, - UserX, -} from 'lucide-react'; -import type { ReactNode } from 'react'; -import { Badge } from '../components/ui/badge'; -import { Button } from '../components/ui/button'; -import { getSiteMode, getSiteName, getSiteUrlForMode } from '../lib/site'; +import { createFileRoute, Link } from "@tanstack/react-router"; +import type { LucideIcon } from "lucide-react"; +import { Banknote, Drama, Eye, EyeOff, ImageOff, ShieldOff, UserX } from "lucide-react"; +import type { ReactNode } from "react"; +import { Badge } from "../components/ui/badge"; +import { Button } from "../components/ui/button"; +import { getSiteMode, getSiteName, getSiteUrlForMode } from "../lib/site"; export function renderWithInlineCode(text: string): ReactNode[] { const parts = text.split(/(`[^`]+`)/g); return parts.map((part, i) => { - if (part.startsWith('`') && part.endsWith('`')) { + if (part.startsWith("`") && part.endsWith("`")) { return ( {part.slice(1, -1)} @@ -30,68 +22,68 @@ export function renderWithInlineCode(text: string): ReactNode[] { const prohibitedCategories: { title: string; icon: LucideIcon; examples: string }[] = [ { - title: 'Bypass and unauthorized access', + title: "Bypass and unauthorized access", icon: ShieldOff, examples: - 'Auth bypass, account takeover, CAPTCHA bypass, Cloudflare or anti-bot evasion, rate-limit bypass, reusable session theft, live call or agent takeover.', + "Auth bypass, account takeover, CAPTCHA bypass, Cloudflare or anti-bot evasion, rate-limit bypass, reusable session theft, live call or agent takeover.", }, { - title: 'Platform abuse and ban evasion', + title: "Platform abuse and ban evasion", icon: UserX, examples: - 'Stealth accounts after bans, account warming/farming, fake engagement, multi-account automation, spam posting, marketplace or social automation built to avoid detection.', + "Stealth accounts after bans, account warming/farming, fake engagement, multi-account automation, spam posting, marketplace or social automation built to avoid detection.", }, { - title: 'Fraud and deception', + title: "Fraud and deception", icon: Banknote, examples: - 'Fake certificates, fake invoices, deceptive payment flows, fake social proof, scam outreach, or synthetic-identity workflows built to create accounts for fraud.', + "Fake certificates, fake invoices, deceptive payment flows, fake social proof, scam outreach, or synthetic-identity workflows built to create accounts for fraud.", }, { - title: 'Privacy-invasive surveillance', + title: "Privacy-invasive surveillance", icon: Eye, examples: - 'Mass contact scraping for spam, doxxing, stalking, covert monitoring, biometric / face-matching workflows without clear consent, or buying, publishing, downloading, or operationalizing leaked data or breach dumps.', + "Mass contact scraping for spam, doxxing, stalking, covert monitoring, biometric / face-matching workflows without clear consent, or buying, publishing, downloading, or operationalizing leaked data or breach dumps.", }, { - title: 'Non-consensual impersonation', + title: "Non-consensual impersonation", icon: Drama, examples: - 'Face swap, digital twins, cloned influencers, fake personas, or other identity manipulation used to impersonate or mislead.', + "Face swap, digital twins, cloned influencers, fake personas, or other identity manipulation used to impersonate or mislead.", }, { - title: 'Explicit sexual content', + title: "Explicit sexual content", icon: ImageOff, examples: - 'NSFW image, video, or text generation, especially wrappers around third-party APIs with safety checks disabled.', + "NSFW image, video, or text generation, especially wrappers around third-party APIs with safety checks disabled.", }, { - title: 'Hidden or misleading execution', + title: "Hidden or misleading execution", icon: EyeOff, examples: - 'Obfuscated install commands, `curl | sh`, undeclared secret requirements, undeclared private-key use, or remote `npx @latest` execution without reviewability.', + "Obfuscated install commands, `curl | sh`, undeclared secret requirements, undeclared private-key use, or remote `npx @latest` execution without reviewability.", }, ]; const recentPatterns = [ - 'Create stealth seller accounts after marketplace bans.', - 'Modify Telegram pairing so unapproved users automatically receive pairing codes.', - 'Cultivate Reddit or Twitter accounts with undetectable automation.', - 'Generate professional certificates or invoices for arbitrary use.', - 'Generate NSFW content with safety checks disabled.', - 'Scrape leads, enrich contacts, and launch cold outreach at scale.', - 'Buy, publish, or download leaked data or breach dumps.', - 'Bulk-create email or social accounts with synthetic identities or CAPTCHA solving.', + "Create stealth seller accounts after marketplace bans.", + "Modify Telegram pairing so unapproved users automatically receive pairing codes.", + "Cultivate Reddit or Twitter accounts with undetectable automation.", + "Generate professional certificates or invoices for arbitrary use.", + "Generate NSFW content with safety checks disabled.", + "Scrape leads, enrich contacts, and launch cold outreach at scale.", + "Buy, publish, or download leaked data or breach dumps.", + "Bulk-create email or social accounts with synthetic identities or CAPTCHA solving.", ]; -export const Route = createFileRoute('/about')({ +export const Route = createFileRoute("/about")({ head: () => { const mode = getSiteMode(); const siteName = getSiteName(mode); const siteUrl = getSiteUrlForMode(mode); const title = `About · ${siteName}`; const description = - 'What ClawHub allows, what we do not host, and the abuse patterns that lead to removal or account bans.'; + "What ClawHub allows, what we do not host, and the abuse patterns that lead to removal or account bans."; return { links: [ @@ -102,11 +94,11 @@ export const Route = createFileRoute('/about')({ ], meta: [ { title }, - { name: 'description', content: description }, - { property: 'og:title', content: title }, - { property: 'og:description', content: description }, - { property: 'og:type', content: 'website' }, - { property: 'og:url', content: `${siteUrl}/about` }, + { name: "description", content: description }, + { property: "og:title", content: title }, + { property: "og:description", content: description }, + { property: "og:type", content: "website" }, + { property: "og:url", content: `${siteUrl}/about` }, ], }; }, @@ -125,9 +117,9 @@ function AboutPage() {

What ClawHub will not host

- ClawHub is for useful agent tooling, not abuse workflows. If a skill is built to - evade defenses, scam people, invade privacy, or enable non-consensual behavior, it - does not belong here. + ClawHub is for useful agent tooling, not abuse workflows. If a skill is built to evade + defenses, scam people, invade privacy, or enable non-consensual behavior, it does not + belong here.

@@ -203,9 +195,7 @@ function AboutPage() {

, + SignInButton: ({ children, ...props }: React.ButtonHTMLAttributes) => ( + + ), })); vi.mock("../../components/ui/card", () => ({ diff --git a/src/routes/cli/auth.tsx b/src/routes/cli/auth.tsx index 88e0f466..c22eefa2 100644 --- a/src/routes/cli/auth.tsx +++ b/src/routes/cli/auth.tsx @@ -18,7 +18,9 @@ type CliAuthProps = { navigate?: (url: string) => void; }; -export function CliAuth({ navigate = (url: string) => window.location.assign(url) }: CliAuthProps = {}) { +export function CliAuth({ + navigate = (url: string) => window.location.assign(url), +}: CliAuthProps = {}) { const { isAuthenticated, isLoading, me } = useAuthStatus(); const { error: authError, clear: clearAuthError } = useAuthError(); const createToken = useMutation(api.tokens.create); @@ -79,7 +81,17 @@ export function CliAuth({ navigate = (url: string) => window.location.assign(url setStatus(message); setToken(null); }); - }, [createToken, isAuthenticated, label, me, navigate, redirectUri, registry, safeRedirect, state]); + }, [ + createToken, + isAuthenticated, + label, + me, + navigate, + redirectUri, + registry, + safeRedirect, + state, + ]); if (!safeRedirect) { return ( @@ -169,9 +181,9 @@ export function CliAuth({ navigate = (url: string) => window.location.assign(url {token ? (
- If the redirect did not complete, copy this token and run{" "} - clawhub login --token <token>: -
+ If the redirect did not complete, copy this token and run{" "} + clawhub login --token <token>: +
{token} {callbackUrl ? (
diff --git a/src/routes/dashboard.tsx b/src/routes/dashboard.tsx index 451847d7..62dc9a01 100644 --- a/src/routes/dashboard.tsx +++ b/src/routes/dashboard.tsx @@ -228,9 +228,7 @@ export function Dashboard() {

Dashboard

-

- View your published skills and plugins. -

+

View your published skills and plugins.

@@ -306,11 +304,7 @@ function SkillRow({ skill, ownerHandle }: { skill: DashboardSkill; ownerHandle:
- + {skill.displayName}
diff --git a/src/routes/import.tsx b/src/routes/import.tsx index 1310719c..658ef2ae 100644 --- a/src/routes/import.tsx +++ b/src/routes/import.tsx @@ -226,9 +226,7 @@ export function ImportGitHub() { title={isLoading ? "Loading..." : "Sign in to import and publish skills"} description="You need to be signed in to import skills from GitHub." > - {!isLoading ? ( - - ) : null} + {!isLoading ? : null} diff --git a/src/routes/plugins/$name/security/$scanner.tsx b/src/routes/plugins/$name/security/$scanner.tsx index a61ce938..5fd8b716 100644 --- a/src/routes/plugins/$name/security/$scanner.tsx +++ b/src/routes/plugins/$name/security/$scanner.tsx @@ -1,8 +1,5 @@ import { createFileRoute, notFound } from "@tanstack/react-router"; -import { - SecurityScannerPage, - type ScannerSlug, -} from "../../../../components/SecurityScannerPage"; +import { SecurityScannerPage, type ScannerSlug } from "../../../../components/SecurityScannerPage"; import { fetchPackageDetail, fetchPackageVersion, diff --git a/src/routes/skills/-SkillsResults.tsx b/src/routes/skills/-SkillsResults.tsx index 86b84e1d..eb2af216 100644 --- a/src/routes/skills/-SkillsResults.tsx +++ b/src/routes/skills/-SkillsResults.tsx @@ -1,7 +1,7 @@ import type { RefObject } from "react"; import { SkillCard } from "../../components/SkillCard"; -import { SkillListItem } from "../../components/SkillListItem"; import { getPlatformLabels } from "../../components/skillDetailUtils"; +import { SkillListItem } from "../../components/SkillListItem"; import { SkillStatsTripletLine } from "../../components/SkillStats"; import { Button } from "../../components/ui/button"; import { UserBadge } from "../../components/UserBadge"; @@ -52,7 +52,9 @@ export function SkillsResults({

No skills found

- {hasQuery ? "Try a different search term or remove filters." : "No skills have been published yet."} + {hasQuery + ? "Try a different search term or remove filters." + : "No skills have been published yet."}

) : view === "cards" ? ( @@ -108,10 +110,7 @@ export function SkillsResults({ )} {canLoadMore || isLoadingMore ? ( -
+
{canAutoLoad ? ( isLoadingMore ? ( "Loading more..." diff --git a/src/routes/skills/-SkillsToolbar.tsx b/src/routes/skills/-SkillsToolbar.tsx index 50fb8c74..0ac586b0 100644 --- a/src/routes/skills/-SkillsToolbar.tsx +++ b/src/routes/skills/-SkillsToolbar.tsx @@ -16,7 +16,6 @@ import { } from "lucide-react"; import type { RefObject } from "react"; import { useMemo } from "react"; -import { SKILL_CATEGORIES, type SkillCategory } from "../../lib/categories"; import { Button } from "../../components/ui/button"; import { Input } from "../../components/ui/input"; import { @@ -26,6 +25,7 @@ import { SelectTrigger, SelectValue, } from "../../components/ui/select"; +import { SKILL_CATEGORIES, type SkillCategory } from "../../lib/categories"; import { type SortDir, type SortKey } from "./-params"; type SkillsToolbarProps = { @@ -89,9 +89,8 @@ export function SkillsToolbar({ const activeCategory = useMemo(() => { if (query === "__other__") return "other"; if (!query) return undefined; - return SKILL_CATEGORIES.find((c) => - c.keywords.some((k) => k === query.trim().toLowerCase()), - )?.slug; + return SKILL_CATEGORIES.find((c) => c.keywords.some((k) => k === query.trim().toLowerCase())) + ?.slug; }, [query]); const handleCategoryChange = (cat: SkillCategory | undefined) => { diff --git a/src/routes/u/$handle.tsx b/src/routes/u/$handle.tsx index 3dfc5064..90da073c 100644 --- a/src/routes/u/$handle.tsx +++ b/src/routes/u/$handle.tsx @@ -114,9 +114,7 @@ function UserProfile() { <> {published.length > 0 ? ( <> -

- Published ({published.length}) -

+

Published ({published.length})

{isLoadingPublished ? (
Loading published skills...
@@ -131,9 +129,7 @@ function UserProfile() { ) : null} -

- Stars ({skills.length}) -

+

Stars ({skills.length})

{isLoadingSkills ? (
Loading stars...
@@ -164,9 +160,7 @@ function InstalledSection(props: { if (data === undefined) { return ( <> -

- Installed -

+

Installed

Loading telemetry…
@@ -177,9 +171,7 @@ function InstalledSection(props: { if (data === null) { return ( <> -

- Installed -

+

Installed

Sign in to view your installed skills. ); @@ -187,9 +179,7 @@ function InstalledSection(props: { return ( <> -

- Installed -

+

Installed

Private view. Only you can see your folders/roots. Everyone else only sees aggregated install counts per skill. @@ -214,9 +204,7 @@ function InstalledSection(props: { {showRaw ? ( -

-            {JSON.stringify(data, null, 2)}
-          
+
{JSON.stringify(data, null, 2)}
) : null} diff --git a/vite.config.ts b/vite.config.ts index 1a571fa4..c61a11e3 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -41,7 +41,8 @@ function handleRollupWarning( type SourceReplacement = readonly [from: string, to: string]; -const reflectHas = (target: string, key: string) => `Reflect.has(${target}, ${JSON.stringify(key)})`; +const reflectHas = (target: string, key: string) => + `Reflect.has(${target}, ${JSON.stringify(key)})`; const arkSafariInOperatorFixes = [ { @@ -89,15 +90,21 @@ const arkSafariInOperatorFixes = [ }, { suffix: "/node_modules/@ark/schema/out/node.js", - replacements: [['"value" in transformedInner', reflectHas("transformedInner", "value")]] satisfies SourceReplacement[], + replacements: [ + ['"value" in transformedInner', reflectHas("transformedInner", "value")], + ] satisfies SourceReplacement[], }, { suffix: "/node_modules/@ark/schema/out/scope.js", - replacements: [['"branches" in schema', reflectHas("schema", "branches")]] satisfies SourceReplacement[], + replacements: [ + ['"branches" in schema', reflectHas("schema", "branches")], + ] satisfies SourceReplacement[], }, { suffix: "/node_modules/@ark/schema/out/structure/optional.js", - replacements: [['"default" in this.inner', reflectHas("this.inner", "default")]] satisfies SourceReplacement[], + replacements: [ + ['"default" in this.inner', reflectHas("this.inner", "default")], + ] satisfies SourceReplacement[], }, { suffix: "/node_modules/@ark/schema/out/structure/sequence.js", @@ -112,11 +119,15 @@ const arkSafariInOperatorFixes = [ }, { suffix: "/node_modules/@ark/schema/out/structure/prop.js", - replacements: [['"default" in this.inner', reflectHas("this.inner", "default")]] satisfies SourceReplacement[], + replacements: [ + ['"default" in this.inner', reflectHas("this.inner", "default")], + ] satisfies SourceReplacement[], }, { suffix: "/node_modules/@ark/schema/out/shared/implement.js", - replacements: [['"description" in ctx', reflectHas("ctx", "description")]] satisfies SourceReplacement[], + replacements: [ + ['"description" in ctx', reflectHas("ctx", "description")], + ] satisfies SourceReplacement[], }, { suffix: "/node_modules/@ark/schema/out/shared/errors.js",