metabase-iac 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +28 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/diff.d.ts +3 -0
- package/dist/commands/diff.d.ts.map +1 -0
- package/dist/commands/diff.js +216 -0
- package/dist/commands/diff.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +183 -0
- package/dist/commands/init.js.map +1 -0
- package/package.json +22 -13
- package/src/cli.ts +28 -0
- package/src/commands/diff.ts +274 -0
- package/src/commands/init.ts +204 -0
package/dist/cli.js
CHANGED
|
@@ -4,12 +4,26 @@ import chalk from 'chalk';
|
|
|
4
4
|
import { createClient } from './auth.js';
|
|
5
5
|
import { apply } from './commands/apply.js';
|
|
6
6
|
import { pull } from './commands/pull.js';
|
|
7
|
+
import { init } from './commands/init.js';
|
|
8
|
+
import { diff } from './commands/diff.js';
|
|
7
9
|
import { saveState } from './state.js';
|
|
8
10
|
const program = new Command();
|
|
9
11
|
program
|
|
10
12
|
.name('metabase-iac')
|
|
11
13
|
.description('Infrastructure as Code for Metabase')
|
|
12
14
|
.version('0.1.0');
|
|
15
|
+
program
|
|
16
|
+
.command('init [directory]')
|
|
17
|
+
.description('Scaffold a new metabase-iac project')
|
|
18
|
+
.action(async (directory) => {
|
|
19
|
+
try {
|
|
20
|
+
await init(directory);
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
13
27
|
program
|
|
14
28
|
.command('pull')
|
|
15
29
|
.description('Import all collections, questions, dashboards from Metabase into local files')
|
|
@@ -28,6 +42,20 @@ program
|
|
|
28
42
|
process.exit(1);
|
|
29
43
|
}
|
|
30
44
|
});
|
|
45
|
+
program
|
|
46
|
+
.command('diff')
|
|
47
|
+
.description('Compare local resources against the current Metabase state')
|
|
48
|
+
.option('-d, --dir <dir>', 'Resources directory', '.')
|
|
49
|
+
.action(async (opts) => {
|
|
50
|
+
try {
|
|
51
|
+
const client = await createClient();
|
|
52
|
+
await diff(client, opts.dir);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
31
59
|
program
|
|
32
60
|
.command('plan')
|
|
33
61
|
.description('Show what changes would be applied (dry run)')
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../src/commands/diff.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAelD,wBAAsB,IAAI,CAAC,MAAM,EAAE,cAAc,EAAE,WAAW,GAAE,MAAY,iBAgH3E"}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { Resolver } from '../resolver.js';
|
|
3
|
+
export async function diff(client, resourceDir = '.') {
|
|
4
|
+
const { loadResources } = await import('../loader.js');
|
|
5
|
+
console.log('Loading local resources...');
|
|
6
|
+
const localResources = await loadResources(resourceDir);
|
|
7
|
+
console.log('Fetching remote state from Metabase...\n');
|
|
8
|
+
const resolver = new Resolver();
|
|
9
|
+
await resolver.init(client);
|
|
10
|
+
const remoteCollections = await client.getCollections();
|
|
11
|
+
const remoteCards = await client.getCards();
|
|
12
|
+
const remoteDashboardsList = await client.getDashboards();
|
|
13
|
+
const activeCollections = remoteCollections.filter((c) => !c.archived && c.id !== 0);
|
|
14
|
+
const activeCards = remoteCards.filter((c) => !c.archived);
|
|
15
|
+
const activeDashboards = remoteDashboardsList.filter((d) => !d.archived);
|
|
16
|
+
const entries = [];
|
|
17
|
+
// Diff collections
|
|
18
|
+
const remoteCollectionsByName = new Map();
|
|
19
|
+
for (const c of activeCollections)
|
|
20
|
+
remoteCollectionsByName.set(c.name, c);
|
|
21
|
+
const localCollections = localResources.filter((r) => r.kind === 'collection');
|
|
22
|
+
const seenCollections = new Set();
|
|
23
|
+
for (const local of localCollections) {
|
|
24
|
+
seenCollections.add(local.name);
|
|
25
|
+
const remote = remoteCollectionsByName.get(local.name);
|
|
26
|
+
if (!remote) {
|
|
27
|
+
entries.push({ kind: 'collection', name: local.name, status: 'added' });
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
const changes = diffCollection(local, remote);
|
|
31
|
+
entries.push({
|
|
32
|
+
kind: 'collection',
|
|
33
|
+
name: local.name,
|
|
34
|
+
status: changes.length > 0 ? 'changed' : 'unchanged',
|
|
35
|
+
changes,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
for (const remote of activeCollections) {
|
|
40
|
+
if (!seenCollections.has(remote.name) && remote.name !== 'Our analytics') {
|
|
41
|
+
entries.push({ kind: 'collection', name: remote.name, status: 'removed' });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Diff questions
|
|
45
|
+
const remoteCardsByName = new Map();
|
|
46
|
+
for (const c of activeCards)
|
|
47
|
+
remoteCardsByName.set(c.name, c);
|
|
48
|
+
const localQuestions = localResources.filter((r) => r.kind === 'question');
|
|
49
|
+
const seenQuestions = new Set();
|
|
50
|
+
for (const local of localQuestions) {
|
|
51
|
+
seenQuestions.add(local.name);
|
|
52
|
+
const remote = remoteCardsByName.get(local.name);
|
|
53
|
+
if (!remote) {
|
|
54
|
+
entries.push({ kind: 'question', name: local.name, status: 'added' });
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const changes = diffQuestion(local, remote, resolver);
|
|
58
|
+
entries.push({
|
|
59
|
+
kind: 'question',
|
|
60
|
+
name: local.name,
|
|
61
|
+
status: changes.length > 0 ? 'changed' : 'unchanged',
|
|
62
|
+
changes,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const remote of activeCards) {
|
|
67
|
+
if (!seenQuestions.has(remote.name)) {
|
|
68
|
+
entries.push({ kind: 'question', name: remote.name, status: 'removed' });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Diff dashboards
|
|
72
|
+
const remoteDashboardsByName = new Map();
|
|
73
|
+
for (const d of activeDashboards) {
|
|
74
|
+
const full = await client.getDashboard(d.id);
|
|
75
|
+
remoteDashboardsByName.set(full.name, full);
|
|
76
|
+
}
|
|
77
|
+
const localDashboards = localResources.filter((r) => r.kind === 'dashboard');
|
|
78
|
+
const seenDashboards = new Set();
|
|
79
|
+
for (const local of localDashboards) {
|
|
80
|
+
seenDashboards.add(local.name);
|
|
81
|
+
const remote = remoteDashboardsByName.get(local.name);
|
|
82
|
+
if (!remote) {
|
|
83
|
+
entries.push({ kind: 'dashboard', name: local.name, status: 'added' });
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
const changes = diffDashboard(local, remote, activeCards, resolver);
|
|
87
|
+
entries.push({
|
|
88
|
+
kind: 'dashboard',
|
|
89
|
+
name: local.name,
|
|
90
|
+
status: changes.length > 0 ? 'changed' : 'unchanged',
|
|
91
|
+
changes,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
for (const [name] of remoteDashboardsByName) {
|
|
96
|
+
if (!seenDashboards.has(name)) {
|
|
97
|
+
entries.push({ kind: 'dashboard', name, status: 'removed' });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Print results
|
|
101
|
+
printDiff(entries);
|
|
102
|
+
}
|
|
103
|
+
function diffCollection(local, remote) {
|
|
104
|
+
const changes = [];
|
|
105
|
+
if (local.description && local.description !== (remote.description ?? '')) {
|
|
106
|
+
changes.push(`description: "${remote.description ?? ''}" → "${local.description}"`);
|
|
107
|
+
}
|
|
108
|
+
return changes;
|
|
109
|
+
}
|
|
110
|
+
function diffQuestion(local, remote, resolver) {
|
|
111
|
+
const changes = [];
|
|
112
|
+
const remoteCollection = resolver.getCollectionName(remote.collection_id);
|
|
113
|
+
if (local.collection && local.collection !== remoteCollection) {
|
|
114
|
+
changes.push(`collection: "${remoteCollection}" → "${local.collection}"`);
|
|
115
|
+
}
|
|
116
|
+
if (local.display && local.display !== remote.display) {
|
|
117
|
+
changes.push(`display: "${remote.display}" → "${local.display}"`);
|
|
118
|
+
}
|
|
119
|
+
const remoteDb = resolver.getDatabaseName(remote.database_id);
|
|
120
|
+
if (local.database && local.database !== remoteDb) {
|
|
121
|
+
changes.push(`database: "${remoteDb}" → "${local.database}"`);
|
|
122
|
+
}
|
|
123
|
+
// Compare native query text
|
|
124
|
+
if (local.type === 'native' && remote.dataset_query?.native) {
|
|
125
|
+
const localQuery = local.query.trim();
|
|
126
|
+
const remoteQuery = (remote.dataset_query.native.query ?? '').trim();
|
|
127
|
+
if (localQuery !== remoteQuery) {
|
|
128
|
+
changes.push('query: content changed');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return changes;
|
|
132
|
+
}
|
|
133
|
+
function diffDashboard(local, remote, allCards, resolver) {
|
|
134
|
+
const changes = [];
|
|
135
|
+
const remoteCollection = resolver.getCollectionName(remote.collection_id);
|
|
136
|
+
if (local.collection && local.collection !== remoteCollection) {
|
|
137
|
+
changes.push(`collection: "${remoteCollection}" → "${local.collection}"`);
|
|
138
|
+
}
|
|
139
|
+
if (local.description !== undefined && local.description !== (remote.description ?? null)) {
|
|
140
|
+
changes.push(`description changed`);
|
|
141
|
+
}
|
|
142
|
+
// Compare card count
|
|
143
|
+
const localCardCount = (local.cards ?? []).length;
|
|
144
|
+
const remoteCardCount = (remote.dashcards ?? []).length;
|
|
145
|
+
if (localCardCount !== remoteCardCount) {
|
|
146
|
+
changes.push(`cards: ${remoteCardCount} → ${localCardCount}`);
|
|
147
|
+
}
|
|
148
|
+
// Compare card positions and sizes
|
|
149
|
+
const cardsById = new Map();
|
|
150
|
+
for (const c of allCards)
|
|
151
|
+
cardsById.set(c.id, c);
|
|
152
|
+
const localCards = local.cards ?? [];
|
|
153
|
+
const remoteCards = remote.dashcards ?? [];
|
|
154
|
+
const minLen = Math.min(localCards.length, remoteCards.length);
|
|
155
|
+
for (let i = 0; i < minLen; i++) {
|
|
156
|
+
const lc = localCards[i];
|
|
157
|
+
const rc = remoteCards[i];
|
|
158
|
+
if ('question' in lc && rc.card_id) {
|
|
159
|
+
const remoteCard = cardsById.get(rc.card_id);
|
|
160
|
+
const remoteName = remoteCard?.name ?? `card-${rc.card_id}`;
|
|
161
|
+
if (lc.question !== remoteName) {
|
|
162
|
+
changes.push(`card[${i}]: question "${remoteName}" → "${lc.question}"`);
|
|
163
|
+
}
|
|
164
|
+
if (lc.width !== rc.size_x || lc.height !== rc.size_y) {
|
|
165
|
+
changes.push(`card[${i}] "${lc.question}": size ${rc.size_x}x${rc.size_y} → ${lc.width}x${lc.height}`);
|
|
166
|
+
}
|
|
167
|
+
if (lc.row !== rc.row || lc.col !== rc.col) {
|
|
168
|
+
changes.push(`card[${i}] "${lc.question}": position [${rc.row},${rc.col}] → [${lc.row},${lc.col}]`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Compare parameters
|
|
173
|
+
const localParams = (local.parameters ?? []).length;
|
|
174
|
+
const remoteParams = (remote.parameters ?? []).length;
|
|
175
|
+
if (localParams !== remoteParams) {
|
|
176
|
+
changes.push(`parameters: ${remoteParams} → ${localParams}`);
|
|
177
|
+
}
|
|
178
|
+
return changes;
|
|
179
|
+
}
|
|
180
|
+
function printDiff(entries) {
|
|
181
|
+
const added = entries.filter((e) => e.status === 'added');
|
|
182
|
+
const removed = entries.filter((e) => e.status === 'removed');
|
|
183
|
+
const changed = entries.filter((e) => e.status === 'changed');
|
|
184
|
+
const unchanged = entries.filter((e) => e.status === 'unchanged');
|
|
185
|
+
if (added.length === 0 && removed.length === 0 && changed.length === 0) {
|
|
186
|
+
console.log(chalk.green('No differences. Local state matches Metabase.'));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// Added (local only)
|
|
190
|
+
if (added.length > 0) {
|
|
191
|
+
console.log(chalk.green.bold(`\n+ Added locally (${added.length}):\n`));
|
|
192
|
+
for (const e of added) {
|
|
193
|
+
console.log(` ${chalk.green('+')} ${e.kind}: ${chalk.bold(e.name)}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Removed (remote only)
|
|
197
|
+
if (removed.length > 0) {
|
|
198
|
+
console.log(chalk.red.bold(`\n- Only in Metabase (${removed.length}):\n`));
|
|
199
|
+
for (const e of removed) {
|
|
200
|
+
console.log(` ${chalk.red('-')} ${e.kind}: ${chalk.bold(e.name)}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Changed
|
|
204
|
+
if (changed.length > 0) {
|
|
205
|
+
console.log(chalk.yellow.bold(`\n~ Changed (${changed.length}):\n`));
|
|
206
|
+
for (const e of changed) {
|
|
207
|
+
console.log(` ${chalk.yellow('~')} ${e.kind}: ${chalk.bold(e.name)}`);
|
|
208
|
+
for (const c of e.changes ?? []) {
|
|
209
|
+
console.log(` ${chalk.gray(c)}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Summary
|
|
214
|
+
console.log(chalk.bold(`\nSummary: ${chalk.green(`+${added.length}`)} added, ${chalk.yellow(`~${changed.length}`)} changed, ${chalk.red(`-${removed.length}`)} remote only, ${unchanged.length} unchanged`));
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=diff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.js","sourceRoot":"","sources":["../../src/commands/diff.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AA0BA,wBAAsB,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,iBAiL5C"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join, resolve, basename } from 'node:path';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { createInterface } from 'node:readline';
|
|
6
|
+
function prompt(rl, question, defaultValue) {
|
|
7
|
+
const suffix = defaultValue ? ` (${defaultValue})` : '';
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
rl.question(`${question}${suffix}: `, (answer) => {
|
|
10
|
+
resolve(answer.trim() || defaultValue || '');
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
function promptChoice(rl, question, choices) {
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
console.log(`\n${question}`);
|
|
17
|
+
choices.forEach((c, i) => console.log(` ${chalk.cyan(String(i + 1))}. ${c}`));
|
|
18
|
+
rl.question(`Choice (1-${choices.length}): `, (answer) => {
|
|
19
|
+
const idx = parseInt(answer.trim(), 10) - 1;
|
|
20
|
+
resolve(choices[idx] ?? choices[0]);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export async function init(targetDir) {
|
|
25
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
26
|
+
try {
|
|
27
|
+
console.log(chalk.bold('\n🏗 metabase-iac init\n'));
|
|
28
|
+
console.log('This will scaffold a new project to manage your Metabase instance as code.\n');
|
|
29
|
+
// Project name / directory
|
|
30
|
+
const dirName = targetDir || await prompt(rl, 'Project directory name', 'my-metabase');
|
|
31
|
+
const projectDir = resolve(dirName);
|
|
32
|
+
const projectName = basename(projectDir);
|
|
33
|
+
if (existsSync(projectDir) && existsSync(join(projectDir, 'package.json'))) {
|
|
34
|
+
console.log(chalk.yellow(`\n${projectDir} already has a package.json. Aborting.`));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
// Metabase URL
|
|
38
|
+
const metabaseUrl = await prompt(rl, 'Metabase URL', 'https://metabase.example.com');
|
|
39
|
+
// Auth method
|
|
40
|
+
const authMethod = await promptChoice(rl, 'How do you want to authenticate?', [
|
|
41
|
+
'Username + Password',
|
|
42
|
+
'Session Token',
|
|
43
|
+
'API Key',
|
|
44
|
+
]);
|
|
45
|
+
let envLines = `METABASE_URL=${metabaseUrl}\n`;
|
|
46
|
+
let envExampleLines = `METABASE_URL=${metabaseUrl}\n`;
|
|
47
|
+
if (authMethod === 'Username + Password') {
|
|
48
|
+
const username = await prompt(rl, 'Metabase admin email');
|
|
49
|
+
envLines += `METABASE_USERNAME=${username}\nMETABASE_PASSWORD=\n`;
|
|
50
|
+
envExampleLines += `METABASE_USERNAME=${username}\nMETABASE_PASSWORD=your-password\n`;
|
|
51
|
+
}
|
|
52
|
+
else if (authMethod === 'Session Token') {
|
|
53
|
+
envLines += `METABASE_SESSION_TOKEN=\n`;
|
|
54
|
+
envExampleLines += `METABASE_SESSION_TOKEN=your-session-token\n`;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
envLines += `METABASE_API_KEY=\n`;
|
|
58
|
+
envExampleLines += `METABASE_API_KEY=mb_xxxx\n`;
|
|
59
|
+
}
|
|
60
|
+
// Package manager
|
|
61
|
+
const pkgManager = await promptChoice(rl, 'Package manager?', ['pnpm', 'npm', 'yarn']);
|
|
62
|
+
// Pull now?
|
|
63
|
+
const pullNow = await prompt(rl, 'Pull resources from Metabase after setup? (y/n)', 'y');
|
|
64
|
+
console.log(chalk.bold('\nCreating project...\n'));
|
|
65
|
+
// Create directories
|
|
66
|
+
await mkdir(join(projectDir, 'collections'), { recursive: true });
|
|
67
|
+
await mkdir(join(projectDir, 'questions'), { recursive: true });
|
|
68
|
+
await mkdir(join(projectDir, 'dashboards'), { recursive: true });
|
|
69
|
+
// package.json
|
|
70
|
+
const packageJson = {
|
|
71
|
+
name: projectName,
|
|
72
|
+
version: '0.1.0',
|
|
73
|
+
private: true,
|
|
74
|
+
description: `Metabase IaC resources for ${metabaseUrl}`,
|
|
75
|
+
type: 'module',
|
|
76
|
+
scripts: {
|
|
77
|
+
pull: 'metabase-iac pull',
|
|
78
|
+
diff: 'metabase-iac diff',
|
|
79
|
+
plan: 'metabase-iac plan',
|
|
80
|
+
apply: 'metabase-iac apply',
|
|
81
|
+
databases: 'metabase-iac databases',
|
|
82
|
+
collections: 'metabase-iac collections',
|
|
83
|
+
},
|
|
84
|
+
dependencies: {
|
|
85
|
+
'metabase-iac': '^0.1.0',
|
|
86
|
+
},
|
|
87
|
+
devDependencies: {
|
|
88
|
+
typescript: '^5.9.0',
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
await writeFile(join(projectDir, 'package.json'), JSON.stringify(packageJson, null, 2) + '\n');
|
|
92
|
+
console.log(` ${chalk.green('+')} package.json`);
|
|
93
|
+
// tsconfig.json
|
|
94
|
+
const tsconfig = {
|
|
95
|
+
compilerOptions: {
|
|
96
|
+
target: 'ES2022',
|
|
97
|
+
module: 'ES2022',
|
|
98
|
+
moduleResolution: 'bundler',
|
|
99
|
+
esModuleInterop: true,
|
|
100
|
+
strict: true,
|
|
101
|
+
skipLibCheck: true,
|
|
102
|
+
noEmit: true,
|
|
103
|
+
},
|
|
104
|
+
include: ['collections/**/*', 'questions/**/*', 'dashboards/**/*'],
|
|
105
|
+
};
|
|
106
|
+
await writeFile(join(projectDir, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2) + '\n');
|
|
107
|
+
console.log(` ${chalk.green('+')} tsconfig.json`);
|
|
108
|
+
// .env
|
|
109
|
+
await writeFile(join(projectDir, '.env'), envLines);
|
|
110
|
+
console.log(` ${chalk.green('+')} .env`);
|
|
111
|
+
// .env.example
|
|
112
|
+
await writeFile(join(projectDir, '.env.example'), envExampleLines);
|
|
113
|
+
console.log(` ${chalk.green('+')} .env.example`);
|
|
114
|
+
// .gitignore
|
|
115
|
+
await writeFile(join(projectDir, '.gitignore'), 'node_modules/\n.env\n.metabase-state.json\n');
|
|
116
|
+
console.log(` ${chalk.green('+')} .gitignore`);
|
|
117
|
+
// README.md
|
|
118
|
+
const readme = `# ${projectName}
|
|
119
|
+
|
|
120
|
+
Metabase IaC resources for [${metabaseUrl}](${metabaseUrl}).
|
|
121
|
+
|
|
122
|
+
Managed with [metabase-iac](https://www.npmjs.com/package/metabase-iac).
|
|
123
|
+
|
|
124
|
+
## Setup
|
|
125
|
+
|
|
126
|
+
\`\`\`bash
|
|
127
|
+
${pkgManager} install
|
|
128
|
+
cp .env.example .env
|
|
129
|
+
# Edit .env with your credentials
|
|
130
|
+
\`\`\`
|
|
131
|
+
|
|
132
|
+
## Commands
|
|
133
|
+
|
|
134
|
+
\`\`\`bash
|
|
135
|
+
${pkgManager} run pull # Import from Metabase
|
|
136
|
+
${pkgManager} run plan # Preview changes (dry run)
|
|
137
|
+
${pkgManager} run apply # Push changes to Metabase
|
|
138
|
+
${pkgManager} run databases # List databases
|
|
139
|
+
${pkgManager} run collections # List collections
|
|
140
|
+
\`\`\`
|
|
141
|
+
|
|
142
|
+
## Structure
|
|
143
|
+
|
|
144
|
+
\`\`\`
|
|
145
|
+
collections/ # Metabase collections
|
|
146
|
+
questions/ # Saved questions (SQL, MongoDB, structured)
|
|
147
|
+
dashboards/ # Dashboards with card layouts
|
|
148
|
+
\`\`\`
|
|
149
|
+
`;
|
|
150
|
+
await writeFile(join(projectDir, 'README.md'), readme);
|
|
151
|
+
console.log(` ${chalk.green('+')} README.md`);
|
|
152
|
+
// Git init
|
|
153
|
+
console.log(`\n${chalk.bold('Initializing git repository...')}`);
|
|
154
|
+
const { execSync } = await import('node:child_process');
|
|
155
|
+
execSync('git init', { cwd: projectDir, stdio: 'inherit' });
|
|
156
|
+
console.log(` ${chalk.green('+')} .git`);
|
|
157
|
+
// Install dependencies
|
|
158
|
+
console.log(`\n${chalk.bold('Installing dependencies...')}\n`);
|
|
159
|
+
execSync(`${pkgManager} install`, { cwd: projectDir, stdio: 'inherit' });
|
|
160
|
+
console.log(chalk.green('\nProject created!\n'));
|
|
161
|
+
// Pull
|
|
162
|
+
if (pullNow.toLowerCase() === 'y') {
|
|
163
|
+
console.log(chalk.bold('Pulling resources from Metabase...\n'));
|
|
164
|
+
try {
|
|
165
|
+
execSync('npx metabase-iac pull', { cwd: projectDir, stdio: 'inherit' });
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
console.log(chalk.yellow('\nPull failed — check your .env credentials and try again with:'));
|
|
169
|
+
console.log(chalk.cyan(` cd ${dirName} && ${pkgManager} run pull\n`));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
console.log(chalk.bold('Next steps:\n'));
|
|
174
|
+
console.log(` ${chalk.cyan('cd')} ${dirName}`);
|
|
175
|
+
console.log(` ${chalk.cyan('# Edit .env with your credentials')}`);
|
|
176
|
+
console.log(` ${chalk.cyan(`${pkgManager} run pull`)}\n`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
rl.close();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=init.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metabase-iac",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Infrastructure as Code for Metabase — manage collections, questions, dashboards as code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/types.js",
|
|
@@ -26,17 +26,14 @@
|
|
|
26
26
|
"src",
|
|
27
27
|
"README.md"
|
|
28
28
|
],
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
"collections": "tsx src/cli.ts collections"
|
|
38
|
-
},
|
|
39
|
-
"keywords": ["metabase", "iac", "infrastructure-as-code", "dashboards", "mongodb", "sql"],
|
|
29
|
+
"keywords": [
|
|
30
|
+
"metabase",
|
|
31
|
+
"iac",
|
|
32
|
+
"infrastructure-as-code",
|
|
33
|
+
"dashboards",
|
|
34
|
+
"mongodb",
|
|
35
|
+
"sql"
|
|
36
|
+
],
|
|
40
37
|
"license": "MIT",
|
|
41
38
|
"dependencies": {
|
|
42
39
|
"chalk": "^5.6.2",
|
|
@@ -49,5 +46,17 @@
|
|
|
49
46
|
"devDependencies": {
|
|
50
47
|
"@types/node": "^25.5.0",
|
|
51
48
|
"typescript": "^5.9.3"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsc",
|
|
52
|
+
"dev": "tsx src/cli.ts",
|
|
53
|
+
"pull": "tsx src/cli.ts pull",
|
|
54
|
+
"plan": "tsx src/cli.ts plan",
|
|
55
|
+
"apply": "tsx src/cli.ts apply",
|
|
56
|
+
"databases": "tsx src/cli.ts databases",
|
|
57
|
+
"collections": "tsx src/cli.ts collections",
|
|
58
|
+
"release:patch": "pnpm version patch && pnpm publish",
|
|
59
|
+
"release:minor": "pnpm version minor && pnpm publish",
|
|
60
|
+
"release:major": "pnpm version major && pnpm publish"
|
|
52
61
|
}
|
|
53
|
-
}
|
|
62
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -4,6 +4,8 @@ import chalk from 'chalk'
|
|
|
4
4
|
import { createClient } from './auth.js'
|
|
5
5
|
import { apply } from './commands/apply.js'
|
|
6
6
|
import { pull } from './commands/pull.js'
|
|
7
|
+
import { init } from './commands/init.js'
|
|
8
|
+
import { diff } from './commands/diff.js'
|
|
7
9
|
import { saveState } from './state.js'
|
|
8
10
|
|
|
9
11
|
const program = new Command()
|
|
@@ -13,6 +15,18 @@ program
|
|
|
13
15
|
.description('Infrastructure as Code for Metabase')
|
|
14
16
|
.version('0.1.0')
|
|
15
17
|
|
|
18
|
+
program
|
|
19
|
+
.command('init [directory]')
|
|
20
|
+
.description('Scaffold a new metabase-iac project')
|
|
21
|
+
.action(async (directory) => {
|
|
22
|
+
try {
|
|
23
|
+
await init(directory)
|
|
24
|
+
} catch (err: any) {
|
|
25
|
+
console.error(chalk.red(`Error: ${err.message}`))
|
|
26
|
+
process.exit(1)
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
16
30
|
program
|
|
17
31
|
.command('pull')
|
|
18
32
|
.description('Import all collections, questions, dashboards from Metabase into local files')
|
|
@@ -31,6 +45,20 @@ program
|
|
|
31
45
|
}
|
|
32
46
|
})
|
|
33
47
|
|
|
48
|
+
program
|
|
49
|
+
.command('diff')
|
|
50
|
+
.description('Compare local resources against the current Metabase state')
|
|
51
|
+
.option('-d, --dir <dir>', 'Resources directory', '.')
|
|
52
|
+
.action(async (opts) => {
|
|
53
|
+
try {
|
|
54
|
+
const client = await createClient()
|
|
55
|
+
await diff(client, opts.dir)
|
|
56
|
+
} catch (err: any) {
|
|
57
|
+
console.error(chalk.red(`Error: ${err.message}`))
|
|
58
|
+
process.exit(1)
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
34
62
|
program
|
|
35
63
|
.command('plan')
|
|
36
64
|
.description('Show what changes would be applied (dry run)')
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import type { MetabaseClient } from '../client.js'
|
|
3
|
+
import { Resolver } from '../resolver.js'
|
|
4
|
+
import type {
|
|
5
|
+
MetabaseCard,
|
|
6
|
+
MetabaseCollection,
|
|
7
|
+
MetabaseDashboard,
|
|
8
|
+
} from '../types.js'
|
|
9
|
+
|
|
10
|
+
interface DiffEntry {
|
|
11
|
+
kind: 'collection' | 'question' | 'dashboard'
|
|
12
|
+
name: string
|
|
13
|
+
status: 'added' | 'removed' | 'changed' | 'unchanged'
|
|
14
|
+
changes?: string[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function diff(client: MetabaseClient, resourceDir: string = '.') {
|
|
18
|
+
const { loadResources } = await import('../loader.js')
|
|
19
|
+
|
|
20
|
+
console.log('Loading local resources...')
|
|
21
|
+
const localResources = await loadResources(resourceDir)
|
|
22
|
+
|
|
23
|
+
console.log('Fetching remote state from Metabase...\n')
|
|
24
|
+
const resolver = new Resolver()
|
|
25
|
+
await resolver.init(client)
|
|
26
|
+
|
|
27
|
+
const remoteCollections = await client.getCollections()
|
|
28
|
+
const remoteCards = await client.getCards()
|
|
29
|
+
const remoteDashboardsList = await client.getDashboards()
|
|
30
|
+
|
|
31
|
+
const activeCollections = remoteCollections.filter((c) => !c.archived && c.id !== 0)
|
|
32
|
+
const activeCards = remoteCards.filter((c) => !c.archived)
|
|
33
|
+
const activeDashboards = remoteDashboardsList.filter((d) => !d.archived)
|
|
34
|
+
|
|
35
|
+
const entries: DiffEntry[] = []
|
|
36
|
+
|
|
37
|
+
// Diff collections
|
|
38
|
+
const remoteCollectionsByName = new Map<string, MetabaseCollection>()
|
|
39
|
+
for (const c of activeCollections) remoteCollectionsByName.set(c.name, c)
|
|
40
|
+
|
|
41
|
+
const localCollections = localResources.filter((r) => r.kind === 'collection')
|
|
42
|
+
const seenCollections = new Set<string>()
|
|
43
|
+
|
|
44
|
+
for (const local of localCollections) {
|
|
45
|
+
seenCollections.add(local.name)
|
|
46
|
+
const remote = remoteCollectionsByName.get(local.name)
|
|
47
|
+
if (!remote) {
|
|
48
|
+
entries.push({ kind: 'collection', name: local.name, status: 'added' })
|
|
49
|
+
} else {
|
|
50
|
+
const changes = diffCollection(local, remote)
|
|
51
|
+
entries.push({
|
|
52
|
+
kind: 'collection',
|
|
53
|
+
name: local.name,
|
|
54
|
+
status: changes.length > 0 ? 'changed' : 'unchanged',
|
|
55
|
+
changes,
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const remote of activeCollections) {
|
|
61
|
+
if (!seenCollections.has(remote.name) && remote.name !== 'Our analytics') {
|
|
62
|
+
entries.push({ kind: 'collection', name: remote.name, status: 'removed' })
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Diff questions
|
|
67
|
+
const remoteCardsByName = new Map<string, MetabaseCard>()
|
|
68
|
+
for (const c of activeCards) remoteCardsByName.set(c.name, c)
|
|
69
|
+
|
|
70
|
+
const localQuestions = localResources.filter((r) => r.kind === 'question')
|
|
71
|
+
const seenQuestions = new Set<string>()
|
|
72
|
+
|
|
73
|
+
for (const local of localQuestions) {
|
|
74
|
+
seenQuestions.add(local.name)
|
|
75
|
+
const remote = remoteCardsByName.get(local.name)
|
|
76
|
+
if (!remote) {
|
|
77
|
+
entries.push({ kind: 'question', name: local.name, status: 'added' })
|
|
78
|
+
} else {
|
|
79
|
+
const changes = diffQuestion(local, remote, resolver)
|
|
80
|
+
entries.push({
|
|
81
|
+
kind: 'question',
|
|
82
|
+
name: local.name,
|
|
83
|
+
status: changes.length > 0 ? 'changed' : 'unchanged',
|
|
84
|
+
changes,
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const remote of activeCards) {
|
|
90
|
+
if (!seenQuestions.has(remote.name)) {
|
|
91
|
+
entries.push({ kind: 'question', name: remote.name, status: 'removed' })
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Diff dashboards
|
|
96
|
+
const remoteDashboardsByName = new Map<string, MetabaseDashboard>()
|
|
97
|
+
for (const d of activeDashboards) {
|
|
98
|
+
const full = await client.getDashboard(d.id)
|
|
99
|
+
remoteDashboardsByName.set(full.name, full)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const localDashboards = localResources.filter((r) => r.kind === 'dashboard')
|
|
103
|
+
const seenDashboards = new Set<string>()
|
|
104
|
+
|
|
105
|
+
for (const local of localDashboards) {
|
|
106
|
+
seenDashboards.add(local.name)
|
|
107
|
+
const remote = remoteDashboardsByName.get(local.name)
|
|
108
|
+
if (!remote) {
|
|
109
|
+
entries.push({ kind: 'dashboard', name: local.name, status: 'added' })
|
|
110
|
+
} else {
|
|
111
|
+
const changes = diffDashboard(local, remote, activeCards, resolver)
|
|
112
|
+
entries.push({
|
|
113
|
+
kind: 'dashboard',
|
|
114
|
+
name: local.name,
|
|
115
|
+
status: changes.length > 0 ? 'changed' : 'unchanged',
|
|
116
|
+
changes,
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const [name] of remoteDashboardsByName) {
|
|
122
|
+
if (!seenDashboards.has(name)) {
|
|
123
|
+
entries.push({ kind: 'dashboard', name, status: 'removed' })
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Print results
|
|
128
|
+
printDiff(entries)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function diffCollection(local: any, remote: MetabaseCollection): string[] {
|
|
132
|
+
const changes: string[] = []
|
|
133
|
+
if (local.description && local.description !== (remote.description ?? '')) {
|
|
134
|
+
changes.push(`description: "${remote.description ?? ''}" → "${local.description}"`)
|
|
135
|
+
}
|
|
136
|
+
return changes
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function diffQuestion(local: any, remote: MetabaseCard, resolver: Resolver): string[] {
|
|
140
|
+
const changes: string[] = []
|
|
141
|
+
|
|
142
|
+
const remoteCollection = resolver.getCollectionName(remote.collection_id)
|
|
143
|
+
if (local.collection && local.collection !== remoteCollection) {
|
|
144
|
+
changes.push(`collection: "${remoteCollection}" → "${local.collection}"`)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (local.display && local.display !== remote.display) {
|
|
148
|
+
changes.push(`display: "${remote.display}" → "${local.display}"`)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const remoteDb = resolver.getDatabaseName(remote.database_id)
|
|
152
|
+
if (local.database && local.database !== remoteDb) {
|
|
153
|
+
changes.push(`database: "${remoteDb}" → "${local.database}"`)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Compare native query text
|
|
157
|
+
if (local.type === 'native' && remote.dataset_query?.native) {
|
|
158
|
+
const localQuery = local.query.trim()
|
|
159
|
+
const remoteQuery = (remote.dataset_query.native.query ?? '').trim()
|
|
160
|
+
if (localQuery !== remoteQuery) {
|
|
161
|
+
changes.push('query: content changed')
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return changes
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function diffDashboard(
|
|
169
|
+
local: any,
|
|
170
|
+
remote: MetabaseDashboard,
|
|
171
|
+
allCards: MetabaseCard[],
|
|
172
|
+
resolver: Resolver,
|
|
173
|
+
): string[] {
|
|
174
|
+
const changes: string[] = []
|
|
175
|
+
|
|
176
|
+
const remoteCollection = resolver.getCollectionName(remote.collection_id)
|
|
177
|
+
if (local.collection && local.collection !== remoteCollection) {
|
|
178
|
+
changes.push(`collection: "${remoteCollection}" → "${local.collection}"`)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (local.description !== undefined && local.description !== (remote.description ?? null)) {
|
|
182
|
+
changes.push(`description changed`)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Compare card count
|
|
186
|
+
const localCardCount = (local.cards ?? []).length
|
|
187
|
+
const remoteCardCount = (remote.dashcards ?? []).length
|
|
188
|
+
if (localCardCount !== remoteCardCount) {
|
|
189
|
+
changes.push(`cards: ${remoteCardCount} → ${localCardCount}`)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Compare card positions and sizes
|
|
193
|
+
const cardsById = new Map<number, MetabaseCard>()
|
|
194
|
+
for (const c of allCards) cardsById.set(c.id, c)
|
|
195
|
+
|
|
196
|
+
const localCards = local.cards ?? []
|
|
197
|
+
const remoteCards = remote.dashcards ?? []
|
|
198
|
+
const minLen = Math.min(localCards.length, remoteCards.length)
|
|
199
|
+
|
|
200
|
+
for (let i = 0; i < minLen; i++) {
|
|
201
|
+
const lc = localCards[i]
|
|
202
|
+
const rc = remoteCards[i]
|
|
203
|
+
|
|
204
|
+
if ('question' in lc && rc.card_id) {
|
|
205
|
+
const remoteCard = cardsById.get(rc.card_id)
|
|
206
|
+
const remoteName = remoteCard?.name ?? `card-${rc.card_id}`
|
|
207
|
+
|
|
208
|
+
if (lc.question !== remoteName) {
|
|
209
|
+
changes.push(`card[${i}]: question "${remoteName}" → "${lc.question}"`)
|
|
210
|
+
}
|
|
211
|
+
if (lc.width !== rc.size_x || lc.height !== rc.size_y) {
|
|
212
|
+
changes.push(`card[${i}] "${lc.question}": size ${rc.size_x}x${rc.size_y} → ${lc.width}x${lc.height}`)
|
|
213
|
+
}
|
|
214
|
+
if (lc.row !== rc.row || lc.col !== rc.col) {
|
|
215
|
+
changes.push(`card[${i}] "${lc.question}": position [${rc.row},${rc.col}] → [${lc.row},${lc.col}]`)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Compare parameters
|
|
221
|
+
const localParams = (local.parameters ?? []).length
|
|
222
|
+
const remoteParams = (remote.parameters ?? []).length
|
|
223
|
+
if (localParams !== remoteParams) {
|
|
224
|
+
changes.push(`parameters: ${remoteParams} → ${localParams}`)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return changes
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function printDiff(entries: DiffEntry[]) {
|
|
231
|
+
const added = entries.filter((e) => e.status === 'added')
|
|
232
|
+
const removed = entries.filter((e) => e.status === 'removed')
|
|
233
|
+
const changed = entries.filter((e) => e.status === 'changed')
|
|
234
|
+
const unchanged = entries.filter((e) => e.status === 'unchanged')
|
|
235
|
+
|
|
236
|
+
if (added.length === 0 && removed.length === 0 && changed.length === 0) {
|
|
237
|
+
console.log(chalk.green('No differences. Local state matches Metabase.'))
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Added (local only)
|
|
242
|
+
if (added.length > 0) {
|
|
243
|
+
console.log(chalk.green.bold(`\n+ Added locally (${added.length}):\n`))
|
|
244
|
+
for (const e of added) {
|
|
245
|
+
console.log(` ${chalk.green('+')} ${e.kind}: ${chalk.bold(e.name)}`)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Removed (remote only)
|
|
250
|
+
if (removed.length > 0) {
|
|
251
|
+
console.log(chalk.red.bold(`\n- Only in Metabase (${removed.length}):\n`))
|
|
252
|
+
for (const e of removed) {
|
|
253
|
+
console.log(` ${chalk.red('-')} ${e.kind}: ${chalk.bold(e.name)}`)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Changed
|
|
258
|
+
if (changed.length > 0) {
|
|
259
|
+
console.log(chalk.yellow.bold(`\n~ Changed (${changed.length}):\n`))
|
|
260
|
+
for (const e of changed) {
|
|
261
|
+
console.log(` ${chalk.yellow('~')} ${e.kind}: ${chalk.bold(e.name)}`)
|
|
262
|
+
for (const c of e.changes ?? []) {
|
|
263
|
+
console.log(` ${chalk.gray(c)}`)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Summary
|
|
269
|
+
console.log(
|
|
270
|
+
chalk.bold(
|
|
271
|
+
`\nSummary: ${chalk.green(`+${added.length}`)} added, ${chalk.yellow(`~${changed.length}`)} changed, ${chalk.red(`-${removed.length}`)} remote only, ${unchanged.length} unchanged`,
|
|
272
|
+
),
|
|
273
|
+
)
|
|
274
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { join, resolve, basename } from 'node:path'
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
import { createInterface } from 'node:readline'
|
|
6
|
+
|
|
7
|
+
function prompt(rl: ReturnType<typeof createInterface>, question: string, defaultValue?: string): Promise<string> {
|
|
8
|
+
const suffix = defaultValue ? ` (${defaultValue})` : ''
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
rl.question(`${question}${suffix}: `, (answer) => {
|
|
11
|
+
resolve(answer.trim() || defaultValue || '')
|
|
12
|
+
})
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function promptChoice(rl: ReturnType<typeof createInterface>, question: string, choices: string[]): Promise<string> {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
console.log(`\n${question}`)
|
|
19
|
+
choices.forEach((c, i) => console.log(` ${chalk.cyan(String(i + 1))}. ${c}`))
|
|
20
|
+
rl.question(`Choice (1-${choices.length}): `, (answer) => {
|
|
21
|
+
const idx = parseInt(answer.trim(), 10) - 1
|
|
22
|
+
resolve(choices[idx] ?? choices[0])
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function init(targetDir?: string) {
|
|
28
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
console.log(chalk.bold('\n🏗 metabase-iac init\n'))
|
|
32
|
+
console.log('This will scaffold a new project to manage your Metabase instance as code.\n')
|
|
33
|
+
|
|
34
|
+
// Project name / directory
|
|
35
|
+
const dirName = targetDir || await prompt(rl, 'Project directory name', 'my-metabase')
|
|
36
|
+
const projectDir = resolve(dirName)
|
|
37
|
+
const projectName = basename(projectDir)
|
|
38
|
+
|
|
39
|
+
if (existsSync(projectDir) && existsSync(join(projectDir, 'package.json'))) {
|
|
40
|
+
console.log(chalk.yellow(`\n${projectDir} already has a package.json. Aborting.`))
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Metabase URL
|
|
45
|
+
const metabaseUrl = await prompt(rl, 'Metabase URL', 'https://metabase.example.com')
|
|
46
|
+
|
|
47
|
+
// Auth method
|
|
48
|
+
const authMethod = await promptChoice(rl, 'How do you want to authenticate?', [
|
|
49
|
+
'Username + Password',
|
|
50
|
+
'Session Token',
|
|
51
|
+
'API Key',
|
|
52
|
+
])
|
|
53
|
+
|
|
54
|
+
let envLines = `METABASE_URL=${metabaseUrl}\n`
|
|
55
|
+
let envExampleLines = `METABASE_URL=${metabaseUrl}\n`
|
|
56
|
+
|
|
57
|
+
if (authMethod === 'Username + Password') {
|
|
58
|
+
const username = await prompt(rl, 'Metabase admin email')
|
|
59
|
+
envLines += `METABASE_USERNAME=${username}\nMETABASE_PASSWORD=\n`
|
|
60
|
+
envExampleLines += `METABASE_USERNAME=${username}\nMETABASE_PASSWORD=your-password\n`
|
|
61
|
+
} else if (authMethod === 'Session Token') {
|
|
62
|
+
envLines += `METABASE_SESSION_TOKEN=\n`
|
|
63
|
+
envExampleLines += `METABASE_SESSION_TOKEN=your-session-token\n`
|
|
64
|
+
} else {
|
|
65
|
+
envLines += `METABASE_API_KEY=\n`
|
|
66
|
+
envExampleLines += `METABASE_API_KEY=mb_xxxx\n`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Package manager
|
|
70
|
+
const pkgManager = await promptChoice(rl, 'Package manager?', ['pnpm', 'npm', 'yarn'])
|
|
71
|
+
|
|
72
|
+
// Pull now?
|
|
73
|
+
const pullNow = await prompt(rl, 'Pull resources from Metabase after setup? (y/n)', 'y')
|
|
74
|
+
|
|
75
|
+
console.log(chalk.bold('\nCreating project...\n'))
|
|
76
|
+
|
|
77
|
+
// Create directories
|
|
78
|
+
await mkdir(join(projectDir, 'collections'), { recursive: true })
|
|
79
|
+
await mkdir(join(projectDir, 'questions'), { recursive: true })
|
|
80
|
+
await mkdir(join(projectDir, 'dashboards'), { recursive: true })
|
|
81
|
+
|
|
82
|
+
// package.json
|
|
83
|
+
const packageJson = {
|
|
84
|
+
name: projectName,
|
|
85
|
+
version: '0.1.0',
|
|
86
|
+
private: true,
|
|
87
|
+
description: `Metabase IaC resources for ${metabaseUrl}`,
|
|
88
|
+
type: 'module',
|
|
89
|
+
scripts: {
|
|
90
|
+
pull: 'metabase-iac pull',
|
|
91
|
+
diff: 'metabase-iac diff',
|
|
92
|
+
plan: 'metabase-iac plan',
|
|
93
|
+
apply: 'metabase-iac apply',
|
|
94
|
+
databases: 'metabase-iac databases',
|
|
95
|
+
collections: 'metabase-iac collections',
|
|
96
|
+
},
|
|
97
|
+
dependencies: {
|
|
98
|
+
'metabase-iac': '^0.1.0',
|
|
99
|
+
},
|
|
100
|
+
devDependencies: {
|
|
101
|
+
typescript: '^5.9.0',
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
await writeFile(join(projectDir, 'package.json'), JSON.stringify(packageJson, null, 2) + '\n')
|
|
105
|
+
console.log(` ${chalk.green('+')} package.json`)
|
|
106
|
+
|
|
107
|
+
// tsconfig.json
|
|
108
|
+
const tsconfig = {
|
|
109
|
+
compilerOptions: {
|
|
110
|
+
target: 'ES2022',
|
|
111
|
+
module: 'ES2022',
|
|
112
|
+
moduleResolution: 'bundler',
|
|
113
|
+
esModuleInterop: true,
|
|
114
|
+
strict: true,
|
|
115
|
+
skipLibCheck: true,
|
|
116
|
+
noEmit: true,
|
|
117
|
+
},
|
|
118
|
+
include: ['collections/**/*', 'questions/**/*', 'dashboards/**/*'],
|
|
119
|
+
}
|
|
120
|
+
await writeFile(join(projectDir, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2) + '\n')
|
|
121
|
+
console.log(` ${chalk.green('+')} tsconfig.json`)
|
|
122
|
+
|
|
123
|
+
// .env
|
|
124
|
+
await writeFile(join(projectDir, '.env'), envLines)
|
|
125
|
+
console.log(` ${chalk.green('+')} .env`)
|
|
126
|
+
|
|
127
|
+
// .env.example
|
|
128
|
+
await writeFile(join(projectDir, '.env.example'), envExampleLines)
|
|
129
|
+
console.log(` ${chalk.green('+')} .env.example`)
|
|
130
|
+
|
|
131
|
+
// .gitignore
|
|
132
|
+
await writeFile(
|
|
133
|
+
join(projectDir, '.gitignore'),
|
|
134
|
+
'node_modules/\n.env\n.metabase-state.json\n',
|
|
135
|
+
)
|
|
136
|
+
console.log(` ${chalk.green('+')} .gitignore`)
|
|
137
|
+
|
|
138
|
+
// README.md
|
|
139
|
+
const readme = `# ${projectName}
|
|
140
|
+
|
|
141
|
+
Metabase IaC resources for [${metabaseUrl}](${metabaseUrl}).
|
|
142
|
+
|
|
143
|
+
Managed with [metabase-iac](https://www.npmjs.com/package/metabase-iac).
|
|
144
|
+
|
|
145
|
+
## Setup
|
|
146
|
+
|
|
147
|
+
\`\`\`bash
|
|
148
|
+
${pkgManager} install
|
|
149
|
+
cp .env.example .env
|
|
150
|
+
# Edit .env with your credentials
|
|
151
|
+
\`\`\`
|
|
152
|
+
|
|
153
|
+
## Commands
|
|
154
|
+
|
|
155
|
+
\`\`\`bash
|
|
156
|
+
${pkgManager} run pull # Import from Metabase
|
|
157
|
+
${pkgManager} run plan # Preview changes (dry run)
|
|
158
|
+
${pkgManager} run apply # Push changes to Metabase
|
|
159
|
+
${pkgManager} run databases # List databases
|
|
160
|
+
${pkgManager} run collections # List collections
|
|
161
|
+
\`\`\`
|
|
162
|
+
|
|
163
|
+
## Structure
|
|
164
|
+
|
|
165
|
+
\`\`\`
|
|
166
|
+
collections/ # Metabase collections
|
|
167
|
+
questions/ # Saved questions (SQL, MongoDB, structured)
|
|
168
|
+
dashboards/ # Dashboards with card layouts
|
|
169
|
+
\`\`\`
|
|
170
|
+
`
|
|
171
|
+
await writeFile(join(projectDir, 'README.md'), readme)
|
|
172
|
+
console.log(` ${chalk.green('+')} README.md`)
|
|
173
|
+
|
|
174
|
+
// Git init
|
|
175
|
+
console.log(`\n${chalk.bold('Initializing git repository...')}`)
|
|
176
|
+
const { execSync } = await import('node:child_process')
|
|
177
|
+
execSync('git init', { cwd: projectDir, stdio: 'inherit' })
|
|
178
|
+
console.log(` ${chalk.green('+')} .git`)
|
|
179
|
+
|
|
180
|
+
// Install dependencies
|
|
181
|
+
console.log(`\n${chalk.bold('Installing dependencies...')}\n`)
|
|
182
|
+
execSync(`${pkgManager} install`, { cwd: projectDir, stdio: 'inherit' })
|
|
183
|
+
|
|
184
|
+
console.log(chalk.green('\nProject created!\n'))
|
|
185
|
+
|
|
186
|
+
// Pull
|
|
187
|
+
if (pullNow.toLowerCase() === 'y') {
|
|
188
|
+
console.log(chalk.bold('Pulling resources from Metabase...\n'))
|
|
189
|
+
try {
|
|
190
|
+
execSync('npx metabase-iac pull', { cwd: projectDir, stdio: 'inherit' })
|
|
191
|
+
} catch {
|
|
192
|
+
console.log(chalk.yellow('\nPull failed — check your .env credentials and try again with:'))
|
|
193
|
+
console.log(chalk.cyan(` cd ${dirName} && ${pkgManager} run pull\n`))
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
console.log(chalk.bold('Next steps:\n'))
|
|
197
|
+
console.log(` ${chalk.cyan('cd')} ${dirName}`)
|
|
198
|
+
console.log(` ${chalk.cyan('# Edit .env with your credentials')}`)
|
|
199
|
+
console.log(` ${chalk.cyan(`${pkgManager} run pull`)}\n`)
|
|
200
|
+
}
|
|
201
|
+
} finally {
|
|
202
|
+
rl.close()
|
|
203
|
+
}
|
|
204
|
+
}
|