brainctl 0.1.4 → 0.1.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/cli.d.ts CHANGED
@@ -2,6 +2,8 @@
2
2
  import { Command } from 'commander';
3
3
  import { type DoctorService } from './services/doctor-service.js';
4
4
  import { type InitService } from './services/init-service.js';
5
+ import { type ProfileExportService } from './services/profile-export-service.js';
6
+ import { type ProfileImportService } from './services/profile-import-service.js';
5
7
  import { type ProfileService } from './services/profile-service.js';
6
8
  import { type RunService } from './services/run-service.js';
7
9
  import { type StatusService } from './services/status-service.js';
@@ -12,6 +14,8 @@ export interface CliServices {
12
14
  statusService: StatusService;
13
15
  doctorService: DoctorService;
14
16
  profileService: ProfileService;
17
+ profileExportService: ProfileExportService;
18
+ profileImportService: ProfileImportService;
15
19
  syncService: SyncService;
16
20
  }
17
21
  export declare function createProgram(overrides?: Partial<CliServices>): Command;
package/dist/cli.js CHANGED
@@ -14,6 +14,8 @@ import { registerUiCommand } from './commands/ui.js';
14
14
  import { printError } from './output.js';
15
15
  import { createDoctorService } from './services/doctor-service.js';
16
16
  import { createInitService } from './services/init-service.js';
17
+ import { createProfileExportService } from './services/profile-export-service.js';
18
+ import { createProfileImportService } from './services/profile-import-service.js';
17
19
  import { createProfileService } from './services/profile-service.js';
18
20
  import { createRunService } from './services/run-service.js';
19
21
  import { createStatusService } from './services/status-service.js';
@@ -31,7 +33,11 @@ export function createProgram(overrides = {}) {
31
33
  registerStatusCommand(program, services.statusService);
32
34
  registerRunCommand(program, services.runService);
33
35
  registerDoctorCommand(program, services.doctorService);
34
- registerProfileCommand(program, services.profileService);
36
+ registerProfileCommand(program, {
37
+ profileService: services.profileService,
38
+ profileExportService: services.profileExportService,
39
+ profileImportService: services.profileImportService,
40
+ });
35
41
  registerSyncCommand(program, services.syncService);
36
42
  registerUiCommand(program);
37
43
  registerMcpCommand(program);
@@ -62,6 +68,8 @@ function createDefaultServices(overrides) {
62
68
  statusService: createStatusService({ resolver }),
63
69
  doctorService: createDoctorService({ resolver }),
64
70
  profileService,
71
+ profileExportService: createProfileExportService({ profileService }),
72
+ profileImportService: createProfileImportService(),
65
73
  syncService: createSyncService({ profileService }),
66
74
  ...overrides
67
75
  };
@@ -1,3 +1,10 @@
1
1
  import type { Command } from 'commander';
2
+ import type { ProfileExportService } from '../services/profile-export-service.js';
3
+ import type { ProfileImportService } from '../services/profile-import-service.js';
2
4
  import type { ProfileService } from '../services/profile-service.js';
3
- export declare function registerProfileCommand(program: Command, profileService: ProfileService): void;
5
+ export interface ProfileCommandServices {
6
+ profileService: ProfileService;
7
+ profileExportService: ProfileExportService;
8
+ profileImportService: ProfileImportService;
9
+ }
10
+ export declare function registerProfileCommand(program: Command, services: ProfileCommandServices): void;
@@ -1,5 +1,6 @@
1
1
  import pc from 'picocolors';
2
- export function registerProfileCommand(program, profileService) {
2
+ export function registerProfileCommand(program, services) {
3
+ const { profileService, profileExportService, profileImportService } = services;
3
4
  const profileCmd = program
4
5
  .command('profile')
5
6
  .description('Manage brainctl profiles');
@@ -40,4 +41,33 @@ export function registerProfileCommand(program, profileService) {
40
41
  const prev = result.previousProfile ? ` (was "${result.previousProfile}")` : '';
41
42
  console.log(`Switched to profile "${name}"${prev}`);
42
43
  });
44
+ profileCmd
45
+ .command('export')
46
+ .argument('<name>', 'Profile name to export')
47
+ .option('-o, --output <path>', 'Output file path')
48
+ .description('Export a profile as a portable tarball')
49
+ .action(async (name, options) => {
50
+ const result = await profileExportService.execute({
51
+ cwd: process.cwd(),
52
+ name,
53
+ outputPath: options.output,
54
+ });
55
+ console.log(`Exported profile to ${result.archivePath}`);
56
+ });
57
+ profileCmd
58
+ .command('import')
59
+ .argument('<archive>', 'Path to profile tarball')
60
+ .option('--force', 'Overwrite existing profile', false)
61
+ .description('Import a profile from a tarball')
62
+ .action(async (archive, options) => {
63
+ const result = await profileImportService.execute({
64
+ cwd: process.cwd(),
65
+ archivePath: archive,
66
+ force: options.force,
67
+ });
68
+ console.log(`Imported profile "${result.profileName}"`);
69
+ if (result.installedMcps.length > 0) {
70
+ console.log(`Installed bundled MCPs: ${result.installedMcps.join(', ')}`);
71
+ }
72
+ });
43
73
  }
@@ -6,6 +6,8 @@ import { loadConfig } from '../config.js';
6
6
  import { loadMemory } from '../context/memory.js';
7
7
  import { createDoctorService } from '../services/doctor-service.js';
8
8
  import { createMemoryWriteService } from '../services/memory-write-service.js';
9
+ import { createProfileExportService } from '../services/profile-export-service.js';
10
+ import { createProfileImportService } from '../services/profile-import-service.js';
9
11
  import { createProfileService } from '../services/profile-service.js';
10
12
  import { createRunService } from '../services/run-service.js';
11
13
  import { createStatusService } from '../services/status-service.js';
@@ -175,6 +177,40 @@ export function createMcpServer(options = {}) {
175
177
  return JSON.stringify(result, null, 2);
176
178
  },
177
179
  });
180
+ server.addTool({
181
+ name: 'brainctl_export_profile',
182
+ description: 'Export a profile as a portable tarball. Packages the profile config and bundled MCP source code for sharing.',
183
+ parameters: z.object({
184
+ name: z.string().describe('Profile name to export'),
185
+ output_path: z.string().optional().describe('Output file path (defaults to <name>.tar.gz in cwd)'),
186
+ }),
187
+ execute: async (args) => {
188
+ const exportService = createProfileExportService();
189
+ const result = await exportService.execute({
190
+ cwd,
191
+ name: args.name,
192
+ outputPath: args.output_path,
193
+ });
194
+ return JSON.stringify(result, null, 2);
195
+ },
196
+ });
197
+ server.addTool({
198
+ name: 'brainctl_import_profile',
199
+ description: 'Import a profile from a tarball. Extracts bundled MCP source, installs dependencies, and registers the profile.',
200
+ parameters: z.object({
201
+ archive_path: z.string().describe('Path to the profile tarball'),
202
+ force: z.boolean().default(false).describe('Overwrite existing profile if it exists'),
203
+ }),
204
+ execute: async (args) => {
205
+ const importService = createProfileImportService();
206
+ const result = await importService.execute({
207
+ cwd,
208
+ archivePath: args.archive_path,
209
+ force: args.force,
210
+ });
211
+ return JSON.stringify(result, null, 2);
212
+ },
213
+ });
178
214
  return server;
179
215
  }
180
216
  export async function startMcpServer(options = {}) {
@@ -0,0 +1,15 @@
1
+ import { type ProfileService } from './profile-service.js';
2
+ export interface ProfileExportService {
3
+ execute(options: {
4
+ cwd?: string;
5
+ name: string;
6
+ outputPath?: string;
7
+ }): Promise<{
8
+ archivePath: string;
9
+ }>;
10
+ }
11
+ interface ProfileExportDependencies {
12
+ profileService?: ProfileService;
13
+ }
14
+ export declare function createProfileExportService(deps?: ProfileExportDependencies): ProfileExportService;
15
+ export {};
@@ -0,0 +1,63 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { cp, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import YAML from 'yaml';
6
+ import { createProfileService } from './profile-service.js';
7
+ export function createProfileExportService(deps = {}) {
8
+ const profileService = deps.profileService ?? createProfileService();
9
+ return {
10
+ async execute(options) {
11
+ const cwd = options.cwd ?? process.cwd();
12
+ const profile = await profileService.get({ cwd, name: options.name });
13
+ const stagingDir = await mkdtemp(path.join(tmpdir(), 'brainctl-export-'));
14
+ try {
15
+ const exportProfile = await stageProfile(profile, cwd, stagingDir);
16
+ await writeFile(path.join(stagingDir, 'profile.yaml'), YAML.stringify(exportProfile), 'utf8');
17
+ const outputPath = options.outputPath ?? path.join(cwd, `${profile.name}.tar.gz`);
18
+ execSync(`tar -czf "${outputPath}" -C "${stagingDir}" .`, {
19
+ stdio: 'pipe',
20
+ });
21
+ return { archivePath: outputPath };
22
+ }
23
+ finally {
24
+ await rm(stagingDir, { recursive: true, force: true });
25
+ }
26
+ },
27
+ };
28
+ }
29
+ async function stageProfile(profile, cwd, stagingDir) {
30
+ const mcpsDir = path.join(stagingDir, 'mcps');
31
+ const exportMcps = {};
32
+ for (const [name, mcp] of Object.entries(profile.mcps)) {
33
+ if (mcp.type === 'bundled') {
34
+ const sourcePath = path.isAbsolute(mcp.path)
35
+ ? mcp.path
36
+ : path.resolve(cwd, mcp.path);
37
+ const destPath = path.join(mcpsDir, name);
38
+ await mkdir(destPath, { recursive: true });
39
+ await cp(sourcePath, destPath, {
40
+ recursive: true,
41
+ filter: (src) => !src.includes('node_modules'),
42
+ });
43
+ exportMcps[name] = {
44
+ type: 'bundled',
45
+ path: `./mcps/${name}`,
46
+ ...(mcp.install ? { install: mcp.install } : {}),
47
+ command: mcp.command,
48
+ ...(mcp.args ? { args: mcp.args } : {}),
49
+ ...(mcp.env ? { env: mcp.env } : {}),
50
+ };
51
+ }
52
+ else {
53
+ exportMcps[name] = mcp;
54
+ }
55
+ }
56
+ return {
57
+ name: profile.name,
58
+ ...(profile.description ? { description: profile.description } : {}),
59
+ skills: profile.skills,
60
+ mcps: exportMcps,
61
+ memory: profile.memory,
62
+ };
63
+ }
@@ -0,0 +1,11 @@
1
+ export interface ProfileImportService {
2
+ execute(options: {
3
+ cwd?: string;
4
+ archivePath: string;
5
+ force?: boolean;
6
+ }): Promise<{
7
+ profileName: string;
8
+ installedMcps: string[];
9
+ }>;
10
+ }
11
+ export declare function createProfileImportService(): ProfileImportService;
@@ -0,0 +1,81 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { cp, mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import YAML from 'yaml';
6
+ import { ProfileError } from '../errors.js';
7
+ import { parseProfile } from './profile-service.js';
8
+ const PROFILES_DIR = '.brainctl/profiles';
9
+ export function createProfileImportService() {
10
+ return {
11
+ async execute(options) {
12
+ const cwd = options.cwd ?? process.cwd();
13
+ const archivePath = path.resolve(cwd, options.archivePath);
14
+ try {
15
+ await stat(archivePath);
16
+ }
17
+ catch {
18
+ throw new ProfileError(`Archive not found: ${archivePath}`);
19
+ }
20
+ const extractDir = await mkdtemp(path.join(tmpdir(), 'brainctl-import-'));
21
+ try {
22
+ execSync(`tar -xzf "${archivePath}" -C "${extractDir}"`, {
23
+ stdio: 'pipe',
24
+ });
25
+ const profileSource = await readFile(path.join(extractDir, 'profile.yaml'), 'utf8');
26
+ const profile = parseProfile(profileSource, 'imported');
27
+ const profileName = profile.name;
28
+ const profilePath = path.join(cwd, PROFILES_DIR, `${profileName}.yaml`);
29
+ if (!options.force) {
30
+ try {
31
+ await stat(profilePath);
32
+ throw new ProfileError(`Profile "${profileName}" already exists. Use --force to overwrite.`);
33
+ }
34
+ catch (err) {
35
+ if (err instanceof ProfileError)
36
+ throw err;
37
+ }
38
+ }
39
+ const installedMcps = [];
40
+ const mcpsBaseDir = path.join(cwd, PROFILES_DIR, profileName, 'mcps');
41
+ for (const [name, mcp] of Object.entries(profile.mcps)) {
42
+ if (mcp.type !== 'bundled')
43
+ continue;
44
+ const extractedMcpPath = path.join(extractDir, 'mcps', name);
45
+ const destMcpPath = path.join(mcpsBaseDir, name);
46
+ try {
47
+ await stat(extractedMcpPath);
48
+ }
49
+ catch {
50
+ throw new ProfileError(`Bundled MCP "${name}" source not found in archive.`);
51
+ }
52
+ await mkdir(destMcpPath, { recursive: true });
53
+ await cp(extractedMcpPath, destMcpPath, { recursive: true });
54
+ const installCmd = mcp.install ?? 'npm install';
55
+ execSync(installCmd, {
56
+ cwd: destMcpPath,
57
+ stdio: 'pipe',
58
+ });
59
+ profile.mcps[name] = {
60
+ ...mcp,
61
+ path: destMcpPath,
62
+ };
63
+ installedMcps.push(name);
64
+ }
65
+ const outputYaml = {
66
+ name: profile.name,
67
+ ...(profile.description ? { description: profile.description } : {}),
68
+ skills: profile.skills,
69
+ mcps: profile.mcps,
70
+ memory: profile.memory,
71
+ };
72
+ await mkdir(path.dirname(profilePath), { recursive: true });
73
+ await writeFile(profilePath, YAML.stringify(outputYaml), 'utf8');
74
+ return { profileName, installedMcps };
75
+ }
76
+ finally {
77
+ await rm(extractDir, { recursive: true, force: true });
78
+ }
79
+ },
80
+ };
81
+ }
@@ -28,3 +28,4 @@ export interface ProfileService {
28
28
  }): Promise<BrainctlMetaConfig>;
29
29
  }
30
30
  export declare function createProfileService(): ProfileService;
31
+ export declare function parseProfile(source: string, name: string): ProfileConfig;
@@ -99,7 +99,7 @@ async function loadMetaConfig(cwd) {
99
99
  return { active_profile: '', agents: ['claude', 'codex', 'gemini'] };
100
100
  }
101
101
  }
102
- function parseProfile(source, name) {
102
+ export function parseProfile(source, name) {
103
103
  let parsed;
104
104
  try {
105
105
  parsed = YAML.parse(source) ?? {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainctl",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "CLI environment manager for consistent AI workflows",
5
5
  "type": "module",
6
6
  "repository": {