#!/usr/bin/env node
import { Command } from 'commander'
import chalk from 'chalk'
import { createClient } from './auth.js'
import { apply } from './commands/apply.js'
import { pull } from './commands/pull.js'
import { init } from './commands/init.js'
import { diff } from './commands/diff.js'
import { saveState } from './state.js'
const program = new Command()
program
.name('metabase-iac')
.description('Infrastructure as Code for Metabase')
.version('0.1.0')
program
.command('init [directory]')
.description('Scaffold a new metabase-iac project')
.action(async (directory) => {
try {
await init(directory)
} catch (err: any) {
console.error(chalk.red(`Error: ${err.message}`))
process.exit(1)
}
})
program
.command('pull')
.description('Import all collections, questions, dashboards from Metabase into local files')
.option('-o, --output
', 'Output directory', '.')
.option('--import-base ', 'Import path for types in generated files', 'metabase-iac/types')
.action(async (opts) => {
try {
const client = await createClient()
console.log(chalk.bold('Pulling from Metabase...\n'))
const state = await pull(client, opts.output, opts.importBase)
await saveState(state)
console.log(chalk.green(`\nPull complete! ${state.length} resource(s) saved to ${opts.output}/`))
} catch (err: any) {
console.error(chalk.red(`Error: ${err.message}`))
process.exit(1)
}
})
program
.command('diff')
.description('Compare local resources against the current Metabase state')
.option('-d, --dir ', 'Resources directory', '.')
.action(async (opts) => {
try {
const client = await createClient()
await diff(client, opts.dir)
} catch (err: any) {
console.error(chalk.red(`Error: ${err.message}`))
process.exit(1)
}
})
program
.command('plan')
.description('Show what changes would be applied (dry run)')
.option('-d, --dir ', 'Resources directory', '.')
.action(async (opts) => {
try {
const client = await createClient()
await apply(client, true, opts.dir)
} catch (err: any) {
console.error(chalk.red(`Error: ${err.message}`))
process.exit(1)
}
})
program
.command('apply')
.description('Apply resource definitions to Metabase')
.option('-d, --dir ', 'Resources directory', '.')
.action(async (opts) => {
try {
const client = await createClient()
await apply(client, false, opts.dir)
} catch (err: any) {
console.error(chalk.red(`Error: ${err.message}`))
process.exit(1)
}
})
program
.command('databases')
.description('List available databases in Metabase')
.action(async () => {
try {
const client = await createClient()
const dbs = await client.getDatabases()
console.log(chalk.bold('Databases:\n'))
for (const db of dbs) {
console.log(` ${chalk.cyan(db.id.toString().padStart(3))} ${db.name} (${db.engine})`)
}
} catch (err: any) {
console.error(chalk.red(`Error: ${err.message}`))
process.exit(1)
}
})
program
.command('collections')
.description('List available collections in Metabase')
.action(async () => {
try {
const client = await createClient()
const collections = await client.getCollections()
console.log(chalk.bold('Collections:\n'))
for (const c of collections) {
if (c.archived) continue
const parent = c.parent_id ? ` (parent: ${c.parent_id})` : ''
console.log(` ${chalk.cyan(c.id.toString().padStart(3))} ${c.name}${chalk.gray(parent)}`)
}
} catch (err: any) {
console.error(chalk.red(`Error: ${err.message}`))
process.exit(1)
}
})
program.parse()