Skip to content

Commit 8cc97fa

Browse files
committed
feat: add MotherDuck support to CLI
Add --mother-duck-token option to create-duckdb command to enable MotherDuck connections. Addresses feedback from PR #124. Changes: - Add --mother-duck-token flag for MotherDuck API token with MOTHERDUCK_TOKEN environment variable support - Remove -d short option from --database-path to avoid conflict with global --debug flag - Wire motherDuckToken through createDuckDbConnectionCommand - Add comprehensive test coverage for token option (CLI flag, environment variable, and without token) The token is properly wired through from CLI option (or env var) to the connection configuration. Commander.js automatically converts --mother-duck-token to motherDuckToken in the options object, matching the DuckDBConnectionOptions interface. Signed-off-by: James Swirhun <james@ms2.co>
1 parent 3c11258 commit 8cc97fa

2 files changed

Lines changed: 135 additions & 1 deletion

File tree

src/cli.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,17 @@ export function createCLI(): Command {
231231
.command('create-duckdb')
232232
.description('add a new DuckDB database connection')
233233
.argument('<name>')
234-
.option('-d, --database-path <database>')
234+
.addOption(
235+
new Option(
236+
'--database-path <database>',
237+
'path to DuckDB database file or MotherDuck database (e.g., "md:my_database")'
238+
)
239+
)
240+
.addOption(
241+
new Option('--mother-duck-token <token>', 'MotherDuck API token').env(
242+
'MOTHERDUCK_TOKEN'
243+
)
244+
)
235245
.action(createDuckDbConnectionCommand);
236246

237247
connections

test/commands/connections.spec.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import {Command} from '@commander-js/extra-typings';
2525
import {createCLI} from '../../src/cli';
2626
import path from 'path';
2727
import {errorMessage} from '../../src/util';
28+
import fs from 'fs';
29+
import os from 'os';
2830

2931
let cli: Command;
3032
let args: string[];
@@ -95,5 +97,127 @@ describe('commands', () => {
9597
);
9698
});
9799
});
100+
101+
describe('create-duckdb', () => {
102+
let tempConfigPath: string;
103+
104+
beforeEach(() => {
105+
// Create a temporary config file for each test
106+
const tempDir = fs.mkdtempSync(
107+
path.join(os.tmpdir(), 'malloy-cli-test-')
108+
);
109+
tempConfigPath = path.join(tempDir, 'config.json');
110+
fs.writeFileSync(
111+
tempConfigPath,
112+
JSON.stringify({connections: []}, null, 2)
113+
);
114+
});
115+
116+
afterEach(() => {
117+
// Clean up temporary config file
118+
if (tempConfigPath && fs.existsSync(tempConfigPath)) {
119+
const configDir = path.dirname(tempConfigPath);
120+
fs.unlinkSync(tempConfigPath);
121+
fs.rmdirSync(configDir);
122+
}
123+
});
124+
125+
it('creates a DuckDB connection with motherDuckToken from command line', async () => {
126+
await runWith(
127+
'-c',
128+
tempConfigPath,
129+
'connections',
130+
'create-duckdb',
131+
'test-motherduck',
132+
'--database-path',
133+
'md:my_database',
134+
'--mother-duck-token',
135+
'test-token-123'
136+
);
137+
138+
// Verify the connection was created with the token
139+
const configContent = JSON.parse(
140+
fs.readFileSync(tempConfigPath, 'utf-8')
141+
);
142+
const connection = configContent.connections.find(
143+
(c: {name: string}) => c.name === 'test-motherduck'
144+
);
145+
expect(connection).toBeDefined();
146+
expect(connection.backend).toBe('duckdb');
147+
expect(connection.databasePath).toBe('md:my_database');
148+
expect(connection.motherDuckToken).toBe('test-token-123');
149+
});
150+
151+
it('creates a DuckDB connection with motherDuckToken from environment variable', async () => {
152+
const originalEnv = process.env.MOTHERDUCK_TOKEN;
153+
process.env.MOTHERDUCK_TOKEN = 'env-token-456';
154+
155+
try {
156+
await runWith(
157+
'-c',
158+
tempConfigPath,
159+
'connections',
160+
'create-duckdb',
161+
'test-motherduck-env',
162+
'--database-path',
163+
'md:'
164+
);
165+
166+
// Verify the connection was created with the token from env
167+
const configContent = JSON.parse(
168+
fs.readFileSync(tempConfigPath, 'utf-8')
169+
);
170+
const connection = configContent.connections.find(
171+
(c: {name: string}) => c.name === 'test-motherduck-env'
172+
);
173+
expect(connection).toBeDefined();
174+
expect(connection.backend).toBe('duckdb');
175+
expect(connection.databasePath).toBe('md:');
176+
expect(connection.motherDuckToken).toBe('env-token-456');
177+
} finally {
178+
// Restore original environment variable
179+
if (originalEnv !== undefined) {
180+
process.env.MOTHERDUCK_TOKEN = originalEnv;
181+
} else {
182+
delete process.env.MOTHERDUCK_TOKEN;
183+
}
184+
}
185+
});
186+
187+
it('creates a DuckDB connection without motherDuckToken', async () => {
188+
// Clear any existing MOTHERDUCK_TOKEN from the environment
189+
const originalEnv = process.env.MOTHERDUCK_TOKEN;
190+
delete process.env.MOTHERDUCK_TOKEN;
191+
192+
try {
193+
await runWith(
194+
'-c',
195+
tempConfigPath,
196+
'connections',
197+
'create-duckdb',
198+
'test-duckdb-local',
199+
'--database-path',
200+
'/path/to/local.duckdb'
201+
);
202+
203+
// Verify the connection was created without the token
204+
const configContent = JSON.parse(
205+
fs.readFileSync(tempConfigPath, 'utf-8')
206+
);
207+
const connection = configContent.connections.find(
208+
(c: {name: string}) => c.name === 'test-duckdb-local'
209+
);
210+
expect(connection).toBeDefined();
211+
expect(connection.backend).toBe('duckdb');
212+
expect(connection.databasePath).toBe('/path/to/local.duckdb');
213+
expect(connection.motherDuckToken).toBeUndefined();
214+
} finally {
215+
// Restore original environment variable
216+
if (originalEnv !== undefined) {
217+
process.env.MOTHERDUCK_TOKEN = originalEnv;
218+
}
219+
}
220+
});
221+
});
98222
});
99223
});

0 commit comments

Comments
 (0)