@lightdash/cli 0.2396.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;
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ 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");
@@ -433,6 +434,35 @@ commander_1.program
433
434
  .option('--page-size <number>', 'Number of rows per page (default: 500, max: 5000)', parseIntArgument)
434
435
  .option('--verbose', 'Show detailed output', false)
435
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);
436
466
  const errorHandler = (err) => {
437
467
  // Use error message with fallback for safety
438
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.2396.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.2396.0",
42
- "@lightdash/warehouses": "0.2396.0"
41
+ "@lightdash/common": "0.2397.0",
42
+ "@lightdash/warehouses": "0.2397.0"
43
43
  },
44
44
  "description": "Lightdash CLI tool",
45
45
  "devDependencies": {