bitcompass 0.3.3 → 0.3.4
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/commands/rules.d.ts +1 -0
- package/dist/commands/rules.js +22 -26
- package/dist/commands/solutions.d.ts +1 -0
- package/dist/commands/solutions.js +22 -26
- package/dist/index.js +4 -2
- package/dist/lib/rule-cache.d.ts +14 -0
- package/dist/lib/rule-cache.js +58 -0
- package/dist/lib/rule-file-ops.d.ts +4 -1
- package/dist/lib/rule-file-ops.js +44 -7
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
package/dist/commands/rules.d.ts
CHANGED
|
@@ -2,5 +2,6 @@ export declare const runRulesSearch: (query?: string) => Promise<void>;
|
|
|
2
2
|
export declare const runRulesList: () => Promise<void>;
|
|
3
3
|
export declare const runRulesPull: (id?: string, options?: {
|
|
4
4
|
global?: boolean;
|
|
5
|
+
copy?: boolean;
|
|
5
6
|
}) => Promise<void>;
|
|
6
7
|
export declare const runRulesPush: (file?: string) => Promise<void>;
|
package/dist/commands/rules.js
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import inquirer from 'inquirer';
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
import { mkdirSync, writeFileSync } from 'fs';
|
|
5
|
-
import { join } from 'path';
|
|
6
|
-
import { homedir } from 'os';
|
|
7
4
|
import { loadCredentials } from '../auth/config.js';
|
|
8
|
-
import { getProjectConfig } from '../auth/project-config.js';
|
|
9
5
|
import { searchRules, fetchRules, getRuleById, insertRule } from '../api/client.js';
|
|
10
|
-
import {
|
|
6
|
+
import { pullRuleToFile } from '../lib/rule-file-ops.js';
|
|
11
7
|
export const runRulesSearch = async (query) => {
|
|
12
8
|
if (!loadCredentials()) {
|
|
13
9
|
console.error(chalk.red('Not logged in. Run bitcompass login.'));
|
|
@@ -66,28 +62,28 @@ export const runRulesPull = async (id, options) => {
|
|
|
66
62
|
]);
|
|
67
63
|
targetId = choice.id;
|
|
68
64
|
}
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
65
|
+
const spinner = ora('Pulling rule…').start();
|
|
66
|
+
try {
|
|
67
|
+
const filename = await pullRuleToFile(targetId, {
|
|
68
|
+
global: options?.global,
|
|
69
|
+
useSymlink: !options?.copy, // Use symlink unless --copy flag is set
|
|
70
|
+
});
|
|
71
|
+
spinner.succeed(chalk.green('Pulled rule'));
|
|
72
|
+
console.log(chalk.dim(filename));
|
|
73
|
+
if (options?.copy) {
|
|
74
|
+
console.log(chalk.dim('Copied as file (not a symlink)'));
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
console.log(chalk.dim('Created symbolic link to cached rule'));
|
|
78
|
+
}
|
|
79
|
+
if (options?.global) {
|
|
80
|
+
console.log(chalk.dim('Installed globally for all projects'));
|
|
81
|
+
}
|
|
83
82
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
console.log(chalk.green('Wrote'), filename);
|
|
89
|
-
if (options?.global) {
|
|
90
|
-
console.log(chalk.dim('Installed globally for all projects'));
|
|
83
|
+
catch (error) {
|
|
84
|
+
spinner.fail(chalk.red('Failed to pull rule'));
|
|
85
|
+
console.error(chalk.red(error.message));
|
|
86
|
+
process.exit(1);
|
|
91
87
|
}
|
|
92
88
|
};
|
|
93
89
|
export const runRulesPush = async (file) => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare const runSolutionsSearch: (query?: string) => Promise<void>;
|
|
2
2
|
export declare const runSolutionsPull: (id?: string, options?: {
|
|
3
3
|
global?: boolean;
|
|
4
|
+
copy?: boolean;
|
|
4
5
|
}) => Promise<void>;
|
|
5
6
|
export declare const runSolutionsPush: (file?: string) => Promise<void>;
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import inquirer from 'inquirer';
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
import { mkdirSync, writeFileSync } from 'fs';
|
|
5
|
-
import { join } from 'path';
|
|
6
|
-
import { homedir } from 'os';
|
|
7
4
|
import { loadCredentials } from '../auth/config.js';
|
|
8
|
-
import { getProjectConfig } from '../auth/project-config.js';
|
|
9
5
|
import { searchRules, fetchRules, getRuleById, insertRule } from '../api/client.js';
|
|
10
|
-
import {
|
|
6
|
+
import { pullRuleToFile } from '../lib/rule-file-ops.js';
|
|
11
7
|
export const runSolutionsSearch = async (query) => {
|
|
12
8
|
if (!loadCredentials()) {
|
|
13
9
|
console.error(chalk.red('Not logged in. Run bitcompass login.'));
|
|
@@ -54,28 +50,28 @@ export const runSolutionsPull = async (id, options) => {
|
|
|
54
50
|
]);
|
|
55
51
|
targetId = choice.id;
|
|
56
52
|
}
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
53
|
+
const spinner = ora('Pulling solution…').start();
|
|
54
|
+
try {
|
|
55
|
+
const filename = await pullRuleToFile(targetId, {
|
|
56
|
+
global: options?.global,
|
|
57
|
+
useSymlink: !options?.copy, // Use symlink unless --copy flag is set
|
|
58
|
+
});
|
|
59
|
+
spinner.succeed(chalk.green('Pulled solution'));
|
|
60
|
+
console.log(chalk.dim(filename));
|
|
61
|
+
if (options?.copy) {
|
|
62
|
+
console.log(chalk.dim('Copied as file (not a symlink)'));
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
console.log(chalk.dim('Created symbolic link to cached solution'));
|
|
66
|
+
}
|
|
67
|
+
if (options?.global) {
|
|
68
|
+
console.log(chalk.dim('Installed globally for all projects'));
|
|
69
|
+
}
|
|
71
70
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
console.log(chalk.green('Wrote'), filename);
|
|
77
|
-
if (options?.global) {
|
|
78
|
-
console.log(chalk.dim('Installed globally for all projects'));
|
|
71
|
+
catch (error) {
|
|
72
|
+
spinner.fail(chalk.red('Failed to pull solution'));
|
|
73
|
+
console.error(chalk.red(error.message));
|
|
74
|
+
process.exit(1);
|
|
79
75
|
}
|
|
80
76
|
};
|
|
81
77
|
export const runSolutionsPush = async (file) => {
|
package/dist/index.js
CHANGED
|
@@ -55,8 +55,9 @@ rules.command('search [query]').description('Search rules').action((query) => ru
|
|
|
55
55
|
rules.command('list').description('List rules').action(() => runRulesList().catch(handleErr));
|
|
56
56
|
rules
|
|
57
57
|
.command('pull [id]')
|
|
58
|
-
.description('Pull a rule by ID or choose from list')
|
|
58
|
+
.description('Pull a rule by ID or choose from list (creates symbolic link by default)')
|
|
59
59
|
.option('-g, --global', 'Install globally to ~/.cursor/rules/ for all projects')
|
|
60
|
+
.option('--copy', 'Copy file instead of creating symbolic link')
|
|
60
61
|
.action((id, options) => runRulesPull(id, options).catch(handleErr));
|
|
61
62
|
rules.command('push [file]').description('Push a rule (file or interactive)').action((file) => runRulesPush(file).catch(handleErr));
|
|
62
63
|
// solutions
|
|
@@ -64,8 +65,9 @@ const solutions = program.command('solutions').description('Manage solutions');
|
|
|
64
65
|
solutions.command('search [query]').description('Search solutions').action((query) => runSolutionsSearch(query).catch(handleErr));
|
|
65
66
|
solutions
|
|
66
67
|
.command('pull [id]')
|
|
67
|
-
.description('Pull a solution by ID or choose from list')
|
|
68
|
+
.description('Pull a solution by ID or choose from list (creates symbolic link by default)')
|
|
68
69
|
.option('-g, --global', 'Install globally to ~/.cursor/rules/ for all projects')
|
|
70
|
+
.option('--copy', 'Copy file instead of creating symbolic link')
|
|
69
71
|
.action((id, options) => runSolutionsPull(id, options).catch(handleErr));
|
|
70
72
|
solutions.command('push [file]').description('Push a solution (file or interactive)').action((file) => runSolutionsPush(file).catch(handleErr));
|
|
71
73
|
// mcp
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Rule } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Gets the cache directory for rules (~/.bitcompass/cache/rules/)
|
|
4
|
+
*/
|
|
5
|
+
export declare const getCacheDir: () => string;
|
|
6
|
+
/**
|
|
7
|
+
* Gets the cached file path for a rule by ID
|
|
8
|
+
*/
|
|
9
|
+
export declare const getCachedRulePath: (rule: Rule) => string;
|
|
10
|
+
/**
|
|
11
|
+
* Ensures a rule is cached. Downloads and caches it if not present or outdated.
|
|
12
|
+
* Returns the path to the cached file.
|
|
13
|
+
*/
|
|
14
|
+
export declare const ensureRuleCached: (id: string) => Promise<string>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, statSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { getConfigDir } from '../auth/config.js';
|
|
4
|
+
import { getRuleById } from '../api/client.js';
|
|
5
|
+
import { ruleFilename, solutionFilename } from './slug.js';
|
|
6
|
+
/**
|
|
7
|
+
* Gets the cache directory for rules (~/.bitcompass/cache/rules/)
|
|
8
|
+
*/
|
|
9
|
+
export const getCacheDir = () => {
|
|
10
|
+
const cacheDir = join(getConfigDir(), 'cache', 'rules');
|
|
11
|
+
if (!existsSync(cacheDir)) {
|
|
12
|
+
mkdirSync(cacheDir, { mode: 0o755, recursive: true });
|
|
13
|
+
}
|
|
14
|
+
return cacheDir;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Gets the cached file path for a rule by ID
|
|
18
|
+
*/
|
|
19
|
+
export const getCachedRulePath = (rule) => {
|
|
20
|
+
const cacheDir = getCacheDir();
|
|
21
|
+
const filename = rule.kind === 'solution'
|
|
22
|
+
? solutionFilename(rule.title, rule.id)
|
|
23
|
+
: ruleFilename(rule.title, rule.id);
|
|
24
|
+
return join(cacheDir, `${rule.id}-${filename}`);
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Ensures a rule is cached. Downloads and caches it if not present or outdated.
|
|
28
|
+
* Returns the path to the cached file.
|
|
29
|
+
*/
|
|
30
|
+
export const ensureRuleCached = async (id) => {
|
|
31
|
+
const rule = await getRuleById(id);
|
|
32
|
+
if (!rule) {
|
|
33
|
+
throw new Error(`Rule or solution with ID ${id} not found.`);
|
|
34
|
+
}
|
|
35
|
+
const cachedPath = getCachedRulePath(rule);
|
|
36
|
+
const needsUpdate = !existsSync(cachedPath) || isCacheOutdated(cachedPath, rule);
|
|
37
|
+
if (needsUpdate) {
|
|
38
|
+
const content = rule.kind === 'solution'
|
|
39
|
+
? `# ${rule.title}\n\n${rule.description}\n\n## Solution\n\n${rule.body}\n`
|
|
40
|
+
: `# ${rule.title}\n\n${rule.description}\n\n${rule.body}\n`;
|
|
41
|
+
writeFileSync(cachedPath, content, 'utf-8');
|
|
42
|
+
}
|
|
43
|
+
return cachedPath;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Checks if the cached file is outdated compared to the rule's updated_at timestamp
|
|
47
|
+
*/
|
|
48
|
+
const isCacheOutdated = (cachedPath, rule) => {
|
|
49
|
+
try {
|
|
50
|
+
const stats = statSync(cachedPath);
|
|
51
|
+
const cacheTime = stats.mtime.getTime();
|
|
52
|
+
const ruleTime = new Date(rule.updated_at).getTime();
|
|
53
|
+
return ruleTime > cacheTime;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return true; // If we can't read the file, consider it outdated
|
|
57
|
+
}
|
|
58
|
+
};
|
|
@@ -3,9 +3,12 @@ export interface PullRuleOptions {
|
|
|
3
3
|
global?: boolean;
|
|
4
4
|
/** Custom output path (overrides project config and global) */
|
|
5
5
|
outputPath?: string;
|
|
6
|
+
/** Use symbolic links instead of copying files (default: true) */
|
|
7
|
+
useSymlink?: boolean;
|
|
6
8
|
}
|
|
7
9
|
/**
|
|
8
|
-
* Pulls a rule or solution to a file
|
|
10
|
+
* Pulls a rule or solution to a file using symbolic links (like Bun init).
|
|
11
|
+
* Returns the file path where it was written/linked.
|
|
9
12
|
* Throws if rule not found or if authentication is required.
|
|
10
13
|
*/
|
|
11
14
|
export declare const pullRuleToFile: (id: string, options?: PullRuleOptions) => Promise<string>;
|
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
1
|
+
import { existsSync, mkdirSync, symlinkSync, unlinkSync, writeFileSync, lstatSync } from 'fs';
|
|
2
|
+
import { join, relative, dirname } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import { getRuleById } from '../api/client.js';
|
|
5
5
|
import { getProjectConfig } from '../auth/project-config.js';
|
|
6
6
|
import { ruleFilename, solutionFilename } from './slug.js';
|
|
7
|
+
import { ensureRuleCached } from './rule-cache.js';
|
|
7
8
|
/**
|
|
8
|
-
* Pulls a rule or solution to a file
|
|
9
|
+
* Pulls a rule or solution to a file using symbolic links (like Bun init).
|
|
10
|
+
* Returns the file path where it was written/linked.
|
|
9
11
|
* Throws if rule not found or if authentication is required.
|
|
10
12
|
*/
|
|
11
13
|
export const pullRuleToFile = async (id, options = {}) => {
|
|
14
|
+
const useSymlink = options.useSymlink !== false; // Default to true
|
|
15
|
+
// Ensure rule is cached in central location
|
|
16
|
+
const cachedPath = await ensureRuleCached(id);
|
|
12
17
|
const rule = await getRuleById(id);
|
|
13
18
|
if (!rule) {
|
|
14
19
|
throw new Error(`Rule or solution with ID ${id} not found.`);
|
|
@@ -31,9 +36,41 @@ export const pullRuleToFile = async (id, options = {}) => {
|
|
|
31
36
|
const filename = rule.kind === 'solution'
|
|
32
37
|
? join(outDir, solutionFilename(rule.title, rule.id))
|
|
33
38
|
: join(outDir, ruleFilename(rule.title, rule.id));
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
// Remove existing file/symlink if it exists
|
|
40
|
+
if (existsSync(filename)) {
|
|
41
|
+
try {
|
|
42
|
+
const stats = lstatSync(filename);
|
|
43
|
+
if (stats.isSymbolicLink() || stats.isFile()) {
|
|
44
|
+
unlinkSync(filename);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Ignore errors when removing
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (useSymlink) {
|
|
52
|
+
// Create symbolic link to cached file
|
|
53
|
+
// Use relative path for portability
|
|
54
|
+
const relativePath = relative(dirname(filename), cachedPath);
|
|
55
|
+
try {
|
|
56
|
+
symlinkSync(relativePath, filename);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
// Fallback to absolute path if relative fails (e.g., on Windows or cross-filesystem)
|
|
60
|
+
if (error.code === 'ENOENT' || error.code === 'EXDEV') {
|
|
61
|
+
symlinkSync(cachedPath, filename);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// Fallback: copy file content (for compatibility or when symlinks aren't desired)
|
|
70
|
+
const content = rule.kind === 'solution'
|
|
71
|
+
? `# ${rule.title}\n\n${rule.description}\n\n## Solution\n\n${rule.body}\n`
|
|
72
|
+
: `# ${rule.title}\n\n${rule.description}\n\n${rule.body}\n`;
|
|
73
|
+
writeFileSync(filename, content);
|
|
74
|
+
}
|
|
38
75
|
return filename;
|
|
39
76
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface Rule {
|
|
|
10
10
|
technologies?: string[];
|
|
11
11
|
user_id: string;
|
|
12
12
|
author_display_name?: string | null;
|
|
13
|
+
version?: string | null;
|
|
13
14
|
created_at: string;
|
|
14
15
|
updated_at: string;
|
|
15
16
|
}
|
|
@@ -21,6 +22,7 @@ export interface RuleInsert {
|
|
|
21
22
|
context?: string | null;
|
|
22
23
|
examples?: string[];
|
|
23
24
|
technologies?: string[];
|
|
25
|
+
version?: string;
|
|
24
26
|
}
|
|
25
27
|
export interface StoredCredentials {
|
|
26
28
|
access_token: string;
|