Highly ambitious ATProtocol AppView service and sdks

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

+276 -42
+94 -23
packages/cli/README.md
··· 5 ## Features 6 7 - ๐Ÿ” **Device Code Authentication** - Secure OAuth 2.0 device flow login 8 - - ๐Ÿ“ **Lexicon Import** - Bulk import and validate lexicon files 9 - โœ… **Validation** - Built-in lexicon validation using `@slices/lexicon` 10 - ๐Ÿ”ง **Configuration Management** - Persistent authentication and settings 11 - ๐Ÿ“Š **Progress Tracking** - Real-time progress for batch operations ··· 27 slices login 28 ``` 29 30 - 2. **Import lexicon files** 31 ```bash 32 - slices import --slice at://did:plc:example/slice --path ./lexicons 33 ``` 34 35 ## Commands ··· 61 slices login --aip-url https://custom-aip.example.com 62 ``` 63 64 - ### `slices import` 65 66 Import lexicon files to your slice with automatic validation. 67 68 ```bash 69 - slices import [OPTIONS] 70 71 OPTIONS: 72 - --path <PATH> Directory containing lexicon files (default: ./lexicons) 73 - --slice <SLICE_URI> Target slice URI (required) 74 --validate-only Only validate files, don't upload 75 --dry-run Show what would be imported without uploading 76 - --api-url <URL> Slices API base URL (default: https://api.slices.network) 77 -h, --help Show help 78 ``` 79 80 **Examples:** 81 ```bash 82 - # Import all lexicons from ./lexicons 83 - slices import --slice at://did:plc:example/slice 84 85 # Import from custom directory 86 - slices import --path ./my-lexicons --slice at://did:plc:example/slice 87 88 # Validate only (no upload) 89 - slices import --validate-only --path ./lexicons 90 91 # Dry run (see what would be imported) 92 - slices import --dry-run --slice at://did:plc:example/slice 93 ``` 94 95 ## Global Options ··· 100 101 ## Configuration 102 103 The CLI stores authentication and configuration in `~/.config/slices/config.json`. 104 105 - **Configuration file structure:** 106 ```json 107 { 108 "auth": { ··· 111 "expiresAt": 1234567890000, 112 "did": "did:plc:example", 113 "aipBaseUrl": "https://auth.slices.network" 114 - }, 115 - "defaultSliceUri": "at://did:plc:example/slice", 116 - "apiBaseUrl": "https://api.slices.network" 117 } 118 ``` 119 120 ## Lexicon File Requirements 121 122 Lexicon files must be valid JSON files with the following structure: ··· 173 # 1. Authenticate 174 slices login 175 176 - # 2. Validate lexicons first 177 - slices import --validate-only --path ./my-lexicons 178 179 - # 3. Import to slice 180 - slices import --slice at://did:plc:user123/awesome-slice --path ./my-lexicons 181 ``` 182 183 - ### Batch Operations 184 185 ```bash 186 # Import multiple lexicon directories 187 for dir in ./lexicons/*/; do 188 - slices import --slice at://did:plc:user123/slice --path "$dir" 189 done 190 ``` 191
··· 5 ## Features 6 7 - ๐Ÿ” **Device Code Authentication** - Secure OAuth 2.0 device flow login 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 11 - โœ… **Validation** - Built-in lexicon validation using `@slices/lexicon` 12 - ๐Ÿ”ง **Configuration Management** - Persistent authentication and settings 13 - ๐Ÿ“Š **Progress Tracking** - Real-time progress for batch operations ··· 29 slices login 30 ``` 31 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** 38 ```bash 39 + slices lexicon import 40 ``` 41 42 ## Commands ··· 68 slices login --aip-url https://custom-aip.example.com 69 ``` 70 71 + ### `slices lexicon import` 72 73 Import lexicon files to your slice with automatic validation. 74 75 ```bash 76 + slices lexicon import [OPTIONS] 77 78 OPTIONS: 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) 81 --validate-only Only validate files, don't upload 82 --dry-run Show what would be imported without uploading 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 112 -h, --help Show help 113 ``` 114 115 **Examples:** 116 ```bash 117 + # Import all lexicons from ./lexicons (using slices.json config) 118 + slices lexicon import 119 120 # Import from custom directory 121 + slices lexicon import --path ./my-lexicons --slice at://did:plc:example/slice 122 123 # Validate only (no upload) 124 + slices lexicon import --validate-only --path ./lexicons 125 126 # Dry run (see what would be imported) 127 + slices lexicon import --dry-run 128 + 129 + # List lexicons in slice 130 + slices lexicon list 131 + 132 + # Generate TypeScript client 133 + slices codegen 134 ``` 135 136 ## Global Options ··· 141 142 ## Configuration 143 144 + ### Authentication Config 145 + 146 The CLI stores authentication and configuration in `~/.config/slices/config.json`. 147 148 + **Authentication config structure:** 149 ```json 150 { 151 "auth": { ··· 154 "expiresAt": 1234567890000, 155 "did": "did:plc:example", 156 "aipBaseUrl": "https://auth.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" 171 } 172 ``` 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 + 182 ## Lexicon File Requirements 183 184 Lexicon files must be valid JSON files with the following structure: ··· 235 # 1. Authenticate 236 slices login 237 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 249 250 + # 6. List imported lexicons 251 + slices lexicon list 252 ``` 253 254 + ### Working Without Config File 255 256 ```bash 257 # Import multiple lexicon directories 258 for dir in ./lexicons/*/; do 259 + slices lexicon import --slice at://did:plc:user123/slice --path "$dir" 260 done 261 ``` 262
+17 -7
packages/cli/src/commands/codegen.ts
··· 4 import { generateTypeScript } from "@slices/codegen"; 5 import { logger } from "../utils/logger.ts"; 6 import { findLexiconFiles, readAndParseLexicon } from "../utils/lexicon.ts"; 7 8 function showCodegenHelp() { 9 console.log(` ··· 13 slices codegen [OPTIONS] 14 15 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) 19 --exclude-slices Exclude @slices/client integration 20 -h, --help Show this help message 21 ··· 23 slices codegen --slice at://did:plc:example/slice 24 slices codegen --lexicons ./my-lexicons --output ./src/client.ts --slice at://did:plc:example/slice 25 slices codegen --exclude-slices --slice at://did:plc:example/slice 26 `); 27 } 28 ··· 43 return; 44 } 45 46 // Validate required arguments 47 - if (!args.slice) { 48 logger.error("--slice is required"); 49 console.log("\nRun 'slices codegen --help' for usage information."); 50 Deno.exit(1); 51 } 52 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; 56 const excludeSlices = args["exclude-slices"] as boolean; 57 58 logger.step("๐Ÿ” Finding lexicon files...");
··· 4 import { generateTypeScript } from "@slices/codegen"; 5 import { logger } from "../utils/logger.ts"; 6 import { findLexiconFiles, readAndParseLexicon } from "../utils/lexicon.ts"; 7 + import { SlicesConfigLoader, mergeConfig } from "../utils/config.ts"; 8 9 function showCodegenHelp() { 10 console.log(` ··· 14 slices codegen [OPTIONS] 15 16 OPTIONS: 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) 20 --exclude-slices Exclude @slices/client integration 21 -h, --help Show this help message 22 ··· 24 slices codegen --slice at://did:plc:example/slice 25 slices codegen --lexicons ./my-lexicons --output ./src/client.ts --slice at://did:plc:example/slice 26 slices codegen --exclude-slices --slice at://did:plc:example/slice 27 + slices codegen # Uses config from slices.json 28 `); 29 } 30 ··· 45 return; 46 } 47 48 + // Load config file 49 + const configLoader = new SlicesConfigLoader(); 50 + const slicesConfig = await configLoader.load(); 51 + const mergedConfig = mergeConfig(slicesConfig, args); 52 + 53 // Validate required arguments 54 + if (!mergedConfig.slice) { 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 + } 59 console.log("\nRun 'slices codegen --help' for usage information."); 60 Deno.exit(1); 61 } 62 63 + const lexiconsPath = resolve(mergedConfig.lexiconPath!); 64 + const outputPath = resolve(mergedConfig.clientOutputPath!); 65 + const sliceUri = mergedConfig.slice!; 66 const excludeSlices = args["exclude-slices"] as boolean; 67 68 logger.step("๐Ÿ” Finding lexicon files...");
+16 -7
packages/cli/src/commands/lexicon/import.ts
··· 4 import { ConfigManager } from "../../auth/config.ts"; 5 import { createAuthenticatedClient } from "../../utils/client.ts"; 6 import { logger } from "../../utils/logger.ts"; 7 import { 8 findLexiconFiles, 9 validateLexiconFiles, ··· 19 slices lexicon import [OPTIONS] 20 21 OPTIONS: 22 - --path <PATH> Directory containing lexicon files (default: ./lexicons) 23 - --slice <SLICE_URI> Target slice URI (required) 24 --validate-only Only validate files, don't upload 25 --dry-run Show what would be imported without uploading 26 - --api-url <URL> Slices API base URL (default: https://api.slices.network) 27 -h, --help Show this help message 28 29 EXAMPLES: ··· 31 slices lexicon import --path ./my-lexicons --slice at://did:plc:example/slice 32 slices lexicon import --validate-only --path ./lexicons 33 slices lexicon import --dry-run --slice at://did:plc:example/slice 34 `); 35 } 36 ··· 208 return; 209 } 210 211 212 // Validate required arguments 213 - if (!args["validate-only"] && !args.slice) { 214 logger.error("--slice is required unless using --validate-only"); 215 console.log("\nRun 'slices lexicon import --help' for usage information."); 216 Deno.exit(1); 217 } 218 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"; 222 const validateOnly = args["validate-only"] as boolean; 223 const dryRun = args["dry-run"] as boolean; 224
··· 4 import { ConfigManager } from "../../auth/config.ts"; 5 import { createAuthenticatedClient } from "../../utils/client.ts"; 6 import { logger } from "../../utils/logger.ts"; 7 + import { SlicesConfigLoader, mergeConfig } from "../../utils/config.ts"; 8 import { 9 findLexiconFiles, 10 validateLexiconFiles, ··· 20 slices lexicon import [OPTIONS] 21 22 OPTIONS: 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) 25 --validate-only Only validate files, don't upload 26 --dry-run Show what would be imported without uploading 27 + --api-url <URL> Slices API base URL (default: https://api.slices.network or from slices.json) 28 -h, --help Show this help message 29 30 EXAMPLES: ··· 32 slices lexicon import --path ./my-lexicons --slice at://did:plc:example/slice 33 slices lexicon import --validate-only --path ./lexicons 34 slices lexicon import --dry-run --slice at://did:plc:example/slice 35 + slices lexicon import # Uses config from slices.json 36 `); 37 } 38 ··· 210 return; 211 } 212 213 + // Load config file 214 + const configLoader = new SlicesConfigLoader(); 215 + const slicesConfig = await configLoader.load(); 216 + const mergedConfig = mergeConfig(slicesConfig, args); 217 218 // Validate required arguments 219 + if (!args["validate-only"] && !mergedConfig.slice) { 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 + } 224 console.log("\nRun 'slices lexicon import --help' for usage information."); 225 Deno.exit(1); 226 } 227 228 + const lexiconPath = resolve(mergedConfig.lexiconPath!); 229 + const sliceUri = mergedConfig.slice!; 230 + const apiUrl = mergedConfig.apiUrl!; 231 const validateOnly = args["validate-only"] as boolean; 232 const dryRun = args["dry-run"] as boolean; 233
+15 -5
packages/cli/src/commands/lexicon/list.ts
··· 1 import { parseArgs } from "@std/cli/parse-args"; 2 import { createAuthenticatedClient } from "../../utils/client.ts"; 3 import { logger } from "../../utils/logger.ts"; 4 5 function showListHelp() { 6 console.log(` ··· 10 slices lexicon list [OPTIONS] 11 12 OPTIONS: 13 - --slice <SLICE_URI> Target slice URI (required) 14 - --api-url <URL> Slices API base URL (default: https://api.slices.network) 15 -h, --help Show this help message 16 17 EXAMPLES: 18 slices lexicon list --slice at://did:plc:example/slice 19 `); 20 } 21 ··· 33 return; 34 } 35 36 // Validate required arguments 37 - if (!args.slice) { 38 logger.error("--slice is required"); 39 console.log("\nRun 'slices lexicon list --help' for usage information."); 40 Deno.exit(1); 41 } 42 43 - const sliceUri = args.slice as string; 44 - const apiUrl = args["api-url"] as string || "https://api.slices.network"; 45 46 try { 47 // Initialize authenticated client
··· 1 import { parseArgs } from "@std/cli/parse-args"; 2 import { createAuthenticatedClient } from "../../utils/client.ts"; 3 import { logger } from "../../utils/logger.ts"; 4 + import { SlicesConfigLoader, mergeConfig } from "../../utils/config.ts"; 5 6 function showListHelp() { 7 console.log(` ··· 11 slices lexicon list [OPTIONS] 12 13 OPTIONS: 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) 16 -h, --help Show this help message 17 18 EXAMPLES: 19 slices lexicon list --slice at://did:plc:example/slice 20 + slices lexicon list # Uses config from slices.json 21 `); 22 } 23 ··· 35 return; 36 } 37 38 + // Load config file 39 + const configLoader = new SlicesConfigLoader(); 40 + const slicesConfig = await configLoader.load(); 41 + const mergedConfig = mergeConfig(slicesConfig, args); 42 + 43 // Validate required arguments 44 + if (!mergedConfig.slice) { 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 + } 49 console.log("\nRun 'slices lexicon list --help' for usage information."); 50 Deno.exit(1); 51 } 52 53 + const sliceUri = mergedConfig.slice!; 54 + const apiUrl = mergedConfig.apiUrl!; 55 56 try { 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 + }