brainctl 0.1.17 → 0.1.18

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 (72) hide show
  1. package/dist/cli.d.ts +4 -6
  2. package/dist/cli.js +11 -16
  3. package/dist/commands/profile.d.ts +4 -0
  4. package/dist/commands/profile.js +106 -16
  5. package/dist/commands/status.js +7 -7
  6. package/dist/mcp/server.js +48 -141
  7. package/dist/services/agent-asset-installer.d.ts +3 -0
  8. package/dist/services/agent-asset-installer.js +109 -0
  9. package/dist/services/agent-availability-service.d.ts +11 -0
  10. package/dist/services/agent-availability-service.js +32 -0
  11. package/dist/services/credential-redaction-service.d.ts +1 -0
  12. package/dist/services/credential-redaction-service.js +9 -3
  13. package/dist/services/doctor-service.d.ts +2 -2
  14. package/dist/services/doctor-service.js +7 -63
  15. package/dist/services/portable-profile-pack-service.d.ts +6 -0
  16. package/dist/services/portable-profile-pack-service.js +78 -4
  17. package/dist/services/profile-apply-service.d.ts +34 -0
  18. package/dist/services/profile-apply-service.js +102 -0
  19. package/dist/services/profile-export-service.d.ts +5 -1
  20. package/dist/services/profile-export-service.js +3 -1
  21. package/dist/services/profile-import-service.js +82 -127
  22. package/dist/services/profile-service.d.ts +3 -11
  23. package/dist/services/profile-service.js +57 -102
  24. package/dist/services/profile-snapshot-service.d.ts +12 -0
  25. package/dist/services/profile-snapshot-service.js +47 -0
  26. package/dist/services/status-service.d.ts +9 -7
  27. package/dist/services/status-service.js +14 -13
  28. package/dist/types.d.ts +2 -57
  29. package/dist/ui/routes.d.ts +0 -2
  30. package/dist/ui/routes.js +71 -120
  31. package/dist/web/assets/index-CGmTbSgk.js +63 -0
  32. package/dist/web/assets/index-EIVU5Woh.css +2 -0
  33. package/dist/web/index.html +2 -2
  34. package/package.json +1 -1
  35. package/dist/commands/init.d.ts +0 -3
  36. package/dist/commands/init.js +0 -27
  37. package/dist/commands/run.d.ts +0 -3
  38. package/dist/commands/run.js +0 -25
  39. package/dist/commands/sync.d.ts +0 -3
  40. package/dist/commands/sync.js +0 -31
  41. package/dist/config.d.ts +0 -14
  42. package/dist/config.js +0 -96
  43. package/dist/context/builder.d.ts +0 -6
  44. package/dist/context/builder.js +0 -13
  45. package/dist/context/memory.d.ts +0 -5
  46. package/dist/context/memory.js +0 -43
  47. package/dist/context/skills.d.ts +0 -2
  48. package/dist/context/skills.js +0 -8
  49. package/dist/executor/claude.d.ts +0 -12
  50. package/dist/executor/claude.js +0 -16
  51. package/dist/executor/codex.d.ts +0 -12
  52. package/dist/executor/codex.js +0 -16
  53. package/dist/executor/process.d.ts +0 -11
  54. package/dist/executor/process.js +0 -40
  55. package/dist/executor/resolver.d.ts +0 -13
  56. package/dist/executor/resolver.js +0 -60
  57. package/dist/executor/types.d.ts +0 -14
  58. package/dist/executor/types.js +0 -1
  59. package/dist/services/config-write-service.d.ts +0 -12
  60. package/dist/services/config-write-service.js +0 -70
  61. package/dist/services/init-service.d.ts +0 -14
  62. package/dist/services/init-service.js +0 -88
  63. package/dist/services/memory-write-service.d.ts +0 -12
  64. package/dist/services/memory-write-service.js +0 -56
  65. package/dist/services/run-service.d.ts +0 -15
  66. package/dist/services/run-service.js +0 -94
  67. package/dist/services/sync-service.d.ts +0 -15
  68. package/dist/services/sync-service.js +0 -69
  69. package/dist/ui/streaming.d.ts +0 -3
  70. package/dist/ui/streaming.js +0 -16
  71. package/dist/web/assets/index-Bbophmwh.css +0 -2
  72. package/dist/web/assets/index-DDG_ylui.js +0 -63
@@ -0,0 +1,109 @@
1
+ import { copyFile, cp, mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import path from 'node:path';
4
+ import { ProfileError } from '../errors.js';
5
+ import { formatTimestamp } from './sync/agent-writer.js';
6
+ export async function installPlugin(sourceDir, plugin) {
7
+ try {
8
+ await stat(sourceDir);
9
+ }
10
+ catch {
11
+ throw new ProfileError(`Bundled plugin "${plugin.name}" source missing at ${sourceDir}.`);
12
+ }
13
+ if (plugin.agent === 'gemini') {
14
+ return;
15
+ }
16
+ const marketplace = plugin.marketplace ?? plugin.source;
17
+ const version = plugin.version ?? 'unknown';
18
+ const cacheRoot = path.join(homedir(), `.${plugin.agent}`, 'plugins', 'cache');
19
+ const targetDir = path.join(cacheRoot, marketplace, plugin.name, version);
20
+ await rm(targetDir, { recursive: true, force: true });
21
+ await mkdir(path.dirname(targetDir), { recursive: true });
22
+ await cp(sourceDir, targetDir, { recursive: true });
23
+ if (plugin.agent === 'claude') {
24
+ await registerClaudePlugin({
25
+ pluginKey: `${plugin.name}@${marketplace}`,
26
+ installPath: targetDir,
27
+ version,
28
+ });
29
+ return;
30
+ }
31
+ if (plugin.agent === 'codex') {
32
+ await registerCodexPlugin({
33
+ pluginKey: `${plugin.name}@${marketplace}`,
34
+ });
35
+ }
36
+ }
37
+ export async function installUserSkill(sourceDir, skill) {
38
+ try {
39
+ await stat(sourceDir);
40
+ }
41
+ catch {
42
+ throw new ProfileError(`Bundled user skill "${skill.name}" source missing at ${sourceDir}.`);
43
+ }
44
+ const targetDir = path.join(homedir(), `.${skill.agent}`, 'skills', skill.name);
45
+ await rm(targetDir, { recursive: true, force: true });
46
+ await mkdir(path.dirname(targetDir), { recursive: true });
47
+ await cp(sourceDir, targetDir, { recursive: true });
48
+ }
49
+ async function registerClaudePlugin(options) {
50
+ const filePath = path.join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
51
+ let existing = { version: 2, plugins: {} };
52
+ try {
53
+ const source = await readFile(filePath, 'utf8');
54
+ existing = JSON.parse(source);
55
+ await backupFile(filePath);
56
+ }
57
+ catch {
58
+ // fresh file
59
+ }
60
+ const plugins = (existing.plugins ?? {});
61
+ const now = new Date().toISOString();
62
+ const entry = {
63
+ scope: 'user',
64
+ installPath: options.installPath,
65
+ version: options.version,
66
+ installedAt: now,
67
+ lastUpdated: now,
68
+ };
69
+ plugins[options.pluginKey] = [entry];
70
+ existing.plugins = plugins;
71
+ if (typeof existing.version !== 'number')
72
+ existing.version = 2;
73
+ await mkdir(path.dirname(filePath), { recursive: true });
74
+ await atomicWrite(filePath, JSON.stringify(existing, null, 2) + '\n');
75
+ }
76
+ async function registerCodexPlugin(options) {
77
+ const filePath = path.join(homedir(), '.codex', 'config.toml');
78
+ let existing = '';
79
+ try {
80
+ existing = await readFile(filePath, 'utf8');
81
+ await backupFile(filePath);
82
+ }
83
+ catch {
84
+ existing = '';
85
+ }
86
+ const header = `[plugins."${options.pluginKey}"]`;
87
+ if (existing.includes(header))
88
+ return;
89
+ const prefix = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
90
+ const separator = existing.length > 0 ? '\n' : '';
91
+ const block = `${header}\nenabled = true\n`;
92
+ const next = existing + prefix + separator + block;
93
+ await mkdir(path.dirname(filePath), { recursive: true });
94
+ await atomicWrite(filePath, next);
95
+ }
96
+ async function backupFile(filePath) {
97
+ const backupPath = `${filePath}.bak.${formatTimestamp()}`;
98
+ try {
99
+ await copyFile(filePath, backupPath);
100
+ }
101
+ catch {
102
+ // file may not exist
103
+ }
104
+ }
105
+ async function atomicWrite(filePath, content) {
106
+ const tmpPath = `${filePath}.tmp.${Date.now()}`;
107
+ await writeFile(tmpPath, content, 'utf8');
108
+ await rename(tmpPath, filePath);
109
+ }
@@ -0,0 +1,11 @@
1
+ import type { AgentName } from '../types.js';
2
+ export interface AgentAvailability {
3
+ agent: AgentName;
4
+ available: boolean;
5
+ command: string;
6
+ resolvedPath?: string;
7
+ }
8
+ export interface AgentAvailabilityService {
9
+ getAll(): Promise<Record<AgentName, AgentAvailability>>;
10
+ }
11
+ export declare function createAgentAvailabilityService(): AgentAvailabilityService;
@@ -0,0 +1,32 @@
1
+ import { findExecutable } from '../system/executables.js';
2
+ const SUPPORTED_AGENTS = ['claude', 'codex', 'gemini'];
3
+ const AGENT_COMMANDS = {
4
+ claude: 'claude',
5
+ codex: 'codex',
6
+ gemini: 'gemini',
7
+ };
8
+ export function createAgentAvailabilityService() {
9
+ const cache = new Map();
10
+ const check = (agent) => {
11
+ if (!cache.has(agent)) {
12
+ cache.set(agent, checkAvailability(agent));
13
+ }
14
+ return cache.get(agent);
15
+ };
16
+ return {
17
+ async getAll() {
18
+ const entries = await Promise.all(SUPPORTED_AGENTS.map(async (agent) => [agent, await check(agent)]));
19
+ return Object.fromEntries(entries);
20
+ },
21
+ };
22
+ }
23
+ async function checkAvailability(agent) {
24
+ const command = AGENT_COMMANDS[agent];
25
+ const resolvedPath = await findExecutable(command);
26
+ return {
27
+ agent,
28
+ command,
29
+ available: resolvedPath !== null,
30
+ resolvedPath: resolvedPath ?? undefined,
31
+ };
32
+ }
@@ -2,6 +2,7 @@ import type { McpServerConfig, PortableCredentialSpec } from '../types.js';
2
2
  export interface CredentialRedactionResult<T extends McpServerConfig> {
3
3
  redacted: T;
4
4
  credentials: PortableCredentialSpec[];
5
+ rawValues: Record<string, string>;
5
6
  }
6
7
  export declare function redactPortableMcpCredentials<T extends McpServerConfig>(config: T): CredentialRedactionResult<T>;
7
8
  interface CredentialAccumulator {
@@ -1,8 +1,9 @@
1
1
  export function redactPortableMcpCredentials(config) {
2
2
  const credentialsByKey = new Map();
3
- const redactedEnv = redactStringMap(config.env, 'env', credentialsByKey);
3
+ const rawValues = {};
4
+ const redactedEnv = redactStringMap(config.env, 'env', credentialsByKey, rawValues);
4
5
  if (config.kind === 'remote') {
5
- const redactedHeaders = redactStringMap(config.headers, 'header', credentialsByKey);
6
+ const redactedHeaders = redactStringMap(config.headers, 'header', credentialsByKey, rawValues);
6
7
  return {
7
8
  redacted: {
8
9
  ...config,
@@ -10,6 +11,7 @@ export function redactPortableMcpCredentials(config) {
10
11
  ...(redactedHeaders ? { headers: redactedHeaders } : {}),
11
12
  },
12
13
  credentials: finalizePortableCredentialSpecs(credentialsByKey),
14
+ rawValues,
13
15
  };
14
16
  }
15
17
  return {
@@ -18,9 +20,10 @@ export function redactPortableMcpCredentials(config) {
18
20
  ...(redactedEnv ? { env: redactedEnv } : {}),
19
21
  },
20
22
  credentials: finalizePortableCredentialSpecs(credentialsByKey),
23
+ rawValues,
21
24
  };
22
25
  }
23
- function redactStringMap(values, source, credentialsByKey) {
26
+ function redactStringMap(values, source, credentialsByKey, rawValues) {
24
27
  if (!values) {
25
28
  return undefined;
26
29
  }
@@ -32,6 +35,9 @@ function redactStringMap(values, source, credentialsByKey) {
32
35
  }
33
36
  const credentialKey = normalizeCredentialKey(key);
34
37
  addCredentialSpec(credentialsByKey, credentialKey, source, key);
38
+ if (!isCredentialPlaceholder(value)) {
39
+ rawValues[credentialKey] = value;
40
+ }
35
41
  redacted[key] = isCredentialPlaceholder(value)
36
42
  ? value
37
43
  : `\${credentials.${credentialKey}}`;
@@ -1,4 +1,4 @@
1
- import type { ExecutorResolver } from '../executor/resolver.js';
1
+ import { type AgentAvailabilityService } from './agent-availability-service.js';
2
2
  import type { DiagnosticCheck } from '../types.js';
3
3
  export interface DoctorResult {
4
4
  checks: DiagnosticCheck[];
@@ -10,5 +10,5 @@ export interface DoctorService {
10
10
  }): Promise<DoctorResult>;
11
11
  }
12
12
  export declare function createDoctorService(dependencies?: {
13
- resolver?: ExecutorResolver;
13
+ availabilityService?: AgentAvailabilityService;
14
14
  }): DoctorService;
@@ -1,79 +1,23 @@
1
- import { stat } from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { loadConfig } from '../config.js';
4
- import { createExecutorResolver } from '../executor/resolver.js';
1
+ import { createAgentAvailabilityService, } from './agent-availability-service.js';
5
2
  export function createDoctorService(dependencies = {}) {
6
- const resolver = dependencies.resolver ?? createExecutorResolver();
3
+ const availabilityService = dependencies.availabilityService ?? createAgentAvailabilityService();
7
4
  return {
8
- async execute(options = {}) {
9
- const cwd = options.cwd ?? process.cwd();
5
+ async execute() {
10
6
  const checks = [];
11
- const configPath = path.join(cwd, 'ai-stack.yaml');
12
- const configExists = await pathExists(configPath);
13
- if (!configExists) {
14
- checks.push({
15
- label: 'Config',
16
- status: 'error',
17
- message: 'ai-stack.yaml was not found.'
18
- });
19
- }
20
- if (configExists) {
21
- try {
22
- const config = await loadConfig({ cwd });
23
- checks.push({
24
- label: 'Config',
25
- status: 'ok',
26
- message: `Loaded ${config.configPath}`
27
- });
28
- for (const memoryPath of config.memory.paths) {
29
- const exists = await pathExists(memoryPath);
30
- checks.push({
31
- label: 'Memory',
32
- status: exists ? 'ok' : 'error',
33
- message: exists
34
- ? `Memory path is available: ${memoryPath}`
35
- : `Memory path is missing: ${memoryPath}`
36
- });
37
- }
38
- checks.push({
39
- label: 'Skills',
40
- status: Object.keys(config.skills).length > 0 ? 'ok' : 'error',
41
- message: Object.keys(config.skills).length > 0
42
- ? `${Object.keys(config.skills).length} skills configured`
43
- : 'No skills are configured.'
44
- });
45
- }
46
- catch (error) {
47
- checks.push({
48
- label: 'Config',
49
- status: 'error',
50
- message: error instanceof Error ? error.message : 'Config validation failed.'
51
- });
52
- }
53
- }
54
- const availability = await resolver.getAgentAvailability();
7
+ const availability = await availabilityService.getAll();
55
8
  for (const agent of Object.values(availability)) {
56
9
  checks.push({
57
10
  label: 'Agent',
58
11
  status: agent.available ? 'ok' : 'warn',
59
12
  message: agent.available
60
13
  ? `${agent.agent} is available`
61
- : `${agent.agent} is not available on PATH`
14
+ : `${agent.agent} is not available on PATH`,
62
15
  });
63
16
  }
64
17
  return {
65
18
  checks,
66
- hasIssues: checks.some((check) => check.status !== 'ok')
19
+ hasIssues: checks.some((c) => c.status !== 'ok'),
67
20
  };
68
- }
21
+ },
69
22
  };
70
23
  }
71
- async function pathExists(targetPath) {
72
- try {
73
- await stat(targetPath);
74
- return true;
75
- }
76
- catch {
77
- return false;
78
- }
79
- }
@@ -9,13 +9,19 @@ export type PortablePackSource = {
9
9
  agent: AgentName;
10
10
  cwd: string;
11
11
  };
12
+ export type PortablePackFormat = 'tarball' | 'folder';
13
+ export type PortableCredentialsMode = 'redact' | 'keep';
12
14
  export interface PortableProfilePackService {
13
15
  execute(options: {
14
16
  cwd?: string;
15
17
  source: PortablePackSource;
16
18
  outputPath?: string;
19
+ format?: PortablePackFormat;
20
+ credentialsMode?: PortableCredentialsMode;
17
21
  }): Promise<{
18
22
  archivePath: string;
23
+ format: PortablePackFormat;
24
+ warnings: string[];
19
25
  }>;
20
26
  }
21
27
  interface PortableProfilePackServiceDependencies {
@@ -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-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
1
  import type { AgentName } from '../types.js';
2
2
  import type { AgentConfigService } from './agent-config-service.js';
3
- import { type PortableProfilePackService } from './portable-profile-pack-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
  };