bitcompass 0.3.9 → 0.4.1

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.
@@ -41,18 +41,7 @@ export const getSupabaseClient = () => {
41
41
  });
42
42
  };
43
43
  /** Client for public read-only (rules/solutions). Works without login when RLS allows public select. */
44
- export const getSupabaseClientForRead = () => {
45
- const pair = getSupabaseUrlAndKey();
46
- if (!pair)
47
- return null;
48
- const creds = loadCredentials();
49
- const accessToken = creds?.access_token;
50
- return createClient(pair.url, pair.key, {
51
- global: accessToken
52
- ? { headers: { Authorization: `Bearer ${accessToken}` } }
53
- : undefined,
54
- });
55
- };
44
+ export const getSupabaseClientForRead = getSupabaseClient;
56
45
  export const fetchRules = async (kind) => {
57
46
  const client = getSupabaseClientForRead();
58
47
  if (!client)
@@ -7,3 +7,5 @@ export declare const loadCredentials: () => StoredCredentials | null;
7
7
  export declare const saveCredentials: (creds: StoredCredentials) => void;
8
8
  export declare const clearCredentials: () => void;
9
9
  export declare const isLoggedIn: () => boolean;
10
+ /** Returns the current user email if logged in, otherwise null. Reused by whoami and init. */
11
+ export declare const getCurrentUserEmail: () => string | null;
@@ -79,3 +79,10 @@ export const isLoggedIn = () => {
79
79
  const creds = loadCredentials();
80
80
  return Boolean(creds?.access_token);
81
81
  };
82
+ /** Returns the current user email if logged in, otherwise null. Reused by whoami and init. */
83
+ export const getCurrentUserEmail = () => {
84
+ const creds = loadCredentials();
85
+ if (!creds?.access_token)
86
+ return null;
87
+ return creds.user?.email ?? null;
88
+ };
@@ -8,4 +8,6 @@ export declare const runCommandsPull: (id?: string, options?: {
8
8
  global?: boolean;
9
9
  copy?: boolean;
10
10
  }) => Promise<void>;
11
- export declare const runCommandsPush: (file?: string) => Promise<void>;
11
+ export declare const runCommandsPush: (file?: string, options?: {
12
+ projectId?: string;
13
+ }) => Promise<void>;
@@ -2,9 +2,10 @@ import inquirer from 'inquirer';
2
2
  import ora from 'ora';
3
3
  import chalk from 'chalk';
4
4
  import { loadCredentials } from '../auth/config.js';
5
- import { searchRules, fetchRules, getRuleById, insertRule } from '../api/client.js';
5
+ import { searchRules, fetchRules, getRuleById } from '../api/client.js';
6
6
  import { pullRuleToFile } from '../lib/rule-file-ops.js';
7
7
  import { formatList, shouldUseTable } from '../lib/list-format.js';
8
+ import { runSharePush } from './share.js';
8
9
  export const runCommandsSearch = async (query, options) => {
9
10
  if (!loadCredentials()) {
10
11
  console.error(chalk.red('Not logged in. Run bitcompass login.'));
@@ -95,35 +96,6 @@ export const runCommandsPull = async (id, options) => {
95
96
  process.exit(message.includes('not found') ? 2 : 1);
96
97
  }
97
98
  };
98
- export const runCommandsPush = async (file) => {
99
- if (!loadCredentials()) {
100
- console.error(chalk.red('Not logged in. Run bitcompass login.'));
101
- process.exit(1);
102
- }
103
- let payload;
104
- if (file) {
105
- const { readFileSync } = await import('fs');
106
- const raw = readFileSync(file, 'utf-8');
107
- try {
108
- payload = JSON.parse(raw);
109
- payload.kind = 'command';
110
- }
111
- catch {
112
- const lines = raw.split('\n');
113
- const title = lines[0].replace(/^#\s*/, '') || 'Untitled';
114
- payload = { kind: 'command', title, description: '', body: raw };
115
- }
116
- }
117
- else {
118
- const answers = await inquirer.prompt([
119
- { name: 'title', message: 'Command title', type: 'input', default: 'Untitled' },
120
- { name: 'description', message: 'Description', type: 'input', default: '' },
121
- { name: 'body', message: 'Command content', type: 'editor', default: '' },
122
- ]);
123
- payload = { kind: 'command', title: answers.title, description: answers.description, body: answers.body };
124
- }
125
- const spinner = ora('Publishing command…').start();
126
- const created = await insertRule(payload);
127
- spinner.succeed(chalk.green('Published command ') + created.id);
128
- console.log(chalk.dim(created.title));
99
+ export const runCommandsPush = async (file, options) => {
100
+ await runSharePush(file, { kind: 'command', projectId: options?.projectId });
129
101
  };
@@ -3,6 +3,11 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
3
3
  import inquirer from 'inquirer';
4
4
  import { join } from 'path';
5
5
  import { getEditorDefaultPath, getProjectConfigDir, loadProjectConfig, saveProjectConfig, } from '../auth/project-config.js';
6
+ import { getCurrentUserEmail, isLoggedIn } from '../auth/config.js';
7
+ import { gradient, printBanner } from '../lib/banner.js';
8
+ const cyan = [0, 212, 255];
9
+ const magenta = [255, 64, 200];
10
+ const INDENT = ' ';
6
11
  const EDITOR_CHOICES = [
7
12
  { name: 'VSCode', value: 'vscode' },
8
13
  { name: 'Cursor', value: 'cursor' },
@@ -26,6 +31,7 @@ const ensureGitignoreEntry = () => {
26
31
  writeFileSync(gitignorePath, `${trimmed}${suffix}\n${GITIGNORE_ENTRY}\n`, 'utf-8');
27
32
  };
28
33
  export const runInit = async () => {
34
+ await printBanner();
29
35
  const existing = loadProjectConfig();
30
36
  const answers = await inquirer.prompt([
31
37
  {
@@ -196,10 +202,21 @@ Use these commands when you need to interact with BitCompass from the terminal:
196
202
  5. **Global vs project:** Use \`--global\` flag when you want a rule available across all projects, otherwise use project-specific rules
197
203
  `;
198
204
  writeFileSync(rulePath, ruleContent, 'utf-8');
199
- console.log(chalk.green('Project configured.'));
200
- console.log(chalk.dim('Config:'), join(getProjectConfigDir(), 'config.json'));
201
- console.log(chalk.dim('Editor:'), config.editor);
202
- console.log(chalk.dim('Output path:'), config.outputPath);
203
- console.log(chalk.dim('.gitignore:'), GITIGNORE_ENTRY, 'added or already present.');
204
- console.log(chalk.green('Rule created:'), rulePath);
205
+ console.log(gradient('Project configured.', cyan, magenta));
206
+ console.log(INDENT + chalk.bold('Config:'), join(getProjectConfigDir(), 'config.json'));
207
+ console.log(INDENT + chalk.bold('Editor:'), config.editor);
208
+ console.log(INDENT + chalk.bold('Output path:'), config.outputPath);
209
+ console.log(INDENT + chalk.bold('.gitignore:'), GITIGNORE_ENTRY, 'added or already present.');
210
+ console.log(INDENT + chalk.bold('Rule created:'), rulePath);
211
+ const email = getCurrentUserEmail();
212
+ const loggedIn = isLoggedIn();
213
+ console.log('');
214
+ if (loggedIn) {
215
+ console.log(email
216
+ ? 'You are ready to go ' + chalk.bold(email) + '!'
217
+ : 'You are ready to go!');
218
+ }
219
+ else {
220
+ console.log(chalk.dim('Run ') + chalk.cyan('bitcompass login') + chalk.dim(' to sign in.'));
221
+ }
205
222
  };
@@ -1,5 +1,10 @@
1
1
  import type { TimeFrame } from '../lib/git-analysis.js';
2
2
  export type LogProgressStep = 'analyzing' | 'pushing';
3
+ type PeriodBounds = {
4
+ period_start: string;
5
+ period_end: string;
6
+ since: string;
7
+ };
3
8
  /**
4
9
  * Shared logic: resolve repo, compute period, gather summary + git analysis, insert log.
5
10
  * Used by both CLI and MCP. Returns the created log id or throws.
@@ -11,11 +16,7 @@ export declare const buildAndPushActivityLog: (timeFrame: TimeFrame, cwd: string
11
16
  /**
12
17
  * Push an activity log for a custom date or date range. timeFrame is used for display (day/week/month).
13
18
  */
14
- export declare const buildAndPushActivityLogWithPeriod: (period: {
15
- period_start: string;
16
- period_end: string;
17
- since: string;
18
- }, timeFrame: TimeFrame, cwd: string, onProgress?: (step: LogProgressStep) => void) => Promise<{
19
+ export declare const buildAndPushActivityLogWithPeriod: (period: PeriodBounds, timeFrame: TimeFrame, cwd: string, onProgress?: (step: LogProgressStep) => void) => Promise<{
19
20
  id: string;
20
21
  }>;
21
22
  /** Parse argv for log: [start] or [start, end] or [start, '-', end]. Returns { start, end } or null for interactive. */
@@ -28,3 +29,4 @@ export declare class ValidationError extends Error {
28
29
  constructor(message: string);
29
30
  }
30
31
  export declare const runLog: (args?: string[]) => Promise<void>;
32
+ export {};
@@ -1,23 +1,14 @@
1
+ import chalk from 'chalk';
1
2
  import inquirer from 'inquirer';
2
3
  import ora from 'ora';
3
- import chalk from 'chalk';
4
4
  import { insertActivityLog } from '../api/client.js';
5
5
  import { loadCredentials } from '../auth/config.js';
6
- import { getRepoRoot, getRepoSummary, getGitAnalysis, getPeriodForTimeFrame, getPeriodForCustomDates, parseDate, } from '../lib/git-analysis.js';
7
- /**
8
- * Shared logic: resolve repo, compute period, gather summary + git analysis, insert log.
9
- * Used by both CLI and MCP. Returns the created log id or throws.
10
- * Optional onProgress callback for CLI to show step-wise spinner (e.g. analyzing → pushing).
11
- */
12
- export const buildAndPushActivityLog = async (timeFrame, cwd, onProgress) => {
13
- const repoRoot = getRepoRoot(cwd);
14
- if (!repoRoot) {
15
- throw new Error('Not a git repository. Run from a project with git or pass a valid repo path.');
16
- }
17
- const period = getPeriodForTimeFrame(timeFrame);
6
+ import { getGitAnalysis, getPeriodForCustomDates, getPeriodForTimeFrame, getRepoRoot, getRepoSummary, parseDate, } from '../lib/git-analysis.js';
7
+ /** Core logic: gather summary + git analysis, insert log. Shared by both build functions. */
8
+ const buildAndPushCore = async (repoRoot, period, timeFrame, onProgress) => {
18
9
  onProgress?.('analyzing');
19
10
  const repo_summary = getRepoSummary(repoRoot);
20
- const git_analysis = getGitAnalysis(repoRoot, period.since);
11
+ const git_analysis = getGitAnalysis(repoRoot, period.since, period.period_end);
21
12
  const payload = {
22
13
  time_frame: timeFrame,
23
14
  period_start: period.period_start,
@@ -29,6 +20,19 @@ export const buildAndPushActivityLog = async (timeFrame, cwd, onProgress) => {
29
20
  const created = await insertActivityLog(payload);
30
21
  return { id: created.id };
31
22
  };
23
+ /**
24
+ * Shared logic: resolve repo, compute period, gather summary + git analysis, insert log.
25
+ * Used by both CLI and MCP. Returns the created log id or throws.
26
+ * Optional onProgress callback for CLI to show step-wise spinner (e.g. analyzing → pushing).
27
+ */
28
+ export const buildAndPushActivityLog = async (timeFrame, cwd, onProgress) => {
29
+ const repoRoot = getRepoRoot(cwd);
30
+ if (!repoRoot) {
31
+ throw new Error('Not a git repository. Run from a project with git or pass a valid repo path.');
32
+ }
33
+ const period = getPeriodForTimeFrame(timeFrame);
34
+ return buildAndPushCore(repoRoot, period, timeFrame, onProgress);
35
+ };
32
36
  /**
33
37
  * Push an activity log for a custom date or date range. timeFrame is used for display (day/week/month).
34
38
  */
@@ -37,19 +41,7 @@ export const buildAndPushActivityLogWithPeriod = async (period, timeFrame, cwd,
37
41
  if (!repoRoot) {
38
42
  throw new Error('Not a git repository. Run from a project with git or pass a valid repo path.');
39
43
  }
40
- onProgress?.('analyzing');
41
- const repo_summary = getRepoSummary(repoRoot);
42
- const git_analysis = getGitAnalysis(repoRoot, period.since, period.period_end);
43
- const payload = {
44
- time_frame: timeFrame,
45
- period_start: period.period_start,
46
- period_end: period.period_end,
47
- repo_summary: repo_summary,
48
- git_analysis: git_analysis,
49
- };
50
- onProgress?.('pushing');
51
- const created = await insertActivityLog(payload);
52
- return { id: created.id };
44
+ return buildAndPushCore(repoRoot, period, timeFrame, onProgress);
53
45
  };
54
46
  const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
55
47
  const isDateArg = (s) => ISO_DATE.test(s.trim());
@@ -86,45 +78,28 @@ const timeFrameForRange = (start, end) => {
86
78
  return 'week';
87
79
  return 'month';
88
80
  };
89
- export const runLog = async (args = []) => {
90
- if (!loadCredentials()) {
91
- console.error(chalk.red('Not logged in. Run bitcompass login.'));
92
- process.exit(1);
81
+ const runLogWithParsedDates = async (parsed, cwd, spinner, onProgress) => {
82
+ if (!parseDate(parsed.start)) {
83
+ spinner.stop();
84
+ throw new ValidationError(`Invalid date "${parsed.start}". Use YYYY-MM-DD (e.g. 2025-02-06).`);
93
85
  }
94
- const cwd = process.cwd();
95
- const repoRoot = getRepoRoot(cwd);
96
- if (!repoRoot) {
97
- console.error(chalk.red('Not a git repository. Run this command from a project with git.'));
98
- process.exit(1);
86
+ if (parsed.end !== undefined && !parseDate(parsed.end)) {
87
+ spinner.stop();
88
+ throw new ValidationError(`Invalid date "${parsed.end}". Use YYYY-MM-DD (e.g. 2025-02-06).`);
99
89
  }
100
- const parsed = parseLogArgs(args);
101
- const spinner = ora('Analyzing repository…').start();
102
- const onProgress = (step) => {
103
- spinner.text = step === 'analyzing' ? 'Analyzing repository…' : 'Pushing activity log…';
104
- };
105
- if (parsed) {
106
- if (!parseDate(parsed.start)) {
107
- spinner.stop();
108
- throw new ValidationError(`Invalid date "${parsed.start}". Use YYYY-MM-DD (e.g. 2025-02-06).`);
109
- }
110
- if (parsed.end !== undefined && !parseDate(parsed.end)) {
111
- spinner.stop();
112
- throw new ValidationError(`Invalid date "${parsed.end}". Use YYYY-MM-DD (e.g. 2025-02-06).`);
113
- }
114
- const period = getPeriodForCustomDates(parsed.start, parsed.end);
115
- const timeFrame = parsed.end ? timeFrameForRange(parsed.start, parsed.end) : 'day';
116
- try {
117
- const result = await buildAndPushActivityLogWithPeriod(period, timeFrame, cwd, onProgress);
118
- spinner.succeed(chalk.green('Log saved.'));
119
- console.log(chalk.dim(result.id));
120
- }
121
- catch (err) {
122
- spinner.fail(chalk.red(err instanceof Error ? err.message : 'Failed'));
123
- throw err;
124
- }
125
- return;
90
+ const period = getPeriodForCustomDates(parsed.start, parsed.end);
91
+ const timeFrame = parsed.end ? timeFrameForRange(parsed.start, parsed.end) : 'day';
92
+ try {
93
+ const result = await buildAndPushActivityLogWithPeriod(period, timeFrame, cwd, onProgress);
94
+ spinner.succeed(chalk.green('Log saved.'));
95
+ console.log(chalk.dim(result.id));
126
96
  }
127
- spinner.stop();
97
+ catch (err) {
98
+ spinner.fail(chalk.red(err instanceof Error ? err.message : 'Failed'));
99
+ throw err;
100
+ }
101
+ };
102
+ const runLogInteractive = async (cwd, onProgress) => {
128
103
  const choice = await inquirer.prompt([
129
104
  {
130
105
  name: 'time_frame',
@@ -137,10 +112,9 @@ export const runLog = async (args = []) => {
137
112
  ],
138
113
  },
139
114
  ]);
140
- const timeFrame = choice.time_frame;
141
- spinner.start('Analyzing repository…');
115
+ const spinner = ora('Analyzing repository…').start();
142
116
  try {
143
- const result = await buildAndPushActivityLog(timeFrame, cwd, onProgress);
117
+ const result = await buildAndPushActivityLog(choice.time_frame, cwd, onProgress);
144
118
  spinner.succeed(chalk.green('Log saved.'));
145
119
  console.log(chalk.dim(result.id));
146
120
  }
@@ -149,3 +123,26 @@ export const runLog = async (args = []) => {
149
123
  throw err;
150
124
  }
151
125
  };
126
+ export const runLog = async (args = []) => {
127
+ if (!loadCredentials()) {
128
+ console.error(chalk.red('Not logged in. Run bitcompass login.'));
129
+ process.exit(1);
130
+ }
131
+ const cwd = process.cwd();
132
+ const repoRoot = getRepoRoot(cwd);
133
+ if (!repoRoot) {
134
+ console.error(chalk.red('Not a git repository. Run this command from a project with git.'));
135
+ process.exit(1);
136
+ }
137
+ const parsed = parseLogArgs(args);
138
+ const spinner = ora('Analyzing repository…').start();
139
+ const onProgress = (step) => {
140
+ spinner.text = step === 'analyzing' ? 'Analyzing repository…' : 'Pushing activity log…';
141
+ };
142
+ if (parsed) {
143
+ await runLogWithParsedDates(parsed, cwd, spinner, onProgress);
144
+ return;
145
+ }
146
+ spinner.stop();
147
+ await runLogInteractive(cwd, onProgress);
148
+ };
@@ -8,4 +8,6 @@ export declare const runRulesPull: (id?: string, options?: {
8
8
  global?: boolean;
9
9
  copy?: boolean;
10
10
  }) => Promise<void>;
11
- export declare const runRulesPush: (file?: string) => Promise<void>;
11
+ export declare const runRulesPush: (file?: string, options?: {
12
+ projectId?: string;
13
+ }) => Promise<void>;
@@ -2,10 +2,10 @@ import inquirer from 'inquirer';
2
2
  import ora from 'ora';
3
3
  import chalk from 'chalk';
4
4
  import { loadCredentials } from '../auth/config.js';
5
- import { searchRules, fetchRules, getRuleById, insertRule } from '../api/client.js';
5
+ import { searchRules, fetchRules, getRuleById } from '../api/client.js';
6
6
  import { pullRuleToFile } from '../lib/rule-file-ops.js';
7
- import { parseRuleMdcContent } from '../lib/mdc-format.js';
8
7
  import { formatList, shouldUseTable } from '../lib/list-format.js';
8
+ import { runSharePush } from './share.js';
9
9
  export const runRulesSearch = async (query, options) => {
10
10
  if (!loadCredentials()) {
11
11
  console.error(chalk.red('Not logged in. Run bitcompass login.'));
@@ -96,48 +96,6 @@ export const runRulesPull = async (id, options) => {
96
96
  process.exit(message.includes('not found') ? 2 : 1);
97
97
  }
98
98
  };
99
- export const runRulesPush = async (file) => {
100
- if (!loadCredentials()) {
101
- console.error(chalk.red('Not logged in. Run bitcompass login.'));
102
- process.exit(1);
103
- }
104
- let payload;
105
- if (file) {
106
- const { readFileSync } = await import('fs');
107
- const raw = readFileSync(file, 'utf-8');
108
- try {
109
- payload = JSON.parse(raw);
110
- }
111
- catch {
112
- const parsed = parseRuleMdcContent(raw);
113
- if (parsed) {
114
- const titleFromBody = parsed.body.split('\n')[0]?.replace(/^#\s*/, '').trim() || 'Untitled';
115
- payload = {
116
- kind: 'rule',
117
- title: titleFromBody,
118
- description: parsed.description,
119
- body: parsed.body,
120
- globs: parsed.globs ?? undefined,
121
- always_apply: parsed.alwaysApply,
122
- };
123
- }
124
- else {
125
- const lines = raw.split('\n');
126
- const title = lines[0].replace(/^#\s*/, '') || 'Untitled';
127
- payload = { kind: 'rule', title, description: '', body: raw };
128
- }
129
- }
130
- }
131
- else {
132
- const answers = await inquirer.prompt([
133
- { name: 'title', message: 'Rule title', type: 'input', default: 'Untitled' },
134
- { name: 'description', message: 'Description', type: 'input', default: '' },
135
- { name: 'body', message: 'Rule content', type: 'editor', default: '' },
136
- ]);
137
- payload = { kind: 'rule', title: answers.title, description: answers.description, body: answers.body };
138
- }
139
- const spinner = ora('Publishing rule…').start();
140
- const created = await insertRule(payload);
141
- spinner.succeed(chalk.green('Published rule ') + created.id);
142
- console.log(chalk.dim(created.title));
99
+ export const runRulesPush = async (file, options) => {
100
+ await runSharePush(file, { kind: 'rule', projectId: options?.projectId });
143
101
  };
@@ -0,0 +1,19 @@
1
+ import type { RuleInsert, RuleKind } from '../types.js';
2
+ /**
3
+ * Infers RuleKind from file content (frontmatter kind) or filename prefix.
4
+ */
5
+ export declare const inferKindFromFile: (filePath: string, rawContent: string) => RuleKind | null;
6
+ /**
7
+ * Parses file content into RuleInsert. Uses explicitKind if provided, else inferred from file.
8
+ * If kind cannot be determined, payload.kind may be left undefined (caller should prompt).
9
+ */
10
+ export declare const parseFileToPayload: (filePath: string, rawContent: string, explicitKind?: RuleKind) => Omit<RuleInsert, "kind"> & {
11
+ kind?: RuleKind;
12
+ };
13
+ /**
14
+ * Unified share push: optionally read from file, infer or prompt for kind, then publish.
15
+ */
16
+ export declare const runSharePush: (file?: string, options?: {
17
+ kind?: RuleKind;
18
+ projectId?: string;
19
+ }) => Promise<void>;
@@ -0,0 +1,137 @@
1
+ import { readFileSync } from 'fs';
2
+ import inquirer from 'inquirer';
3
+ import ora from 'ora';
4
+ import chalk from 'chalk';
5
+ import { loadCredentials } from '../auth/config.js';
6
+ import { insertRule } from '../api/client.js';
7
+ import { parseRuleMdcContent, parseFrontmatterKind } from '../lib/mdc-format.js';
8
+ import { SHARE_KIND_CHOICES, inferKindFromFilename } from '../lib/share-types.js';
9
+ const KIND_LABELS = {
10
+ rule: 'rule',
11
+ solution: 'solution',
12
+ skill: 'skill',
13
+ command: 'command',
14
+ };
15
+ /**
16
+ * Infers RuleKind from file content (frontmatter kind) or filename prefix.
17
+ */
18
+ export const inferKindFromFile = (filePath, rawContent) => {
19
+ const fromFrontmatter = parseFrontmatterKind(rawContent);
20
+ if (fromFrontmatter)
21
+ return fromFrontmatter;
22
+ return inferKindFromFilename(filePath);
23
+ };
24
+ /**
25
+ * Parses file content into RuleInsert. Uses explicitKind if provided, else inferred from file.
26
+ * If kind cannot be determined, payload.kind may be left undefined (caller should prompt).
27
+ */
28
+ export const parseFileToPayload = (filePath, rawContent, explicitKind) => {
29
+ const inferredKind = inferKindFromFile(filePath, rawContent);
30
+ const kind = explicitKind ?? inferredKind ?? undefined;
31
+ try {
32
+ const parsed = JSON.parse(rawContent);
33
+ const payload = {
34
+ kind: parsed.kind ?? kind ?? 'rule',
35
+ title: parsed.title ?? 'Untitled',
36
+ description: parsed.description ?? '',
37
+ body: parsed.body ?? '',
38
+ context: typeof parsed.context === 'string' ? parsed.context : undefined,
39
+ examples: Array.isArray(parsed.examples) ? parsed.examples : undefined,
40
+ technologies: Array.isArray(parsed.technologies) ? parsed.technologies : undefined,
41
+ globs: typeof parsed.globs === 'string' ? parsed.globs : undefined,
42
+ always_apply: Boolean(parsed.always_apply),
43
+ };
44
+ if (explicitKind)
45
+ payload.kind = explicitKind;
46
+ else if (kind)
47
+ payload.kind = kind;
48
+ return payload;
49
+ }
50
+ catch {
51
+ // not JSON
52
+ }
53
+ const mdc = parseRuleMdcContent(rawContent);
54
+ if (mdc) {
55
+ const titleFromBody = mdc.body.split('\n')[0]?.replace(/^#\s*/, '').trim() || 'Untitled';
56
+ return {
57
+ kind: explicitKind ?? mdc.kind ?? inferredKind ?? undefined,
58
+ title: titleFromBody,
59
+ description: mdc.description,
60
+ body: mdc.body,
61
+ globs: mdc.globs ?? undefined,
62
+ always_apply: mdc.alwaysApply,
63
+ };
64
+ }
65
+ const lines = rawContent.split('\n');
66
+ const title = (lines[0] ?? '').replace(/^#\s*/, '').trim() || 'Untitled';
67
+ return {
68
+ kind: explicitKind ?? inferredKind ?? undefined,
69
+ title,
70
+ description: '',
71
+ body: rawContent,
72
+ };
73
+ };
74
+ const promptForKind = async () => {
75
+ const { kind } = await inquirer.prompt([
76
+ {
77
+ name: 'kind',
78
+ message: 'What are you sharing?',
79
+ type: 'list',
80
+ choices: SHARE_KIND_CHOICES.map((c) => ({ name: `${c.name} – ${c.description}`, value: c.value })),
81
+ },
82
+ ]);
83
+ return kind;
84
+ };
85
+ /**
86
+ * Unified share push: optionally read from file, infer or prompt for kind, then publish.
87
+ */
88
+ export const runSharePush = async (file, options) => {
89
+ if (!loadCredentials()) {
90
+ console.error(chalk.red('Not logged in. Run bitcompass login.'));
91
+ process.exit(1);
92
+ }
93
+ let payload;
94
+ if (file) {
95
+ const raw = readFileSync(file, 'utf-8');
96
+ const parsed = parseFileToPayload(file, raw, options?.kind);
97
+ let kind = parsed.kind;
98
+ if (!kind) {
99
+ kind = await promptForKind();
100
+ }
101
+ payload = {
102
+ kind,
103
+ title: parsed.title,
104
+ description: parsed.description ?? '',
105
+ body: parsed.body,
106
+ context: parsed.context,
107
+ examples: parsed.examples,
108
+ technologies: parsed.technologies,
109
+ project_id: options?.projectId,
110
+ globs: parsed.globs,
111
+ always_apply: parsed.always_apply,
112
+ };
113
+ }
114
+ else {
115
+ const kind = options?.kind ?? (await promptForKind());
116
+ const answers = await inquirer.prompt([
117
+ { name: 'title', message: 'Title', type: 'input', default: 'Untitled' },
118
+ { name: 'description', message: 'Description', type: 'input', default: '' },
119
+ { name: 'body', message: 'Content', type: 'editor', default: '' },
120
+ ]);
121
+ payload = {
122
+ kind,
123
+ title: answers.title,
124
+ description: answers.description,
125
+ body: answers.body,
126
+ project_id: options?.projectId,
127
+ };
128
+ }
129
+ if (options?.projectId) {
130
+ payload = { ...payload, project_id: options.projectId };
131
+ }
132
+ const label = KIND_LABELS[payload.kind];
133
+ const spinner = ora(`Publishing ${label}…`).start();
134
+ const created = await insertRule(payload);
135
+ spinner.succeed(chalk.green(`Published ${label} `) + created.id);
136
+ console.log(chalk.dim(created.title));
137
+ };
@@ -8,4 +8,6 @@ export declare const runSkillsPull: (id?: string, options?: {
8
8
  global?: boolean;
9
9
  copy?: boolean;
10
10
  }) => Promise<void>;
11
- export declare const runSkillsPush: (file?: string) => Promise<void>;
11
+ export declare const runSkillsPush: (file?: string, options?: {
12
+ projectId?: string;
13
+ }) => Promise<void>;
@@ -2,9 +2,10 @@ import inquirer from 'inquirer';
2
2
  import ora from 'ora';
3
3
  import chalk from 'chalk';
4
4
  import { loadCredentials } from '../auth/config.js';
5
- import { searchRules, fetchRules, getRuleById, insertRule } from '../api/client.js';
5
+ import { searchRules, fetchRules, getRuleById } from '../api/client.js';
6
6
  import { pullRuleToFile } from '../lib/rule-file-ops.js';
7
7
  import { formatList, shouldUseTable } from '../lib/list-format.js';
8
+ import { runSharePush } from './share.js';
8
9
  export const runSkillsSearch = async (query, options) => {
9
10
  if (!loadCredentials()) {
10
11
  console.error(chalk.red('Not logged in. Run bitcompass login.'));
@@ -95,35 +96,6 @@ export const runSkillsPull = async (id, options) => {
95
96
  process.exit(message.includes('not found') ? 2 : 1);
96
97
  }
97
98
  };
98
- export const runSkillsPush = async (file) => {
99
- if (!loadCredentials()) {
100
- console.error(chalk.red('Not logged in. Run bitcompass login.'));
101
- process.exit(1);
102
- }
103
- let payload;
104
- if (file) {
105
- const { readFileSync } = await import('fs');
106
- const raw = readFileSync(file, 'utf-8');
107
- try {
108
- payload = JSON.parse(raw);
109
- payload.kind = 'skill';
110
- }
111
- catch {
112
- const lines = raw.split('\n');
113
- const title = lines[0].replace(/^#\s*/, '') || 'Untitled';
114
- payload = { kind: 'skill', title, description: '', body: raw };
115
- }
116
- }
117
- else {
118
- const answers = await inquirer.prompt([
119
- { name: 'title', message: 'Skill title', type: 'input', default: 'Untitled' },
120
- { name: 'description', message: 'Description', type: 'input', default: '' },
121
- { name: 'body', message: 'Skill content', type: 'editor', default: '' },
122
- ]);
123
- payload = { kind: 'skill', title: answers.title, description: answers.description, body: answers.body };
124
- }
125
- const spinner = ora('Publishing skill…').start();
126
- const created = await insertRule(payload);
127
- spinner.succeed(chalk.green('Published skill ') + created.id);
128
- console.log(chalk.dim(created.title));
99
+ export const runSkillsPush = async (file, options) => {
100
+ await runSharePush(file, { kind: 'skill', projectId: options?.projectId });
129
101
  };
@@ -8,4 +8,6 @@ export declare const runSolutionsPull: (id?: string, options?: {
8
8
  global?: boolean;
9
9
  copy?: boolean;
10
10
  }) => Promise<void>;
11
- export declare const runSolutionsPush: (file?: string) => Promise<void>;
11
+ export declare const runSolutionsPush: (file?: string, options?: {
12
+ projectId?: string;
13
+ }) => Promise<void>;