@lightdash/cli 0.2395.0 → 0.2397.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.
@@ -0,0 +1,10 @@
1
+ type AgentType = 'claude' | 'cursor' | 'codex';
2
+ type InstallSkillsOptions = {
3
+ verbose: boolean;
4
+ agent: AgentType;
5
+ global: boolean;
6
+ path?: string;
7
+ };
8
+ export declare const installSkillsHandler: (options: InstallSkillsOptions) => Promise<void>;
9
+ export {};
10
+ //# sourceMappingURL=installSkills.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"installSkills.d.ts","sourceRoot":"","sources":["../../src/handlers/installSkills.ts"],"names":[],"mappings":"AAYA,KAAK,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE/C,KAAK,oBAAoB,GAAG;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,SAAS,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAsMF,eAAO,MAAM,oBAAoB,YACpB,oBAAoB,KAC9B,OAAO,CAAC,IAAI,CA8Dd,CAAC"}
@@ -0,0 +1,198 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.installSkillsHandler = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const fs = tslib_1.__importStar(require("fs"));
6
+ const node_fetch_1 = tslib_1.__importDefault(require("node-fetch"));
7
+ const os = tslib_1.__importStar(require("os"));
8
+ const path = tslib_1.__importStar(require("path"));
9
+ const globalState_1 = tslib_1.__importDefault(require("../globalState"));
10
+ const styles = tslib_1.__importStar(require("../styles"));
11
+ const GITHUB_API_BASE = 'https://api.github.com/repos/lightdash/lightdash/contents';
12
+ const GITHUB_RAW_BASE = 'https://raw.githubusercontent.com/lightdash/lightdash/main';
13
+ function getAgentSkillsDir(agent) {
14
+ switch (agent) {
15
+ case 'claude':
16
+ return '.claude/skills';
17
+ case 'cursor':
18
+ return '.cursor/skills';
19
+ case 'codex':
20
+ return '.codex/skills';
21
+ default:
22
+ throw new Error(`Unknown agent type: ${agent}`);
23
+ }
24
+ }
25
+ function findGitRoot(startDir) {
26
+ let currentDir = startDir;
27
+ while (currentDir !== path.parse(currentDir).root) {
28
+ if (fs.existsSync(path.join(currentDir, '.git'))) {
29
+ return currentDir;
30
+ }
31
+ currentDir = path.dirname(currentDir);
32
+ }
33
+ // Check root directory
34
+ if (fs.existsSync(path.join(currentDir, '.git'))) {
35
+ return currentDir;
36
+ }
37
+ return null;
38
+ }
39
+ function getInstallPath(options) {
40
+ const skillsDir = getAgentSkillsDir(options.agent);
41
+ // If explicit path provided, use it
42
+ if (options.path) {
43
+ return path.join(options.path, skillsDir);
44
+ }
45
+ // If global, use home directory
46
+ if (options.global) {
47
+ return path.join(os.homedir(), skillsDir);
48
+ }
49
+ // Project install: find git root
50
+ const cwd = process.cwd();
51
+ const gitRoot = findGitRoot(cwd);
52
+ if (gitRoot) {
53
+ globalState_1.default.debug(`> Found git root at: ${gitRoot}`);
54
+ return path.join(gitRoot, skillsDir);
55
+ }
56
+ // No git root found, use current directory
57
+ globalState_1.default.debug(`> No git root found, using current directory: ${cwd}`);
58
+ return path.join(cwd, skillsDir);
59
+ }
60
+ async function fetchGitHubDirectory(repoPath) {
61
+ const url = `${GITHUB_API_BASE}/${repoPath}`;
62
+ globalState_1.default.debug(`> Fetching GitHub directory: ${url}`);
63
+ const response = await (0, node_fetch_1.default)(url, {
64
+ headers: {
65
+ Accept: 'application/vnd.github.v3+json',
66
+ 'User-Agent': 'lightdash-cli',
67
+ },
68
+ });
69
+ if (!response.ok) {
70
+ throw new Error(`Failed to fetch GitHub directory: ${response.status} ${response.statusText}`);
71
+ }
72
+ return response.json();
73
+ }
74
+ async function fetchFileContent(downloadUrl) {
75
+ globalState_1.default.debug(`> Fetching file: ${downloadUrl}`);
76
+ const response = await (0, node_fetch_1.default)(downloadUrl, {
77
+ headers: {
78
+ 'User-Agent': 'lightdash-cli',
79
+ },
80
+ });
81
+ if (!response.ok) {
82
+ throw new Error(`Failed to fetch file: ${response.status} ${response.statusText}`);
83
+ }
84
+ return response.text();
85
+ }
86
+ async function resolveSymlinkTarget(symlinkPath, target) {
87
+ // Resolve the symlink relative to its location
88
+ const symlinkDir = path.dirname(symlinkPath);
89
+ const resolvedPath = path.posix.normalize(path.posix.join(symlinkDir, target));
90
+ return resolvedPath;
91
+ }
92
+ /* eslint-disable no-await-in-loop */
93
+ // Sequential downloads are intentional to avoid GitHub rate limits and handle symlinks
94
+ async function downloadSkillFiles(repoPath, localDir, visited = new Set()) {
95
+ // Prevent infinite loops with symlinks
96
+ if (visited.has(repoPath)) {
97
+ globalState_1.default.debug(`> Skipping already visited path: ${repoPath}`);
98
+ return;
99
+ }
100
+ visited.add(repoPath);
101
+ const items = await fetchGitHubDirectory(repoPath);
102
+ for (const item of items) {
103
+ const localPath = path.join(localDir, item.name);
104
+ if (item.type === 'dir') {
105
+ fs.mkdirSync(localPath, { recursive: true });
106
+ await downloadSkillFiles(item.path, localPath, visited);
107
+ }
108
+ else if (item.type === 'symlink' && item.target) {
109
+ // Resolve the symlink and fetch the actual content
110
+ const resolvedPath = await resolveSymlinkTarget(item.path, item.target);
111
+ globalState_1.default.debug(`> Resolving symlink: ${item.path} -> ${resolvedPath}`);
112
+ // Check if the target is a directory or file by fetching it
113
+ try {
114
+ const targetItems = await fetchGitHubDirectory(resolvedPath);
115
+ // It's a directory, create it and download contents
116
+ fs.mkdirSync(localPath, { recursive: true });
117
+ for (const targetItem of targetItems) {
118
+ const targetLocalPath = path.join(localPath, targetItem.name);
119
+ if (targetItem.type === 'dir') {
120
+ fs.mkdirSync(targetLocalPath, { recursive: true });
121
+ await downloadSkillFiles(targetItem.path, targetLocalPath, visited);
122
+ }
123
+ else if (targetItem.download_url) {
124
+ const content = await fetchFileContent(targetItem.download_url);
125
+ fs.writeFileSync(targetLocalPath, content);
126
+ }
127
+ }
128
+ }
129
+ catch {
130
+ // It's a file, fetch its content directly
131
+ const downloadUrl = `${GITHUB_RAW_BASE}/${resolvedPath}`;
132
+ const content = await fetchFileContent(downloadUrl);
133
+ // Ensure parent directory exists
134
+ fs.mkdirSync(path.dirname(localPath), { recursive: true });
135
+ fs.writeFileSync(localPath, content);
136
+ }
137
+ }
138
+ else if (item.type === 'file' && item.download_url) {
139
+ const content = await fetchFileContent(item.download_url);
140
+ // Ensure parent directory exists
141
+ fs.mkdirSync(path.dirname(localPath), { recursive: true });
142
+ fs.writeFileSync(localPath, content);
143
+ }
144
+ }
145
+ }
146
+ /* eslint-enable no-await-in-loop */
147
+ async function listAvailableSkills() {
148
+ const items = await fetchGitHubDirectory('skills');
149
+ return items.filter((item) => item.type === 'dir').map((item) => item.name);
150
+ }
151
+ const installSkillsHandler = async (options) => {
152
+ globalState_1.default.setVerbose(options.verbose);
153
+ const installPath = getInstallPath(options);
154
+ console.error(styles.title('\n⚡ Lightdash Skills Installer\n'));
155
+ console.error(`Agent: ${styles.bold(options.agent)}`);
156
+ console.error(`Scope: ${styles.bold(options.global ? 'global' : 'project')}`);
157
+ console.error(`Install path: ${styles.bold(installPath)}\n`);
158
+ const spinner = globalState_1.default.startSpinner('Fetching available skills...');
159
+ try {
160
+ const skills = await listAvailableSkills();
161
+ if (skills.length === 0) {
162
+ spinner.fail('No skills found in the repository');
163
+ return;
164
+ }
165
+ spinner.text = `Found ${skills.length} skill(s): ${skills.join(', ')}`;
166
+ spinner.succeed();
167
+ // Create install directory
168
+ fs.mkdirSync(installPath, { recursive: true });
169
+ // Install skills sequentially to provide meaningful progress feedback
170
+ /* eslint-disable no-await-in-loop */
171
+ for (const skill of skills) {
172
+ const skillSpinner = globalState_1.default.startSpinner(`Installing skill: ${skill}...`);
173
+ const skillLocalPath = path.join(installPath, skill);
174
+ try {
175
+ // Remove existing skill directory if it exists
176
+ if (fs.existsSync(skillLocalPath)) {
177
+ fs.rmSync(skillLocalPath, { recursive: true, force: true });
178
+ }
179
+ fs.mkdirSync(skillLocalPath, { recursive: true });
180
+ await downloadSkillFiles(`skills/${skill}`, skillLocalPath);
181
+ skillSpinner.succeed(`Installed skill: ${skill}`);
182
+ }
183
+ catch (err) {
184
+ const errorMessage = err instanceof Error ? err.message : String(err);
185
+ skillSpinner.fail(`Failed to install skill ${skill}: ${errorMessage}`);
186
+ }
187
+ }
188
+ /* eslint-enable no-await-in-loop */
189
+ console.error(styles.success('\n✓ Skills installed successfully!\n'));
190
+ console.error(`Skills are available at: ${styles.bold(installPath)}\n`);
191
+ }
192
+ catch (err) {
193
+ const errorMessage = err instanceof Error ? err.message : String(err);
194
+ spinner.fail(`Failed to fetch skills: ${errorMessage}`);
195
+ throw err;
196
+ }
197
+ };
198
+ exports.installSkillsHandler = installSkillsHandler;
@@ -0,0 +1,9 @@
1
+ type SqlHandlerOptions = {
2
+ output: string;
3
+ limit?: number;
4
+ pageSize?: number;
5
+ verbose?: boolean;
6
+ };
7
+ export declare const sqlHandler: (sql: string, options: SqlHandlerOptions) => Promise<void>;
8
+ export {};
9
+ //# sourceMappingURL=sql.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../../src/handlers/sql.ts"],"names":[],"mappings":"AAYA,KAAK,iBAAiB,GAAG;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AA0FF,eAAO,MAAM,UAAU,QACd,MAAM,WACF,iBAAiB,KAC3B,OAAO,CAAC,IAAI,CAiEd,CAAC"}
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sqlHandler = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const common_1 = require("@lightdash/common");
6
+ const fs_1 = require("fs");
7
+ const config_1 = require("../config");
8
+ const globalState_1 = tslib_1.__importDefault(require("../globalState"));
9
+ const styles = tslib_1.__importStar(require("../styles"));
10
+ const apiClient_1 = require("./dbt/apiClient");
11
+ const DEFAULT_PAGE_SIZE = 500;
12
+ const POLL_INTERVAL_MS = 500;
13
+ /**
14
+ * Convert ResultRow array to CSV string
15
+ */
16
+ function resultsToCsv(columns, rows) {
17
+ // Escape CSV value: wrap in quotes if contains comma, quote, or newline
18
+ const escapeValue = (value) => {
19
+ if (value === null || value === undefined) {
20
+ return '';
21
+ }
22
+ const str = String(value);
23
+ if (str.includes(',') || str.includes('"') || str.includes('\n')) {
24
+ return `"${str.replace(/"/g, '""')}"`;
25
+ }
26
+ return str;
27
+ };
28
+ const header = columns.map(escapeValue).join(',');
29
+ const dataRows = rows.map((row) => columns.map((col) => escapeValue(row[col]?.value?.raw)).join(','));
30
+ return [header, ...dataRows].join('\n');
31
+ }
32
+ /**
33
+ * Fetch query results once
34
+ */
35
+ async function fetchQueryResults(projectUuid, queryUuid, pageSize) {
36
+ const url = `/api/v2/projects/${projectUuid}/query/${queryUuid}?pageSize=${pageSize}`;
37
+ return (0, apiClient_1.lightdashApi)({
38
+ method: 'GET',
39
+ url,
40
+ body: undefined,
41
+ });
42
+ }
43
+ /**
44
+ * Sleep for a given duration
45
+ */
46
+ function sleep(ms) {
47
+ return new Promise((resolve) => {
48
+ setTimeout(resolve, ms);
49
+ });
50
+ }
51
+ /**
52
+ * Poll for query results until status is READY or ERROR
53
+ */
54
+ async function pollQueryResults(projectUuid, queryUuid, pageSize) {
55
+ const poll = async () => {
56
+ const result = await fetchQueryResults(projectUuid, queryUuid, pageSize);
57
+ globalState_1.default.debug(`> Query status: ${result.status}`);
58
+ if (result.status === common_1.QueryHistoryStatus.READY) {
59
+ return result;
60
+ }
61
+ if (result.status === common_1.QueryHistoryStatus.ERROR) {
62
+ return result;
63
+ }
64
+ if (result.status === common_1.QueryHistoryStatus.CANCELLED) {
65
+ throw new Error('Query was cancelled');
66
+ }
67
+ // Still pending, wait and poll again
68
+ await sleep(POLL_INTERVAL_MS);
69
+ return poll();
70
+ };
71
+ return poll();
72
+ }
73
+ const sqlHandler = async (sql, options) => {
74
+ globalState_1.default.setVerbose(options.verbose ?? false);
75
+ const config = await (0, config_1.getConfig)();
76
+ const projectUuid = config.context?.project;
77
+ if (!projectUuid) {
78
+ throw new Error(`No project selected. Run 'lightdash config set-project' first.`);
79
+ }
80
+ globalState_1.default.debug(`> Running SQL query against project: ${projectUuid}`);
81
+ globalState_1.default.debug(`> SQL: ${sql}`);
82
+ const pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE;
83
+ // Submit the query
84
+ const spinner = globalState_1.default.startSpinner('Submitting SQL query...');
85
+ const submitResult = await (0, apiClient_1.lightdashApi)({
86
+ method: 'POST',
87
+ url: `/api/v2/projects/${projectUuid}/query/sql`,
88
+ body: JSON.stringify({
89
+ sql,
90
+ limit: options.limit,
91
+ context: 'cli',
92
+ }),
93
+ });
94
+ globalState_1.default.debug(`> Query UUID: ${submitResult.queryUuid}`);
95
+ spinner.text = 'Waiting for query results...';
96
+ // Poll for results
97
+ const result = await pollQueryResults(projectUuid, submitResult.queryUuid, pageSize);
98
+ if (result.status === common_1.QueryHistoryStatus.ERROR) {
99
+ spinner.fail('Query failed');
100
+ throw new Error(result.error ?? 'Query execution failed');
101
+ }
102
+ if (result.status !== common_1.QueryHistoryStatus.READY) {
103
+ spinner.fail('Unexpected query status');
104
+ throw new Error(`Unexpected query status: ${result.status}`);
105
+ }
106
+ // Extract column names from the columns metadata
107
+ const columns = Object.keys(result.columns);
108
+ const rowCount = result.rows.length;
109
+ spinner.text = `Writing ${rowCount} rows to ${options.output}...`;
110
+ // Convert to CSV and write to file
111
+ const csv = resultsToCsv(columns, result.rows);
112
+ await fs_1.promises.writeFile(options.output, csv, 'utf8');
113
+ spinner.succeed(`${styles.success('Success!')} Wrote ${rowCount} rows to ${options.output}`);
114
+ };
115
+ exports.sqlHandler = sqlHandler;
package/dist/index.js CHANGED
@@ -15,12 +15,14 @@ const download_1 = require("./handlers/download");
15
15
  const generate_1 = require("./handlers/generate");
16
16
  const generateExposures_1 = require("./handlers/generateExposures");
17
17
  const getProject_1 = require("./handlers/getProject");
18
+ const installSkills_1 = require("./handlers/installSkills");
18
19
  const lint_1 = require("./handlers/lint");
19
20
  const listProjects_1 = require("./handlers/listProjects");
20
21
  const login_1 = require("./handlers/login");
21
22
  const preview_1 = require("./handlers/preview");
22
23
  const renameHandler_1 = require("./handlers/renameHandler");
23
24
  const setProject_1 = require("./handlers/setProject");
25
+ const sql_1 = require("./handlers/sql");
24
26
  const validate_1 = require("./handlers/validate");
25
27
  const styles = tslib_1.__importStar(require("./styles"));
26
28
  // Trigger CLI tests
@@ -423,6 +425,44 @@ ${styles.bold('Examples:')}
423
425
  .option('--verbose', 'Show detailed output', false)
424
426
  .option('-f, --format <format>', 'Output format: cli (default) or json (SARIF format)', 'cli')
425
427
  .action(lint_1.lintHandler);
428
+ commander_1.program
429
+ .command('sql')
430
+ .description('Run raw SQL query against the warehouse using project credentials')
431
+ .argument('<query>', 'SQL query to execute')
432
+ .requiredOption('-o, --output <file>', 'Output file path for CSV results')
433
+ .option('--limit <number>', 'Maximum rows to return from query', parseIntArgument)
434
+ .option('--page-size <number>', 'Number of rows per page (default: 500, max: 5000)', parseIntArgument)
435
+ .option('--verbose', 'Show detailed output', false)
436
+ .action(sql_1.sqlHandler);
437
+ commander_1.program
438
+ .command('install-skills')
439
+ .description('Installs Lightdash skills for AI coding assistants (Claude, Cursor, Codex)')
440
+ .addHelpText('after', `
441
+ ${styles.bold('Examples:')}
442
+ ${styles.title('⚡')}️lightdash ${styles.bold('install-skills')} ${styles.secondary('-- installs skills for Claude at git root (default)')}
443
+ ${styles.title('⚡')}️lightdash ${styles.bold('install-skills')} --agent cursor ${styles.secondary('-- installs skills for Cursor')}
444
+ ${styles.title('⚡')}️lightdash ${styles.bold('install-skills')} --global ${styles.secondary('-- installs skills globally to ~/.claude/skills/')}
445
+ ${styles.title('⚡')}️lightdash ${styles.bold('install-skills')} --agent codex --global ${styles.secondary('-- installs skills globally for Codex')}
446
+ ${styles.title('⚡')}️lightdash ${styles.bold('install-skills')} --path ./my-project ${styles.secondary('-- installs skills to a specific path')}
447
+
448
+ ${styles.bold('Installation paths:')}
449
+ ${styles.secondary('Project-level (default):')}
450
+ .claude/skills/ (Claude)
451
+ .cursor/skills/ (Cursor)
452
+ .codex/skills/ (Codex)
453
+
454
+ ${styles.secondary('Global (--global):')}
455
+ ~/.claude/skills/ (Claude)
456
+ ~/.cursor/skills/ (Cursor)
457
+ ~/.codex/skills/ (Codex)
458
+ `)
459
+ .option('--verbose', 'Show detailed output', false)
460
+ .addOption(new commander_1.Option('--agent <agent>', 'Target agent for skill installation')
461
+ .choices(['claude', 'cursor', 'codex'])
462
+ .default('claude'))
463
+ .option('--global', 'Install skills globally to home directory instead of project', false)
464
+ .option('--path <path>', 'Override the install path (skills directory will be created inside)', undefined)
465
+ .action(installSkills_1.installSkillsHandler);
426
466
  const errorHandler = (err) => {
427
467
  // Use error message with fallback for safety
428
468
  const errorMessage = (0, common_1.getErrorMessage)(err) || 'An unexpected error occurred';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightdash/cli",
3
- "version": "0.2395.0",
3
+ "version": "0.2397.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -38,8 +38,8 @@
38
38
  "unique-names-generator": "^4.7.1",
39
39
  "uuid": "^11.0.3",
40
40
  "yaml": "^2.7.0",
41
- "@lightdash/common": "0.2395.0",
42
- "@lightdash/warehouses": "0.2395.0"
41
+ "@lightdash/common": "0.2397.0",
42
+ "@lightdash/warehouses": "0.2397.0"
43
43
  },
44
44
  "description": "Lightdash CLI tool",
45
45
  "devDependencies": {