Skip to content

Commit ef22284

Browse files
a6b8claude
andcommitted
Add v3 CLI support and migrate command #12
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 109d4a1 commit ef22284

5 files changed

Lines changed: 832 additions & 10 deletions

File tree

src/index.mjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ const args = parseArgs( {
1818
'no-cache': { type: 'boolean' },
1919
'refresh': { type: 'boolean' },
2020
'file': { type: 'string' },
21+
'all': { type: 'boolean' },
22+
'dry-run': { type: 'boolean' },
2123
'help': { type: 'boolean', short: 'h' }
2224
}
2325
} )
@@ -272,6 +274,16 @@ const runCommand = async () => {
272274
return true
273275
}
274276

277+
if( command === 'migrate' ) {
278+
const targetPath = positionals[ 1 ]
279+
const all = values[ 'all' ] || false
280+
const dryRun = values[ 'dry-run' ] || false
281+
const { result } = await FlowMcpCli.migrate( { 'schemaPath': targetPath, cwd, all, dryRun } )
282+
output( { result } )
283+
284+
return true
285+
}
286+
275287
if( command === 'validate' ) {
276288
const group = values[ 'group' ]
277289
const { result } = await FlowMcpCli.validate( { schemaPath, cwd, group } )

src/task/FlowMcpCli.mjs

Lines changed: 263 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1377,9 +1377,13 @@ class FlowMcpCli {
13771377
const results = groupSchemas
13781378
.map( ( { main, file } ) => {
13791379
try {
1380-
const { status, messages } = FlowMCP.validateMain( { main } )
1380+
const normalizedMain = FlowMcpCli.#normalizeMainForValidation( { main } )
1381+
const { status, messages } = FlowMCP.validateMain( { 'main': normalizedMain } )
13811382
const namespace = main[ 'namespace' ] || 'unknown'
1382-
const entry = { file, namespace, status, messages }
1383+
const toolCount = Object.keys( main[ 'tools' ] || main[ 'routes' ] || {} ).length
1384+
const resourceCount = Object.keys( main[ 'resources' ] || {} ).length
1385+
const skillCount = ( main[ 'skills' ] || [] ).length
1386+
const entry = { file, namespace, status, messages, 'tools': toolCount, 'resources': resourceCount, 'skills': skillCount }
13831387

13841388
return entry
13851389
} catch( err ) {
@@ -1432,9 +1436,13 @@ class FlowMcpCli {
14321436
const results = schemas
14331437
.map( ( { main, file } ) => {
14341438
try {
1435-
const { status, messages } = FlowMCP.validateMain( { main } )
1439+
const normalizedMain = FlowMcpCli.#normalizeMainForValidation( { main } )
1440+
const { status, messages } = FlowMCP.validateMain( { 'main': normalizedMain } )
14361441
const namespace = main[ 'namespace' ] || 'unknown'
1437-
const entry = { file, namespace, status, messages }
1442+
const toolCount = Object.keys( main[ 'tools' ] || main[ 'routes' ] || {} ).length
1443+
const resourceCount = Object.keys( main[ 'resources' ] || {} ).length
1444+
const skillCount = ( main[ 'skills' ] || [] ).length
1445+
const entry = { file, namespace, status, messages, 'tools': toolCount, 'resources': resourceCount, 'skills': skillCount }
14381446

14391447
return entry
14401448
} catch( err ) {
@@ -1471,6 +1479,169 @@ class FlowMcpCli {
14711479
}
14721480

14731481

1482+
static async migrate( { schemaPath, cwd, all = false, dryRun = false } ) {
1483+
const { initialized, error: initError, fix: initFix } = await FlowMcpCli.#requireInit()
1484+
if( !initialized ) {
1485+
const result = FlowMcpCli.#error( { 'error': initError, 'fix': initFix } )
1486+
1487+
return { result }
1488+
}
1489+
1490+
const { status: validStatus, messages: validMessages } = FlowMcpCli.validationMigrate( { schemaPath, all } )
1491+
if( !validStatus ) {
1492+
const result = { 'status': false, 'messages': validMessages }
1493+
1494+
return { result }
1495+
}
1496+
1497+
const routesKeyPattern = /(['"])routes\1(\s*:)/g
1498+
const versionPattern = /(['"]version['"])\s*:\s*['"]2\.\d+\.\d+['"]/g
1499+
1500+
let filePaths = []
1501+
1502+
if( all ) {
1503+
const dirPath = schemaPath || cwd
1504+
const resolvedDir = resolve( dirPath )
1505+
1506+
let dirStat
1507+
try {
1508+
dirStat = await stat( resolvedDir )
1509+
} catch {
1510+
const result = FlowMcpCli.#error( { 'error': `Path not found: ${dirPath}` } )
1511+
1512+
return { result }
1513+
}
1514+
1515+
if( !dirStat.isDirectory() ) {
1516+
const result = FlowMcpCli.#error( { 'error': `Path is not a directory: ${dirPath}. Use --all with a directory.` } )
1517+
1518+
return { result }
1519+
}
1520+
1521+
const { files } = await FlowMcpCli.#findSchemaFiles( { dirPath: resolvedDir } )
1522+
filePaths = files
1523+
} else {
1524+
const resolvedPath = resolve( schemaPath )
1525+
1526+
let pathStat
1527+
try {
1528+
pathStat = await stat( resolvedPath )
1529+
} catch {
1530+
const result = FlowMcpCli.#error( { 'error': `Path not found: ${schemaPath}` } )
1531+
1532+
return { result }
1533+
}
1534+
1535+
if( pathStat.isDirectory() ) {
1536+
const { files } = await FlowMcpCli.#findSchemaFiles( { dirPath: resolvedPath } )
1537+
filePaths = files
1538+
} else {
1539+
filePaths = [ resolvedPath ]
1540+
}
1541+
}
1542+
1543+
if( filePaths.length === 0 ) {
1544+
const result = {
1545+
'status': true,
1546+
'total': 0,
1547+
'migrated': 0,
1548+
'skipped': 0,
1549+
'failed': 0,
1550+
dryRun,
1551+
'results': []
1552+
}
1553+
1554+
return { result }
1555+
}
1556+
1557+
const results = []
1558+
let migrated = 0
1559+
let skipped = 0
1560+
let failed = 0
1561+
1562+
const processFile = async ( filePath ) => {
1563+
try {
1564+
const content = await readFile( filePath, 'utf-8' )
1565+
const hasRoutes = routesKeyPattern.test( content )
1566+
routesKeyPattern.lastIndex = 0
1567+
const hasV2Version = versionPattern.test( content )
1568+
versionPattern.lastIndex = 0
1569+
1570+
if( !hasRoutes && !hasV2Version ) {
1571+
skipped += 1
1572+
results.push( { 'file': filePath, 'action': 'skipped', 'reason': 'No v2 routes or version found' } )
1573+
1574+
return
1575+
}
1576+
1577+
if( dryRun ) {
1578+
migrated += 1
1579+
const changes = []
1580+
if( hasRoutes ) { changes.push( 'routes -> tools' ) }
1581+
if( hasV2Version ) { changes.push( 'version 2.x.x -> 3.0.0' ) }
1582+
results.push( { 'file': filePath, 'action': 'migrated', 'reason': `[dry-run] Would apply: ${changes.join( ', ' )}` } )
1583+
1584+
return
1585+
}
1586+
1587+
let updatedContent = content
1588+
updatedContent = updatedContent.replace( routesKeyPattern, '$1tools$1$2' )
1589+
routesKeyPattern.lastIndex = 0
1590+
updatedContent = updatedContent.replace( versionPattern, `$1: '3.0.0'` )
1591+
versionPattern.lastIndex = 0
1592+
1593+
await writeFile( filePath, updatedContent, 'utf-8' )
1594+
migrated += 1
1595+
results.push( { 'file': filePath, 'action': 'migrated', 'reason': 'Successfully migrated to v3' } )
1596+
} catch( err ) {
1597+
failed += 1
1598+
results.push( { 'file': filePath, 'action': 'failed', 'reason': err.message } )
1599+
}
1600+
}
1601+
1602+
await Promise.all(
1603+
filePaths
1604+
.map( ( filePath ) => {
1605+
const promise = processFile( filePath )
1606+
1607+
return promise
1608+
} )
1609+
)
1610+
1611+
const total = migrated + skipped + failed
1612+
const result = {
1613+
'status': failed === 0,
1614+
total,
1615+
migrated,
1616+
skipped,
1617+
failed,
1618+
dryRun,
1619+
results
1620+
}
1621+
1622+
return { result }
1623+
}
1624+
1625+
1626+
static validationMigrate( { schemaPath, all } ) {
1627+
const struct = { 'status': false, 'messages': [] }
1628+
1629+
if( !all && ( schemaPath === undefined || schemaPath === null ) ) {
1630+
struct[ 'messages' ].push( 'schemaPath: Missing value. Provide a path or use --all.' )
1631+
} else if( !all && typeof schemaPath !== 'string' ) {
1632+
struct[ 'messages' ].push( 'schemaPath: Must be a string.' )
1633+
}
1634+
1635+
if( struct[ 'messages' ].length > 0 ) {
1636+
return struct
1637+
}
1638+
1639+
struct[ 'status' ] = true
1640+
1641+
return struct
1642+
}
1643+
1644+
14741645
static async test( { schemaPath, route, cwd, group, all } ) {
14751646
const { initialized, error: initError, fix: initFix } = await FlowMcpCli.#requireInit()
14761647
if( !initialized ) {
@@ -2579,6 +2750,7 @@ class FlowMcpCli {
25792750
namespace,
25802751
tags,
25812752
score,
2753+
'type': tool[ 'type' ] || 'tool',
25822754
'add': `${appConfig[ 'cliCommand' ]} add ${toolName}`
25832755
}
25842756

@@ -3952,8 +4124,9 @@ class FlowMcpCli {
39524124
const filePath = join( schemasBaseDir, schemaRef )
39534125
const { main } = await FlowMcpCli.#loadSchema( { filePath } )
39544126

3955-
if( main && main[ 'routes' ] ) {
3956-
Object.entries( main[ 'routes' ] )
4127+
const toolEntries = main ? ( main[ 'tools' ] || main[ 'routes' ] ) : null
4128+
if( main && toolEntries ) {
4129+
Object.entries( toolEntries )
39574130
.forEach( ( [ routeName, routeConfig ] ) => {
39584131
const routeDescription = routeConfig[ 'description' ] || ''
39594132
const toolRef = `${schemaRef}::${routeName}`
@@ -3970,7 +4143,51 @@ class FlowMcpCli {
39704143
namespace,
39714144
'description': routeDescription,
39724145
'tags': main[ 'tags' ] || [],
3973-
'schemaName': main[ 'name' ] || ''
4146+
'schemaName': main[ 'name' ] || '',
4147+
'type': 'tool'
4148+
} )
4149+
} )
4150+
}
4151+
4152+
if( main && main[ 'resources' ] ) {
4153+
Object.entries( main[ 'resources' ] )
4154+
.forEach( ( [ resourceName, resourceConfig ] ) => {
4155+
const resourceDescription = resourceConfig[ 'description' ] || ''
4156+
const toolRef = `${schemaRef}::resource::${resourceName}`
4157+
const toolName = `${resourceName}_${main[ 'namespace' ] || namespace}`
4158+
4159+
tools.push( {
4160+
toolRef,
4161+
toolName,
4162+
schemaRef,
4163+
'routeName': resourceName,
4164+
namespace,
4165+
'description': resourceDescription,
4166+
'tags': main[ 'tags' ] || [],
4167+
'schemaName': main[ 'name' ] || '',
4168+
'type': 'resource'
4169+
} )
4170+
} )
4171+
}
4172+
4173+
if( main && main[ 'skills' ] ) {
4174+
main[ 'skills' ]
4175+
.forEach( ( skillDef ) => {
4176+
const skillName = skillDef[ 'name' ] || 'unknown'
4177+
const skillDescription = skillDef[ 'description' ] || ''
4178+
const toolRef = `${schemaRef}::skill::${skillName}`
4179+
const toolName = `${skillName}_${main[ 'namespace' ] || namespace}`
4180+
4181+
tools.push( {
4182+
toolRef,
4183+
toolName,
4184+
schemaRef,
4185+
'routeName': skillName,
4186+
namespace,
4187+
'description': skillDescription,
4188+
'tags': main[ 'tags' ] || [],
4189+
'schemaName': main[ 'name' ] || '',
4190+
'type': 'skill'
39744191
} )
39754192
} )
39764193
}
@@ -5389,10 +5606,10 @@ Note: Run "${cmd} init" first. This is the only interactive command.
53895606

53905607

53915608
static #getAllTests( { main } ) {
5392-
const routes = main[ 'routes' ] || {}
5609+
const tools = main[ 'tools' ] || main[ 'routes' ] || {}
53935610
const tests = []
53945611

5395-
Object.entries( routes )
5612+
Object.entries( tools )
53965613
.forEach( ( [ routeName, routeConfig ] ) => {
53975614
const routeTests = routeConfig[ 'tests' ] || []
53985615

@@ -5505,7 +5722,7 @@ Note: Run "${cmd} init" first. This is the only interactive command.
55055722

55065723
static #prepareServerTool( { main, handlerMap, serverParams, routeName } ) {
55075724
const namespace = main[ 'namespace' ] || 'unknown'
5508-
const routes = main[ 'routes' ] || {}
5725+
const routes = main[ 'tools' ] || main[ 'routes' ] || {}
55095726
const routeConfig = routes[ routeName ]
55105727

55115728
if( !routeConfig ) {
@@ -5943,6 +6160,26 @@ Note: Run "${cmd} init" first. This is the only interactive command.
59436160
}
59446161

59456162

6163+
static async #findSchemaFiles( { dirPath } ) {
6164+
const entries = await readdir( dirPath, { recursive: true } )
6165+
const files = entries
6166+
.filter( ( entry ) => {
6167+
const ext = extname( entry )
6168+
const isSchema = ext === '.mjs' || ext === '.js'
6169+
6170+
return isSchema
6171+
} )
6172+
.map( ( entry ) => {
6173+
const fullPath = join( dirPath, entry )
6174+
6175+
return fullPath
6176+
} )
6177+
.sort()
6178+
6179+
return { files }
6180+
}
6181+
6182+
59466183
static async #removeToolSchema( { toolName, cwd } ) {
59476184
const filePath = join( cwd, appConfig[ 'localConfigDirName' ], 'tools', `${toolName}.json` )
59486185

@@ -5954,6 +6191,22 @@ Note: Run "${cmd} init" first. This is the only interactive command.
59546191
return { 'removed': false }
59556192
}
59566193
}
6194+
6195+
6196+
static #normalizeMainForValidation( { main } ) {
6197+
if( !main ) {
6198+
return main
6199+
}
6200+
6201+
if( main[ 'tools' ] && !main[ 'routes' ] ) {
6202+
const { tools, resources, skills, ...rest } = main
6203+
const normalizedMain = { ...rest, 'routes': tools }
6204+
6205+
return normalizedMain
6206+
}
6207+
6208+
return main
6209+
}
59576210
}
59586211

59596212

0 commit comments

Comments
 (0)