brainctl 0.1.17 → 0.1.19

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.
Files changed (111) hide show
  1. package/dist/cli.d.ts +9 -11
  2. package/dist/cli.js +17 -22
  3. package/dist/commands/doctor.d.ts +1 -1
  4. package/dist/commands/mcp.js +2 -2
  5. package/dist/commands/profile.d.ts +7 -3
  6. package/dist/commands/profile.js +106 -16
  7. package/dist/commands/status.d.ts +1 -1
  8. package/dist/commands/status.js +7 -7
  9. package/dist/{mcp/server.d.ts → mcp-server.d.ts} +1 -1
  10. package/dist/{mcp/server.js → mcp-server.js} +56 -149
  11. package/dist/services/agent/agent-asset-installer.d.ts +3 -0
  12. package/dist/services/agent/agent-asset-installer.js +109 -0
  13. package/dist/services/agent/agent-availability-service.d.ts +11 -0
  14. package/dist/services/agent/agent-availability-service.js +32 -0
  15. package/dist/services/{agent-config-service.d.ts → agent/agent-config-service.d.ts} +6 -6
  16. package/dist/services/{agent-config-service.js → agent/agent-config-service.js} +6 -6
  17. package/dist/services/{credential-redaction-service.d.ts → credential/credential-redaction-service.d.ts} +2 -1
  18. package/dist/services/{credential-redaction-service.js → credential/credential-redaction-service.js} +9 -3
  19. package/dist/services/{credential-resolution-service.d.ts → credential/credential-resolution-service.d.ts} +1 -1
  20. package/dist/services/{doctor-service.d.ts → platform/doctor-service.d.ts} +3 -3
  21. package/dist/services/platform/doctor-service.js +23 -0
  22. package/dist/services/{mcp-preflight-service.d.ts → platform/mcp-preflight-service.d.ts} +2 -2
  23. package/dist/services/{mcp-preflight-service.js → platform/mcp-preflight-service.js} +1 -1
  24. package/dist/services/{runtime-detector.d.ts → platform/runtime-detector.d.ts} +1 -1
  25. package/dist/services/platform/status-service.d.ts +19 -0
  26. package/dist/services/platform/status-service.js +22 -0
  27. package/dist/services/{update-check-service.js → platform/update-check-service.js} +1 -1
  28. package/dist/services/plugin/plugin-install-bundle.d.ts +20 -0
  29. package/dist/services/plugin/plugin-install-bundle.js +80 -0
  30. package/dist/services/plugin/plugin-install-compatibility.d.ts +15 -0
  31. package/dist/services/plugin/plugin-install-compatibility.js +91 -0
  32. package/dist/services/plugin/plugin-install-fs.d.ts +27 -0
  33. package/dist/services/plugin/plugin-install-fs.js +65 -0
  34. package/dist/services/{plugin-install-service.d.ts → plugin/plugin-install-service.d.ts} +4 -18
  35. package/dist/services/{plugin-install-service.js → plugin/plugin-install-service.js} +7 -308
  36. package/dist/services/plugin/plugin-install-uninstall.d.ts +12 -0
  37. package/dist/services/plugin/plugin-install-uninstall.js +76 -0
  38. package/dist/services/{skill-paths.d.ts → plugin/skill-paths.d.ts} +1 -1
  39. package/dist/services/{skill-preflight-service.d.ts → plugin/skill-preflight-service.d.ts} +1 -1
  40. package/dist/services/{portable-mcp-classifier.d.ts → profile/portable-mcp-classifier.d.ts} +3 -3
  41. package/dist/services/{portable-mcp-classifier.js → profile/portable-mcp-classifier.js} +2 -2
  42. package/dist/services/{portable-profile-pack-service.d.ts → profile/portable-profile-pack-service.d.ts} +8 -2
  43. package/dist/services/{portable-profile-pack-service.js → profile/portable-profile-pack-service.js} +83 -9
  44. package/dist/services/profile/profile-apply-service.d.ts +34 -0
  45. package/dist/services/profile/profile-apply-service.js +102 -0
  46. package/dist/services/{profile-export-service.d.ts → profile/profile-export-service.d.ts} +7 -3
  47. package/dist/services/{profile-export-service.js → profile/profile-export-service.js} +3 -1
  48. package/dist/services/{profile-import-service.d.ts → profile/profile-import-service.d.ts} +1 -1
  49. package/dist/services/{profile-import-service.js → profile/profile-import-service.js} +85 -130
  50. package/dist/services/{profile-service.d.ts → profile/profile-service.d.ts} +3 -11
  51. package/dist/services/{profile-service.js → profile/profile-service.js} +58 -103
  52. package/dist/services/profile/profile-snapshot-service.d.ts +12 -0
  53. package/dist/services/profile/profile-snapshot-service.js +47 -0
  54. package/dist/types.d.ts +2 -57
  55. package/dist/ui/routes.d.ts +1 -3
  56. package/dist/ui/routes.js +79 -128
  57. package/dist/ui/server.d.ts +1 -1
  58. package/dist/web/assets/index-CGmTbSgk.js +63 -0
  59. package/dist/web/assets/index-EIVU5Woh.css +2 -0
  60. package/dist/web/index.html +2 -2
  61. package/package.json +1 -1
  62. package/dist/commands/init.d.ts +0 -3
  63. package/dist/commands/init.js +0 -27
  64. package/dist/commands/run.d.ts +0 -3
  65. package/dist/commands/run.js +0 -25
  66. package/dist/commands/sync.d.ts +0 -3
  67. package/dist/commands/sync.js +0 -31
  68. package/dist/config.d.ts +0 -14
  69. package/dist/config.js +0 -96
  70. package/dist/context/builder.d.ts +0 -6
  71. package/dist/context/builder.js +0 -13
  72. package/dist/context/memory.d.ts +0 -5
  73. package/dist/context/memory.js +0 -43
  74. package/dist/context/skills.d.ts +0 -2
  75. package/dist/context/skills.js +0 -8
  76. package/dist/executor/claude.d.ts +0 -12
  77. package/dist/executor/claude.js +0 -16
  78. package/dist/executor/codex.d.ts +0 -12
  79. package/dist/executor/codex.js +0 -16
  80. package/dist/executor/process.d.ts +0 -11
  81. package/dist/executor/process.js +0 -40
  82. package/dist/executor/resolver.d.ts +0 -13
  83. package/dist/executor/resolver.js +0 -60
  84. package/dist/executor/types.d.ts +0 -14
  85. package/dist/executor/types.js +0 -1
  86. package/dist/services/config-write-service.d.ts +0 -12
  87. package/dist/services/config-write-service.js +0 -70
  88. package/dist/services/doctor-service.js +0 -79
  89. package/dist/services/init-service.d.ts +0 -14
  90. package/dist/services/init-service.js +0 -88
  91. package/dist/services/memory-write-service.d.ts +0 -12
  92. package/dist/services/memory-write-service.js +0 -56
  93. package/dist/services/run-service.d.ts +0 -15
  94. package/dist/services/run-service.js +0 -94
  95. package/dist/services/status-service.d.ts +0 -17
  96. package/dist/services/status-service.js +0 -21
  97. package/dist/services/sync-service.d.ts +0 -15
  98. package/dist/services/sync-service.js +0 -69
  99. package/dist/ui/streaming.d.ts +0 -3
  100. package/dist/ui/streaming.js +0 -16
  101. package/dist/web/assets/index-Bbophmwh.css +0 -2
  102. package/dist/web/assets/index-DDG_ylui.js +0 -63
  103. /package/dist/{system/executables.d.ts → executables.d.ts} +0 -0
  104. /package/dist/{system/executables.js → executables.js} +0 -0
  105. /package/dist/services/{agent-converter-service.d.ts → agent/agent-converter-service.d.ts} +0 -0
  106. /package/dist/services/{agent-converter-service.js → agent/agent-converter-service.js} +0 -0
  107. /package/dist/services/{credential-resolution-service.js → credential/credential-resolution-service.js} +0 -0
  108. /package/dist/services/{runtime-detector.js → platform/runtime-detector.js} +0 -0
  109. /package/dist/services/{update-check-service.d.ts → platform/update-check-service.d.ts} +0 -0
  110. /package/dist/services/{skill-paths.js → plugin/skill-paths.js} +0 -0
  111. /package/dist/services/{skill-preflight-service.js → plugin/skill-preflight-service.js} +0 -0
@@ -4,13 +4,13 @@ import { cp, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
4
4
  import { homedir, tmpdir } from 'node:os';
5
5
  import path from 'node:path';
6
6
  import YAML from 'yaml';
7
- import { ProfileError } from '../errors.js';
8
- import { createAgentConfigService } from './agent-config-service.js';
9
- import { redactPortableMcpCredentials } from './credential-redaction-service.js';
7
+ import { ProfileError } from '../../errors.js';
8
+ import { createAgentConfigService } from '../agent/agent-config-service.js';
9
+ import { redactPortableMcpCredentials } from '../credential/credential-redaction-service.js';
10
10
  import { createProfileService } from './profile-service.js';
11
11
  import { classifyPortableMcp } from './portable-mcp-classifier.js';
12
- import { findProjectRoot, getDefaultExclude, getDefaultInstall } from './runtime-detector.js';
13
- const packageVersion = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
12
+ import { findProjectRoot, getDefaultExclude, getDefaultInstall } from '../platform/runtime-detector.js';
13
+ const packageVersion = JSON.parse(readFileSync(new URL('../../../package.json', import.meta.url), 'utf8'));
14
14
  export function createPortableProfilePackService(deps = {}) {
15
15
  const profileService = deps.profileService ?? createProfileService();
16
16
  const agentConfigService = deps.agentConfigService ?? createAgentConfigService();
@@ -52,11 +52,26 @@ export function createPortableProfilePackService(deps = {}) {
52
52
  filter: (src) => !matchesPluginExclude(src),
53
53
  });
54
54
  }
55
+ const format = options.format ?? 'tarball';
56
+ const credentialsMode = options.credentialsMode ?? 'redact';
57
+ const warnings = [];
58
+ if (credentialsMode === 'keep' && Object.keys(packed.rawCredentials).length > 0) {
59
+ await writeFile(path.join(stagingDir, '.env'), renderDotEnv(packed.rawCredentials), 'utf8');
60
+ warnings.push(`Wrote .env with ${Object.keys(packed.rawCredentials).length} real credential value(s). Do NOT publish this archive publicly.`);
61
+ }
62
+ if (format === 'folder') {
63
+ await writeRepoReadyFiles(stagingDir, packed);
64
+ const outputPath = options.outputPath ?? path.join(cwd, packed.profile.name);
65
+ await rm(outputPath, { recursive: true, force: true });
66
+ await mkdir(path.dirname(outputPath), { recursive: true });
67
+ await cp(stagingDir, outputPath, { recursive: true });
68
+ return { archivePath: outputPath, format, warnings };
69
+ }
55
70
  const outputPath = options.outputPath ?? path.join(cwd, `${packed.profile.name}.tar.gz`);
56
71
  execSync(`tar -czf "${outputPath}" -C "${stagingDir}" .`, {
57
72
  stdio: 'pipe',
58
73
  });
59
- return { archivePath: outputPath };
74
+ return { archivePath: outputPath, format, warnings };
60
75
  }
61
76
  finally {
62
77
  await rm(stagingDir, { recursive: true, force: true });
@@ -64,6 +79,62 @@ export function createPortableProfilePackService(deps = {}) {
64
79
  },
65
80
  };
66
81
  }
82
+ function renderDotEnv(values) {
83
+ return (Object.entries(values)
84
+ .sort(([a], [b]) => a.localeCompare(b))
85
+ .map(([key, value]) => `${key.toUpperCase()}=${escapeEnvValue(value)}`)
86
+ .join('\n') + '\n');
87
+ }
88
+ function escapeEnvValue(value) {
89
+ if (/[\s"'#$\\]/.test(value)) {
90
+ return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
91
+ }
92
+ return value;
93
+ }
94
+ async function writeRepoReadyFiles(stagingDir, packed) {
95
+ const credentials = packed.manifest.credentials ?? [];
96
+ const envLines = credentials.map((c) => `${c.key.toUpperCase()}=${c.description ? `# ${c.description}` : ''}`);
97
+ const envExample = envLines.length > 0
98
+ ? `# Credentials required by this profile\n# Copy to .env and fill in values before \`brainctl profile import\`.\n${envLines.join('\n')}\n`
99
+ : '# No credentials required by this profile.\n';
100
+ const gitignore = ['.env', 'node_modules/', '.DS_Store', '*.bak.*', ''].join('\n');
101
+ const readme = renderReadme(packed);
102
+ await writeFile(path.join(stagingDir, '.env.example'), envExample, 'utf8');
103
+ await writeFile(path.join(stagingDir, '.gitignore'), gitignore, 'utf8');
104
+ await writeFile(path.join(stagingDir, 'README.md'), readme, 'utf8');
105
+ }
106
+ function renderReadme(packed) {
107
+ const { manifest, profile } = packed;
108
+ const mcpNames = Object.keys(profile.mcps);
109
+ const credNames = (manifest.credentials ?? []).map((c) => `- \`${c.key}\`${c.description ? ` — ${c.description}` : ''}`);
110
+ const lines = [
111
+ `# ${profile.name}`,
112
+ '',
113
+ profile.description ?? 'A brainctl portable profile.',
114
+ '',
115
+ `Packed by ${manifest.createdBy?.tool ?? 'brainctl'} v${manifest.createdBy?.version ?? packageVersion.version}.`,
116
+ '',
117
+ '## Contents',
118
+ '',
119
+ `- **MCPs:** ${mcpNames.length > 0 ? mcpNames.join(', ') : '(none)'}`,
120
+ ...(manifest.plugins ? [`- **Plugins:** ${manifest.plugins.map((p) => `${p.agent}:${p.name}`).join(', ')}`] : []),
121
+ ...(manifest.userSkills ? [`- **User skills:** ${manifest.userSkills.map((s) => `${s.agent}:${s.name}`).join(', ')}`] : []),
122
+ '',
123
+ '## Install',
124
+ '',
125
+ '```bash',
126
+ `brainctl profile import ./${profile.name}`,
127
+ '```',
128
+ '',
129
+ ];
130
+ if (credNames.length > 0) {
131
+ lines.push('## Required credentials', '', 'Copy `.env.example` to `.env` (gitignored) and fill in values, or pass them at import time:', '', '```bash', `brainctl profile import ./${profile.name} \\`, ...credNames.map((c) => {
132
+ const key = c.match(/`([^`]+)`/)?.[1] ?? '';
133
+ return ` --credential ${key}=<value> \\`;
134
+ }), '```', '', '### Keys', '', ...credNames, '');
135
+ }
136
+ return lines.join('\n');
137
+ }
67
138
  async function buildPackedProfile(options) {
68
139
  if (options.source.source === 'profile') {
69
140
  const profile = await options.profileService.get({
@@ -117,9 +188,7 @@ async function buildPackedProfile(options) {
117
188
  const extras = collectAgentExtras(agentConfig);
118
189
  return redactAndNormalizeProfile({
119
190
  name: profileName,
120
- skills: {},
121
191
  mcps,
122
- memory: { paths: [] },
123
192
  }, agentSource.cwd, {
124
193
  kind: 'agent',
125
194
  agent: agentSource.agent,
@@ -186,14 +255,18 @@ function inferPluginVersion(agent, installPath) {
186
255
  const base = path.basename(installPath);
187
256
  return base.length > 0 ? base : undefined;
188
257
  }
189
- function redactAndNormalizeProfile(profile, cwd, source, extras) {
258
+ async function redactAndNormalizeProfile(profile, cwd, source, extras) {
190
259
  const bundledSources = new Map();
191
260
  const credentials = new Map();
261
+ const rawCredentials = {};
192
262
  const mcps = Object.fromEntries(Object.entries(profile.mcps).map(([key, config]) => {
193
263
  const result = redactPortableMcpCredentials(config);
194
264
  for (const credential of result.credentials) {
195
265
  credentials.set(credential.key, credential);
196
266
  }
267
+ for (const [credKey, credValue] of Object.entries(result.rawValues)) {
268
+ rawCredentials[credKey] = credValue;
269
+ }
197
270
  if (result.redacted.kind === 'local' && result.redacted.source === 'bundled') {
198
271
  const sourcePath = path.isAbsolute(result.redacted.path)
199
272
  ? result.redacted.path
@@ -230,6 +303,7 @@ function redactAndNormalizeProfile(profile, cwd, source, extras) {
230
303
  bundledSources,
231
304
  bundledPlugins: extras?.bundledPlugins ?? new Map(),
232
305
  bundledUserSkills: extras?.bundledUserSkills ?? new Map(),
306
+ rawCredentials,
233
307
  };
234
308
  }
235
309
  function sanitizePackName(value) {
@@ -0,0 +1,34 @@
1
+ import type { AgentName, SyncResult } from '../../types.js';
2
+ import { type ProfileSnapshotService } from './profile-snapshot-service.js';
3
+ import { type ProfileService } from './profile-service.js';
4
+ import type { AgentConfigWriter } from '../sync/agent-writer.js';
5
+ export type ItemType = 'mcp' | 'plugin' | 'skill';
6
+ export interface ItemSelector {
7
+ type: ItemType;
8
+ name: string;
9
+ }
10
+ export interface ApplyOptions {
11
+ cwd?: string;
12
+ profileName: string;
13
+ agents: AgentName[];
14
+ items?: ItemSelector[];
15
+ backup?: boolean;
16
+ }
17
+ export interface ApplyResult extends SyncResult {
18
+ }
19
+ export interface ProfileApplyService {
20
+ execute(options: ApplyOptions): Promise<{
21
+ backups: Array<{
22
+ agent: AgentName;
23
+ profileName: string;
24
+ }>;
25
+ applied: ApplyResult;
26
+ }>;
27
+ }
28
+ interface ProfileApplyDependencies {
29
+ profileService?: ProfileService;
30
+ snapshotService?: ProfileSnapshotService;
31
+ writers?: Partial<Record<AgentName, AgentConfigWriter>>;
32
+ }
33
+ export declare function createProfileApplyService(deps?: ProfileApplyDependencies): ProfileApplyService;
34
+ export {};
@@ -0,0 +1,102 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import YAML from 'yaml';
4
+ import { ProfileError } from '../../errors.js';
5
+ import { installPlugin, installUserSkill } from '../agent/agent-asset-installer.js';
6
+ import { createProfileSnapshotService, defaultBackupProfileName, } from './profile-snapshot-service.js';
7
+ import { createProfileService, profileDir } from './profile-service.js';
8
+ import { createClaudeWriter } from '../sync/claude-writer.js';
9
+ import { createCodexWriter } from '../sync/codex-writer.js';
10
+ import { createGeminiWriter } from '../sync/gemini-writer.js';
11
+ export function createProfileApplyService(deps = {}) {
12
+ const profileService = deps.profileService ?? createProfileService();
13
+ const snapshotService = deps.snapshotService ?? createProfileSnapshotService();
14
+ const defaultWriters = {
15
+ claude: createClaudeWriter(),
16
+ codex: createCodexWriter(),
17
+ gemini: createGeminiWriter(),
18
+ };
19
+ const writers = { ...defaultWriters, ...deps.writers };
20
+ return {
21
+ async execute(options) {
22
+ const cwd = options.cwd ?? process.cwd();
23
+ const profile = await profileService.get({ cwd, name: options.profileName });
24
+ const remoteMcpName = Object.entries(profile.mcps).find(([, config]) => config.kind === 'remote')?.[0];
25
+ if (remoteMcpName) {
26
+ throw new ProfileError(`Profile "${profile.name}" includes remote MCP "${remoteMcpName}". Remote MCP apply is not supported yet.`);
27
+ }
28
+ const isPartial = options.items !== undefined && options.items.length > 0;
29
+ const shouldBackup = options.backup ?? !isPartial;
30
+ const backups = [];
31
+ if (shouldBackup) {
32
+ for (const agent of options.agents) {
33
+ const backupName = defaultBackupProfileName(agent);
34
+ try {
35
+ await snapshotService.execute({ cwd, agent, profileName: backupName });
36
+ backups.push({ agent, profileName: backupName });
37
+ }
38
+ catch {
39
+ // Agent may not have a live config to back up — skip silently
40
+ }
41
+ }
42
+ }
43
+ const folder = profileDir(cwd, options.profileName);
44
+ const manifest = await readProfileManifest(folder);
45
+ const applied = [];
46
+ const wantMcp = (name) => matches(options.items, 'mcp', name);
47
+ const wantPlugin = (name) => matches(options.items, 'plugin', name);
48
+ const wantSkill = (name) => matches(options.items, 'skill', name);
49
+ for (const agent of options.agents) {
50
+ const writer = writers[agent];
51
+ if (!writer)
52
+ continue;
53
+ const filteredMcps = Object.fromEntries(Object.entries(profile.mcps).filter(([name]) => wantMcp(name)));
54
+ let mcpResult;
55
+ if (Object.keys(filteredMcps).length > 0 || options.items === undefined) {
56
+ mcpResult = await writer.write({ mcpServers: filteredMcps, cwd });
57
+ }
58
+ else {
59
+ mcpResult = { configPath: '', backedUpTo: null };
60
+ }
61
+ const pluginsInstalled = [];
62
+ for (const plugin of (manifest?.plugins ?? []).filter((p) => p.agent === agent && wantPlugin(p.name))) {
63
+ const sourceDir = path.join(folder, plugin.archivePath);
64
+ await installPlugin(sourceDir, plugin);
65
+ pluginsInstalled.push(plugin.name);
66
+ }
67
+ const userSkillsInstalled = [];
68
+ for (const skill of (manifest?.userSkills ?? []).filter((s) => s.agent === agent && wantSkill(s.name))) {
69
+ const sourceDir = path.join(folder, skill.archivePath);
70
+ await installUserSkill(sourceDir, skill);
71
+ userSkillsInstalled.push(skill.name);
72
+ }
73
+ applied.push({
74
+ agent,
75
+ configPath: mcpResult.configPath,
76
+ backedUpTo: mcpResult.backedUpTo,
77
+ mcpCount: Object.keys(filteredMcps).length,
78
+ ...(pluginsInstalled.length > 0 ? { pluginsInstalled } : {}),
79
+ ...(userSkillsInstalled.length > 0 ? { userSkillsInstalled } : {}),
80
+ });
81
+ }
82
+ return { backups, applied };
83
+ },
84
+ };
85
+ }
86
+ function matches(items, type, name) {
87
+ if (items === undefined)
88
+ return true;
89
+ return items.some((s) => s.type === type && s.name === name);
90
+ }
91
+ async function readProfileManifest(folder) {
92
+ try {
93
+ const source = await readFile(path.join(folder, 'manifest.yaml'), 'utf8');
94
+ const parsed = YAML.parse(source);
95
+ if (!parsed || typeof parsed !== 'object')
96
+ return null;
97
+ return parsed;
98
+ }
99
+ catch {
100
+ return null;
101
+ }
102
+ }
@@ -1,6 +1,6 @@
1
- import type { AgentName } from '../types.js';
2
- import type { AgentConfigService } from './agent-config-service.js';
3
- import { type PortableProfilePackService } from './portable-profile-pack-service.js';
1
+ import type { AgentName } from '../../types.js';
2
+ import type { AgentConfigService } from '../agent/agent-config-service.js';
3
+ import { type PortableCredentialsMode, type PortablePackFormat, type PortableProfilePackService } from './portable-profile-pack-service.js';
4
4
  import type { ProfileService } from './profile-service.js';
5
5
  export interface ProfileExportService {
6
6
  execute(options: {
@@ -14,8 +14,12 @@ export interface ProfileExportService {
14
14
  cwd: string;
15
15
  };
16
16
  outputPath?: string;
17
+ format?: PortablePackFormat;
18
+ credentialsMode?: PortableCredentialsMode;
17
19
  }): Promise<{
18
20
  archivePath: string;
21
+ format: PortablePackFormat;
22
+ warnings: string[];
19
23
  }>;
20
24
  }
21
25
  interface ProfileExportDependencies {
@@ -1,4 +1,4 @@
1
- import { createPortableProfilePackService } from './portable-profile-pack-service.js';
1
+ import { createPortableProfilePackService, } from './portable-profile-pack-service.js';
2
2
  export function createProfileExportService(deps = {}) {
3
3
  const portableProfilePackService = deps.portableProfilePackService ?? createPortableProfilePackService({
4
4
  profileService: deps.profileService,
@@ -10,6 +10,8 @@ export function createProfileExportService(deps = {}) {
10
10
  cwd: options.cwd,
11
11
  source: options.source,
12
12
  outputPath: options.outputPath,
13
+ format: options.format,
14
+ credentialsMode: options.credentialsMode,
13
15
  });
14
16
  },
15
17
  };
@@ -1,4 +1,4 @@
1
- import { type McpPreflightService } from './mcp-preflight-service.js';
1
+ import { type McpPreflightService } from '../platform/mcp-preflight-service.js';
2
2
  export interface ProfileImportService {
3
3
  execute(options: {
4
4
  cwd?: string;
@@ -1,31 +1,46 @@
1
1
  import { execSync } from 'node:child_process';
2
- import { copyFile, cp, mkdir, mkdtemp, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
3
- import { homedir, tmpdir } from 'node:os';
2
+ import { copyFile, cp, mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
4
  import path from 'node:path';
5
5
  import YAML from 'yaml';
6
- import { ProfileError } from '../errors.js';
7
- import { formatTimestamp } from './sync/agent-writer.js';
8
- import { resolvePortableMcpCredentials } from './credential-resolution-service.js';
9
- import { createMcpPreflightService } from './mcp-preflight-service.js';
6
+ import { ProfileError } from '../../errors.js';
7
+ import { installPlugin, installUserSkill } from '../agent/agent-asset-installer.js';
8
+ import { resolvePortableMcpCredentials } from '../credential/credential-resolution-service.js';
9
+ import { createMcpPreflightService } from '../platform/mcp-preflight-service.js';
10
10
  import { parseProfile } from './profile-service.js';
11
11
  const PROFILES_DIR = '.brainctl/profiles';
12
+ async function pathExists(p) {
13
+ try {
14
+ await stat(p);
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
12
21
  export function createProfileImportService(deps = {}) {
13
22
  const mcpPreflightService = deps.mcpPreflightService ?? createMcpPreflightService();
14
23
  return {
15
24
  async execute(options) {
16
25
  const cwd = options.cwd ?? process.cwd();
17
26
  const archivePath = path.resolve(cwd, options.archivePath);
27
+ let archiveStats;
18
28
  try {
19
- await stat(archivePath);
29
+ archiveStats = await stat(archivePath);
20
30
  }
21
31
  catch {
22
32
  throw new ProfileError(`Archive not found: ${archivePath}`);
23
33
  }
24
- const extractDir = await mkdtemp(path.join(tmpdir(), 'brainctl-import-'));
34
+ const isFolderSource = archiveStats.isDirectory();
35
+ const extractDir = isFolderSource
36
+ ? archivePath
37
+ : await mkdtemp(path.join(tmpdir(), 'brainctl-import-'));
25
38
  try {
26
- execSync(`tar -xzf "${archivePath}" -C "${extractDir}"`, {
27
- stdio: 'pipe',
28
- });
39
+ if (!isFolderSource) {
40
+ execSync(`tar -xzf "${archivePath}" -C "${extractDir}"`, {
41
+ stdio: 'pipe',
42
+ });
43
+ }
29
44
  const manifest = await readPortableManifest(extractDir);
30
45
  const profileSource = await readFile(path.join(extractDir, 'profile.yaml'), 'utf8');
31
46
  const profile = parseProfile(profileSource, 'imported');
@@ -33,21 +48,26 @@ export function createProfileImportService(deps = {}) {
33
48
  if (manifest.profileName !== profileName) {
34
49
  throw new ProfileError(`Portable profile manifest name "${manifest.profileName}" does not match profile name "${profileName}".`);
35
50
  }
36
- const profilePath = path.join(cwd, PROFILES_DIR, `${profileName}.yaml`);
51
+ const profileFolder = path.join(cwd, PROFILES_DIR, profileName);
52
+ const profilePath = path.join(profileFolder, 'profile.yaml');
53
+ const legacyProfilePath = path.join(cwd, PROFILES_DIR, `${profileName}.yaml`);
37
54
  if (!options.force) {
38
- try {
39
- await stat(profilePath);
55
+ if ((await pathExists(profilePath)) || (await pathExists(legacyProfilePath))) {
40
56
  throw new ProfileError(`Profile "${profileName}" already exists. Use --force to overwrite.`);
41
57
  }
42
- catch (err) {
43
- if (err instanceof ProfileError)
44
- throw err;
58
+ }
59
+ else {
60
+ // clean up legacy single-file profile so it doesn't shadow the new layout
61
+ if (await pathExists(legacyProfilePath)) {
62
+ await rm(legacyProfilePath, { force: true });
45
63
  }
46
64
  }
65
+ const dotEnvCreds = await readDotEnvCredentials(extractDir);
66
+ const combinedCreds = { ...dotEnvCreds, ...(options.credentials ?? {}) };
47
67
  const missingCredentials = new Map();
48
68
  for (const [name, mcp] of Object.entries(profile.mcps)) {
49
69
  const resolution = resolvePortableMcpCredentials(mcp, {
50
- credentials: options.credentials,
70
+ credentials: combinedCreds,
51
71
  credentialSpecs: manifest.credentials,
52
72
  environment: process.env,
53
73
  });
@@ -102,27 +122,44 @@ export function createProfileImportService(deps = {}) {
102
122
  await validateImportedMcps(profile, cwd, mcpPreflightService);
103
123
  const installedPlugins = [];
104
124
  for (const plugin of manifest.plugins ?? []) {
105
- await restorePlugin(extractDir, plugin);
125
+ const sourceDir = resolveBundledArchivePath(extractDir, plugin.archivePath);
126
+ const profileLocalDir = path.join(cwd, PROFILES_DIR, profileName, plugin.archivePath);
127
+ await rm(profileLocalDir, { recursive: true, force: true });
128
+ await mkdir(path.dirname(profileLocalDir), { recursive: true });
129
+ await cp(sourceDir, profileLocalDir, { recursive: true });
130
+ await installPlugin(profileLocalDir, plugin);
106
131
  installedPlugins.push(`${plugin.agent}:${plugin.name}`);
107
132
  }
108
133
  const installedUserSkills = [];
109
134
  for (const skill of manifest.userSkills ?? []) {
110
- await restoreUserSkill(extractDir, skill);
135
+ const sourceDir = resolveBundledArchivePath(extractDir, skill.archivePath);
136
+ const profileLocalDir = path.join(cwd, PROFILES_DIR, profileName, skill.archivePath);
137
+ await rm(profileLocalDir, { recursive: true, force: true });
138
+ await mkdir(path.dirname(profileLocalDir), { recursive: true });
139
+ await cp(sourceDir, profileLocalDir, { recursive: true });
140
+ await installUserSkill(profileLocalDir, skill);
111
141
  installedUserSkills.push(`${skill.agent}:${skill.name}`);
112
142
  }
143
+ // retain manifest in profile folder so sync can reapply assets
144
+ try {
145
+ await copyFile(path.join(extractDir, 'manifest.yaml'), path.join(cwd, PROFILES_DIR, profileName, 'manifest.yaml'));
146
+ }
147
+ catch {
148
+ // best-effort
149
+ }
113
150
  const outputYaml = {
114
151
  name: profile.name,
115
152
  ...(profile.description ? { description: profile.description } : {}),
116
- skills: profile.skills,
117
153
  mcps: profile.mcps,
118
- memory: profile.memory,
119
154
  };
120
155
  await mkdir(path.dirname(profilePath), { recursive: true });
121
156
  await writeFile(profilePath, YAML.stringify(outputYaml), 'utf8');
122
157
  return { profileName, installedMcps, installedPlugins, installedUserSkills };
123
158
  }
124
159
  finally {
125
- await rm(extractDir, { recursive: true, force: true });
160
+ if (!isFolderSource) {
161
+ await rm(extractDir, { recursive: true, force: true });
162
+ }
126
163
  }
127
164
  },
128
165
  };
@@ -187,6 +224,31 @@ function formatExecError(error) {
187
224
  }
188
225
  return 'Unknown install error.';
189
226
  }
227
+ async function readDotEnvCredentials(extractDir) {
228
+ try {
229
+ const content = await readFile(path.join(extractDir, '.env'), 'utf8');
230
+ const out = {};
231
+ for (const rawLine of content.split(/\r?\n/)) {
232
+ const line = rawLine.trim();
233
+ if (!line || line.startsWith('#'))
234
+ continue;
235
+ const eq = line.indexOf('=');
236
+ if (eq <= 0)
237
+ continue;
238
+ const key = line.slice(0, eq).trim();
239
+ let value = line.slice(eq + 1).trim();
240
+ if ((value.startsWith('"') && value.endsWith('"')) ||
241
+ (value.startsWith("'") && value.endsWith("'"))) {
242
+ value = value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
243
+ }
244
+ out[key.toLowerCase()] = value;
245
+ }
246
+ return out;
247
+ }
248
+ catch {
249
+ return {};
250
+ }
251
+ }
190
252
  async function readPortableManifest(extractDir) {
191
253
  let source;
192
254
  try {
@@ -227,110 +289,3 @@ function resolveBundledArchivePath(extractDir, bundlePath) {
227
289
  }
228
290
  return resolved;
229
291
  }
230
- async function restorePlugin(extractDir, plugin) {
231
- const sourceDir = resolveBundledArchivePath(extractDir, plugin.archivePath);
232
- try {
233
- await stat(sourceDir);
234
- }
235
- catch {
236
- throw new ProfileError(`Bundled plugin "${plugin.name}" source missing in archive at ${plugin.archivePath}.`);
237
- }
238
- if (plugin.agent === 'gemini') {
239
- // Gemini has no plugin cache concept. Treat as user-skill-equivalent no-op.
240
- return;
241
- }
242
- const marketplace = plugin.marketplace ?? plugin.source;
243
- const version = plugin.version ?? 'unknown';
244
- const cacheRoot = path.join(homedir(), `.${plugin.agent}`, 'plugins', 'cache');
245
- const targetDir = path.join(cacheRoot, marketplace, plugin.name, version);
246
- await rm(targetDir, { recursive: true, force: true });
247
- await mkdir(path.dirname(targetDir), { recursive: true });
248
- await cp(sourceDir, targetDir, { recursive: true });
249
- if (plugin.agent === 'claude') {
250
- await registerClaudePlugin({
251
- pluginKey: `${plugin.name}@${marketplace}`,
252
- installPath: targetDir,
253
- version,
254
- });
255
- return;
256
- }
257
- if (plugin.agent === 'codex') {
258
- await registerCodexPlugin({
259
- pluginKey: `${plugin.name}@${marketplace}`,
260
- });
261
- }
262
- }
263
- async function restoreUserSkill(extractDir, skill) {
264
- const sourceDir = resolveBundledArchivePath(extractDir, skill.archivePath);
265
- try {
266
- await stat(sourceDir);
267
- }
268
- catch {
269
- throw new ProfileError(`Bundled user skill "${skill.name}" source missing in archive at ${skill.archivePath}.`);
270
- }
271
- const targetDir = path.join(homedir(), `.${skill.agent}`, 'skills', skill.name);
272
- await rm(targetDir, { recursive: true, force: true });
273
- await mkdir(path.dirname(targetDir), { recursive: true });
274
- await cp(sourceDir, targetDir, { recursive: true });
275
- }
276
- async function registerClaudePlugin(options) {
277
- const filePath = path.join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
278
- let existing = { version: 2, plugins: {} };
279
- try {
280
- const source = await readFile(filePath, 'utf8');
281
- existing = JSON.parse(source);
282
- await backupFile(filePath);
283
- }
284
- catch {
285
- // fresh file
286
- }
287
- const plugins = (existing.plugins ?? {});
288
- const now = new Date().toISOString();
289
- const entry = {
290
- scope: 'user',
291
- installPath: options.installPath,
292
- version: options.version,
293
- installedAt: now,
294
- lastUpdated: now,
295
- };
296
- plugins[options.pluginKey] = [entry];
297
- existing.plugins = plugins;
298
- if (typeof existing.version !== 'number')
299
- existing.version = 2;
300
- await mkdir(path.dirname(filePath), { recursive: true });
301
- await atomicWrite(filePath, JSON.stringify(existing, null, 2) + '\n');
302
- }
303
- async function registerCodexPlugin(options) {
304
- const filePath = path.join(homedir(), '.codex', 'config.toml');
305
- let existing = '';
306
- try {
307
- existing = await readFile(filePath, 'utf8');
308
- await backupFile(filePath);
309
- }
310
- catch {
311
- existing = '';
312
- }
313
- const header = `[plugins."${options.pluginKey}"]`;
314
- if (existing.includes(header))
315
- return;
316
- const prefix = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
317
- const separator = existing.length > 0 ? '\n' : '';
318
- const block = `${header}\nenabled = true\n`;
319
- const next = existing + prefix + separator + block;
320
- await mkdir(path.dirname(filePath), { recursive: true });
321
- await atomicWrite(filePath, next);
322
- }
323
- async function backupFile(filePath) {
324
- const backupPath = `${filePath}.bak.${formatTimestamp()}`;
325
- try {
326
- await copyFile(filePath, backupPath);
327
- }
328
- catch {
329
- // file may not exist
330
- }
331
- }
332
- async function atomicWrite(filePath, content) {
333
- const tmpPath = `${filePath}.tmp.${Date.now()}`;
334
- await writeFile(tmpPath, content, 'utf8');
335
- await rename(tmpPath, filePath);
336
- }
@@ -1,10 +1,11 @@
1
- import type { BrainctlMetaConfig, ProfileConfig } from '../types.js';
1
+ import type { ProfileConfig } from '../../types.js';
2
+ export declare function profileDir(cwd: string, name: string): string;
3
+ export declare function profileFile(cwd: string, name: string): string;
2
4
  export interface ProfileService {
3
5
  list(options?: {
4
6
  cwd?: string;
5
7
  }): Promise<{
6
8
  profiles: string[];
7
- activeProfile: string | null;
8
9
  }>;
9
10
  get(options: {
10
11
  cwd?: string;
@@ -26,15 +27,6 @@ export interface ProfileService {
26
27
  cwd?: string;
27
28
  name: string;
28
29
  }): Promise<void>;
29
- use(options: {
30
- cwd?: string;
31
- name: string;
32
- }): Promise<{
33
- previousProfile: string | null;
34
- }>;
35
- getMetaConfig(options?: {
36
- cwd?: string;
37
- }): Promise<BrainctlMetaConfig>;
38
30
  }
39
31
  export declare function createProfileService(): ProfileService;
40
32
  export declare function parseProfile(source: string, name: string): ProfileConfig;