From 05b1161f65e7293967bd88f58aca875eaeccf92f Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 10 Oct 2025 03:36:42 +0100 Subject: [PATCH] inline config in example --- packages/example/package.json | 2 +- packages/example/tspconfig.yaml | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) delete mode 100644 packages/example/tspconfig.yaml diff --git a/packages/example/package.json b/packages/example/package.json index 5302a5b..9d8d658 100644 --- a/packages/example/package.json +++ b/packages/example/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "build": "pnpm run build:lexicons && pnpm run build:codegen", - "build:lexicons": "tsp compile typelex/main.tsp", + "build:lexicons": "tsp compile typelex/main.tsp --emit @typelex/emitter --option '@typelex/emitter.emitter-output-dir={project-root}/lexicons'", "build:codegen": "lex gen-server --yes ./src lexicons/app/example/*.json" }, "dependencies": { diff --git a/packages/example/tspconfig.yaml b/packages/example/tspconfig.yaml deleted file mode 100644 index cabf8c0..0000000 --- a/packages/example/tspconfig.yaml +++ /dev/null @@ -1,5 +0,0 @@ -emit: - - "@typelex/emitter" -options: - "@typelex/emitter": - output-dir: "./lexicons" -- 2.43.0 From ab8478a15d225637839a4fafc2d11f18ffbe8509 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 10 Oct 2025 05:09:30 +0100 Subject: [PATCH] add cli --- package.json | 3 +- packages/cli/.gitignore | 3 + packages/cli/README.md | 66 +++++ packages/cli/package.json | 40 +++ packages/cli/src/cli.ts | 64 +++++ packages/cli/src/commands/compile.ts | 68 +++++ packages/cli/src/index.ts | 1 + packages/cli/src/utils/ensure-imports.ts | 38 +++ packages/cli/src/utils/externals-generator.ts | 105 ++++++++ packages/cli/src/utils/lexicon.ts | 78 ++++++ packages/cli/tsconfig.json | 20 ++ packages/cli/typelex/externals.tsp | 4 + packages/example/package.json | 6 +- packages/example/src/index.ts | 51 +++- packages/example/src/lexicons.ts | 253 +++++++----------- .../example/src/types/app/example/defs.ts | 79 ------ .../example/src/types/app/example/like.ts | 30 --- .../example/src/types/app/example/post.ts | 36 --- .../example/src/types/app/example/profile.ts | 34 --- .../example/src/types/app/example/repost.ts | 30 --- .../example/src/types/xyz/statusphere/defs.ts | 45 ++++ .../src/types/xyz/statusphere/getStatuses.ts | 36 +++ .../src/types/xyz/statusphere/getUser.ts | 36 +++ .../src/types/xyz/statusphere/sendStatus.ts | 40 +++ .../follow.ts => xyz/statusphere/status.ts} | 8 +- packages/example/typelex/externals.tsp | 235 ++++++++++++++++ packages/example/typelex/main.tsp | 168 ++++-------- pnpm-lock.yaml | 49 +++- 28 files changed, 1124 insertions(+), 502 deletions(-) create mode 100644 packages/cli/.gitignore create mode 100644 packages/cli/README.md create mode 100644 packages/cli/package.json create mode 100644 packages/cli/src/cli.ts create mode 100644 packages/cli/src/commands/compile.ts create mode 100644 packages/cli/src/index.ts create mode 100644 packages/cli/src/utils/ensure-imports.ts create mode 100644 packages/cli/src/utils/externals-generator.ts create mode 100644 packages/cli/src/utils/lexicon.ts create mode 100644 packages/cli/tsconfig.json create mode 100644 packages/cli/typelex/externals.tsp delete mode 100644 packages/example/src/types/app/example/defs.ts delete mode 100644 packages/example/src/types/app/example/like.ts delete mode 100644 packages/example/src/types/app/example/post.ts delete mode 100644 packages/example/src/types/app/example/profile.ts delete mode 100644 packages/example/src/types/app/example/repost.ts create mode 100644 packages/example/src/types/xyz/statusphere/defs.ts create mode 100644 packages/example/src/types/xyz/statusphere/getStatuses.ts create mode 100644 packages/example/src/types/xyz/statusphere/getUser.ts create mode 100644 packages/example/src/types/xyz/statusphere/sendStatus.ts rename packages/example/src/types/{app/example/follow.ts => xyz/statusphere/status.ts} (79%) create mode 100644 packages/example/typelex/externals.tsp diff --git a/package.json b/package.json index 9b2d2c6..9d9a564 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "example": "pnpm --filter @typelex/example build", "playground": "pnpm --filter @typelex/playground dev", "validate": "pnpm build && pnpm run validate-lexicons && pnpm test", - "validate-lexicons": "node scripts/validate-lexicons.js" + "validate-lexicons": "node scripts/validate-lexicons.js", + "cli": "pnpm --filter @typelex/cli" }, "repository": { "type": "git", diff --git a/packages/cli/.gitignore b/packages/cli/.gitignore new file mode 100644 index 0000000..6b3405c --- /dev/null +++ b/packages/cli/.gitignore @@ -0,0 +1,3 @@ +dist +node_modules +*.log diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 0000000..15fd687 --- /dev/null +++ b/packages/cli/README.md @@ -0,0 +1,66 @@ +# @typelex/cli + +Experimental CLI for typelex + +## Installation + +```bash +pnpm add -D @typelex/cli @typelex/emitter +``` + +## Usage + +```bash +typelex compile xyz.statusphere.* +``` + +This command: +1. Scans `lexicons/` for all external lexicons (not matching `xyz.statusphere`) +2. Generates `typelex/externals.tsp` with `@external` stubs +3. Compiles `typelex/main.tsp` to `lexicons/` (or custom output via `--out`) + +Fixed paths: +- Entry point: `typelex/main.tsp` +- Externals: `typelex/externals.tsp` + +## Example + +```typescript +// typelex/main.tsp +import "@typelex/emitter"; +import "./externals.tsp"; + +namespace xyz.statusphere.defs { + model StatusView { + @required uri: atUri; + @required status: string; + @required profile: app.bsky.actor.defs.ProfileView; + } +} +``` + +```bash +typelex compile 'xyz.statusphere.*' +``` + +The CLI scans `lexicons/` for external types and auto-generates `typelex/externals.tsp` with stubs + +### Integration + +```json +{ + "scripts": { + "build:lexicons": "typelex compile 'xyz.statusphere.*'", + "build:codegen": "lex gen-server --yes ./src lexicons/xyz/statusphere/*.json" + } +} +``` + +## Options + +- `--out ` - Output directory for generated Lexicon files (default: `./lexicons`) +- `--watch` - Watch mode for continuous compilation + +## License + +MIT diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 0000000..64897e6 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,40 @@ +{ + "name": "@typelex/cli", + "version": "0.2.0", + "description": "CLI for typelex - TypeSpec-based IDL for ATProto Lexicons", + "main": "dist/index.js", + "type": "module", + "bin": { + "typelex": "dist/cli.js" + }, + "files": [ + "dist", + "src" + ], + "scripts": { + "build": "tsc", + "clean": "rm -rf dist", + "watch": "tsc --watch", + "prepublishOnly": "npm run build" + }, + "keywords": [ + "typespec", + "atproto", + "lexicon", + "cli" + ], + "author": "Dan Abramov ", + "license": "MIT", + "dependencies": { + "@typespec/compiler": "^1.4.0", + "yargs": "^18.0.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/yargs": "^17.0.33", + "typescript": "^5.0.0" + }, + "peerDependencies": { + "@typelex/emitter": "^0.2.0" + } +} diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts new file mode 100644 index 0000000..807c682 --- /dev/null +++ b/packages/cli/src/cli.ts @@ -0,0 +1,64 @@ +#!/usr/bin/env node +import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; +import { compileCommand } from "./commands/compile.js"; + +async function main() { + await yargs(hideBin(process.argv)) + .scriptName("typelex") + .usage("$0 compile ") + .command( + "compile ", + "Compile TypeSpec files to Lexicon JSON", + (yargs) => { + return yargs + .positional("namespace", { + describe: "Primary namespace pattern (e.g., app.bsky.*)", + type: "string", + demandOption: true, + }) + .option("out", { + describe: "Output directory for generated Lexicon files (relative to cwd)", + type: "string", + default: "./lexicons", + }); + }, + async (argv) => { + const options: Record = {}; + if (argv.watch) { + options.watch = true; + } + if (argv.out) { + options.out = argv.out; + } + await compileCommand(argv.namespace, options); + } + ) + .option("watch", { + describe: "Watch mode", + type: "boolean", + default: false, + }) + .demandCommand(1, "You must specify a command") + .help() + .version() + .fail((msg, err) => { + if (err) { + console.error(err); + } else { + console.error(msg); + } + process.exit(1); + }).argv; +} + +process.on("unhandledRejection", (error: unknown) => { + console.error("Unhandled promise rejection!"); + console.error(error); + process.exit(1); +}); + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/packages/cli/src/commands/compile.ts b/packages/cli/src/commands/compile.ts new file mode 100644 index 0000000..0613541 --- /dev/null +++ b/packages/cli/src/commands/compile.ts @@ -0,0 +1,68 @@ +import { resolve } from "path"; +import { spawn } from "child_process"; +import { generateExternalsFile } from "../utils/externals-generator.js"; +import { ensureMainImports } from "../utils/ensure-imports.js"; + +/** + * Compile TypeSpec files to Lexicon JSON + * + * @param namespace - Primary namespace pattern (e.g., "app.bsky.*") + * @param options - Additional compiler options + */ +export async function compileCommand( + namespace: string, + options: Record = {} +): Promise { + const cwd = process.cwd(); + const outDir = (options.out as string) || "./lexicons"; + + // Validate that output directory ends with 'lexicons' + const normalizedPath = outDir.replace(/\\/g, '/').replace(/\/+$/, ''); + if (!normalizedPath.endsWith('/lexicons') && normalizedPath !== 'lexicons' && normalizedPath !== './lexicons') { + console.error(`Error: Output directory must end with 'lexicons'`); + console.error(`Got: ${outDir}`); + console.error(`Valid examples: ./lexicons, ../../lexicons, /path/to/lexicons`); + process.exit(1); + } + + // Generate externals first (scans the output directory for external lexicons) + await generateExternalsFile(namespace, cwd, outDir); + + // Ensure required imports are present in main.tsp + await ensureMainImports(cwd); + + // Compile TypeSpec using the TypeSpec CLI + const entrypoint = resolve(cwd, "typelex/main.tsp"); + const args = [ + "compile", + entrypoint, + "--emit", + "@typelex/emitter", + "--option", + `@typelex/emitter.emitter-output-dir={project-root}/${outDir}`, + ]; + + if (options.watch) { + args.push("--watch"); + } + + return new Promise((resolve, reject) => { + const tsp = spawn("tsp", args, { + cwd, + stdio: "inherit", + }); + + tsp.on("close", (code) => { + if (code === 0) { + resolve(); + } else { + process.exit(code ?? 1); + } + }); + + tsp.on("error", (err) => { + console.error("Failed to start TypeSpec compiler:", err); + reject(err); + }); + }); +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts new file mode 100644 index 0000000..1874d49 --- /dev/null +++ b/packages/cli/src/index.ts @@ -0,0 +1 @@ +export { compileCommand } from "./commands/compile.js"; diff --git a/packages/cli/src/utils/ensure-imports.ts b/packages/cli/src/utils/ensure-imports.ts new file mode 100644 index 0000000..f1bc10b --- /dev/null +++ b/packages/cli/src/utils/ensure-imports.ts @@ -0,0 +1,38 @@ +import { readFile } from "fs/promises"; +import { resolve } from "path"; + +const REQUIRED_FIRST_LINE = 'import "@typelex/emitter";'; +const REQUIRED_SECOND_LINE = 'import "./externals.tsp";'; + +/** + * Validates that main.tsp starts with the required imports. + * Fails the build if the first two lines are not exactly as expected. + * + * @param cwd - Current working directory + */ +export async function ensureMainImports(cwd: string): Promise { + const mainPath = resolve(cwd, "typelex/main.tsp"); + + try { + const content = await readFile(mainPath, "utf-8"); + const lines = content.split("\n"); + + if (lines[0]?.trim() !== REQUIRED_FIRST_LINE) { + console.error(`Error: main.tsp must start with: ${REQUIRED_FIRST_LINE}`); + console.error(`Found: ${lines[0] || "(empty line)"}`); + process.exit(1); + } + + if (lines[1]?.trim() !== REQUIRED_SECOND_LINE) { + console.error(`Error: Line 2 of main.tsp must be: ${REQUIRED_SECOND_LINE}`); + console.error(`Found: ${lines[1] || "(empty line)"}`); + process.exit(1); + } + } catch (err) { + if ((err as NodeJS.ErrnoException).code === "ENOENT") { + console.error("Error: typelex/main.tsp not found"); + process.exit(1); + } + throw err; + } +} diff --git a/packages/cli/src/utils/externals-generator.ts b/packages/cli/src/utils/externals-generator.ts new file mode 100644 index 0000000..48f3262 --- /dev/null +++ b/packages/cli/src/utils/externals-generator.ts @@ -0,0 +1,105 @@ +import { resolve } from "path"; +import { writeFile, mkdir } from "fs/promises"; +import { findExternalLexicons, LexiconDoc, isTokenDef, isModelDef } from "./lexicon.js"; + +/** + * Convert camelCase to PascalCase + */ +function toPascalCase(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +/** + * Extract namespace prefix from pattern (e.g., "app.bsky.*" -> "app.bsky") + */ +function getNamespacePrefix(pattern: string): string { + if (!pattern.endsWith(".*")) { + throw new Error(`Namespace pattern must end with .*: ${pattern}`); + } + return pattern.slice(0, -2); +} + +/** + * Generate TypeSpec external definitions from lexicon documents + */ +function generateExternalsCode(lexicons: Map): string { + const lines: string[] = []; + + lines.push('import "@typelex/emitter";'); + lines.push(""); + lines.push("// Generated by typelex"); + lines.push("// This file is auto-generated. Do not edit manually."); + lines.push(""); + + // Sort namespaces for consistent output + const sortedNamespaces = Array.from(lexicons.entries()).sort(([a], [b]) => + a.localeCompare(b) + ); + + for (const [nsid, lexicon] of sortedNamespaces) { + lines.push("@external"); + // Escape reserved keywords in namespace (like 'record') + const escapedNsid = nsid.replace(/\b(record|union|enum|interface|namespace|model|op|import|using|extends|is|scalar|alias|if|else|return|void|never|unknown|any|true|false|null)\b/g, '`$1`'); + lines.push(`namespace ${escapedNsid} {`); + + // Sort definitions for consistent output + const sortedDefs = Object.entries(lexicon.defs).sort(([a], [b]) => + a.localeCompare(b) + ); + + for (const [defName, def] of sortedDefs) { + if (!isModelDef(def)) { + continue; + } + + const modelName = toPascalCase(defName); + const isToken = isTokenDef(def); + + if (isToken) { + lines.push(` @token model ${modelName} { }`); + } else { + lines.push(` model ${modelName} { }`); + } + } + + lines.push("}"); + lines.push(""); + } + + return lines.join("\n"); +} + +/** + * Generate externals.tsp file for the given namespace pattern + */ +export async function generateExternalsFile( + namespacePattern: string, + cwd: string, + outDir: string = "./lexicons" +): Promise { + try { + const prefix = getNamespacePrefix(namespacePattern); + const lexiconsDir = resolve(cwd, outDir); + const outputFile = resolve(cwd, "typelex/externals.tsp"); + + const externals = await findExternalLexicons(lexiconsDir, prefix); + + if (externals.size === 0) { + // No externals, create empty file + await mkdir(resolve(cwd, "typelex"), { recursive: true }); + await writeFile( + outputFile, + 'import "@typelex/emitter";\n\n// Generated by typelex\n// No external lexicons found\n', + "utf-8" + ); + return; + } + + const code = generateExternalsCode(externals); + await mkdir(resolve(cwd, "typelex"), { recursive: true }); + await writeFile(outputFile, code, "utf-8"); + } catch (error) { + // Re-throw with better context + throw new Error(`Failed to generate externals: ${error instanceof Error ? error.message : String(error)}`); + } +} diff --git a/packages/cli/src/utils/lexicon.ts b/packages/cli/src/utils/lexicon.ts new file mode 100644 index 0000000..4ea3416 --- /dev/null +++ b/packages/cli/src/utils/lexicon.ts @@ -0,0 +1,78 @@ +import { readFile } from "fs/promises"; +import { resolve } from "path"; +import { globby } from "globby"; + +export interface LexiconDef { + type: string; + [key: string]: unknown; +} + +export interface LexiconDoc { + lexicon: number; + id: string; + defs: Record; +} + +/** + * Read and parse a lexicon JSON file + */ +export async function readLexicon(path: string): Promise { + const content = await readFile(path, "utf-8"); + return JSON.parse(content); +} + +/** + * Find all lexicon files in a directory + */ +export async function findLexicons(dir: string): Promise { + try { + const pattern = resolve(dir, "**/*.json"); + return await globby(pattern); + } catch { + // If directory doesn't exist, return empty array + return []; + } +} + +/** + * Extract external lexicons that don't match the given namespace + */ +export async function findExternalLexicons( + lexiconsDir: string, + primaryNamespace: string +): Promise> { + const files = await findLexicons(lexiconsDir); + const externals = new Map(); + + for (const file of files) { + const lexicon = await readLexicon(file); + if (!lexicon.id.startsWith(primaryNamespace)) { + externals.set(lexicon.id, lexicon); + } + } + + return externals; +} + +/** + * Check if a definition is a token type + */ +export function isTokenDef(def: LexiconDef): boolean { + return def.type === "token"; +} + +/** + * Check if a definition should become a model in TypeSpec + */ +export function isModelDef(def: LexiconDef): boolean { + const type = def.type; + return ( + type === "object" || + type === "token" || + type === "record" || + type === "union" || + type === "string" || + type === "bytes" || + type === "cid-link" + ); +} diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json new file mode 100644 index 0000000..f787ae4 --- /dev/null +++ b/packages/cli/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "lib": ["ES2022"], + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/cli/typelex/externals.tsp b/packages/cli/typelex/externals.tsp new file mode 100644 index 0000000..aaf0d7f --- /dev/null +++ b/packages/cli/typelex/externals.tsp @@ -0,0 +1,4 @@ +import "@typelex/emitter"; + +// Generated by typelex +// No external lexicons found diff --git a/packages/example/package.json b/packages/example/package.json index 9d8d658..b52ad5f 100644 --- a/packages/example/package.json +++ b/packages/example/package.json @@ -5,13 +5,13 @@ "type": "module", "scripts": { "build": "pnpm run build:lexicons && pnpm run build:codegen", - "build:lexicons": "tsp compile typelex/main.tsp --emit @typelex/emitter --option '@typelex/emitter.emitter-output-dir={project-root}/lexicons'", - "build:codegen": "lex gen-server --yes ./src lexicons/app/example/*.json" + "build:lexicons": "typelex compile xyz.statusphere.*", + "build:codegen": "lex gen-server --yes ./src lexicons/xyz/statusphere/*.json" }, "dependencies": { "@atproto/lex-cli": "^0.9.5", "@atproto/xrpc-server": "^0.9.5", - "@typespec/compiler": "^1.4.0", + "@typelex/cli": "workspace:*", "@typelex/emitter": "workspace:*" }, "devDependencies": { diff --git a/packages/example/src/index.ts b/packages/example/src/index.ts index 9f90542..007cd47 100644 --- a/packages/example/src/index.ts +++ b/packages/example/src/index.ts @@ -10,6 +10,9 @@ import { createServer as createXrpcServer, } from '@atproto/xrpc-server' import { schemas } from './lexicons.js' +import * as XyzStatusphereGetStatuses from './types/xyz/statusphere/getStatuses.js' +import * as XyzStatusphereGetUser from './types/xyz/statusphere/getUser.js' +import * as XyzStatusphereSendStatus from './types/xyz/statusphere/sendStatus.js' export function createServer(options?: XrpcOptions): Server { return new Server(options) @@ -17,28 +20,64 @@ export function createServer(options?: XrpcOptions): Server { export class Server { xrpc: XrpcServer - app: AppNS + xyz: XyzNS constructor(options?: XrpcOptions) { this.xrpc = createXrpcServer(schemas, options) - this.app = new AppNS(this) + this.xyz = new XyzNS(this) } } -export class AppNS { +export class XyzNS { _server: Server - example: AppExampleNS + statusphere: XyzStatusphereNS constructor(server: Server) { this._server = server - this.example = new AppExampleNS(server) + this.statusphere = new XyzStatusphereNS(server) } } -export class AppExampleNS { +export class XyzStatusphereNS { _server: Server constructor(server: Server) { this._server = server } + + getStatuses( + cfg: MethodConfigOrHandler< + A, + XyzStatusphereGetStatuses.QueryParams, + XyzStatusphereGetStatuses.HandlerInput, + XyzStatusphereGetStatuses.HandlerOutput + >, + ) { + const nsid = 'xyz.statusphere.getStatuses' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + getUser( + cfg: MethodConfigOrHandler< + A, + XyzStatusphereGetUser.QueryParams, + XyzStatusphereGetUser.HandlerInput, + XyzStatusphereGetUser.HandlerOutput + >, + ) { + const nsid = 'xyz.statusphere.getUser' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } + + sendStatus( + cfg: MethodConfigOrHandler< + A, + XyzStatusphereSendStatus.QueryParams, + XyzStatusphereSendStatus.HandlerInput, + XyzStatusphereSendStatus.HandlerOutput + >, + ) { + const nsid = 'xyz.statusphere.sendStatus' // @ts-ignore + return this._server.xrpc.method(nsid, cfg) + } } diff --git a/packages/example/src/lexicons.ts b/packages/example/src/lexicons.ts index 8d76522..2223b49 100644 --- a/packages/example/src/lexicons.ts +++ b/packages/example/src/lexicons.ts @@ -10,206 +10,155 @@ import { import { type $Typed, is$typed, maybe$typed } from './util.js' export const schemaDict = { - AppExampleDefs: { + XyzStatusphereDefs: { lexicon: 1, - id: 'app.example.defs', + id: 'xyz.statusphere.defs', defs: { - postRef: { + statusView: { type: 'object', properties: { uri: { type: 'string', - description: 'AT URI of the post', + format: 'at-uri', }, - cid: { + status: { type: 'string', - description: 'CID of the post', + maxLength: 32, + minLength: 1, + maxGraphemes: 1, }, - }, - description: 'Reference to a post', - required: ['uri', 'cid'], - }, - replyRef: { - type: 'object', - properties: { - root: { - type: 'ref', - ref: 'lex:app.example.defs#postRef', - description: 'Root post in the thread', + createdAt: { + type: 'string', + format: 'datetime', }, - parent: { + profile: { type: 'ref', - ref: 'lex:app.example.defs#postRef', - description: 'Direct parent post being replied to', + ref: 'lex:xyz.statusphere.defs#profileView', }, }, - description: 'Reference to a parent post in a reply chain', - required: ['root', 'parent'], + required: ['uri', 'status', 'createdAt', 'profile'], }, - entity: { + profileView: { type: 'object', properties: { - start: { - type: 'integer', - description: 'Start index in text', - }, - end: { - type: 'integer', - description: 'End index in text', - }, - type: { + did: { type: 'string', - description: 'Entity type', + format: 'did', }, - value: { + handle: { type: 'string', - description: 'Entity value (handle, URL, or tag)', + format: 'handle', }, }, - description: 'Text entity (mention, link, or tag)', - required: ['start', 'end', 'type', 'value'], - }, - notificationType: { - type: 'string', - knownValues: ['like', 'repost', 'follow', 'mention', 'reply'], - description: 'Type of notification', + required: ['did', 'handle'], }, }, }, - AppExampleFollow: { + XyzStatusphereGetStatuses: { lexicon: 1, - id: 'app.example.follow', + id: 'xyz.statusphere.getStatuses', defs: { main: { - type: 'record', - key: 'tid', - record: { - type: 'object', + type: 'query', + description: 'Get a list of the most recent statuses on the network.', + parameters: { + type: 'params', properties: { - subject: { - type: 'string', - description: 'DID of the account being followed', - }, - createdAt: { - type: 'string', - format: 'datetime', - description: 'When the follow was created', + limit: { + type: 'integer', + minimum: 1, + maximum: 100, + default: 50, }, }, - required: ['subject', 'createdAt'], }, - description: 'A follow relationship', - }, - }, - }, - AppExampleLike: { - lexicon: 1, - id: 'app.example.like', - defs: { - main: { - type: 'record', - key: 'tid', - record: { - type: 'object', - properties: { - subject: { - type: 'ref', - ref: 'lex:app.example.defs#postRef', - description: 'Post being liked', - }, - createdAt: { - type: 'string', - format: 'datetime', - description: 'When the like was created', + output: { + encoding: 'application/json', + schema: { + type: 'object', + properties: { + statuses: { + type: 'array', + items: { + type: 'ref', + ref: 'lex:xyz.statusphere.defs#statusView', + }, + }, }, + required: ['statuses'], }, - required: ['subject', 'createdAt'], }, - description: 'A like on a post', }, }, }, - AppExamplePost: { + XyzStatusphereGetUser: { lexicon: 1, - id: 'app.example.post', + id: 'xyz.statusphere.getUser', defs: { main: { - type: 'record', - key: 'tid', - record: { - type: 'object', - properties: { - text: { - type: 'string', - description: 'Post text content', - }, - createdAt: { - type: 'string', - format: 'datetime', - description: 'Creation timestamp', - }, - langs: { - type: 'array', - items: { - type: 'string', + type: 'query', + description: "Get the current user's profile and status.", + output: { + encoding: 'application/json', + schema: { + type: 'object', + properties: { + profile: { + type: 'ref', + ref: 'lex:app.bsky.actor.defs#profileView', }, - description: 'Languages the post is written in', - }, - entities: { - type: 'array', - items: { + status: { type: 'ref', - ref: 'lex:app.example.defs#entity', + ref: 'lex:xyz.statusphere.defs#statusView', }, - description: 'Referenced entities in the post', - }, - reply: { - type: 'ref', - ref: 'lex:app.example.defs#replyRef', - description: 'Post the user is replying to', }, + required: ['profile'], }, - required: ['text', 'createdAt'], }, - description: 'A post in the feed', }, }, }, - AppExampleProfile: { + XyzStatusphereSendStatus: { lexicon: 1, - id: 'app.example.profile', + id: 'xyz.statusphere.sendStatus', defs: { main: { - type: 'record', - key: 'self', - record: { - type: 'object', - properties: { - displayName: { - type: 'string', - description: 'Display name', - }, - description: { - type: 'string', - description: 'Profile description', - }, - avatar: { - type: 'string', - description: 'Profile avatar image', + type: 'procedure', + description: 'Send a status into the ATmosphere.', + input: { + encoding: 'application/json', + schema: { + type: 'object', + properties: { + status: { + type: 'string', + maxLength: 32, + minLength: 1, + maxGraphemes: 1, + }, }, - banner: { - type: 'string', - description: 'Profile banner image', + required: ['status'], + }, + }, + output: { + encoding: 'application/json', + schema: { + type: 'object', + properties: { + status: { + type: 'ref', + ref: 'lex:xyz.statusphere.defs#statusView', + }, }, + required: ['status'], }, }, - description: 'User profile information', }, }, }, - AppExampleRepost: { + XyzStatusphereStatus: { lexicon: 1, - id: 'app.example.repost', + id: 'xyz.statusphere.status', defs: { main: { type: 'record', @@ -217,20 +166,19 @@ export const schemaDict = { record: { type: 'object', properties: { - subject: { - type: 'ref', - ref: 'lex:app.example.defs#postRef', - description: 'Post being reposted', + status: { + type: 'string', + maxLength: 32, + minLength: 1, + maxGraphemes: 1, }, createdAt: { type: 'string', format: 'datetime', - description: 'When the repost was created', }, }, - required: ['subject', 'createdAt'], + required: ['status', 'createdAt'], }, - description: 'A repost of another post', }, }, }, @@ -267,10 +215,9 @@ export function validate( } export const ids = { - AppExampleDefs: 'app.example.defs', - AppExampleFollow: 'app.example.follow', - AppExampleLike: 'app.example.like', - AppExamplePost: 'app.example.post', - AppExampleProfile: 'app.example.profile', - AppExampleRepost: 'app.example.repost', + XyzStatusphereDefs: 'xyz.statusphere.defs', + XyzStatusphereGetStatuses: 'xyz.statusphere.getStatuses', + XyzStatusphereGetUser: 'xyz.statusphere.getUser', + XyzStatusphereSendStatus: 'xyz.statusphere.sendStatus', + XyzStatusphereStatus: 'xyz.statusphere.status', } as const diff --git a/packages/example/src/types/app/example/defs.ts b/packages/example/src/types/app/example/defs.ts deleted file mode 100644 index 8156acc..0000000 --- a/packages/example/src/types/app/example/defs.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * GENERATED CODE - DO NOT MODIFY - */ -import { type ValidationResult, BlobRef } from '@atproto/lexicon' -import { CID } from 'multiformats/cid' -import { validate as _validate } from '../../../lexicons' -import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' - -const is$typed = _is$typed, - validate = _validate -const id = 'app.example.defs' - -/** Reference to a post */ -export interface PostRef { - $type?: 'app.example.defs#postRef' - /** AT URI of the post */ - uri: string - /** CID of the post */ - cid: string -} - -const hashPostRef = 'postRef' - -export function isPostRef(v: V) { - return is$typed(v, id, hashPostRef) -} - -export function validatePostRef(v: V) { - return validate(v, id, hashPostRef) -} - -/** Reference to a parent post in a reply chain */ -export interface ReplyRef { - $type?: 'app.example.defs#replyRef' - root: PostRef - parent: PostRef -} - -const hashReplyRef = 'replyRef' - -export function isReplyRef(v: V) { - return is$typed(v, id, hashReplyRef) -} - -export function validateReplyRef(v: V) { - return validate(v, id, hashReplyRef) -} - -/** Text entity (mention, link, or tag) */ -export interface Entity { - $type?: 'app.example.defs#entity' - /** Start index in text */ - start: number - /** End index in text */ - end: number - /** Entity type */ - type: string - /** Entity value (handle, URL, or tag) */ - value: string -} - -const hashEntity = 'entity' - -export function isEntity(v: V) { - return is$typed(v, id, hashEntity) -} - -export function validateEntity(v: V) { - return validate(v, id, hashEntity) -} - -/** Type of notification */ -export type NotificationType = - | 'like' - | 'repost' - | 'follow' - | 'mention' - | 'reply' - | (string & {}) diff --git a/packages/example/src/types/app/example/like.ts b/packages/example/src/types/app/example/like.ts deleted file mode 100644 index 367e49a..0000000 --- a/packages/example/src/types/app/example/like.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * GENERATED CODE - DO NOT MODIFY - */ -import { type ValidationResult, BlobRef } from '@atproto/lexicon' -import { CID } from 'multiformats/cid' -import { validate as _validate } from '../../../lexicons' -import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' -import type * as AppExampleDefs from './defs.js' - -const is$typed = _is$typed, - validate = _validate -const id = 'app.example.like' - -export interface Record { - $type: 'app.example.like' - subject: AppExampleDefs.PostRef - /** When the like was created */ - createdAt: string - [k: string]: unknown -} - -const hashRecord = 'main' - -export function isRecord(v: V) { - return is$typed(v, id, hashRecord) -} - -export function validateRecord(v: V) { - return validate(v, id, hashRecord, true) -} diff --git a/packages/example/src/types/app/example/post.ts b/packages/example/src/types/app/example/post.ts deleted file mode 100644 index c5390ed..0000000 --- a/packages/example/src/types/app/example/post.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * GENERATED CODE - DO NOT MODIFY - */ -import { type ValidationResult, BlobRef } from '@atproto/lexicon' -import { CID } from 'multiformats/cid' -import { validate as _validate } from '../../../lexicons' -import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' -import type * as AppExampleDefs from './defs.js' - -const is$typed = _is$typed, - validate = _validate -const id = 'app.example.post' - -export interface Record { - $type: 'app.example.post' - /** Post text content */ - text: string - /** Creation timestamp */ - createdAt: string - /** Languages the post is written in */ - langs?: string[] - /** Referenced entities in the post */ - entities?: AppExampleDefs.Entity[] - reply?: AppExampleDefs.ReplyRef - [k: string]: unknown -} - -const hashRecord = 'main' - -export function isRecord(v: V) { - return is$typed(v, id, hashRecord) -} - -export function validateRecord(v: V) { - return validate(v, id, hashRecord, true) -} diff --git a/packages/example/src/types/app/example/profile.ts b/packages/example/src/types/app/example/profile.ts deleted file mode 100644 index 286dd2e..0000000 --- a/packages/example/src/types/app/example/profile.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * GENERATED CODE - DO NOT MODIFY - */ -import { type ValidationResult, BlobRef } from '@atproto/lexicon' -import { CID } from 'multiformats/cid' -import { validate as _validate } from '../../../lexicons' -import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' - -const is$typed = _is$typed, - validate = _validate -const id = 'app.example.profile' - -export interface Record { - $type: 'app.example.profile' - /** Display name */ - displayName?: string - /** Profile description */ - description?: string - /** Profile avatar image */ - avatar?: string - /** Profile banner image */ - banner?: string - [k: string]: unknown -} - -const hashRecord = 'main' - -export function isRecord(v: V) { - return is$typed(v, id, hashRecord) -} - -export function validateRecord(v: V) { - return validate(v, id, hashRecord, true) -} diff --git a/packages/example/src/types/app/example/repost.ts b/packages/example/src/types/app/example/repost.ts deleted file mode 100644 index d503d59..0000000 --- a/packages/example/src/types/app/example/repost.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * GENERATED CODE - DO NOT MODIFY - */ -import { type ValidationResult, BlobRef } from '@atproto/lexicon' -import { CID } from 'multiformats/cid' -import { validate as _validate } from '../../../lexicons' -import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' -import type * as AppExampleDefs from './defs.js' - -const is$typed = _is$typed, - validate = _validate -const id = 'app.example.repost' - -export interface Record { - $type: 'app.example.repost' - subject: AppExampleDefs.PostRef - /** When the repost was created */ - createdAt: string - [k: string]: unknown -} - -const hashRecord = 'main' - -export function isRecord(v: V) { - return is$typed(v, id, hashRecord) -} - -export function validateRecord(v: V) { - return validate(v, id, hashRecord, true) -} diff --git a/packages/example/src/types/xyz/statusphere/defs.ts b/packages/example/src/types/xyz/statusphere/defs.ts new file mode 100644 index 0000000..f616662 --- /dev/null +++ b/packages/example/src/types/xyz/statusphere/defs.ts @@ -0,0 +1,45 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { type ValidationResult, BlobRef } from '@atproto/lexicon' +import { CID } from 'multiformats/cid' +import { validate as _validate } from '../../../lexicons' +import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' + +const is$typed = _is$typed, + validate = _validate +const id = 'xyz.statusphere.defs' + +export interface StatusView { + $type?: 'xyz.statusphere.defs#statusView' + uri: string + status: string + createdAt: string + profile: ProfileView +} + +const hashStatusView = 'statusView' + +export function isStatusView(v: V) { + return is$typed(v, id, hashStatusView) +} + +export function validateStatusView(v: V) { + return validate(v, id, hashStatusView) +} + +export interface ProfileView { + $type?: 'xyz.statusphere.defs#profileView' + did: string + handle: string +} + +const hashProfileView = 'profileView' + +export function isProfileView(v: V) { + return is$typed(v, id, hashProfileView) +} + +export function validateProfileView(v: V) { + return validate(v, id, hashProfileView) +} diff --git a/packages/example/src/types/xyz/statusphere/getStatuses.ts b/packages/example/src/types/xyz/statusphere/getStatuses.ts new file mode 100644 index 0000000..e809298 --- /dev/null +++ b/packages/example/src/types/xyz/statusphere/getStatuses.ts @@ -0,0 +1,36 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { type ValidationResult, BlobRef } from '@atproto/lexicon' +import { CID } from 'multiformats/cid' +import { validate as _validate } from '../../../lexicons' +import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' +import type * as XyzStatusphereDefs from './defs.js' + +const is$typed = _is$typed, + validate = _validate +const id = 'xyz.statusphere.getStatuses' + +export type QueryParams = { + limit: number +} +export type InputSchema = undefined + +export interface OutputSchema { + statuses: XyzStatusphereDefs.StatusView[] +} + +export type HandlerInput = void + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | HandlerSuccess diff --git a/packages/example/src/types/xyz/statusphere/getUser.ts b/packages/example/src/types/xyz/statusphere/getUser.ts new file mode 100644 index 0000000..98dd84c --- /dev/null +++ b/packages/example/src/types/xyz/statusphere/getUser.ts @@ -0,0 +1,36 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { type ValidationResult, BlobRef } from '@atproto/lexicon' +import { CID } from 'multiformats/cid' +import { validate as _validate } from '../../../lexicons' +import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' +import type * as AppBskyActorDefs from '../../app/bsky/actor/defs.js' +import type * as XyzStatusphereDefs from './defs.js' + +const is$typed = _is$typed, + validate = _validate +const id = 'xyz.statusphere.getUser' + +export type QueryParams = {} +export type InputSchema = undefined + +export interface OutputSchema { + profile: AppBskyActorDefs.ProfileView + status?: XyzStatusphereDefs.StatusView +} + +export type HandlerInput = void + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | HandlerSuccess diff --git a/packages/example/src/types/xyz/statusphere/sendStatus.ts b/packages/example/src/types/xyz/statusphere/sendStatus.ts new file mode 100644 index 0000000..1af361d --- /dev/null +++ b/packages/example/src/types/xyz/statusphere/sendStatus.ts @@ -0,0 +1,40 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { type ValidationResult, BlobRef } from '@atproto/lexicon' +import { CID } from 'multiformats/cid' +import { validate as _validate } from '../../../lexicons' +import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' +import type * as XyzStatusphereDefs from './defs.js' + +const is$typed = _is$typed, + validate = _validate +const id = 'xyz.statusphere.sendStatus' + +export type QueryParams = {} + +export interface InputSchema { + status: string +} + +export interface OutputSchema { + status: XyzStatusphereDefs.StatusView +} + +export interface HandlerInput { + encoding: 'application/json' + body: InputSchema +} + +export interface HandlerSuccess { + encoding: 'application/json' + body: OutputSchema + headers?: { [key: string]: string } +} + +export interface HandlerError { + status: number + message?: string +} + +export type HandlerOutput = HandlerError | HandlerSuccess diff --git a/packages/example/src/types/app/example/follow.ts b/packages/example/src/types/xyz/statusphere/status.ts similarity index 79% rename from packages/example/src/types/app/example/follow.ts rename to packages/example/src/types/xyz/statusphere/status.ts index 9059f8b..4199b32 100644 --- a/packages/example/src/types/app/example/follow.ts +++ b/packages/example/src/types/xyz/statusphere/status.ts @@ -8,13 +8,11 @@ import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' const is$typed = _is$typed, validate = _validate -const id = 'app.example.follow' +const id = 'xyz.statusphere.status' export interface Record { - $type: 'app.example.follow' - /** DID of the account being followed */ - subject: string - /** When the follow was created */ + $type: 'xyz.statusphere.status' + status: string createdAt: string [k: string]: unknown } diff --git a/packages/example/typelex/externals.tsp b/packages/example/typelex/externals.tsp new file mode 100644 index 0000000..1ddeac3 --- /dev/null +++ b/packages/example/typelex/externals.tsp @@ -0,0 +1,235 @@ +import "@typelex/emitter"; + +// Generated by typelex +// This file is auto-generated. Do not edit manually. + +@external +namespace app.bsky.actor.defs { + model AdultContentPref { } + model BskyAppProgressGuide { } + model BskyAppStatePref { } + model ContentLabelPref { } + model FeedViewPref { } + model HiddenPostsPref { } + model InterestsPref { } + model KnownFollowers { } + model LabelerPrefItem { } + model LabelersPref { } + model MutedWord { } + model MutedWordsPref { } + model MutedWordTarget { } + model Nux { } + model PersonalDetailsPref { } + model PostInteractionSettingsPref { } + model ProfileAssociated { } + model ProfileAssociatedChat { } + model ProfileView { } + model ProfileViewBasic { } + model ProfileViewDetailed { } + model SavedFeed { } + model SavedFeedsPref { } + model SavedFeedsPrefV2 { } + model ThreadViewPref { } + model ViewerState { } +} + +@external +namespace app.bsky.actor.profile { + model Main { } +} + +@external +namespace app.bsky.embed.defs { + model AspectRatio { } +} + +@external +namespace app.bsky.embed.external { + model External { } + model Main { } + model View { } + model ViewExternal { } +} + +@external +namespace app.bsky.embed.images { + model Image { } + model Main { } + model View { } + model ViewImage { } +} + +@external +namespace app.bsky.embed.`record` { + model Main { } + model View { } + model ViewBlocked { } + model ViewDetached { } + model ViewNotFound { } + model ViewRecord { } +} + +@external +namespace app.bsky.embed.recordWithMedia { + model Main { } + model View { } +} + +@external +namespace app.bsky.embed.video { + model Caption { } + model Main { } + model View { } +} + +@external +namespace app.bsky.feed.defs { + model BlockedAuthor { } + model BlockedPost { } + @token model ClickthroughAuthor { } + @token model ClickthroughEmbed { } + @token model ClickthroughItem { } + @token model ClickthroughReposter { } + @token model ContentModeUnspecified { } + @token model ContentModeVideo { } + model FeedViewPost { } + model GeneratorView { } + model GeneratorViewerState { } + model Interaction { } + @token model InteractionLike { } + @token model InteractionQuote { } + @token model InteractionReply { } + @token model InteractionRepost { } + @token model InteractionSeen { } + @token model InteractionShare { } + model NotFoundPost { } + model PostView { } + model ReasonPin { } + model ReasonRepost { } + model ReplyRef { } + @token model RequestLess { } + @token model RequestMore { } + model SkeletonFeedPost { } + model SkeletonReasonPin { } + model SkeletonReasonRepost { } + model ThreadContext { } + model ThreadgateView { } + model ThreadViewPost { } + model ViewerState { } +} + +@external +namespace app.bsky.feed.postgate { + model DisableRule { } + model Main { } +} + +@external +namespace app.bsky.feed.threadgate { + model FollowerRule { } + model FollowingRule { } + model ListRule { } + model Main { } + model MentionRule { } +} + +@external +namespace app.bsky.graph.defs { + @token model Curatelist { } + model ListItemView { } + model ListPurpose { } + model ListView { } + model ListViewBasic { } + model ListViewerState { } + @token model Modlist { } + model NotFoundActor { } + @token model Referencelist { } + model Relationship { } + model StarterPackView { } + model StarterPackViewBasic { } +} + +@external +namespace app.bsky.labeler.defs { + model LabelerPolicies { } + model LabelerView { } + model LabelerViewDetailed { } + model LabelerViewerState { } +} + +@external +namespace app.bsky.richtext.facet { + model ByteSlice { } + model Link { } + model Main { } + model Mention { } + model Tag { } +} + +@external +namespace com.atproto.label.defs { + model Label { } + model LabelValue { } + model LabelValueDefinition { } + model LabelValueDefinitionStrings { } + model SelfLabel { } + model SelfLabels { } +} + +@external +namespace com.atproto.repo.applyWrites { + model Create { } + model CreateResult { } + model Delete { } + model DeleteResult { } + model Update { } + model UpdateResult { } +} + +@external +namespace com.atproto.repo.createRecord { +} + +@external +namespace com.atproto.repo.defs { + model CommitMeta { } +} + +@external +namespace com.atproto.repo.deleteRecord { +} + +@external +namespace com.atproto.repo.describeRepo { +} + +@external +namespace com.atproto.repo.getRecord { +} + +@external +namespace com.atproto.repo.importRepo { +} + +@external +namespace com.atproto.repo.listMissingBlobs { + model RecordBlob { } +} + +@external +namespace com.atproto.repo.listRecords { + model Record { } +} + +@external +namespace com.atproto.repo.putRecord { +} + +@external +namespace com.atproto.repo.strongRef { + model Main { } +} + +@external +namespace com.atproto.repo.uploadBlob { +} diff --git a/packages/example/typelex/main.tsp b/packages/example/typelex/main.tsp index 43d1d4d..10ce8d6 100644 --- a/packages/example/typelex/main.tsp +++ b/packages/example/typelex/main.tsp @@ -1,144 +1,68 @@ import "@typelex/emitter"; +import "./externals.tsp"; -// Example showing typelex as source of truth for atproto lexicons +namespace xyz.statusphere.defs { + model StatusView { + @required uri: atUri; -// ============ Common Types ============ - -namespace app.example.defs { - @doc("Type of notification") - union notificationType { - string, - - Like: "like", - Repost: "repost", - Follow: "follow", - Mention: "mention", - Reply: "reply", - } - - @doc("Reference to a post") - model PostRef { - @doc("AT URI of the post") - @required - uri: string; - - @doc("CID of the post") @required - cid: string; - } - - @doc("Reference to a parent post in a reply chain") - model ReplyRef { - @doc("Root post in the thread") - @required - root: PostRef; + @minLength(1) + @maxGraphemes(1) + @maxLength(32) + status: string; - @doc("Direct parent post being replied to") - @required - parent: PostRef; + @required createdAt: datetime; + @required profile: ProfileView; } - @doc("Text entity (mention, link, or tag)") - model Entity { - @doc("Start index in text") - @required - start: int32; - - @doc("End index in text") - @required - end: int32; - - @doc("Entity type") - @required - type: string; - - @doc("Entity value (handle, URL, or tag)") - @required - value: string; + model ProfileView { + @required did: did; + @required handle: handle; } } -// ============ Records ============ - -namespace app.example.post { +namespace xyz.statusphere.status { @rec("tid") - @doc("A post in the feed") model Main { - @doc("Post text content") @required - text: string; + @minLength(1) + @maxGraphemes(1) + @maxLength(32) + status: string; - @doc("Creation timestamp") - @required - createdAt: datetime; - - @doc("Languages the post is written in") - langs?: string[]; - - @doc("Referenced entities in the post") - entities?: app.example.defs.Entity[]; - - @doc("Post the user is replying to") - reply?: app.example.defs.ReplyRef; + @required createdAt: datetime; } } -namespace app.example.follow { - @rec("tid") - @doc("A follow relationship") - model Main { - @doc("DID of the account being followed") - @required - subject: string; - - @doc("When the follow was created") - @required - createdAt: datetime; - } +namespace xyz.statusphere.sendStatus { + @procedure + @doc("Send a status into the ATmosphere.") + op main( + input: { + @required + @minLength(1) + @maxGraphemes(1) + @maxLength(32) + status: string; + }, + ): { + @required status: xyz.statusphere.defs.StatusView; + }; } -namespace app.example.like { - @rec("tid") - @doc("A like on a post") - model Main { - @doc("Post being liked") - @required - subject: app.example.defs.PostRef; - - @doc("When the like was created") - @required - createdAt: datetime; - } +namespace xyz.statusphere.getStatuses { + @query + @doc("Get a list of the most recent statuses on the network.") + op main(@minValue(1) @maxValue(100) limit?: integer = 50): { + @required statuses: xyz.statusphere.defs.StatusView[]; + }; } -namespace app.example.repost { - @rec("tid") - @doc("A repost of another post") - model Main { - @doc("Post being reposted") - @required - subject: app.example.defs.PostRef; - - @doc("When the repost was created") - @required - createdAt: datetime; - } -} - -namespace app.example.profile { - @rec("self") - @doc("User profile information") - model Main { - @doc("Display name") - displayName?: string; - - @doc("Profile description") - description?: string; - - @doc("Profile avatar image") - avatar?: string; - - @doc("Profile banner image") - banner?: string; - } +namespace xyz.statusphere.getUser { + @query + @doc("Get the current user's profile and status.") + op main(): { + @required profile: app.bsky.actor.defs.ProfileView; + status?: xyz.statusphere.defs.StatusView; + }; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0cac1ef..d6e76ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,28 @@ importers: specifier: ^5.0.0 version: 5.9.3 + packages/cli: + dependencies: + '@typelex/emitter': + specifier: ^0.2.0 + version: 0.2.0(@typespec/compiler@1.4.0(@types/node@20.19.19)) + '@typespec/compiler': + specifier: ^1.4.0 + version: 1.4.0(@types/node@20.19.19) + yargs: + specifier: ^18.0.0 + version: 18.0.0 + devDependencies: + '@types/node': + specifier: ^20.0.0 + version: 20.19.19 + '@types/yargs': + specifier: ^17.0.33 + version: 17.0.33 + typescript: + specifier: ^5.0.0 + version: 5.9.3 + packages/emitter: dependencies: '@typespec/compiler': @@ -48,12 +70,12 @@ importers: '@atproto/xrpc-server': specifier: ^0.9.5 version: 0.9.5 + '@typelex/cli': + specifier: workspace:* + version: link:../cli '@typelex/emitter': specifier: workspace:* version: link:../emitter - '@typespec/compiler': - specifier: ^1.4.0 - version: 1.4.0(@types/node@20.19.19) devDependencies: typescript: specifier: ^5.0.0 @@ -1645,6 +1667,11 @@ packages: '@ts-morph/common@0.25.0': resolution: {integrity: sha512-kMnZz+vGGHi4GoHnLmMhGNjm44kGtKUXGnOvrKmMwAuvNjM/PgKVGfUnL7IDvK7Jb2QQ82jq3Zmp04Gy+r3Dkg==} + '@typelex/emitter@0.2.0': + resolution: {integrity: sha512-4Iw6VAnd9nCFGOkJcu9utWdmu9ZyPeAb1QX/B7KerGBmfc2FuIDqgZZ/mZ6c56atcZd62pb2oYF/3RgSFhEsoQ==} + peerDependencies: + '@typespec/compiler': ^1.4.0 + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1706,6 +1733,12 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.33': + resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} + '@typespec/asset-emitter@0.74.0': resolution: {integrity: sha512-DWIdlSNhRgBeZ8exfqubfUn0H6mRg4gr0s7zLTdBMUEDHL3Yh0ljnRPkd8AXTZhoW3maTFT69loWTrqx09T5oQ==} engines: {node: '>=20.0.0'} @@ -7411,6 +7444,10 @@ snapshots: path-browserify: 1.0.1 tinyglobby: 0.2.15 + '@typelex/emitter@0.2.0(@typespec/compiler@1.4.0(@types/node@20.19.19))': + dependencies: + '@typespec/compiler': 1.4.0(@types/node@20.19.19) + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.28.4 @@ -7483,6 +7520,12 @@ snapshots: '@types/unist@3.0.3': {} + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.33': + dependencies: + '@types/yargs-parser': 21.0.3 + '@typespec/asset-emitter@0.74.0(@typespec/compiler@1.4.0(@types/node@20.19.19))': dependencies: '@typespec/compiler': 1.4.0(@types/node@20.19.19) -- 2.43.0