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.
@@ -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
+ };
@@ -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>;
@@ -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 { ruleFilename } from '../lib/slug.js';
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 rule = await getRuleById(targetId);
70
- if (!rule) {
71
- console.error(chalk.red('Rule not found.'));
72
- process.exit(1);
73
- }
74
- let outDir;
75
- if (options?.global) {
76
- // Use global location: ~/.cursor/rules/
77
- outDir = join(homedir(), '.cursor', 'rules');
78
- }
79
- else {
80
- // Use project config (default behavior)
81
- const { outputPath } = getProjectConfig({ warnIfMissing: true });
82
- outDir = join(process.cwd(), outputPath);
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
- mkdirSync(outDir, { recursive: true });
85
- const filename = join(outDir, ruleFilename(rule.title, rule.id));
86
- const content = `# ${rule.title}\n\n${rule.description}\n\n${rule.body}\n`;
87
- writeFileSync(filename, content);
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 { solutionFilename } from '../lib/slug.js';
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 rule = await getRuleById(targetId);
58
- if (!rule) {
59
- console.error(chalk.red('Solution not found.'));
60
- process.exit(1);
61
- }
62
- let outDir;
63
- if (options?.global) {
64
- // Use global location: ~/.cursor/rules/
65
- outDir = join(homedir(), '.cursor', 'rules');
66
- }
67
- else {
68
- // Use project config (default behavior)
69
- const { outputPath } = getProjectConfig({ warnIfMissing: true });
70
- outDir = join(process.cwd(), outputPath);
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
- mkdirSync(outDir, { recursive: true });
73
- const filename = join(outDir, solutionFilename(rule.title, rule.id));
74
- const content = `# ${rule.title}\n\n${rule.description}\n\n## Solution\n\n${rule.body}\n`;
75
- writeFileSync(filename, content);
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. Returns the file path where it was written.
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. Returns the file path where it was written.
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
- const filename = rule.kind === 'solution'
32
- ? join(outDir, solutionFilename(rule.title, rule.id))
33
- : join(outDir, ruleFilename(rule.title, rule.id));
34
- const content = rule.kind === 'solution'
35
- ? `# ${rule.title}\n\n${rule.description}\n\n## Solution\n\n${rule.body}\n`
36
- : `# ${rule.title}\n\n${rule.description}\n\n${rule.body}\n`;
37
- writeFileSync(filename, content);
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
  };
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bitcompass",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "BitCompass CLI - rules, solutions, and MCP server",
5
5
  "type": "module",
6
6
  "bin": {