Highly ambitious ATProtocol AppView service and sdks

add slices.json config to cli to reduce the number of args you have to pass

Changed files
+276 -42
packages
cli
src
commands
utils
+94 -23
packages/cli/README.md
··· 5 5 ## Features 6 6 7 7 - ๐Ÿ” **Device Code Authentication** - Secure OAuth 2.0 device flow login 8 - - ๐Ÿ“ **Lexicon Import** - Bulk import and validate lexicon files 8 + - ๐Ÿ“ **Lexicon Management** - Import, list, and validate lexicon files 9 + - ๐Ÿงฌ **Code Generation** - Generate TypeScript clients from lexicons 10 + - โš™๏ธ **Project Configuration** - Optional `slices.json` config file support 9 11 - โœ… **Validation** - Built-in lexicon validation using `@slices/lexicon` 10 12 - ๐Ÿ”ง **Configuration Management** - Persistent authentication and settings 11 13 - ๐Ÿ“Š **Progress Tracking** - Real-time progress for batch operations ··· 27 29 slices login 28 30 ``` 29 31 30 - 2. **Import lexicon files** 32 + 2. **Create a project config (optional)** 33 + ```bash 34 + echo '{"slice": "at://did:plc:example/slice"}' > slices.json 35 + ``` 36 + 37 + 3. **Import lexicon files** 31 38 ```bash 32 - slices import --slice at://did:plc:example/slice --path ./lexicons 39 + slices lexicon import 33 40 ``` 34 41 35 42 ## Commands ··· 61 68 slices login --aip-url https://custom-aip.example.com 62 69 ``` 63 70 64 - ### `slices import` 71 + ### `slices lexicon import` 65 72 66 73 Import lexicon files to your slice with automatic validation. 67 74 68 75 ```bash 69 - slices import [OPTIONS] 76 + slices lexicon import [OPTIONS] 70 77 71 78 OPTIONS: 72 - --path <PATH> Directory containing lexicon files (default: ./lexicons) 73 - --slice <SLICE_URI> Target slice URI (required) 79 + --path <PATH> Directory containing lexicon files (default: ./lexicons or from slices.json) 80 + --slice <SLICE_URI> Target slice URI (required, or from slices.json) 74 81 --validate-only Only validate files, don't upload 75 82 --dry-run Show what would be imported without uploading 76 - --api-url <URL> Slices API base URL (default: https://api.slices.network) 83 + --api-url <URL> Slices API base URL (default: https://api.slices.network or from slices.json) 84 + -h, --help Show help 85 + ``` 86 + 87 + ### `slices lexicon list` 88 + 89 + List all lexicons in your slice. 90 + 91 + ```bash 92 + slices lexicon list [OPTIONS] 93 + 94 + OPTIONS: 95 + --slice <SLICE_URI> Target slice URI (required, or from slices.json) 96 + --api-url <URL> Slices API base URL (default: https://api.slices.network or from slices.json) 97 + -h, --help Show help 98 + ``` 99 + 100 + ### `slices codegen` 101 + 102 + Generate TypeScript client from lexicon files. 103 + 104 + ```bash 105 + slices codegen [OPTIONS] 106 + 107 + OPTIONS: 108 + --lexicons <PATH> Directory containing lexicon files (default: ./lexicons or from slices.json) 109 + --output <PATH> Output file path (default: ./generated_client.ts or from slices.json) 110 + --slice <SLICE_URI> Target slice URI (required, or from slices.json) 111 + --exclude-slices Exclude @slices/client integration 77 112 -h, --help Show help 78 113 ``` 79 114 80 115 **Examples:** 81 116 ```bash 82 - # Import all lexicons from ./lexicons 83 - slices import --slice at://did:plc:example/slice 117 + # Import all lexicons from ./lexicons (using slices.json config) 118 + slices lexicon import 84 119 85 120 # Import from custom directory 86 - slices import --path ./my-lexicons --slice at://did:plc:example/slice 121 + slices lexicon import --path ./my-lexicons --slice at://did:plc:example/slice 87 122 88 123 # Validate only (no upload) 89 - slices import --validate-only --path ./lexicons 124 + slices lexicon import --validate-only --path ./lexicons 90 125 91 126 # Dry run (see what would be imported) 92 - slices import --dry-run --slice at://did:plc:example/slice 127 + slices lexicon import --dry-run 128 + 129 + # List lexicons in slice 130 + slices lexicon list 131 + 132 + # Generate TypeScript client 133 + slices codegen 93 134 ``` 94 135 95 136 ## Global Options ··· 100 141 101 142 ## Configuration 102 143 144 + ### Authentication Config 145 + 103 146 The CLI stores authentication and configuration in `~/.config/slices/config.json`. 104 147 105 - **Configuration file structure:** 148 + **Authentication config structure:** 106 149 ```json 107 150 { 108 151 "auth": { ··· 111 154 "expiresAt": 1234567890000, 112 155 "did": "did:plc:example", 113 156 "aipBaseUrl": "https://auth.slices.network" 114 - }, 115 - "defaultSliceUri": "at://did:plc:example/slice", 116 - "apiBaseUrl": "https://api.slices.network" 157 + } 158 + } 159 + ``` 160 + 161 + ### Project Config 162 + 163 + Create a `slices.json` file in your project root to avoid passing common options every time: 164 + 165 + ```json 166 + { 167 + "slice": "at://did:plc:example/slice", 168 + "lexiconPath": "./lexicons", 169 + "clientOutputPath": "./generated_client.ts", 170 + "apiUrl": "https://api.slices.network" 117 171 } 118 172 ``` 119 173 174 + **Config options:** 175 + - `slice` - Your slice URI (used by `lexicon import`, `lexicon list`, `codegen`) 176 + - `lexiconPath` - Directory containing lexicon files (default: `./lexicons`) 177 + - `clientOutputPath` - Output path for generated TypeScript client (default: `./generated_client.ts`) 178 + - `apiUrl` - Slices API base URL (default: `https://api.slices.network`) 179 + 180 + The CLI will search for `slices.json` starting from the current directory and walking up the directory tree. Command line arguments always take precedence over config file values. 181 + 120 182 ## Lexicon File Requirements 121 183 122 184 Lexicon files must be valid JSON files with the following structure: ··· 173 235 # 1. Authenticate 174 236 slices login 175 237 176 - # 2. Validate lexicons first 177 - slices import --validate-only --path ./my-lexicons 238 + # 2. Set up project config 239 + echo '{"slice": "at://did:plc:user123/awesome-slice"}' > slices.json 240 + 241 + # 3. Validate lexicons first 242 + slices lexicon import --validate-only 243 + 244 + # 4. Import to slice 245 + slices lexicon import 246 + 247 + # 5. Generate TypeScript client 248 + slices codegen 178 249 179 - # 3. Import to slice 180 - slices import --slice at://did:plc:user123/awesome-slice --path ./my-lexicons 250 + # 6. List imported lexicons 251 + slices lexicon list 181 252 ``` 182 253 183 - ### Batch Operations 254 + ### Working Without Config File 184 255 185 256 ```bash 186 257 # Import multiple lexicon directories 187 258 for dir in ./lexicons/*/; do 188 - slices import --slice at://did:plc:user123/slice --path "$dir" 259 + slices lexicon import --slice at://did:plc:user123/slice --path "$dir" 189 260 done 190 261 ``` 191 262
+17 -7
packages/cli/src/commands/codegen.ts
··· 4 4 import { generateTypeScript } from "@slices/codegen"; 5 5 import { logger } from "../utils/logger.ts"; 6 6 import { findLexiconFiles, readAndParseLexicon } from "../utils/lexicon.ts"; 7 + import { SlicesConfigLoader, mergeConfig } from "../utils/config.ts"; 7 8 8 9 function showCodegenHelp() { 9 10 console.log(` ··· 13 14 slices codegen [OPTIONS] 14 15 15 16 OPTIONS: 16 - --lexicons <PATH> Directory containing lexicon files (default: ./lexicons) 17 - --output <PATH> Output file path (default: ./generated_client.ts) 18 - --slice <SLICE_URI> Target slice URI (required) 17 + --lexicons <PATH> Directory containing lexicon files (default: ./lexicons or from slices.json) 18 + --output <PATH> Output file path (default: ./generated_client.ts or from slices.json) 19 + --slice <SLICE_URI> Target slice URI (required, or from slices.json) 19 20 --exclude-slices Exclude @slices/client integration 20 21 -h, --help Show this help message 21 22 ··· 23 24 slices codegen --slice at://did:plc:example/slice 24 25 slices codegen --lexicons ./my-lexicons --output ./src/client.ts --slice at://did:plc:example/slice 25 26 slices codegen --exclude-slices --slice at://did:plc:example/slice 27 + slices codegen # Uses config from slices.json 26 28 `); 27 29 } 28 30 ··· 43 45 return; 44 46 } 45 47 48 + // Load config file 49 + const configLoader = new SlicesConfigLoader(); 50 + const slicesConfig = await configLoader.load(); 51 + const mergedConfig = mergeConfig(slicesConfig, args); 52 + 46 53 // Validate required arguments 47 - if (!args.slice) { 54 + if (!mergedConfig.slice) { 48 55 logger.error("--slice is required"); 56 + if (!slicesConfig.slice) { 57 + logger.info("๐Ÿ’ก Tip: Create a slices.json file with your slice URI to avoid passing --slice every time"); 58 + } 49 59 console.log("\nRun 'slices codegen --help' for usage information."); 50 60 Deno.exit(1); 51 61 } 52 62 53 - const lexiconsPath = resolve(args.lexicons as string || "./lexicons"); 54 - const outputPath = resolve(args.output as string || "./generated_client.ts"); 55 - const sliceUri = args.slice as string; 63 + const lexiconsPath = resolve(mergedConfig.lexiconPath!); 64 + const outputPath = resolve(mergedConfig.clientOutputPath!); 65 + const sliceUri = mergedConfig.slice!; 56 66 const excludeSlices = args["exclude-slices"] as boolean; 57 67 58 68 logger.step("๐Ÿ” Finding lexicon files...");
+16 -7
packages/cli/src/commands/lexicon/import.ts
··· 4 4 import { ConfigManager } from "../../auth/config.ts"; 5 5 import { createAuthenticatedClient } from "../../utils/client.ts"; 6 6 import { logger } from "../../utils/logger.ts"; 7 + import { SlicesConfigLoader, mergeConfig } from "../../utils/config.ts"; 7 8 import { 8 9 findLexiconFiles, 9 10 validateLexiconFiles, ··· 19 20 slices lexicon import [OPTIONS] 20 21 21 22 OPTIONS: 22 - --path <PATH> Directory containing lexicon files (default: ./lexicons) 23 - --slice <SLICE_URI> Target slice URI (required) 23 + --path <PATH> Directory containing lexicon files (default: ./lexicons or from slices.json) 24 + --slice <SLICE_URI> Target slice URI (required, or from slices.json) 24 25 --validate-only Only validate files, don't upload 25 26 --dry-run Show what would be imported without uploading 26 - --api-url <URL> Slices API base URL (default: https://api.slices.network) 27 + --api-url <URL> Slices API base URL (default: https://api.slices.network or from slices.json) 27 28 -h, --help Show this help message 28 29 29 30 EXAMPLES: ··· 31 32 slices lexicon import --path ./my-lexicons --slice at://did:plc:example/slice 32 33 slices lexicon import --validate-only --path ./lexicons 33 34 slices lexicon import --dry-run --slice at://did:plc:example/slice 35 + slices lexicon import # Uses config from slices.json 34 36 `); 35 37 } 36 38 ··· 208 210 return; 209 211 } 210 212 213 + // Load config file 214 + const configLoader = new SlicesConfigLoader(); 215 + const slicesConfig = await configLoader.load(); 216 + const mergedConfig = mergeConfig(slicesConfig, args); 211 217 212 218 // Validate required arguments 213 - if (!args["validate-only"] && !args.slice) { 219 + if (!args["validate-only"] && !mergedConfig.slice) { 214 220 logger.error("--slice is required unless using --validate-only"); 221 + if (!slicesConfig.slice) { 222 + logger.info("๐Ÿ’ก Tip: Create a slices.json file with your slice URI to avoid passing --slice every time"); 223 + } 215 224 console.log("\nRun 'slices lexicon import --help' for usage information."); 216 225 Deno.exit(1); 217 226 } 218 227 219 - const lexiconPath = resolve(args.path as string || "./lexicons"); 220 - const sliceUri = args.slice as string; 221 - const apiUrl = args["api-url"] as string || "https://api.slices.network"; 228 + const lexiconPath = resolve(mergedConfig.lexiconPath!); 229 + const sliceUri = mergedConfig.slice!; 230 + const apiUrl = mergedConfig.apiUrl!; 222 231 const validateOnly = args["validate-only"] as boolean; 223 232 const dryRun = args["dry-run"] as boolean; 224 233
+15 -5
packages/cli/src/commands/lexicon/list.ts
··· 1 1 import { parseArgs } from "@std/cli/parse-args"; 2 2 import { createAuthenticatedClient } from "../../utils/client.ts"; 3 3 import { logger } from "../../utils/logger.ts"; 4 + import { SlicesConfigLoader, mergeConfig } from "../../utils/config.ts"; 4 5 5 6 function showListHelp() { 6 7 console.log(` ··· 10 11 slices lexicon list [OPTIONS] 11 12 12 13 OPTIONS: 13 - --slice <SLICE_URI> Target slice URI (required) 14 - --api-url <URL> Slices API base URL (default: https://api.slices.network) 14 + --slice <SLICE_URI> Target slice URI (required, or from slices.json) 15 + --api-url <URL> Slices API base URL (default: https://api.slices.network or from slices.json) 15 16 -h, --help Show this help message 16 17 17 18 EXAMPLES: 18 19 slices lexicon list --slice at://did:plc:example/slice 20 + slices lexicon list # Uses config from slices.json 19 21 `); 20 22 } 21 23 ··· 33 35 return; 34 36 } 35 37 38 + // Load config file 39 + const configLoader = new SlicesConfigLoader(); 40 + const slicesConfig = await configLoader.load(); 41 + const mergedConfig = mergeConfig(slicesConfig, args); 42 + 36 43 // Validate required arguments 37 - if (!args.slice) { 44 + if (!mergedConfig.slice) { 38 45 logger.error("--slice is required"); 46 + if (!slicesConfig.slice) { 47 + logger.info("๐Ÿ’ก Tip: Create a slices.json file with your slice URI to avoid passing --slice every time"); 48 + } 39 49 console.log("\nRun 'slices lexicon list --help' for usage information."); 40 50 Deno.exit(1); 41 51 } 42 52 43 - const sliceUri = args.slice as string; 44 - const apiUrl = args["api-url"] as string || "https://api.slices.network"; 53 + const sliceUri = mergedConfig.slice!; 54 + const apiUrl = mergedConfig.apiUrl!; 45 55 46 56 try { 47 57 // Initialize authenticated client
+134
packages/cli/src/utils/config.ts
··· 1 + import { resolve, dirname } from "@std/path"; 2 + import { existsSync } from "@std/fs"; 3 + import { logger } from "./logger.ts"; 4 + 5 + export interface SlicesConfig { 6 + slice?: string; 7 + apiUrl?: string; 8 + lexiconPath?: string; 9 + clientOutputPath?: string; 10 + } 11 + 12 + export class SlicesConfigLoader { 13 + private configCache: SlicesConfig | null = null; 14 + private configPath: string | null = null; 15 + 16 + /** 17 + * Find and load slices.json config file, starting from current directory 18 + * and walking up the directory tree 19 + */ 20 + async load(startPath = Deno.cwd()): Promise<SlicesConfig> { 21 + if (this.configCache && this.configPath) { 22 + return this.configCache; 23 + } 24 + 25 + const configPath = this.findConfigFile(startPath); 26 + if (!configPath) { 27 + logger.debug("No slices.json config file found"); 28 + return {}; 29 + } 30 + 31 + this.configPath = configPath; 32 + logger.debug(`Loading config from: ${configPath}`); 33 + 34 + try { 35 + const configText = await Deno.readTextFile(configPath); 36 + const config = JSON.parse(configText) as SlicesConfig; 37 + 38 + // Validate config structure 39 + this.validateConfig(config); 40 + 41 + this.configCache = config; 42 + return config; 43 + } catch (error) { 44 + const err = error as Error; 45 + logger.warn(`Failed to load config file ${configPath}: ${err.message}`); 46 + return {}; 47 + } 48 + } 49 + 50 + /** 51 + * Get the directory containing the config file 52 + */ 53 + getConfigDir(): string | null { 54 + return this.configPath ? dirname(this.configPath) : null; 55 + } 56 + 57 + /** 58 + * Clear the config cache (useful for testing) 59 + */ 60 + clearCache(): void { 61 + this.configCache = null; 62 + this.configPath = null; 63 + } 64 + 65 + /** 66 + * Find slices.json file by walking up the directory tree 67 + */ 68 + private findConfigFile(startPath: string): string | null { 69 + let currentPath = resolve(startPath); 70 + let lastPath = ""; 71 + 72 + while (currentPath !== lastPath) { 73 + const configPath = resolve(currentPath, "slices.json"); 74 + if (existsSync(configPath)) { 75 + return configPath; 76 + } 77 + 78 + lastPath = currentPath; 79 + currentPath = dirname(currentPath); 80 + } 81 + 82 + return null; 83 + } 84 + 85 + /** 86 + * Validate the config file structure 87 + */ 88 + private validateConfig(config: unknown): void { 89 + if (typeof config !== "object" || config === null) { 90 + throw new Error("Config must be an object"); 91 + } 92 + 93 + const cfg = config as Record<string, unknown>; 94 + 95 + if (cfg.slice !== undefined && typeof cfg.slice !== "string") { 96 + throw new Error("Config 'slice' must be a string"); 97 + } 98 + 99 + if (cfg.apiUrl !== undefined && typeof cfg.apiUrl !== "string") { 100 + throw new Error("Config 'apiUrl' must be a string"); 101 + } 102 + 103 + if (cfg.lexiconPath !== undefined && typeof cfg.lexiconPath !== "string") { 104 + throw new Error("Config 'lexiconPath' must be a string"); 105 + } 106 + 107 + if (cfg.clientOutputPath !== undefined && typeof cfg.clientOutputPath !== "string") { 108 + throw new Error("Config 'clientOutputPath' must be a string"); 109 + } 110 + 111 + // Validate slice URI format if provided 112 + if (cfg.slice && typeof cfg.slice === "string") { 113 + if (!cfg.slice.startsWith("at://")) { 114 + throw new Error("Config 'slice' must be a valid AT URI (starting with 'at://')"); 115 + } 116 + } 117 + } 118 + } 119 + 120 + /** 121 + * Merge command line arguments with config file values 122 + * Command line arguments take precedence over config file 123 + */ 124 + export function mergeConfig( 125 + config: SlicesConfig, 126 + args: Record<string, unknown> 127 + ): SlicesConfig { 128 + return { 129 + slice: (args.slice as string) || config.slice, 130 + apiUrl: (args["api-url"] as string) || config.apiUrl || "https://api.slices.network", 131 + lexiconPath: (args.path as string) || config.lexiconPath || "./lexicons", 132 + clientOutputPath: (args.output as string) || config.clientOutputPath || "./generated_client.ts", 133 + }; 134 + }