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 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,3 @@
1
+ import type { MetabaseClient } from '../client.js';
2
+ export declare function diff(client: MetabaseClient, resourceDir?: string): Promise<void>;
3
+ //# sourceMappingURL=diff.d.ts.map
@@ -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":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAczC,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,MAAsB,EAAE,cAAsB,GAAG;IAC1E,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAA;IAEtD,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAA;IACzC,MAAM,cAAc,GAAG,MAAM,aAAa,CAAC,WAAW,CAAC,CAAA;IAEvD,OAAO,CAAC,GAAG,CAAC,0CAA0C,CAAC,CAAA;IACvD,MAAM,QAAQ,GAAG,IAAI,QAAQ,EAAE,CAAA;IAC/B,MAAM,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IAE3B,MAAM,iBAAiB,GAAG,MAAM,MAAM,CAAC,cAAc,EAAE,CAAA;IACvD,MAAM,WAAW,GAAG,MAAM,MAAM,CAAC,QAAQ,EAAE,CAAA;IAC3C,MAAM,oBAAoB,GAAG,MAAM,MAAM,CAAC,aAAa,EAAE,CAAA;IAEzD,MAAM,iBAAiB,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAA;IACpF,MAAM,WAAW,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAA;IAC1D,MAAM,gBAAgB,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAA;IAExE,MAAM,OAAO,GAAgB,EAAE,CAAA;IAE/B,mBAAmB;IACnB,MAAM,uBAAuB,GAAG,IAAI,GAAG,EAA8B,CAAA;IACrE,KAAK,MAAM,CAAC,IAAI,iBAAiB;QAAE,uBAAuB,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAA;IAEzE,MAAM,gBAAgB,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,YAAY,CAAC,CAAA;IAC9E,MAAM,eAAe,GAAG,IAAI,GAAG,EAAU,CAAA;IAEzC,KAAK,MAAM,KAAK,IAAI,gBAAgB,EAAE,CAAC;QACrC,eAAe,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAC/B,MAAM,MAAM,GAAG,uBAAuB,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QACtD,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAA;QACzE,CAAC;aAAM,CAAC;YACN,MAAM,OAAO,GAAG,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;YAC7C,OAAO,CAAC,IAAI,CAAC;gBACX,IAAI,EAAE,YAAY;gBAClB,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,MAAM,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW;gBACpD,OAAO;aACR,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IAED,KAAK,MAAM,MAAM,IAAI,iBAAiB,EAAE,CAAC;QACvC,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,IAAI,KAAK,eAAe,EAAE,CAAC;YACzE,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAA;QAC5E,CAAC;IACH,CAAC;IAED,iBAAiB;IACjB,MAAM,iBAAiB,GAAG,IAAI,GAAG,EAAwB,CAAA;IACzD,KAAK,MAAM,CAAC,IAAI,WAAW;QAAE,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAA;IAE7D,MAAM,cAAc,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,CAAA;IAC1E,MAAM,aAAa,GAAG,IAAI,GAAG,EAAU,CAAA;IAEvC,KAAK,MAAM,KAAK,IAAI,cAAc,EAAE,CAAC;QACnC,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAC7B,MAAM,MAAM,GAAG,iBAAiB,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAChD,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAA;QACvE,CAAC;aAAM,CAAC;YACN,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAA;YACrD,OAAO,CAAC,IAAI,CAAC;gBACX,IAAI,EAAE,UAAU;gBAChB,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,MAAM,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW;gBACpD,OAAO;aACR,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IAED,KAAK,MAAM,MAAM,IAAI,WAAW,EAAE,CAAC;QACjC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;YACpC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAA;QAC1E,CAAC;IACH,CAAC;IAED,kBAAkB;IAClB,MAAM,sBAAsB,GAAG,IAAI,GAAG,EAA6B,CAAA;IACnE,KAAK,MAAM,CAAC,IAAI,gBAAgB,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;QAC5C,sBAAsB,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;IAC7C,CAAC;IAED,MAAM,eAAe,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,WAAW,CAAC,CAAA;IAC5E,MAAM,cAAc,GAAG,IAAI,GAAG,EAAU,CAAA;IAExC,KAAK,MAAM,KAAK,IAAI,eAAe,EAAE,CAAC;QACpC,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAC9B,MAAM,MAAM,GAAG,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QACrD,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAA;QACxE,CAAC;aAAM,CAAC;YACN,MAAM,OAAO,GAAG,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAA;YACnE,OAAO,CAAC,IAAI,CAAC;gBACX,IAAI,EAAE,WAAW;gBACjB,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,MAAM,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW;gBACpD,OAAO;aACR,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IAED,KAAK,MAAM,CAAC,IAAI,CAAC,IAAI,sBAAsB,EAAE,CAAC;QAC5C,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YAC9B,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAA;QAC9D,CAAC;IACH,CAAC;IAED,gBAAgB;IAChB,SAAS,CAAC,OAAO,CAAC,CAAA;AACpB,CAAC;AAED,SAAS,cAAc,CAAC,KAAU,EAAE,MAA0B;IAC5D,MAAM,OAAO,GAAa,EAAE,CAAA;IAC5B,IAAI,KAAK,CAAC,WAAW,IAAI,KAAK,CAAC,WAAW,KAAK,CAAC,MAAM,CAAC,WAAW,IAAI,EAAE,CAAC,EAAE,CAAC;QAC1E,OAAO,CAAC,IAAI,CAAC,iBAAiB,MAAM,CAAC,WAAW,IAAI,EAAE,QAAQ,KAAK,CAAC,WAAW,GAAG,CAAC,CAAA;IACrF,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,SAAS,YAAY,CAAC,KAAU,EAAE,MAAoB,EAAE,QAAkB;IACxE,MAAM,OAAO,GAAa,EAAE,CAAA;IAE5B,MAAM,gBAAgB,GAAG,QAAQ,CAAC,iBAAiB,CAAC,MAAM,CAAC,aAAa,CAAC,CAAA;IACzE,IAAI,KAAK,CAAC,UAAU,IAAI,KAAK,CAAC,UAAU,KAAK,gBAAgB,EAAE,CAAC;QAC9D,OAAO,CAAC,IAAI,CAAC,gBAAgB,gBAAgB,QAAQ,KAAK,CAAC,UAAU,GAAG,CAAC,CAAA;IAC3E,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO,KAAK,MAAM,CAAC,OAAO,EAAE,CAAC;QACtD,OAAO,CAAC,IAAI,CAAC,aAAa,MAAM,CAAC,OAAO,QAAQ,KAAK,CAAC,OAAO,GAAG,CAAC,CAAA;IACnE,CAAC;IAED,MAAM,QAAQ,GAAG,QAAQ,CAAC,eAAe,CAAC,MAAM,CAAC,WAAW,CAAC,CAAA;IAC7D,IAAI,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAClD,OAAO,CAAC,IAAI,CAAC,cAAc,QAAQ,QAAQ,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAA;IAC/D,CAAC;IAED,4BAA4B;IAC5B,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,MAAM,CAAC,aAAa,EAAE,MAAM,EAAE,CAAC;QAC5D,MAAM,UAAU,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAA;QACrC,MAAM,WAAW,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;QACpE,IAAI,UAAU,KAAK,WAAW,EAAE,CAAC;YAC/B,OAAO,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAA;QACxC,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,SAAS,aAAa,CACpB,KAAU,EACV,MAAyB,EACzB,QAAwB,EACxB,QAAkB;IAElB,MAAM,OAAO,GAAa,EAAE,CAAA;IAE5B,MAAM,gBAAgB,GAAG,QAAQ,CAAC,iBAAiB,CAAC,MAAM,CAAC,aAAa,CAAC,CAAA;IACzE,IAAI,KAAK,CAAC,UAAU,IAAI,KAAK,CAAC,UAAU,KAAK,gBAAgB,EAAE,CAAC;QAC9D,OAAO,CAAC,IAAI,CAAC,gBAAgB,gBAAgB,QAAQ,KAAK,CAAC,UAAU,GAAG,CAAC,CAAA;IAC3E,CAAC;IAED,IAAI,KAAK,CAAC,WAAW,KAAK,SAAS,IAAI,KAAK,CAAC,WAAW,KAAK,CAAC,MAAM,CAAC,WAAW,IAAI,IAAI,CAAC,EAAE,CAAC;QAC1F,OAAO,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAA;IACrC,CAAC;IAED,qBAAqB;IACrB,MAAM,cAAc,GAAG,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,CAAA;IACjD,MAAM,eAAe,GAAG,CAAC,MAAM,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,MAAM,CAAA;IACvD,IAAI,cAAc,KAAK,eAAe,EAAE,CAAC;QACvC,OAAO,CAAC,IAAI,CAAC,UAAU,eAAe,MAAM,cAAc,EAAE,CAAC,CAAA;IAC/D,CAAC;IAED,mCAAmC;IACnC,MAAM,SAAS,GAAG,IAAI,GAAG,EAAwB,CAAA;IACjD,KAAK,MAAM,CAAC,IAAI,QAAQ;QAAE,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;IAEhD,MAAM,UAAU,GAAG,KAAK,CAAC,KAAK,IAAI,EAAE,CAAA;IACpC,MAAM,WAAW,GAAG,MAAM,CAAC,SAAS,IAAI,EAAE,CAAA;IAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;IAE9D,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAChC,MAAM,EAAE,GAAG,UAAU,CAAC,CAAC,CAAC,CAAA;QACxB,MAAM,EAAE,GAAG,WAAW,CAAC,CAAC,CAAC,CAAA;QAEzB,IAAI,UAAU,IAAI,EAAE,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC;YACnC,MAAM,UAAU,GAAG,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,CAAC,CAAA;YAC5C,MAAM,UAAU,GAAG,UAAU,EAAE,IAAI,IAAI,QAAQ,EAAE,CAAC,OAAO,EAAE,CAAA;YAE3D,IAAI,EAAE,CAAC,QAAQ,KAAK,UAAU,EAAE,CAAC;gBAC/B,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,gBAAgB,UAAU,QAAQ,EAAE,CAAC,QAAQ,GAAG,CAAC,CAAA;YACzE,CAAC;YACD,IAAI,EAAE,CAAC,KAAK,KAAK,EAAE,CAAC,MAAM,IAAI,EAAE,CAAC,MAAM,KAAK,EAAE,CAAC,MAAM,EAAE,CAAC;gBACtD,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,QAAQ,WAAW,EAAE,CAAC,MAAM,IAAI,EAAE,CAAC,MAAM,MAAM,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,MAAM,EAAE,CAAC,CAAA;YACxG,CAAC;YACD,IAAI,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,EAAE,CAAC;gBAC3C,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,QAAQ,gBAAgB,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,GAAG,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,GAAG,GAAG,CAAC,CAAA;YACrG,CAAC;QACH,CAAC;IACH,CAAC;IAED,qBAAqB;IACrB,MAAM,WAAW,GAAG,CAAC,KAAK,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,MAAM,CAAA;IACnD,MAAM,YAAY,GAAG,CAAC,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,MAAM,CAAA;IACrD,IAAI,WAAW,KAAK,YAAY,EAAE,CAAC;QACjC,OAAO,CAAC,IAAI,CAAC,eAAe,YAAY,MAAM,WAAW,EAAE,CAAC,CAAA;IAC9D,CAAC;IAED,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,SAAS,SAAS,CAAC,OAAoB;IACrC,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,OAAO,CAAC,CAAA;IACzD,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,CAAC,CAAA;IAC7D,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,CAAC,CAAA;IAC7D,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,WAAW,CAAC,CAAA;IAEjE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,+CAA+C,CAAC,CAAC,CAAA;QACzE,OAAM;IACR,CAAC;IAED,qBAAqB;IACrB,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,sBAAsB,KAAK,CAAC,MAAM,MAAM,CAAC,CAAC,CAAA;QACvE,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;YACtB,OAAO,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QACvE,CAAC;IACH,CAAC;IAED,wBAAwB;IACxB,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,yBAAyB,OAAO,CAAC,MAAM,MAAM,CAAC,CAAC,CAAA;QAC1E,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACxB,OAAO,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QACrE,CAAC;IACH,CAAC;IAED,UAAU;IACV,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,gBAAgB,OAAO,CAAC,MAAM,MAAM,CAAC,CAAC,CAAA;QACpE,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACxB,OAAO,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;YACtE,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC;gBAChC,OAAO,CAAC,GAAG,CAAC,SAAS,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;YACvC,CAAC;QACH,CAAC;IACH,CAAC;IAED,UAAU;IACV,OAAO,CAAC,GAAG,CACT,KAAK,CAAC,IAAI,CACR,cAAc,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC,WAAW,KAAK,CAAC,MAAM,CAAC,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,aAAa,KAAK,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,iBAAiB,SAAS,CAAC,MAAM,YAAY,CACpL,CACF,CAAA;AACH,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function init(targetDir?: string): Promise<void>;
2
+ //# sourceMappingURL=init.d.ts.map
@@ -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.1.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
- "scripts": {
30
- "build": "tsc",
31
- "prepublishOnly": "pnpm build",
32
- "dev": "tsx src/cli.ts",
33
- "pull": "tsx src/cli.ts pull",
34
- "plan": "tsx src/cli.ts plan",
35
- "apply": "tsx src/cli.ts apply",
36
- "databases": "tsx src/cli.ts databases",
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
+ }