+94
-23
packages/cli/README.md
+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
+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
+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
+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
+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
+
}