1313import process from 'node:process' ;
1414import { Result } from '@praha/byethrow' ;
1515import { $ } from 'bun' ;
16+ import { allArgs } from '../src/commands/all.ts' ;
1617// Import command definitions to access their args
1718import { subCommandUnion } from '../src/commands/index.ts' ;
1819import { logger } from '../src/logger.ts' ;
1920
20- import { sharedArgs } from '../src/shared-args.ts' ;
21-
2221/**
2322 * The filename for the generated JSON Schema file.
2423 * Used for both root directory and docs/public directory output.
@@ -39,19 +38,33 @@ const COMMAND_EXCLUDE_KEYS: Record<string, string[]> = {
3938 blocks : [ 'live' , 'refreshInterval' ] ,
4039} ;
4140
41+ const AGENT_NAMES = [ 'claude' , 'codex' , 'opencode' , 'amp' , 'pi' ] as const ;
42+ type AgentName = ( typeof AGENT_NAMES ) [ number ] ;
43+ type JsonSchemaNode = {
44+ [ key : string ] : unknown ;
45+ type ?: string ;
46+ properties ?: Record < string , JsonSchemaNode > ;
47+ definitions ?: Record < string , JsonSchemaNode > ;
48+ } ;
49+ type TokenDefinition = {
50+ [ key : string ] : unknown ;
51+ type : string ;
52+ choices ?: readonly unknown [ ] ;
53+ description ?: string ;
54+ default ?: unknown ;
55+ } ;
56+ type TokenSchema = Record < string , TokenDefinition > ;
57+
4258/**
4359 * Convert args-tokens schema to JSON Schema format
4460 */
45- function tokensSchemaToJsonSchema ( schema : Record < string , any > ) : Record < string , any > {
46- const properties : Record < string , any > = { } ;
61+ function tokensSchemaToJsonSchema ( schema : TokenSchema ) : JsonSchemaNode {
62+ const properties : Record < string , JsonSchemaNode > = { } ;
4763
48- for ( const [ key , arg ] of Object . entries ( schema ) ) {
49- // eslint-disable-next-line ts/no-unsafe-assignment
50- const argTyped = arg ;
51- const property : Record < string , any > = { } ;
64+ for ( const [ key , argTyped ] of Object . entries ( schema ) ) {
65+ const property : JsonSchemaNode = { } ;
5266
5367 // Handle type conversion
54- // eslint-disable-next-line ts/no-unsafe-member-access
5568 switch ( argTyped . type ) {
5669 case 'boolean' :
5770 property . type = 'boolean' ;
@@ -65,9 +78,7 @@ function tokensSchemaToJsonSchema(schema: Record<string, any>): Record<string, a
6578 break ;
6679 case 'enum' :
6780 property . type = 'string' ;
68- // eslint-disable-next-line ts/no-unsafe-member-access
6981 if ( argTyped . choices != null && Array . isArray ( argTyped . choices ) ) {
70- // eslint-disable-next-line ts/no-unsafe-assignment, ts/no-unsafe-member-access
7182 property . enum = argTyped . choices ;
7283 }
7384 break ;
@@ -76,18 +87,13 @@ function tokensSchemaToJsonSchema(schema: Record<string, any>): Record<string, a
7687 }
7788
7889 // Add description
79- // eslint-disable-next-line ts/no-unsafe-member-access
8090 if ( argTyped . description != null ) {
81- // eslint-disable-next-line ts/no-unsafe-assignment, ts/no-unsafe-member-access
8291 property . description = argTyped . description ;
83- // eslint-disable-next-line ts/no-unsafe-assignment, ts/no-unsafe-member-access
8492 property . markdownDescription = argTyped . description ;
8593 }
8694
8795 // Add default value
88- // eslint-disable-next-line ts/no-unsafe-member-access
8996 if ( 'default' in argTyped && argTyped . default !== undefined ) {
90- // eslint-disable-next-line ts/no-unsafe-assignment, ts/no-unsafe-member-access
9197 property . default = argTyped . default ;
9298 }
9399
@@ -101,42 +107,116 @@ function tokensSchemaToJsonSchema(schema: Record<string, any>): Record<string, a
101107 } ;
102108}
103109
104- /**
105- * Create the complete configuration schema from all command definitions
106- */
107- function createConfigSchemaJson ( ) {
108- // Create schema for default/shared arguments (excluding CLI-only options)
109- const defaultsSchema = Object . fromEntries (
110- Object . entries ( sharedArgs ) . filter ( ( [ key ] ) => ! EXCLUDE_KEYS . includes ( key ) ) ,
110+ function splitCommandName ( name : string ) : { agent ?: AgentName ; report : string } {
111+ const [ prefix , report ] = name . split ( ':' ) ;
112+ if ( report != null && AGENT_NAMES . includes ( prefix as AgentName ) ) {
113+ return { agent : prefix as AgentName , report } ;
114+ }
115+ return { report : name } ;
116+ }
117+
118+ function filterCommandSchema ( report : string , schema : TokenSchema ) : TokenSchema {
119+ const commandExcludes = COMMAND_EXCLUDE_KEYS [ report ] ?? [ ] ;
120+ return Object . fromEntries (
121+ Object . entries ( schema ) . filter (
122+ ( [ key ] ) => ! EXCLUDE_KEYS . includes ( key ) && ! commandExcludes . includes ( key ) ,
123+ ) ,
111124 ) ;
125+ }
112126
113- // Create schemas for each command's specific arguments (excluding CLI-only options)
114- const commandSchemas : Record < string , any > = { } ;
115- for ( const [ commandName , command ] of subCommandUnion ) {
116- const commandExcludes = COMMAND_EXCLUDE_KEYS [ commandName ] ?? [ ] ;
117- commandSchemas [ commandName ] = Object . fromEntries (
118- Object . entries ( command . args as Record < string , any > ) . filter (
119- ( [ key ] ) => ! EXCLUDE_KEYS . includes ( key ) && ! commandExcludes . includes ( key ) ,
120- ) ,
121- ) ;
127+ function commonSchemaProperties ( commandSchemas : Record < string , TokenSchema > ) : TokenSchema {
128+ const schemas = Object . values ( commandSchemas ) ;
129+ const firstSchema = schemas [ 0 ] ;
130+ if ( firstSchema == null ) {
131+ return { } ;
122132 }
133+ const restSchemas = schemas . slice ( 1 ) ;
134+ return Object . fromEntries (
135+ Object . entries ( firstSchema ) . filter ( ( [ key ] ) =>
136+ restSchemas . every ( ( schema ) => Object . hasOwn ( schema , key ) ) ,
137+ ) ,
138+ ) ;
139+ }
123140
124- // Convert to JSON Schema format
125-
126- const defaultsJsonSchema = tokensSchemaToJsonSchema ( defaultsSchema ) ;
127- const commandsJsonSchema = {
141+ function createCommandsJsonSchema (
142+ commandSchemas : Record < string , TokenSchema > ,
143+ description : string ,
144+ ) : JsonSchemaNode {
145+ return {
128146 type : 'object' ,
129147 properties : Object . fromEntries (
130148 Object . entries ( commandSchemas ) . map ( ( [ name , schema ] ) => [
131149 name ,
132- // eslint-disable-next-line ts/no-unsafe-argument
133150 tokensSchemaToJsonSchema ( schema ) ,
134151 ] ) ,
135152 ) ,
136153 additionalProperties : false ,
137- description : 'Command-specific configuration overrides' ,
138- markdownDescription : 'Command-specific configuration overrides' ,
154+ description,
155+ markdownDescription : description ,
156+ } ;
157+ }
158+
159+ function createAgentJsonSchema (
160+ agentName : AgentName ,
161+ commandSchemas : Record < string , TokenSchema > ,
162+ ) : JsonSchemaNode {
163+ const agentLabel = agentName === 'pi' ? 'pi-agent' : agentName ;
164+ return {
165+ type : 'object' ,
166+ properties : {
167+ defaults : {
168+ ...tokensSchemaToJsonSchema ( commonSchemaProperties ( commandSchemas ) ) ,
169+ description : `Default values for ${ agentLabel } commands` ,
170+ markdownDescription : `Default values for ${ agentLabel } commands` ,
171+ } ,
172+ commands : createCommandsJsonSchema (
173+ commandSchemas ,
174+ `Command-specific configuration overrides for ${ agentLabel } ` ,
175+ ) ,
176+ } ,
177+ additionalProperties : false ,
178+ description : `${ agentLabel } command configuration` ,
179+ markdownDescription : `${ agentLabel } command configuration` ,
139180 } ;
181+ }
182+
183+ /**
184+ * Create the complete configuration schema from all command definitions
185+ */
186+ function createConfigSchemaJson ( ) : JsonSchemaNode {
187+ const topLevelCommandSchemas : Record < string , TokenSchema > = { } ;
188+ const agentCommandSchemas = Object . fromEntries ( AGENT_NAMES . map ( ( agent ) => [ agent , { } ] ) ) as Record <
189+ AgentName ,
190+ Record < string , TokenSchema >
191+ > ;
192+
193+ for ( const [ commandName , command ] of subCommandUnion ) {
194+ const { agent, report } = splitCommandName ( commandName ) ;
195+ const commandSchema = filterCommandSchema ( report , command . args as TokenSchema ) ;
196+ if ( agent == null ) {
197+ topLevelCommandSchemas [ report ] = commandSchema ;
198+ } else {
199+ agentCommandSchemas [ agent ] [ report ] = commandSchema ;
200+ }
201+ }
202+
203+ const legacyTopLevelCommandSchemas = Object . fromEntries (
204+ Object . entries ( topLevelCommandSchemas ) . map ( ( [ report , schema ] ) => [
205+ report ,
206+ {
207+ ...( agentCommandSchemas . claude [ report ] ?? { } ) ,
208+ ...schema ,
209+ } ,
210+ ] ) ,
211+ ) ;
212+ const defaultsJsonSchema = tokensSchemaToJsonSchema ( {
213+ ...commonSchemaProperties ( agentCommandSchemas . claude ) ,
214+ ...filterCommandSchema ( 'daily' , allArgs as TokenSchema ) ,
215+ } ) ;
216+ const commandsJsonSchema = createCommandsJsonSchema (
217+ legacyTopLevelCommandSchemas ,
218+ 'Command-specific configuration overrides for all-agent reports' ,
219+ ) ;
140220
141221 // Main configuration schema
142222 return {
@@ -152,10 +232,15 @@ function createConfigSchemaJson() {
152232 } ,
153233 defaults : {
154234 ...defaultsJsonSchema ,
155- description : 'Default values for all commands' ,
156- markdownDescription : 'Default values for all commands' ,
235+ description : 'Default values for all-agent reports and legacy Claude commands' ,
236+ markdownDescription : 'Default values for all-agent reports and legacy Claude commands' ,
157237 } ,
158238 commands : commandsJsonSchema ,
239+ claude : createAgentJsonSchema ( 'claude' , agentCommandSchemas . claude ) ,
240+ codex : createAgentJsonSchema ( 'codex' , agentCommandSchemas . codex ) ,
241+ opencode : createAgentJsonSchema ( 'opencode' , agentCommandSchemas . opencode ) ,
242+ amp : createAgentJsonSchema ( 'amp' , agentCommandSchemas . amp ) ,
243+ pi : createAgentJsonSchema ( 'pi' , agentCommandSchemas . pi ) ,
159244 } ,
160245 additionalProperties : false ,
161246 } ,
@@ -168,15 +253,24 @@ function createConfigSchemaJson() {
168253 $schema : 'https://ccusage.com/config-schema.json' ,
169254 defaults : {
170255 json : false ,
171- mode : 'auto' ,
172256 timezone : 'Asia/Tokyo' ,
173257 } ,
174- commands : {
175- daily : {
176- instances : true ,
258+ claude : {
259+ defaults : {
260+ mode : 'auto' ,
177261 } ,
178- blocks : {
179- tokenLimit : '500000' ,
262+ commands : {
263+ daily : {
264+ instances : true ,
265+ } ,
266+ blocks : {
267+ tokenLimit : '500000' ,
268+ } ,
269+ } ,
270+ } ,
271+ codex : {
272+ defaults : {
273+ speed : 'auto' ,
180274 } ,
181275 } ,
182276 } ,
@@ -290,6 +384,14 @@ if (import.meta.main) {
290384 await generateJsonSchema ( ) ;
291385}
292386if ( import . meta. vitest != null ) {
387+ function schemaProperties ( schema : JsonSchemaNode ) : Record < string , JsonSchemaNode > {
388+ return schema . properties ?? { } ;
389+ }
390+
391+ function configSchemaDefinition ( schema : JsonSchemaNode ) : JsonSchemaNode {
392+ return schema . definitions ?. [ 'ccusage-config' ] ?? { } ;
393+ }
394+
293395 describe ( 'tokensSchemaToJsonSchema' , ( ) => {
294396 it ( 'should convert boolean args to JSON Schema' , ( ) => {
295397 const schema = {
@@ -298,10 +400,10 @@ if (import.meta.vitest != null) {
298400 description : 'Enable debug mode' ,
299401 default : false ,
300402 } ,
301- } ;
403+ } satisfies TokenSchema ;
302404
303405 const jsonSchema = tokensSchemaToJsonSchema ( schema ) ;
304- expect ( ( jsonSchema . properties as Record < string , any > ) . debug ) . toEqual ( {
406+ expect ( schemaProperties ( jsonSchema ) . debug ) . toEqual ( {
305407 type : 'boolean' ,
306408 description : 'Enable debug mode' ,
307409 markdownDescription : 'Enable debug mode' ,
@@ -317,10 +419,10 @@ if (import.meta.vitest != null) {
317419 choices : [ 'auto' , 'manual' ] ,
318420 default : 'auto' ,
319421 } ,
320- } ;
422+ } satisfies TokenSchema ;
321423
322424 const jsonSchema = tokensSchemaToJsonSchema ( schema ) ;
323- expect ( ( jsonSchema . properties as Record < string , any > ) . mode ) . toEqual ( {
425+ expect ( schemaProperties ( jsonSchema ) . mode ) . toEqual ( {
324426 type : 'string' ,
325427 enum : [ 'auto' , 'manual' ] ,
326428 description : 'Mode selection' ,
@@ -337,29 +439,62 @@ if (import.meta.vitest != null) {
337439 expect ( jsonSchema ) . toBeDefined ( ) ;
338440 expect ( jsonSchema . $ref ) . toBe ( '#/definitions/ccusage-config' ) ;
339441 expect ( jsonSchema . definitions ) . toBeDefined ( ) ;
340- expect ( jsonSchema . definitions [ 'ccusage-config' ] ) . toBeDefined ( ) ;
341- expect ( jsonSchema . definitions [ 'ccusage-config' ] . type ) . toBe ( 'object' ) ;
442+ expect ( configSchemaDefinition ( jsonSchema ) ) . toBeDefined ( ) ;
443+ expect ( configSchemaDefinition ( jsonSchema ) . type ) . toBe ( 'object' ) ;
342444 } ) ;
343445
344446 it ( 'should include all expected properties' , ( ) => {
345447 const jsonSchema = createConfigSchemaJson ( ) ;
346- const mainSchema = jsonSchema . definitions [ 'ccusage-config' ] ;
448+ const mainSchema = configSchemaDefinition ( jsonSchema ) ;
449+ const properties = schemaProperties ( mainSchema ) ;
450+
451+ expect ( properties ) . toHaveProperty ( '$schema' ) ;
452+ expect ( properties ) . toHaveProperty ( 'defaults' ) ;
453+ expect ( properties ) . toHaveProperty ( 'commands' ) ;
454+ expect ( properties ) . toHaveProperty ( 'claude' ) ;
455+ expect ( properties ) . toHaveProperty ( 'codex' ) ;
456+ } ) ;
347457
348- expect ( mainSchema . properties ) . toHaveProperty ( '$schema' ) ;
349- expect ( mainSchema . properties ) . toHaveProperty ( 'defaults' ) ;
350- expect ( mainSchema . properties ) . toHaveProperty ( 'commands' ) ;
458+ it ( 'should keep legacy top-level Claude config properties' , ( ) => {
459+ const jsonSchema = createConfigSchemaJson ( ) ;
460+ const mainSchema = configSchemaDefinition ( jsonSchema ) ;
461+ const properties = schemaProperties ( mainSchema ) ;
462+ const defaultsSchema = properties . defaults ?? { } ;
463+ const commandsSchema = properties . commands ?? { } ;
464+ const dailySchema = schemaProperties ( commandsSchema ) . daily ?? { } ;
465+
466+ expect ( schemaProperties ( defaultsSchema ) ) . toHaveProperty ( 'mode' ) ;
467+ expect ( schemaProperties ( dailySchema ) ) . toHaveProperty ( 'instances' ) ;
351468 } ) ;
352469
353470 it ( 'should include all command schemas' , ( ) => {
354471 const jsonSchema = createConfigSchemaJson ( ) ;
355- const commandsSchema = jsonSchema . definitions [ 'ccusage-config' ] . properties . commands ;
356-
357- expect ( commandsSchema . properties ) . toHaveProperty ( 'daily' ) ;
358- expect ( commandsSchema . properties ) . toHaveProperty ( 'monthly' ) ;
359- expect ( commandsSchema . properties ) . toHaveProperty ( 'weekly' ) ;
360- expect ( commandsSchema . properties ) . toHaveProperty ( 'session' ) ;
361- expect ( commandsSchema . properties ) . toHaveProperty ( 'blocks' ) ;
362- expect ( commandsSchema . properties ) . toHaveProperty ( 'statusline' ) ;
472+ const mainSchema = configSchemaDefinition ( jsonSchema ) ;
473+ const commandsSchema = schemaProperties ( mainSchema ) . commands ?? { } ;
474+ const commandProperties = schemaProperties ( commandsSchema ) ;
475+
476+ expect ( commandProperties ) . toHaveProperty ( 'daily' ) ;
477+ expect ( commandProperties ) . toHaveProperty ( 'monthly' ) ;
478+ expect ( commandProperties ) . toHaveProperty ( 'weekly' ) ;
479+ expect ( commandProperties ) . toHaveProperty ( 'session' ) ;
480+ expect ( commandProperties ) . not . toHaveProperty ( 'codex:daily' ) ;
481+ } ) ;
482+
483+ it ( 'should include agent command schemas under agent namespaces' , ( ) => {
484+ const jsonSchema = createConfigSchemaJson ( ) ;
485+ const mainSchema = configSchemaDefinition ( jsonSchema ) ;
486+ const properties = schemaProperties ( mainSchema ) ;
487+ const claudeCommands = schemaProperties ( properties . claude ?? { } ) . commands ?? { } ;
488+ const codexCommands = schemaProperties ( properties . codex ?? { } ) . commands ?? { } ;
489+ const claudeCommandProperties = schemaProperties ( claudeCommands ) ;
490+ const codexCommandProperties = schemaProperties ( codexCommands ) ;
491+
492+ expect ( claudeCommandProperties ) . toHaveProperty ( 'daily' ) ;
493+ expect ( claudeCommandProperties ) . toHaveProperty ( 'blocks' ) ;
494+ expect ( claudeCommandProperties ) . toHaveProperty ( 'statusline' ) ;
495+ expect ( codexCommandProperties ) . toHaveProperty ( 'daily' ) ;
496+ expect ( codexCommandProperties ) . toHaveProperty ( 'monthly' ) ;
497+ expect ( codexCommandProperties ) . toHaveProperty ( 'session' ) ;
363498 } ) ;
364499 } ) ;
365500}
0 commit comments