refactor: extract @clawdhub/schema package

- Move ArkType schemas into packages/schema

- Wire app/convex/cli imports to workspace dep

- Add workspaces + update lint/coverage

- Build schema dist (tracked)
This commit is contained in:
Peter Steinberger 2026-01-04 03:49:57 +01:00
parent 586a08ed4d
commit 5c28a2a252
29 changed files with 368 additions and 33 deletions

View File

@ -6,6 +6,7 @@
"name": "clawdhub",
"dependencies": {
"@auth/core": "^0.41.1",
"@clawdhub/schema": "workspace:*",
"@convex-dev/auth": "^0.0.90",
"@fontsource/bricolage-grotesque": "^5.2.10",
"@fontsource/ibm-plex-mono": "^5.2.7",
@ -20,7 +21,6 @@
"@tanstack/react-router-ssr-query": "^1.144.0",
"@tanstack/react-start": "^1.145.3",
"@tanstack/router-plugin": "^1.145.2",
"arktype": "^2.1.29",
"clsx": "^2.1.1",
"convex": "^1.31.2",
"fflate": "^0.8.2",
@ -46,9 +46,7 @@
"@types/semver": "^7.7.1",
"@vitejs/plugin-react": "^5.1.2",
"@vitest/coverage-v8": "^4.0.16",
"commander": "^14.0.2",
"jsdom": "^27.4.0",
"ora": "^9.0.0",
"oxlint": "^1.36.0",
"oxlint-tsgolint": "^0.10.1",
"typescript": "^5.9.3",
@ -57,6 +55,34 @@
"web-vitals": "^5.1.0",
},
},
"packages/clawdhub": {
"name": "clawdhub",
"version": "0.1.0",
"bin": {
"clawdhub": "./bin/clawdhub.js",
},
"dependencies": {
"@clawdhub/schema": "workspace:*",
"commander": "^14.0.2",
"fflate": "^0.8.2",
"ora": "^9.0.0",
"semver": "^7.7.3",
},
"devDependencies": {
"@types/node": "^25.0.3",
"typescript": "^5.9.3",
},
},
"packages/schema": {
"name": "@clawdhub/schema",
"version": "0.1.0",
"dependencies": {
"arktype": "^2.1.29",
},
"devDependencies": {
"typescript": "^5.9.3",
},
},
},
"packages": {
"@acemir/cssom": ["@acemir/cssom@0.9.30", "", {}, "sha512-9CnlMCI0LmCIq0olalQqdWrJHPzm0/tw3gzOA9zJSgvFX7Xau3D24mAGa4BtwxwY69nsuJW6kQqqCzf/mEcQgg=="],
@ -137,6 +163,8 @@
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.11", "", { "os": "win32", "cpu": "x64" }, "sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg=="],
"@clawdhub/schema": ["@clawdhub/schema@workspace:packages/schema"],
"@convex-dev/auth": ["@convex-dev/auth@0.0.90", "", { "dependencies": { "@oslojs/crypto": "^1.0.1", "@oslojs/encoding": "^1.1.0", "cookie": "^1.0.1", "is-network-error": "^1.1.0", "jose": "^5.2.2", "jwt-decode": "^4.0.0", "lucia": "^3.2.0", "oauth4webapi": "^3.1.2", "path-to-regexp": "^6.3.0", "server-only": "^0.0.1" }, "peerDependencies": { "@auth/core": "^0.37.0", "convex": "^1.17.0", "react": "^18.2.0 || ^19.0.0-0" }, "optionalPeers": ["react"], "bin": { "auth": "dist/bin.cjs" } }, "sha512-aqw88EB042HvnaF4wcf/f/wTocmT2Bus2VDQRuV79cM0+8kORM0ICK/ByZ6XsHgQ9qr6TmidNbXm6QAgndrdpQ=="],
"@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="],
@ -677,6 +705,8 @@
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"clawdhub": ["clawdhub@workspace:packages/clawdhub"],
"cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
"cli-spinners": ["cli-spinners@3.3.0", "", {}, "sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ=="],

View File

@ -1,5 +1,4 @@
import { parseArk } from '../packages/clawdhub/src/shared/ark.js'
import { CliPublishRequestSchema } from '../packages/clawdhub/src/shared/schemas.js'
import { CliPublishRequestSchema, parseArk } from '@clawdhub/schema'
import { api, internal } from './_generated/api'
import type { Id } from './_generated/dataModel'
import { httpAction } from './_generated/server'

View File

@ -1,9 +1,9 @@
import { parseArk } from '../../packages/clawdhub/src/shared/ark.js'
import {
type ClawdisSkillMetadata,
ClawdisSkillMetadataSchema,
parseArk,
type SkillInstallSpec,
} from '../../packages/clawdhub/src/shared/schemas.js'
} from '@clawdhub/schema'
export type ParsedSkillFrontmatter = Record<string, string>
export type { ClawdisSkillMetadata, SkillInstallSpec }

View File

@ -2,6 +2,9 @@
"name": "clawdhub",
"private": true,
"type": "module",
"workspaces": [
"packages/*"
],
"scripts": {
"dev": "bun --bun vite dev --port 3000",
"build": "bun --bun vite build",
@ -12,10 +15,11 @@
"convex:deploy": "bunx convex deploy --typecheck=disable --yes",
"lint": "bun run lint:biome && bun run lint:oxlint",
"lint:biome": "biome check .",
"lint:oxlint": "oxlint --type-aware --tsconfig ./tsconfig.oxlint.json ./src ./convex ./packages/clawdhub/src",
"lint:oxlint": "oxlint --type-aware --tsconfig ./tsconfig.oxlint.json ./src ./convex ./packages/clawdhub/src ./packages/schema/src",
"format": "biome format --write ."
},
"dependencies": {
"@clawdhub/schema": "workspace:*",
"@auth/core": "^0.41.1",
"@convex-dev/auth": "^0.0.90",
"@fontsource/bricolage-grotesque": "^5.2.10",
@ -31,7 +35,6 @@
"@tanstack/react-router-ssr-query": "^1.144.0",
"@tanstack/react-start": "^1.145.3",
"@tanstack/router-plugin": "^1.145.2",
"arktype": "^2.1.29",
"clsx": "^2.1.1",
"convex": "^1.31.2",
"fflate": "^0.8.2",
@ -57,9 +60,7 @@
"@types/semver": "^7.7.1",
"@vitejs/plugin-react": "^5.1.2",
"@vitest/coverage-v8": "^4.0.16",
"commander": "^14.0.2",
"jsdom": "^27.4.0",
"ora": "^9.0.0",
"oxlint": "^1.36.0",
"oxlint-tsgolint": "^0.10.1",
"typescript": "^5.9.3",

View File

@ -19,7 +19,7 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
"arktype": "^2.1.29",
"@clawdhub/schema": "workspace:*",
"commander": "^14.0.2",
"fflate": "^0.8.2",
"ora": "^9.0.0",

View File

@ -3,12 +3,6 @@ import { mkdir, rm, stat } from 'node:fs/promises'
import { basename, join, resolve } from 'node:path'
import { stdin } from 'node:process'
import { createInterface } from 'node:readline/promises'
import { Command } from 'commander'
import ora from 'ora'
import semver from 'semver'
import { getGlobalConfigPath, readGlobalConfig, writeGlobalConfig } from './config.js'
import { apiRequest, downloadZip } from './http.js'
import { parseArk } from './shared/ark.js'
import {
ApiCliPublishResponseSchema,
ApiCliUploadUrlResponseSchema,
@ -18,7 +12,13 @@ import {
ApiSkillResolveResponseSchema,
ApiUploadFileResponseSchema,
CliPublishRequestSchema,
} from './shared/schemas.js'
parseArk,
} from '@clawdhub/schema'
import { Command } from 'commander'
import ora from 'ora'
import semver from 'semver'
import { getGlobalConfigPath, readGlobalConfig, writeGlobalConfig } from './config.js'
import { apiRequest, downloadZip } from './http.js'
import {
extractZipToDir,
hashSkillFiles,

View File

@ -1,8 +1,7 @@
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { homedir } from 'node:os'
import { dirname, join } from 'node:path'
import { parseArk } from './shared/ark.js'
import { type GlobalConfig, GlobalConfigSchema } from './shared/schemas.js'
import { type GlobalConfig, GlobalConfigSchema, parseArk } from '@clawdhub/schema'
export function getGlobalConfigPath() {
const home = homedir()

View File

@ -1,6 +1,6 @@
/* @vitest-environment node */
import { type } from 'arktype'
import { ApiCliWhoamiResponseSchema } from '@clawdhub/schema'
import { describe, expect, it, vi } from 'vitest'
import { apiRequest, downloadZip } from './http'
@ -8,15 +8,15 @@ describe('apiRequest', () => {
it('adds bearer token and parses json', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ ok: true }),
json: async () => ({ user: { handle: null } }),
})
vi.stubGlobal('fetch', fetchMock)
const result = await apiRequest(
'https://example.com',
{ method: 'GET', path: '/x', token: 'clh_token' },
type({ ok: 'boolean' }),
ApiCliWhoamiResponseSchema,
)
expect(result.ok).toBe(true)
expect(result.user.handle).toBeNull()
expect(fetchMock).toHaveBeenCalledTimes(1)
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]
expect((init.headers as Record<string, string>).Authorization).toBe('Bearer clh_token')

View File

@ -1,5 +1,5 @@
import type { ArkValidator } from './shared/ark.js'
import { parseArk } from './shared/ark.js'
import type { ArkValidator } from '@clawdhub/schema'
import { parseArk } from '@clawdhub/schema'
type RequestArgs =
| { method: 'GET' | 'POST'; path: string; token?: string; body?: unknown }

View File

@ -1,9 +1,8 @@
import { createHash } from 'node:crypto'
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
import { dirname, join, relative, resolve, sep } from 'node:path'
import { type Lockfile, LockfileSchema, parseArk } from '@clawdhub/schema'
import { unzipSync } from 'fflate'
import { parseArk } from './shared/ark.js'
import { type Lockfile, LockfileSchema } from './shared/schemas.js'
const TEXT_EXTENSIONS = new Set([
'md',

View File

@ -0,0 +1,4 @@
# @clawdhub/schema
Shared runtime schemas (ArkType) for ClawdHub.

4
packages/schema/dist/ark.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
import { ArkErrors } from 'arktype';
export type ArkValidator<T> = (data: unknown) => T | ArkErrors;
export declare function parseArk<T>(schema: ArkValidator<T>, data: unknown, label: string): T;
export declare function formatArkErrors(errors: ArkErrors): string;

26
packages/schema/dist/ark.js vendored Normal file
View File

@ -0,0 +1,26 @@
import { ArkErrors } from 'arktype';
export function parseArk(schema, data, label) {
const result = schema(data);
if (result instanceof ArkErrors) {
throw new Error(`${label}: ${formatArkErrors(result)}`);
}
return result;
}
export function formatArkErrors(errors) {
const parts = [];
for (const error of errors) {
if (parts.length >= 3)
break;
const path = Array.isArray(error.path) ? error.path.join('.') : '';
const location = path ? `${path}: ` : '';
const description = typeof error.description === 'string'
? error.description
: 'invalid value';
parts.push(`${location}${description}`);
}
if (errors.count > parts.length) {
parts.push(`+${errors.count - parts.length} more`);
}
return parts.join('; ');
}
//# sourceMappingURL=ark.js.map

1
packages/schema/dist/ark.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"ark.js","sourceRoot":"","sources":["../src/ark.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAInC,MAAM,UAAU,QAAQ,CAAI,MAAuB,EAAE,IAAa,EAAE,KAAa;IAC/E,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,CAAA;IAC3B,IAAI,MAAM,YAAY,SAAS,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,KAAK,eAAe,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;IACzD,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,MAAiB;IAC/C,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC;YAAE,MAAK;QAC5B,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;QAClE,MAAM,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,CAAA;QACxC,MAAM,WAAW,GACf,OAAQ,KAAmC,CAAC,WAAW,KAAK,QAAQ;YAClE,CAAC,CAAG,KAAiC,CAAC,WAAsB;YAC5D,CAAC,CAAC,eAAe,CAAA;QACrB,KAAK,CAAC,IAAI,CAAC,GAAG,QAAQ,GAAG,WAAW,EAAE,CAAC,CAAA;IACzC,CAAC;IACD,IAAI,MAAM,CAAC,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;QAChC,KAAK,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,KAAK,GAAG,KAAK,CAAC,MAAM,OAAO,CAAC,CAAA;IACpD,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACzB,CAAC"}

3
packages/schema/dist/index.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
export type { ArkValidator } from './ark.js';
export { formatArkErrors, parseArk } from './ark.js';
export * from './schemas.js';

3
packages/schema/dist/index.js vendored Normal file
View File

@ -0,0 +1,3 @@
export { formatArkErrors, parseArk } from './ark.js';
export * from './schemas.js';
//# sourceMappingURL=index.js.map

1
packages/schema/dist/index.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAA;AACpD,cAAc,cAAc,CAAA"}

120
packages/schema/dist/schemas.d.ts vendored Normal file
View File

@ -0,0 +1,120 @@
import { type inferred } from 'arktype';
export declare const GlobalConfigSchema: import("arktype/internal/variants/object.ts").ObjectType<{
registry: string;
token: string;
}, {}>;
export type GlobalConfig = (typeof GlobalConfigSchema)[inferred];
export declare const LockfileSchema: import("arktype/internal/variants/object.ts").ObjectType<{
version: 1;
skills: {
[x: string]: {
version: string | null;
installedAt: number;
};
};
}, {}>;
export type Lockfile = (typeof LockfileSchema)[inferred];
export declare const ApiCliWhoamiResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{
user: {
handle: string | null;
};
}, {}>;
export declare const ApiSearchResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{
results: [{
score: number;
slug?: string | undefined;
displayName?: string | undefined;
version?: string | null | undefined;
}];
}, {}>;
export declare const ApiSkillMetaResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{
latestVersion?: {
version: string;
} | undefined;
skill?: unknown;
}, {}>;
export declare const ApiCliUploadUrlResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{
uploadUrl: string;
}, {}>;
export declare const ApiUploadFileResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{
storageId: string;
}, {}>;
export declare const CliPublishFileSchema: import("arktype/internal/variants/object.ts").ObjectType<{
path: string;
size: number;
storageId: string;
sha256: string;
contentType?: string | undefined;
}, {}>;
export type CliPublishFile = (typeof CliPublishFileSchema)[inferred];
export declare const CliPublishRequestSchema: import("arktype/internal/variants/object.ts").ObjectType<{
slug: string;
displayName: string;
version: string;
changelog: string;
files: {
path: string;
size: number;
storageId: string;
sha256: string;
contentType?: string | undefined;
}[];
tags?: string[] | undefined;
}, {}>;
export type CliPublishRequest = (typeof CliPublishRequestSchema)[inferred];
export declare const ApiCliPublishResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{
ok: true;
skillId: string;
versionId: string;
}, {}>;
export declare const ApiSkillResolveResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{
match: {
version: string;
} | null;
latestVersion: {
version: string;
} | null;
}, {}>;
export declare const SkillInstallSpecSchema: import("arktype/internal/variants/object.ts").ObjectType<{
kind: "brew" | "node" | "go" | "uv";
id?: string | undefined;
label?: string | undefined;
bins?: string[] | undefined;
formula?: string | undefined;
tap?: string | undefined;
package?: string | undefined;
module?: string | undefined;
}, {}>;
export type SkillInstallSpec = (typeof SkillInstallSpecSchema)[inferred];
export declare const ClawdisRequiresSchema: import("arktype/internal/variants/object.ts").ObjectType<{
bins?: string[] | undefined;
anyBins?: string[] | undefined;
env?: string[] | undefined;
config?: string[] | undefined;
}, {}>;
export type ClawdisRequires = (typeof ClawdisRequiresSchema)[inferred];
export declare const ClawdisSkillMetadataSchema: import("arktype/internal/variants/object.ts").ObjectType<{
always?: boolean | undefined;
skillKey?: string | undefined;
primaryEnv?: string | undefined;
emoji?: string | undefined;
homepage?: string | undefined;
os?: string[] | undefined;
requires?: {
bins?: string[] | undefined;
anyBins?: string[] | undefined;
env?: string[] | undefined;
config?: string[] | undefined;
} | undefined;
install?: {
kind: "brew" | "node" | "go" | "uv";
id?: string | undefined;
label?: string | undefined;
bins?: string[] | undefined;
formula?: string | undefined;
tap?: string | undefined;
package?: string | undefined;
module?: string | undefined;
}[] | undefined;
}, {}>;
export type ClawdisSkillMetadata = (typeof ClawdisSkillMetadataSchema)[inferred];

92
packages/schema/dist/schemas.js vendored Normal file
View File

@ -0,0 +1,92 @@
import { type } from 'arktype';
export const GlobalConfigSchema = type({
registry: 'string',
token: 'string',
});
export const LockfileSchema = type({
version: '1',
skills: {
'[string]': {
version: 'string|null',
installedAt: 'number',
},
},
});
export const ApiCliWhoamiResponseSchema = type({
user: {
handle: 'string|null',
},
});
export const ApiSearchResponseSchema = type({
results: [
{
slug: 'string?',
displayName: 'string?',
version: 'string|null?',
score: 'number',
},
],
});
export const ApiSkillMetaResponseSchema = type({
latestVersion: type({
version: 'string',
}).optional(),
skill: 'unknown|null?',
});
export const ApiCliUploadUrlResponseSchema = type({
uploadUrl: 'string',
});
export const ApiUploadFileResponseSchema = type({
storageId: 'string',
});
export const CliPublishFileSchema = type({
path: 'string',
size: 'number',
storageId: 'string',
sha256: 'string',
contentType: 'string?',
});
export const CliPublishRequestSchema = type({
slug: 'string',
displayName: 'string',
version: 'string',
changelog: 'string',
tags: 'string[]?',
files: CliPublishFileSchema.array(),
});
export const ApiCliPublishResponseSchema = type({
ok: 'true',
skillId: 'string',
versionId: 'string',
});
export const ApiSkillResolveResponseSchema = type({
match: type({ version: 'string' }).or('null'),
latestVersion: type({ version: 'string' }).or('null'),
});
export const SkillInstallSpecSchema = type({
id: 'string?',
kind: '"brew"|"node"|"go"|"uv"',
label: 'string?',
bins: 'string[]?',
formula: 'string?',
tap: 'string?',
package: 'string?',
module: 'string?',
});
export const ClawdisRequiresSchema = type({
bins: 'string[]?',
anyBins: 'string[]?',
env: 'string[]?',
config: 'string[]?',
});
export const ClawdisSkillMetadataSchema = type({
always: 'boolean?',
skillKey: 'string?',
primaryEnv: 'string?',
emoji: 'string?',
homepage: 'string?',
os: 'string[]?',
requires: ClawdisRequiresSchema.optional(),
install: SkillInstallSpecSchema.array().optional(),
});
//# sourceMappingURL=schemas.js.map

1
packages/schema/dist/schemas.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"schemas.js","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"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"}

View File

@ -0,0 +1,25 @@
{
"name": "@clawdhub/schema",
"version": "0.1.0",
"private": true,
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"files": [
"dist",
"README.md"
],
"scripts": {
"build": "tsc -p tsconfig.json"
},
"dependencies": {
"arktype": "^2.1.29"
},
"devDependencies": {
"typescript": "^5.9.3"
}
}

View File

@ -0,0 +1,3 @@
export type { ArkValidator } from './ark.js'
export { formatArkErrors, parseArk } from './ark.js'
export * from './schemas.js'

View File

@ -1,9 +1,10 @@
/* @vitest-environment node */
import { describe, expect, it } from 'vitest'
import { parseArk } from './ark'
import { CliPublishRequestSchema, LockfileSchema } from './schemas'
describe('shared schemas', () => {
describe('@clawdhub/schema', () => {
it('parses lockfile records', () => {
const lock = parseArk(
LockfileSchema,
@ -29,7 +30,7 @@ describe('shared schemas', () => {
expect(payload.files[0]?.path).toBe('SKILL.md')
})
it('formats parse errors with label', () => {
it('throws labeled errors', () => {
expect(() => parseArk(LockfileSchema, null, 'Lockfile')).toThrow(/Lockfile:/)
})
})

View File

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Bundler",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"declaration": true,
"sourceMap": true,
"skipLibCheck": true
},
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.test.ts"]
}

View File

@ -1,3 +1,4 @@
import type { ClawdisSkillMetadata, SkillInstallSpec } from '@clawdhub/schema'
import { createFileRoute } from '@tanstack/react-router'
import { useAction, useConvexAuth, useMutation, useQuery } from 'convex/react'
import { useEffect, useMemo, useState } from 'react'
@ -5,7 +6,6 @@ import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { api } from '../../../convex/_generated/api'
import type { Doc, Id } from '../../../convex/_generated/dataModel'
import type { ClawdisSkillMetadata, SkillInstallSpec } from '../../../convex/lib/skills'
export const Route = createFileRoute('/skills/$slug')({
component: SkillDetail,

View File

@ -1,5 +1,5 @@
{
"include": ["src", "convex", "packages/clawdhub/src"],
"include": ["src", "convex", "packages/clawdhub/src", "packages/schema/src"],
"compilerOptions": {
"target": "ES2022",
"jsx": "react-jsx",

View File

@ -5,6 +5,12 @@ export default defineConfig({
environment: 'jsdom',
globals: true,
setupFiles: ['./vitest.setup.ts'],
exclude: [
'**/node_modules/**',
'**/dist/**',
'**/coverage/**',
'**/convex/_generated/**',
],
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
@ -20,6 +26,7 @@ export default defineConfig({
'convex/lib/tokens.ts',
'convex/httpApi.ts',
'packages/clawdhub/src/**/*.ts',
'packages/schema/src/**/*.ts',
],
exclude: [
'node_modules/',
@ -29,6 +36,7 @@ export default defineConfig({
'packages/clawdhub/src/cli.ts',
'packages/clawdhub/src/config.ts',
'packages/clawdhub/src/types.ts',
'packages/schema/dist/',
],
},
},