bitcompass 0.3.3 → 0.3.5
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/commands.d.ts +7 -0
- package/dist/commands/commands.js +120 -0
- package/dist/commands/rules.d.ts +1 -0
- package/dist/commands/rules.js +22 -26
- package/dist/commands/skills.d.ts +7 -0
- package/dist/commands/skills.js +120 -0
- package/dist/commands/solutions.d.ts +1 -0
- package/dist/commands/solutions.js +22 -26
- package/dist/index.js +28 -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 +74 -11
- package/dist/lib/slug.d.ts +10 -0
- package/dist/lib/slug.js +18 -0
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const runCommandsSearch: (query?: string) => Promise<void>;
|
|
2
|
+
export declare const runCommandsList: () => Promise<void>;
|
|
3
|
+
export declare const runCommandsPull: (id?: string, options?: {
|
|
4
|
+
global?: boolean;
|
|
5
|
+
copy?: boolean;
|
|
6
|
+
}) => Promise<void>;
|
|
7
|
+
export declare const runCommandsPush: (file?: string) => Promise<void>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { loadCredentials } from '../auth/config.js';
|
|
5
|
+
import { searchRules, fetchRules, getRuleById, insertRule } from '../api/client.js';
|
|
6
|
+
import { pullRuleToFile } from '../lib/rule-file-ops.js';
|
|
7
|
+
export const runCommandsSearch = async (query) => {
|
|
8
|
+
if (!loadCredentials()) {
|
|
9
|
+
console.error(chalk.red('Not logged in. Run bitcompass login.'));
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
const q = query ?? (await inquirer.prompt([{ name: 'q', message: 'Search query', type: 'input' }])).q;
|
|
13
|
+
const spinner = ora('Searching commands…').start();
|
|
14
|
+
const list = await searchRules(q, { kind: 'command', limit: 20 });
|
|
15
|
+
spinner.stop();
|
|
16
|
+
if (list.length === 0) {
|
|
17
|
+
console.log(chalk.yellow('No commands found.'));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const choice = await inquirer.prompt([
|
|
21
|
+
{
|
|
22
|
+
name: 'id',
|
|
23
|
+
message: 'Select a command',
|
|
24
|
+
type: 'list',
|
|
25
|
+
choices: list.map((r) => ({ name: `${r.title} (${r.id})`, value: r.id })),
|
|
26
|
+
},
|
|
27
|
+
]);
|
|
28
|
+
const rule = await getRuleById(choice.id);
|
|
29
|
+
if (rule) {
|
|
30
|
+
console.log(chalk.cyan(rule.title));
|
|
31
|
+
console.log(rule.body);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
export const runCommandsList = async () => {
|
|
35
|
+
if (!loadCredentials()) {
|
|
36
|
+
console.error(chalk.red('Not logged in. Run bitcompass login.'));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
const spinner = ora('Loading commands…').start();
|
|
40
|
+
const list = await fetchRules('command');
|
|
41
|
+
spinner.stop();
|
|
42
|
+
list.forEach((r) => console.log(`${chalk.cyan(r.title)} ${chalk.dim(r.id)}`));
|
|
43
|
+
if (list.length === 0)
|
|
44
|
+
console.log(chalk.yellow('No commands yet.'));
|
|
45
|
+
};
|
|
46
|
+
export const runCommandsPull = async (id, options) => {
|
|
47
|
+
if (!loadCredentials()) {
|
|
48
|
+
console.error(chalk.red('Not logged in. Run bitcompass login.'));
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
let targetId = id;
|
|
52
|
+
if (!targetId) {
|
|
53
|
+
const spinner = ora('Loading commands…').start();
|
|
54
|
+
const list = await fetchRules('command');
|
|
55
|
+
spinner.stop();
|
|
56
|
+
if (list.length === 0) {
|
|
57
|
+
console.log(chalk.yellow('No commands to pull.'));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const choice = await inquirer.prompt([
|
|
61
|
+
{ name: 'id', message: 'Select command', type: 'list', choices: list.map((r) => ({ name: r.title, value: r.id })) },
|
|
62
|
+
]);
|
|
63
|
+
targetId = choice.id;
|
|
64
|
+
}
|
|
65
|
+
const spinner = ora('Pulling command…').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 command'));
|
|
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 command'));
|
|
78
|
+
}
|
|
79
|
+
if (options?.global) {
|
|
80
|
+
console.log(chalk.dim('Installed globally for all projects'));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
spinner.fail(chalk.red('Failed to pull command'));
|
|
85
|
+
console.error(chalk.red(error.message));
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
export const runCommandsPush = async (file) => {
|
|
90
|
+
if (!loadCredentials()) {
|
|
91
|
+
console.error(chalk.red('Not logged in. Run bitcompass login.'));
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
let payload;
|
|
95
|
+
if (file) {
|
|
96
|
+
const { readFileSync } = await import('fs');
|
|
97
|
+
const raw = readFileSync(file, 'utf-8');
|
|
98
|
+
try {
|
|
99
|
+
payload = JSON.parse(raw);
|
|
100
|
+
payload.kind = 'command';
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
const lines = raw.split('\n');
|
|
104
|
+
const title = lines[0].replace(/^#\s*/, '') || 'Untitled';
|
|
105
|
+
payload = { kind: 'command', title, description: '', body: raw };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
const answers = await inquirer.prompt([
|
|
110
|
+
{ name: 'title', message: 'Command title', type: 'input', default: 'Untitled' },
|
|
111
|
+
{ name: 'description', message: 'Description', type: 'input', default: '' },
|
|
112
|
+
{ name: 'body', message: 'Command content', type: 'editor', default: '' },
|
|
113
|
+
]);
|
|
114
|
+
payload = { kind: 'command', title: answers.title, description: answers.description, body: answers.body };
|
|
115
|
+
}
|
|
116
|
+
const spinner = ora('Publishing command…').start();
|
|
117
|
+
const created = await insertRule(payload);
|
|
118
|
+
spinner.succeed(chalk.green('Published command ') + created.id);
|
|
119
|
+
console.log(chalk.dim(created.title));
|
|
120
|
+
};
|
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) => {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const runSkillsSearch: (query?: string) => Promise<void>;
|
|
2
|
+
export declare const runSkillsList: () => Promise<void>;
|
|
3
|
+
export declare const runSkillsPull: (id?: string, options?: {
|
|
4
|
+
global?: boolean;
|
|
5
|
+
copy?: boolean;
|
|
6
|
+
}) => Promise<void>;
|
|
7
|
+
export declare const runSkillsPush: (file?: string) => Promise<void>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { loadCredentials } from '../auth/config.js';
|
|
5
|
+
import { searchRules, fetchRules, getRuleById, insertRule } from '../api/client.js';
|
|
6
|
+
import { pullRuleToFile } from '../lib/rule-file-ops.js';
|
|
7
|
+
export const runSkillsSearch = async (query) => {
|
|
8
|
+
if (!loadCredentials()) {
|
|
9
|
+
console.error(chalk.red('Not logged in. Run bitcompass login.'));
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
const q = query ?? (await inquirer.prompt([{ name: 'q', message: 'Search query', type: 'input' }])).q;
|
|
13
|
+
const spinner = ora('Searching skills…').start();
|
|
14
|
+
const list = await searchRules(q, { kind: 'skill', limit: 20 });
|
|
15
|
+
spinner.stop();
|
|
16
|
+
if (list.length === 0) {
|
|
17
|
+
console.log(chalk.yellow('No skills found.'));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const choice = await inquirer.prompt([
|
|
21
|
+
{
|
|
22
|
+
name: 'id',
|
|
23
|
+
message: 'Select a skill',
|
|
24
|
+
type: 'list',
|
|
25
|
+
choices: list.map((r) => ({ name: `${r.title} (${r.id})`, value: r.id })),
|
|
26
|
+
},
|
|
27
|
+
]);
|
|
28
|
+
const rule = await getRuleById(choice.id);
|
|
29
|
+
if (rule) {
|
|
30
|
+
console.log(chalk.cyan(rule.title));
|
|
31
|
+
console.log(rule.body);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
export const runSkillsList = async () => {
|
|
35
|
+
if (!loadCredentials()) {
|
|
36
|
+
console.error(chalk.red('Not logged in. Run bitcompass login.'));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
const spinner = ora('Loading skills…').start();
|
|
40
|
+
const list = await fetchRules('skill');
|
|
41
|
+
spinner.stop();
|
|
42
|
+
list.forEach((r) => console.log(`${chalk.cyan(r.title)} ${chalk.dim(r.id)}`));
|
|
43
|
+
if (list.length === 0)
|
|
44
|
+
console.log(chalk.yellow('No skills yet.'));
|
|
45
|
+
};
|
|
46
|
+
export const runSkillsPull = async (id, options) => {
|
|
47
|
+
if (!loadCredentials()) {
|
|
48
|
+
console.error(chalk.red('Not logged in. Run bitcompass login.'));
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
let targetId = id;
|
|
52
|
+
if (!targetId) {
|
|
53
|
+
const spinner = ora('Loading skills…').start();
|
|
54
|
+
const list = await fetchRules('skill');
|
|
55
|
+
spinner.stop();
|
|
56
|
+
if (list.length === 0) {
|
|
57
|
+
console.log(chalk.yellow('No skills to pull.'));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const choice = await inquirer.prompt([
|
|
61
|
+
{ name: 'id', message: 'Select skill', type: 'list', choices: list.map((r) => ({ name: r.title, value: r.id })) },
|
|
62
|
+
]);
|
|
63
|
+
targetId = choice.id;
|
|
64
|
+
}
|
|
65
|
+
const spinner = ora('Pulling skill…').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 skill'));
|
|
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 skill'));
|
|
78
|
+
}
|
|
79
|
+
if (options?.global) {
|
|
80
|
+
console.log(chalk.dim('Installed globally for all projects'));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
spinner.fail(chalk.red('Failed to pull skill'));
|
|
85
|
+
console.error(chalk.red(error.message));
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
export const runSkillsPush = async (file) => {
|
|
90
|
+
if (!loadCredentials()) {
|
|
91
|
+
console.error(chalk.red('Not logged in. Run bitcompass login.'));
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
let payload;
|
|
95
|
+
if (file) {
|
|
96
|
+
const { readFileSync } = await import('fs');
|
|
97
|
+
const raw = readFileSync(file, 'utf-8');
|
|
98
|
+
try {
|
|
99
|
+
payload = JSON.parse(raw);
|
|
100
|
+
payload.kind = 'skill';
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
const lines = raw.split('\n');
|
|
104
|
+
const title = lines[0].replace(/^#\s*/, '') || 'Untitled';
|
|
105
|
+
payload = { kind: 'skill', title, description: '', body: raw };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
const answers = await inquirer.prompt([
|
|
110
|
+
{ name: 'title', message: 'Skill title', type: 'input', default: 'Untitled' },
|
|
111
|
+
{ name: 'description', message: 'Description', type: 'input', default: '' },
|
|
112
|
+
{ name: 'body', message: 'Skill content', type: 'editor', default: '' },
|
|
113
|
+
]);
|
|
114
|
+
payload = { kind: 'skill', title: answers.title, description: answers.description, body: answers.body };
|
|
115
|
+
}
|
|
116
|
+
const spinner = ora('Publishing skill…').start();
|
|
117
|
+
const created = await insertRule(payload);
|
|
118
|
+
spinner.succeed(chalk.green('Published skill ') + created.id);
|
|
119
|
+
console.log(chalk.dim(created.title));
|
|
120
|
+
};
|
|
@@ -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
|
@@ -13,6 +13,8 @@ import { runMcpStart, runMcpStatus } from './commands/mcp.js';
|
|
|
13
13
|
import { runLog } from './commands/log.js';
|
|
14
14
|
import { runRulesList, runRulesPull, runRulesPush, runRulesSearch } from './commands/rules.js';
|
|
15
15
|
import { runSolutionsPull, runSolutionsPush, runSolutionsSearch } from './commands/solutions.js';
|
|
16
|
+
import { runSkillsList, runSkillsPull, runSkillsPush, runSkillsSearch } from './commands/skills.js';
|
|
17
|
+
import { runCommandsList, runCommandsPull, runCommandsPush, runCommandsSearch } from './commands/commands.js';
|
|
16
18
|
import { runWhoami } from './commands/whoami.js';
|
|
17
19
|
// Read version from package.json
|
|
18
20
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -55,8 +57,9 @@ rules.command('search [query]').description('Search rules').action((query) => ru
|
|
|
55
57
|
rules.command('list').description('List rules').action(() => runRulesList().catch(handleErr));
|
|
56
58
|
rules
|
|
57
59
|
.command('pull [id]')
|
|
58
|
-
.description('Pull a rule by ID or choose from list')
|
|
60
|
+
.description('Pull a rule by ID or choose from list (creates symbolic link by default)')
|
|
59
61
|
.option('-g, --global', 'Install globally to ~/.cursor/rules/ for all projects')
|
|
62
|
+
.option('--copy', 'Copy file instead of creating symbolic link')
|
|
60
63
|
.action((id, options) => runRulesPull(id, options).catch(handleErr));
|
|
61
64
|
rules.command('push [file]').description('Push a rule (file or interactive)').action((file) => runRulesPush(file).catch(handleErr));
|
|
62
65
|
// solutions
|
|
@@ -64,10 +67,33 @@ const solutions = program.command('solutions').description('Manage solutions');
|
|
|
64
67
|
solutions.command('search [query]').description('Search solutions').action((query) => runSolutionsSearch(query).catch(handleErr));
|
|
65
68
|
solutions
|
|
66
69
|
.command('pull [id]')
|
|
67
|
-
.description('Pull a solution by ID or choose from list')
|
|
70
|
+
.description('Pull a solution by ID or choose from list (creates symbolic link by default)')
|
|
68
71
|
.option('-g, --global', 'Install globally to ~/.cursor/rules/ for all projects')
|
|
72
|
+
.option('--copy', 'Copy file instead of creating symbolic link')
|
|
69
73
|
.action((id, options) => runSolutionsPull(id, options).catch(handleErr));
|
|
70
74
|
solutions.command('push [file]').description('Push a solution (file or interactive)').action((file) => runSolutionsPush(file).catch(handleErr));
|
|
75
|
+
// skills
|
|
76
|
+
const skills = program.command('skills').description('Manage skills');
|
|
77
|
+
skills.command('search [query]').description('Search skills').action((query) => runSkillsSearch(query).catch(handleErr));
|
|
78
|
+
skills.command('list').description('List skills').action(() => runSkillsList().catch(handleErr));
|
|
79
|
+
skills
|
|
80
|
+
.command('pull [id]')
|
|
81
|
+
.description('Pull a skill by ID or choose from list (creates symbolic link by default)')
|
|
82
|
+
.option('-g, --global', 'Install globally to ~/.cursor/rules/ for all projects')
|
|
83
|
+
.option('--copy', 'Copy file instead of creating symbolic link')
|
|
84
|
+
.action((id, options) => runSkillsPull(id, options).catch(handleErr));
|
|
85
|
+
skills.command('push [file]').description('Push a skill (file or interactive)').action((file) => runSkillsPush(file).catch(handleErr));
|
|
86
|
+
// commands
|
|
87
|
+
const commands = program.command('commands').description('Manage commands');
|
|
88
|
+
commands.command('search [query]').description('Search commands').action((query) => runCommandsSearch(query).catch(handleErr));
|
|
89
|
+
commands.command('list').description('List commands').action(() => runCommandsList().catch(handleErr));
|
|
90
|
+
commands
|
|
91
|
+
.command('pull [id]')
|
|
92
|
+
.description('Pull a command by ID or choose from list (creates symbolic link by default)')
|
|
93
|
+
.option('-g, --global', 'Install globally to ~/.cursor/rules/ for all projects')
|
|
94
|
+
.option('--copy', 'Copy file instead of creating symbolic link')
|
|
95
|
+
.action((id, options) => runCommandsPull(id, options).catch(handleErr));
|
|
96
|
+
commands.command('push [file]').description('Push a command (file or interactive)').action((file) => runCommandsPush(file).catch(handleErr));
|
|
71
97
|
// mcp
|
|
72
98
|
const mcp = program.command('mcp').description('MCP server');
|
|
73
99
|
mcp.command('start').description('Start MCP server (stdio)').action(() => runMcpStart().catch(handleErr));
|
|
@@ -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
|
-
import { ruleFilename, solutionFilename } from './slug.js';
|
|
6
|
+
import { ruleFilename, solutionFilename, skillFilename, commandFilename } 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.`);
|
|
@@ -28,12 +33,70 @@ export const pullRuleToFile = async (id, options = {}) => {
|
|
|
28
33
|
outDir = join(process.cwd(), outputPath);
|
|
29
34
|
}
|
|
30
35
|
mkdirSync(outDir, { recursive: true });
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
:
|
|
37
|
-
|
|
36
|
+
let filename;
|
|
37
|
+
switch (rule.kind) {
|
|
38
|
+
case 'solution':
|
|
39
|
+
filename = join(outDir, solutionFilename(rule.title, rule.id));
|
|
40
|
+
break;
|
|
41
|
+
case 'skill':
|
|
42
|
+
filename = join(outDir, skillFilename(rule.title, rule.id));
|
|
43
|
+
break;
|
|
44
|
+
case 'command':
|
|
45
|
+
filename = join(outDir, commandFilename(rule.title, rule.id));
|
|
46
|
+
break;
|
|
47
|
+
case 'rule':
|
|
48
|
+
default:
|
|
49
|
+
filename = join(outDir, ruleFilename(rule.title, rule.id));
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
// Remove existing file/symlink if it exists
|
|
53
|
+
if (existsSync(filename)) {
|
|
54
|
+
try {
|
|
55
|
+
const stats = lstatSync(filename);
|
|
56
|
+
if (stats.isSymbolicLink() || stats.isFile()) {
|
|
57
|
+
unlinkSync(filename);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Ignore errors when removing
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (useSymlink) {
|
|
65
|
+
// Create symbolic link to cached file
|
|
66
|
+
// Use relative path for portability
|
|
67
|
+
const relativePath = relative(dirname(filename), cachedPath);
|
|
68
|
+
try {
|
|
69
|
+
symlinkSync(relativePath, filename);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
// Fallback to absolute path if relative fails (e.g., on Windows or cross-filesystem)
|
|
73
|
+
if (error.code === 'ENOENT' || error.code === 'EXDEV') {
|
|
74
|
+
symlinkSync(cachedPath, filename);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
// Fallback: copy file content (for compatibility or when symlinks aren't desired)
|
|
83
|
+
let content;
|
|
84
|
+
switch (rule.kind) {
|
|
85
|
+
case 'solution':
|
|
86
|
+
content = `# ${rule.title}\n\n${rule.description}\n\n## Solution\n\n${rule.body}\n`;
|
|
87
|
+
break;
|
|
88
|
+
case 'skill':
|
|
89
|
+
content = `# ${rule.title}\n\n${rule.description}\n\n${rule.body}\n`;
|
|
90
|
+
break;
|
|
91
|
+
case 'command':
|
|
92
|
+
content = `# ${rule.title}\n\n${rule.description}\n\n${rule.body}\n`;
|
|
93
|
+
break;
|
|
94
|
+
case 'rule':
|
|
95
|
+
default:
|
|
96
|
+
content = `# ${rule.title}\n\n${rule.description}\n\n${rule.body}\n`;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
writeFileSync(filename, content);
|
|
100
|
+
}
|
|
38
101
|
return filename;
|
|
39
102
|
};
|
package/dist/lib/slug.d.ts
CHANGED
|
@@ -13,3 +13,13 @@ export declare const ruleFilename: (title: string, id: string) => string;
|
|
|
13
13
|
* Falls back to id if slug is empty.
|
|
14
14
|
*/
|
|
15
15
|
export declare const solutionFilename: (title: string, id: string) => string;
|
|
16
|
+
/**
|
|
17
|
+
* Returns the skill filename (e.g. skill-strava-api-authentication-flow.md).
|
|
18
|
+
* Falls back to id if slug is empty.
|
|
19
|
+
*/
|
|
20
|
+
export declare const skillFilename: (title: string, id: string) => string;
|
|
21
|
+
/**
|
|
22
|
+
* Returns the command filename (e.g. command-strava-api-authentication-flow.md).
|
|
23
|
+
* Falls back to id if slug is empty.
|
|
24
|
+
*/
|
|
25
|
+
export declare const commandFilename: (title: string, id: string) => string;
|
package/dist/lib/slug.js
CHANGED
|
@@ -31,3 +31,21 @@ export const solutionFilename = (title, id) => {
|
|
|
31
31
|
const base = slug ? `solution-${slug}` : `solution-${id}`;
|
|
32
32
|
return `${base}.md`;
|
|
33
33
|
};
|
|
34
|
+
/**
|
|
35
|
+
* Returns the skill filename (e.g. skill-strava-api-authentication-flow.md).
|
|
36
|
+
* Falls back to id if slug is empty.
|
|
37
|
+
*/
|
|
38
|
+
export const skillFilename = (title, id) => {
|
|
39
|
+
const slug = titleToSlug(title);
|
|
40
|
+
const base = slug ? `skill-${slug}` : `skill-${id}`;
|
|
41
|
+
return `${base}.md`;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Returns the command filename (e.g. command-strava-api-authentication-flow.md).
|
|
45
|
+
* Falls back to id if slug is empty.
|
|
46
|
+
*/
|
|
47
|
+
export const commandFilename = (title, id) => {
|
|
48
|
+
const slug = titleToSlug(title);
|
|
49
|
+
const base = slug ? `command-${slug}` : `command-${id}`;
|
|
50
|
+
return `${base}.md`;
|
|
51
|
+
};
|
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;
|