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.
- package/dist/cli.d.ts +9 -11
- package/dist/cli.js +17 -22
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/mcp.js +2 -2
- package/dist/commands/profile.d.ts +7 -3
- package/dist/commands/profile.js +106 -16
- package/dist/commands/status.d.ts +1 -1
- package/dist/commands/status.js +7 -7
- package/dist/{mcp/server.d.ts → mcp-server.d.ts} +1 -1
- package/dist/{mcp/server.js → mcp-server.js} +56 -149
- package/dist/services/agent/agent-asset-installer.d.ts +3 -0
- package/dist/services/agent/agent-asset-installer.js +109 -0
- package/dist/services/agent/agent-availability-service.d.ts +11 -0
- package/dist/services/agent/agent-availability-service.js +32 -0
- package/dist/services/{agent-config-service.d.ts → agent/agent-config-service.d.ts} +6 -6
- package/dist/services/{agent-config-service.js → agent/agent-config-service.js} +6 -6
- package/dist/services/{credential-redaction-service.d.ts → credential/credential-redaction-service.d.ts} +2 -1
- package/dist/services/{credential-redaction-service.js → credential/credential-redaction-service.js} +9 -3
- package/dist/services/{credential-resolution-service.d.ts → credential/credential-resolution-service.d.ts} +1 -1
- package/dist/services/{doctor-service.d.ts → platform/doctor-service.d.ts} +3 -3
- package/dist/services/platform/doctor-service.js +23 -0
- package/dist/services/{mcp-preflight-service.d.ts → platform/mcp-preflight-service.d.ts} +2 -2
- package/dist/services/{mcp-preflight-service.js → platform/mcp-preflight-service.js} +1 -1
- package/dist/services/{runtime-detector.d.ts → platform/runtime-detector.d.ts} +1 -1
- package/dist/services/platform/status-service.d.ts +19 -0
- package/dist/services/platform/status-service.js +22 -0
- package/dist/services/{update-check-service.js → platform/update-check-service.js} +1 -1
- package/dist/services/plugin/plugin-install-bundle.d.ts +20 -0
- package/dist/services/plugin/plugin-install-bundle.js +80 -0
- package/dist/services/plugin/plugin-install-compatibility.d.ts +15 -0
- package/dist/services/plugin/plugin-install-compatibility.js +91 -0
- package/dist/services/plugin/plugin-install-fs.d.ts +27 -0
- package/dist/services/plugin/plugin-install-fs.js +65 -0
- package/dist/services/{plugin-install-service.d.ts → plugin/plugin-install-service.d.ts} +4 -18
- package/dist/services/{plugin-install-service.js → plugin/plugin-install-service.js} +7 -308
- package/dist/services/plugin/plugin-install-uninstall.d.ts +12 -0
- package/dist/services/plugin/plugin-install-uninstall.js +76 -0
- package/dist/services/{skill-paths.d.ts → plugin/skill-paths.d.ts} +1 -1
- package/dist/services/{skill-preflight-service.d.ts → plugin/skill-preflight-service.d.ts} +1 -1
- package/dist/services/{portable-mcp-classifier.d.ts → profile/portable-mcp-classifier.d.ts} +3 -3
- package/dist/services/{portable-mcp-classifier.js → profile/portable-mcp-classifier.js} +2 -2
- package/dist/services/{portable-profile-pack-service.d.ts → profile/portable-profile-pack-service.d.ts} +8 -2
- package/dist/services/{portable-profile-pack-service.js → profile/portable-profile-pack-service.js} +83 -9
- package/dist/services/profile/profile-apply-service.d.ts +34 -0
- package/dist/services/profile/profile-apply-service.js +102 -0
- package/dist/services/{profile-export-service.d.ts → profile/profile-export-service.d.ts} +7 -3
- package/dist/services/{profile-export-service.js → profile/profile-export-service.js} +3 -1
- package/dist/services/{profile-import-service.d.ts → profile/profile-import-service.d.ts} +1 -1
- package/dist/services/{profile-import-service.js → profile/profile-import-service.js} +85 -130
- package/dist/services/{profile-service.d.ts → profile/profile-service.d.ts} +3 -11
- package/dist/services/{profile-service.js → profile/profile-service.js} +58 -103
- package/dist/services/profile/profile-snapshot-service.d.ts +12 -0
- package/dist/services/profile/profile-snapshot-service.js +47 -0
- package/dist/types.d.ts +2 -57
- package/dist/ui/routes.d.ts +1 -3
- package/dist/ui/routes.js +79 -128
- package/dist/ui/server.d.ts +1 -1
- package/dist/web/assets/index-CGmTbSgk.js +63 -0
- package/dist/web/assets/index-EIVU5Woh.css +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/commands/init.d.ts +0 -3
- package/dist/commands/init.js +0 -27
- package/dist/commands/run.d.ts +0 -3
- package/dist/commands/run.js +0 -25
- package/dist/commands/sync.d.ts +0 -3
- package/dist/commands/sync.js +0 -31
- package/dist/config.d.ts +0 -14
- package/dist/config.js +0 -96
- package/dist/context/builder.d.ts +0 -6
- package/dist/context/builder.js +0 -13
- package/dist/context/memory.d.ts +0 -5
- package/dist/context/memory.js +0 -43
- package/dist/context/skills.d.ts +0 -2
- package/dist/context/skills.js +0 -8
- package/dist/executor/claude.d.ts +0 -12
- package/dist/executor/claude.js +0 -16
- package/dist/executor/codex.d.ts +0 -12
- package/dist/executor/codex.js +0 -16
- package/dist/executor/process.d.ts +0 -11
- package/dist/executor/process.js +0 -40
- package/dist/executor/resolver.d.ts +0 -13
- package/dist/executor/resolver.js +0 -60
- package/dist/executor/types.d.ts +0 -14
- package/dist/executor/types.js +0 -1
- package/dist/services/config-write-service.d.ts +0 -12
- package/dist/services/config-write-service.js +0 -70
- package/dist/services/doctor-service.js +0 -79
- package/dist/services/init-service.d.ts +0 -14
- package/dist/services/init-service.js +0 -88
- package/dist/services/memory-write-service.d.ts +0 -12
- package/dist/services/memory-write-service.js +0 -56
- package/dist/services/run-service.d.ts +0 -15
- package/dist/services/run-service.js +0 -94
- package/dist/services/status-service.d.ts +0 -17
- package/dist/services/status-service.js +0 -21
- package/dist/services/sync-service.d.ts +0 -15
- package/dist/services/sync-service.js +0 -69
- package/dist/ui/streaming.d.ts +0 -3
- package/dist/ui/streaming.js +0 -16
- package/dist/web/assets/index-Bbophmwh.css +0 -2
- package/dist/web/assets/index-DDG_ylui.js +0 -63
- /package/dist/{system/executables.d.ts → executables.d.ts} +0 -0
- /package/dist/{system/executables.js → executables.js} +0 -0
- /package/dist/services/{agent-converter-service.d.ts → agent/agent-converter-service.d.ts} +0 -0
- /package/dist/services/{agent-converter-service.js → agent/agent-converter-service.js} +0 -0
- /package/dist/services/{credential-resolution-service.js → credential/credential-resolution-service.js} +0 -0
- /package/dist/services/{runtime-detector.js → platform/runtime-detector.js} +0 -0
- /package/dist/services/{update-check-service.d.ts → platform/update-check-service.d.ts} +0 -0
- /package/dist/services/{skill-paths.js → plugin/skill-paths.js} +0 -0
- /package/dist/services/{skill-preflight-service.js → plugin/skill-preflight-service.js} +0 -0
package/dist/services/{portable-profile-pack-service.js → profile/portable-profile-pack-service.js}
RENAMED
|
@@ -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 '
|
|
8
|
-
import { createAgentConfigService } from '
|
|
9
|
-
import { redactPortableMcpCredentials } from '
|
|
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 '
|
|
13
|
-
const packageVersion = JSON.parse(readFileSync(new URL('
|
|
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 '
|
|
2
|
-
import type { AgentConfigService } from '
|
|
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,31 +1,46 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
|
-
import { copyFile, cp, mkdir, mkdtemp, readFile,
|
|
3
|
-
import {
|
|
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 '
|
|
7
|
-
import {
|
|
8
|
-
import { resolvePortableMcpCredentials } from '
|
|
9
|
-
import { createMcpPreflightService } from '
|
|
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
|
|
34
|
+
const isFolderSource = archiveStats.isDirectory();
|
|
35
|
+
const extractDir = isFolderSource
|
|
36
|
+
? archivePath
|
|
37
|
+
: await mkdtemp(path.join(tmpdir(), 'brainctl-import-'));
|
|
25
38
|
try {
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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;
|