brainctl 0.1.18 → 0.1.20

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 +7 -7
  2. package/dist/cli.js +9 -9
  3. package/dist/commands/doctor.d.ts +1 -1
  4. package/dist/commands/mcp.js +40 -27
  5. package/dist/commands/profile.d.ts +5 -5
  6. package/dist/commands/profile.js +1 -1
  7. package/dist/commands/status.d.ts +1 -1
  8. package/dist/{mcp/server.d.ts → mcp-server.d.ts} +1 -1
  9. package/dist/{mcp/server.js → mcp-server.js} +10 -10
  10. package/dist/services/agent/agent-asset-installer.d.ts +3 -0
  11. package/dist/services/{agent-asset-installer.js → agent/agent-asset-installer.js} +51 -5
  12. package/dist/services/{agent-availability-service.d.ts → agent/agent-availability-service.d.ts} +1 -1
  13. package/dist/services/{agent-availability-service.js → agent/agent-availability-service.js} +1 -1
  14. package/dist/services/{agent-config-service.d.ts → agent/agent-config-service.d.ts} +6 -6
  15. package/dist/services/{agent-config-service.js → agent/agent-config-service.js} +6 -6
  16. package/dist/services/{credential-redaction-service.d.ts → credential/credential-redaction-service.d.ts} +1 -1
  17. package/dist/services/{credential-resolution-service.d.ts → credential/credential-resolution-service.d.ts} +1 -1
  18. package/dist/services/{doctor-service.d.ts → platform/doctor-service.d.ts} +2 -2
  19. package/dist/services/{doctor-service.js → platform/doctor-service.js} +1 -1
  20. package/dist/services/{mcp-preflight-service.d.ts → platform/mcp-preflight-service.d.ts} +2 -2
  21. package/dist/services/{mcp-preflight-service.js → platform/mcp-preflight-service.js} +1 -1
  22. package/dist/services/{runtime-detector.d.ts → platform/runtime-detector.d.ts} +1 -1
  23. package/dist/services/{status-service.d.ts → platform/status-service.d.ts} +3 -3
  24. package/dist/services/{status-service.js → platform/status-service.js} +2 -2
  25. package/dist/services/{update-check-service.js → platform/update-check-service.js} +1 -1
  26. package/dist/services/plugin/plugin-install-bundle.d.ts +20 -0
  27. package/dist/services/plugin/plugin-install-bundle.js +80 -0
  28. package/dist/services/plugin/plugin-install-compatibility.d.ts +15 -0
  29. package/dist/services/plugin/plugin-install-compatibility.js +91 -0
  30. package/dist/services/plugin/plugin-install-fs.d.ts +27 -0
  31. package/dist/services/plugin/plugin-install-fs.js +65 -0
  32. package/dist/services/{plugin-install-service.d.ts → plugin/plugin-install-service.d.ts} +4 -18
  33. package/dist/services/{plugin-install-service.js → plugin/plugin-install-service.js} +7 -308
  34. package/dist/services/plugin/plugin-install-uninstall.d.ts +12 -0
  35. package/dist/services/plugin/plugin-install-uninstall.js +76 -0
  36. package/dist/services/{skill-paths.d.ts → plugin/skill-paths.d.ts} +1 -1
  37. package/dist/services/{skill-preflight-service.d.ts → plugin/skill-preflight-service.d.ts} +1 -1
  38. package/dist/services/{portable-mcp-classifier.d.ts → profile/portable-mcp-classifier.d.ts} +3 -3
  39. package/dist/services/{portable-mcp-classifier.js → profile/portable-mcp-classifier.js} +2 -2
  40. package/dist/services/{portable-profile-pack-service.d.ts → profile/portable-profile-pack-service.d.ts} +2 -2
  41. package/dist/services/{portable-profile-pack-service.js → profile/portable-profile-pack-service.js} +9 -9
  42. package/dist/services/{profile-apply-service.d.ts → profile/profile-apply-service.d.ts} +2 -2
  43. package/dist/services/{profile-apply-service.js → profile/profile-apply-service.js} +26 -9
  44. package/dist/services/{profile-export-service.d.ts → profile/profile-export-service.d.ts} +2 -2
  45. package/dist/services/{profile-import-service.d.ts → profile/profile-import-service.d.ts} +1 -1
  46. package/dist/services/{profile-import-service.js → profile/profile-import-service.js} +5 -5
  47. package/dist/services/{profile-service.d.ts → profile/profile-service.d.ts} +6 -1
  48. package/dist/services/{profile-service.js → profile/profile-service.js} +49 -6
  49. package/dist/services/{profile-snapshot-service.d.ts → profile/profile-snapshot-service.d.ts} +1 -1
  50. package/dist/types.d.ts +1 -1
  51. package/dist/ui/routes.d.ts +1 -1
  52. package/dist/ui/routes.js +63 -10
  53. package/dist/ui/server.d.ts +1 -1
  54. package/dist/web/assets/index-BSstQoDu.js +63 -0
  55. package/dist/web/assets/index-BdziBx2s.css +2 -0
  56. package/dist/web/index.html +2 -2
  57. package/package.json +3 -1
  58. package/dist/services/agent-asset-installer.d.ts +0 -3
  59. package/dist/web/assets/index-CGmTbSgk.js +0 -63
  60. package/dist/web/assets/index-EIVU5Woh.css +0 -2
  61. /package/dist/{system/executables.d.ts → executables.d.ts} +0 -0
  62. /package/dist/{system/executables.js → executables.js} +0 -0
  63. /package/dist/services/{agent-converter-service.d.ts → agent/agent-converter-service.d.ts} +0 -0
  64. /package/dist/services/{agent-converter-service.js → agent/agent-converter-service.js} +0 -0
  65. /package/dist/services/{credential-redaction-service.js → credential/credential-redaction-service.js} +0 -0
  66. /package/dist/services/{credential-resolution-service.js → credential/credential-resolution-service.js} +0 -0
  67. /package/dist/services/{runtime-detector.js → platform/runtime-detector.js} +0 -0
  68. /package/dist/services/{update-check-service.d.ts → platform/update-check-service.d.ts} +0 -0
  69. /package/dist/services/{skill-paths.js → plugin/skill-paths.js} +0 -0
  70. /package/dist/services/{skill-preflight-service.js → plugin/skill-preflight-service.js} +0 -0
  71. /package/dist/services/{profile-export-service.js → profile/profile-export-service.js} +0 -0
  72. /package/dist/services/{profile-snapshot-service.js → profile/profile-snapshot-service.js} +0 -0
@@ -1,5 +1,5 @@
1
- import type { AgentName } from '../types.js';
2
- import { type AgentConfigService } from './agent-config-service.js';
1
+ import type { AgentName } from '../../types.js';
2
+ import { type AgentConfigService } from '../agent/agent-config-service.js';
3
3
  import { type ProfileService } from './profile-service.js';
4
4
  export type PortablePackSource = {
5
5
  source: 'profile';
@@ -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();
@@ -205,8 +205,8 @@ function collectAgentExtras(agentConfig) {
205
205
  const source = skill.source;
206
206
  if (!installPath || !source)
207
207
  continue;
208
- const safeName = sanitizePackName(`${skill.name}--${source}`);
209
- const archivePath = `plugins/${agentConfig.agent}/${safeName}`;
208
+ const safeName = sanitizePackName(skill.name);
209
+ const archivePath = `plugins/${safeName}`;
210
210
  bundledPlugins.set(archivePath, installPath);
211
211
  plugins.push({
212
212
  agent: agentConfig.agent,
@@ -225,7 +225,7 @@ function collectAgentExtras(agentConfig) {
225
225
  }
226
226
  if (skill.kind === 'skill' && skill.source === 'local') {
227
227
  const localSkillDir = path.join(homedir(), `.${agentConfig.agent}`, 'skills', skill.name);
228
- const archivePath = `skills/${agentConfig.agent}/${skill.name}`;
228
+ const archivePath = `skills/${sanitizePackName(skill.name)}`;
229
229
  bundledUserSkills.set(archivePath, localSkillDir);
230
230
  userSkills.push({
231
231
  agent: agentConfig.agent,
@@ -285,7 +285,7 @@ async function redactAndNormalizeProfile(profile, cwd, source, extras) {
285
285
  const hasExtras = extras !== undefined && (extras.plugins.length > 0 || extras.userSkills.length > 0);
286
286
  return {
287
287
  manifest: {
288
- schemaVersion: hasExtras ? 2 : 1,
288
+ schemaVersion: hasExtras ? 3 : 1,
289
289
  profileName: profile.name,
290
290
  createdBy: {
291
291
  tool: 'brainctl',
@@ -1,7 +1,7 @@
1
- import type { AgentName, SyncResult } from '../types.js';
1
+ import type { AgentName, SyncResult } from '../../types.js';
2
2
  import { type ProfileSnapshotService } from './profile-snapshot-service.js';
3
3
  import { type ProfileService } from './profile-service.js';
4
- import type { AgentConfigWriter } from './sync/agent-writer.js';
4
+ import type { AgentConfigWriter } from '../sync/agent-writer.js';
5
5
  export type ItemType = 'mcp' | 'plugin' | 'skill';
6
6
  export interface ItemSelector {
7
7
  type: ItemType;
@@ -1,13 +1,13 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import YAML from 'yaml';
4
- import { ProfileError } from '../errors.js';
5
- import { installPlugin, installUserSkill } from './agent-asset-installer.js';
4
+ import { ProfileError } from '../../errors.js';
5
+ import { installPlugin, installUserSkill } from '../agent/agent-asset-installer.js';
6
6
  import { createProfileSnapshotService, defaultBackupProfileName, } from './profile-snapshot-service.js';
7
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';
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
11
  export function createProfileApplyService(deps = {}) {
12
12
  const profileService = deps.profileService ?? createProfileService();
13
13
  const snapshotService = deps.snapshotService ?? createProfileSnapshotService();
@@ -59,15 +59,32 @@ export function createProfileApplyService(deps = {}) {
59
59
  mcpResult = { configPath: '', backedUpTo: null };
60
60
  }
61
61
  const pluginsInstalled = [];
62
- for (const plugin of (manifest?.plugins ?? []).filter((p) => p.agent === agent && wantPlugin(p.name))) {
62
+ const pluginsByName = new Map();
63
+ for (const p of manifest?.plugins ?? []) {
64
+ if (!wantPlugin(p.name))
65
+ continue;
66
+ const existing = pluginsByName.get(p.name);
67
+ if (!existing || p.agent === agent)
68
+ pluginsByName.set(p.name, p);
69
+ }
70
+ for (const plugin of pluginsByName.values()) {
63
71
  const sourceDir = path.join(folder, plugin.archivePath);
64
- await installPlugin(sourceDir, plugin);
72
+ await installPlugin(sourceDir, plugin, agent);
65
73
  pluginsInstalled.push(plugin.name);
66
74
  }
67
75
  const userSkillsInstalled = [];
68
- for (const skill of (manifest?.userSkills ?? []).filter((s) => s.agent === agent && wantSkill(s.name))) {
76
+ const skillsByName = new Map();
77
+ for (const s of manifest?.userSkills ?? []) {
78
+ if (!wantSkill(s.name))
79
+ continue;
80
+ if (!skillsByName.has(s.name))
81
+ skillsByName.set(s.name, s);
82
+ else if (s.agent === agent)
83
+ skillsByName.set(s.name, s); // prefer matching source if available
84
+ }
85
+ for (const skill of skillsByName.values()) {
69
86
  const sourceDir = path.join(folder, skill.archivePath);
70
- await installUserSkill(sourceDir, skill);
87
+ await installUserSkill(sourceDir, skill, agent);
71
88
  userSkillsInstalled.push(skill.name);
72
89
  }
73
90
  applied.push({
@@ -1,5 +1,5 @@
1
- import type { AgentName } from '../types.js';
2
- import type { AgentConfigService } from './agent-config-service.js';
1
+ import type { AgentName } from '../../types.js';
2
+ import type { AgentConfigService } from '../agent/agent-config-service.js';
3
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 {
@@ -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;
@@ -3,10 +3,10 @@ import { copyFile, cp, mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'nod
3
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 { installPlugin, installUserSkill } from './agent-asset-installer.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
12
  async function pathExists(p) {
@@ -268,7 +268,7 @@ async function readPortableManifest(extractDir) {
268
268
  throw new ProfileError('Portable profile manifest has invalid structure.');
269
269
  }
270
270
  const manifest = parsed;
271
- if (manifest.schemaVersion !== 1 && manifest.schemaVersion !== 2) {
271
+ if (manifest.schemaVersion !== 1 && manifest.schemaVersion !== 2 && manifest.schemaVersion !== 3) {
272
272
  throw new ProfileError(`Unsupported portable profile schema version: ${String(manifest.schemaVersion)}.`);
273
273
  }
274
274
  if (typeof manifest.profileName !== 'string' || manifest.profileName.trim().length === 0) {
@@ -1,4 +1,4 @@
1
- import type { ProfileConfig } from '../types.js';
1
+ import type { ProfileConfig } from '../../types.js';
2
2
  export declare function profileDir(cwd: string, name: string): string;
3
3
  export declare function profileFile(cwd: string, name: string): string;
4
4
  export interface ProfileService {
@@ -23,6 +23,11 @@ export interface ProfileService {
23
23
  name: string;
24
24
  config: ProfileConfig;
25
25
  }): Promise<void>;
26
+ rename(options: {
27
+ cwd?: string;
28
+ oldName: string;
29
+ newName: string;
30
+ }): Promise<void>;
26
31
  delete(options: {
27
32
  cwd?: string;
28
33
  name: string;
@@ -1,7 +1,7 @@
1
1
  import { readdir, readFile, writeFile, mkdir, rename, rm, stat } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import YAML from 'yaml';
4
- import { ProfileError, ProfileNotFoundError } from '../errors.js';
4
+ import { ProfileError, ProfileNotFoundError } from '../../errors.js';
5
5
  const VALID_RUNTIMES = new Set(['node', 'python', 'java', 'go', 'rust', 'binary']);
6
6
  const PROFILES_DIR = '.brainctl/profiles';
7
7
  const PROFILE_FILE = 'profile.yaml';
@@ -25,6 +25,7 @@ async function migrateLegacyProfile(cwd, name) {
25
25
  await mkdir(folder, { recursive: true });
26
26
  await rename(legacy, newFile);
27
27
  }
28
+ const PROFILE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
28
29
  export function createProfileService() {
29
30
  return {
30
31
  async list(options = {}) {
@@ -68,14 +69,18 @@ export function createProfileService() {
68
69
  },
69
70
  async create(options) {
70
71
  const cwd = options.cwd ?? process.cwd();
71
- const folder = profileDir(cwd, options.name);
72
- const filePath = profileFile(cwd, options.name);
72
+ const trimmed = options.name.trim();
73
+ if (!PROFILE_NAME_PATTERN.test(trimmed)) {
74
+ throw new ProfileError(`Invalid profile name "${trimmed}". Use letters, numbers, ".", "_", or "-".`);
75
+ }
76
+ const folder = profileDir(cwd, trimmed);
77
+ const filePath = profileFile(cwd, trimmed);
73
78
  if ((await pathExists(filePath)) ||
74
- (await pathExists(legacyProfileFile(cwd, options.name)))) {
75
- throw new ProfileError(`Profile "${options.name}" already exists.`);
79
+ (await pathExists(legacyProfileFile(cwd, trimmed)))) {
80
+ throw new ProfileError(`Profile "${trimmed}" already exists.`);
76
81
  }
77
82
  const scaffold = {
78
- name: options.name,
83
+ name: trimmed,
79
84
  description: options.description ?? '',
80
85
  mcps: {},
81
86
  };
@@ -98,6 +103,44 @@ export function createProfileService() {
98
103
  };
99
104
  await writeFile(filePath, YAML.stringify(data), 'utf8');
100
105
  },
106
+ async rename(options) {
107
+ const cwd = options.cwd ?? process.cwd();
108
+ const trimmedNew = options.newName.trim();
109
+ if (!PROFILE_NAME_PATTERN.test(trimmedNew)) {
110
+ throw new ProfileError(`Invalid profile name "${trimmedNew}". Use letters, numbers, ".", "_", or "-".`);
111
+ }
112
+ if (trimmedNew === options.oldName)
113
+ return;
114
+ await migrateLegacyProfile(cwd, options.oldName);
115
+ const oldFolder = profileDir(cwd, options.oldName);
116
+ const oldFile = profileFile(cwd, options.oldName);
117
+ const newFolder = profileDir(cwd, trimmedNew);
118
+ const newFile = profileFile(cwd, trimmedNew);
119
+ if (!(await pathExists(oldFile))) {
120
+ throw new ProfileNotFoundError(`Profile "${options.oldName}" not found.`);
121
+ }
122
+ if ((await pathExists(newFolder)) ||
123
+ (await pathExists(legacyProfileFile(cwd, trimmedNew)))) {
124
+ throw new ProfileError(`Profile "${trimmedNew}" already exists.`);
125
+ }
126
+ await rename(oldFolder, newFolder);
127
+ const profileSource = await readFile(newFile, 'utf8');
128
+ const parsed = YAML.parse(profileSource) ?? {};
129
+ parsed.name = trimmedNew;
130
+ await writeFile(newFile, YAML.stringify(parsed), 'utf8');
131
+ const manifestPath = path.join(newFolder, 'manifest.yaml');
132
+ if (await pathExists(manifestPath)) {
133
+ try {
134
+ const manifestSource = await readFile(manifestPath, 'utf8');
135
+ const manifest = YAML.parse(manifestSource) ?? {};
136
+ manifest.profileName = trimmedNew;
137
+ await writeFile(manifestPath, YAML.stringify(manifest), 'utf8');
138
+ }
139
+ catch {
140
+ // Manifest is best-effort; pack-time will rewrite it on next export.
141
+ }
142
+ }
143
+ },
101
144
  async delete(options) {
102
145
  const cwd = options.cwd ?? process.cwd();
103
146
  await migrateLegacyProfile(cwd, options.name);
@@ -1,4 +1,4 @@
1
- import type { AgentName } from '../types.js';
1
+ import type { AgentName } from '../../types.js';
2
2
  export interface ProfileSnapshotService {
3
3
  execute(options: {
4
4
  cwd: string;
package/dist/types.d.ts CHANGED
@@ -39,7 +39,7 @@ export interface PortableUserSkillSnapshot {
39
39
  archivePath: string;
40
40
  }
41
41
  export interface PortableProfileManifest {
42
- schemaVersion: 1 | 2;
42
+ schemaVersion: 1 | 2 | 3;
43
43
  profileName: string;
44
44
  createdBy?: {
45
45
  tool: string;
@@ -1,5 +1,5 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
- import type { StatusService } from '../services/status-service.js';
2
+ import type { StatusService } from '../services/platform/status-service.js';
3
3
  export interface UiRouteDependencies {
4
4
  cwd: string;
5
5
  statusService?: StatusService;
package/dist/ui/routes.js CHANGED
@@ -1,18 +1,19 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { BrainctlError, ProfileError, ProfileNotFoundError, ValidationError } from '../errors.js';
4
- import { createAgentConfigService } from '../services/agent-config-service.js';
5
- import { createMcpPreflightService } from '../services/mcp-preflight-service.js';
6
- import { createPluginInstallService } from '../services/plugin-install-service.js';
7
- import { createProfileExportService } from '../services/profile-export-service.js';
8
- import { createProfileImportService } from '../services/profile-import-service.js';
9
- import { createProfileService } from '../services/profile-service.js';
10
- import { createSkillPreflightService } from '../services/skill-preflight-service.js';
11
- import { createStatusService } from '../services/status-service.js';
12
- import { createProfileApplyService, } from '../services/profile-apply-service.js';
13
- import { createProfileSnapshotService, defaultBackupProfileName, } from '../services/profile-snapshot-service.js';
4
+ import { createAgentConfigService } from '../services/agent/agent-config-service.js';
5
+ import { createMcpPreflightService } from '../services/platform/mcp-preflight-service.js';
6
+ import { createPluginInstallService } from '../services/plugin/plugin-install-service.js';
7
+ import { createProfileExportService } from '../services/profile/profile-export-service.js';
8
+ import { createProfileImportService } from '../services/profile/profile-import-service.js';
9
+ import { createProfileService } from '../services/profile/profile-service.js';
10
+ import { createSkillPreflightService } from '../services/plugin/skill-preflight-service.js';
11
+ import { createStatusService } from '../services/platform/status-service.js';
12
+ import { createProfileApplyService, } from '../services/profile/profile-apply-service.js';
13
+ import { createProfileSnapshotService, defaultBackupProfileName, } from '../services/profile/profile-snapshot-service.js';
14
14
  import path from 'node:path';
15
15
  import { fileURLToPath } from 'node:url';
16
+ import { spawn } from 'node:child_process';
16
17
  const uiAssetRoot = resolveUiAssetRoot();
17
18
  export function createUiRouteHandler(dependencies) {
18
19
  const statusService = dependencies.statusService ?? createStatusService();
@@ -162,6 +163,32 @@ export function createUiRouteHandler(dependencies) {
162
163
  }
163
164
  }
164
165
  default: {
166
+ // Open profile folder in OS file explorer: POST /api/profiles/:name/open-folder
167
+ const openFolderMatch = url.pathname.match(/^\/api\/profiles\/([^/]+)\/open-folder$/);
168
+ if (openFolderMatch) {
169
+ if (request.method !== 'POST') {
170
+ return sendJson(response, 405, { error: 'Method not allowed' });
171
+ }
172
+ const profileName = decodeURIComponent(openFolderMatch[1]);
173
+ const folderPath = path.join(dependencies.cwd, '.brainctl', 'profiles', profileName);
174
+ if (!existsSync(folderPath)) {
175
+ return sendJson(response, 404, { error: `Profile folder not found: ${folderPath}` });
176
+ }
177
+ const opener = process.platform === 'darwin'
178
+ ? 'open'
179
+ : process.platform === 'win32'
180
+ ? 'explorer'
181
+ : 'xdg-open';
182
+ try {
183
+ spawn(opener, [folderPath], { detached: true, stdio: 'ignore' }).unref();
184
+ return sendJson(response, 200, { ok: true, path: folderPath });
185
+ }
186
+ catch (error) {
187
+ return sendJson(response, 500, {
188
+ error: error instanceof Error ? error.message : 'Failed to open folder',
189
+ });
190
+ }
191
+ }
165
192
  // Profile apply: POST /api/profiles/:name/apply
166
193
  const applyMatch = url.pathname.match(/^\/api\/profiles\/([^/]+)\/apply$/);
167
194
  if (applyMatch) {
@@ -441,6 +468,32 @@ export function createUiRouteHandler(dependencies) {
441
468
  return sendProfileError(response, error);
442
469
  }
443
470
  }
471
+ const profileRenameMatch = url.pathname.match(/^\/api\/profiles\/([^/]+)\/rename$/);
472
+ if (profileRenameMatch) {
473
+ const oldName = decodeURIComponent(profileRenameMatch[1]);
474
+ if (request.method !== 'POST') {
475
+ return sendJson(response, 405, { error: 'Method not allowed' });
476
+ }
477
+ const body = await readJsonBody(request);
478
+ if (!body.ok) {
479
+ return sendJson(response, 400, { error: 'Invalid JSON body' });
480
+ }
481
+ const data = body.value;
482
+ if (typeof data.newName !== 'string' || data.newName.trim().length === 0) {
483
+ return sendJson(response, 400, { error: 'Missing newName' });
484
+ }
485
+ try {
486
+ await profileService.rename({
487
+ cwd: dependencies.cwd,
488
+ oldName,
489
+ newName: data.newName.trim(),
490
+ });
491
+ return sendJson(response, 200, { ok: true, name: data.newName.trim() });
492
+ }
493
+ catch (error) {
494
+ return sendProfileError(response, error);
495
+ }
496
+ }
444
497
  const profileMatch = url.pathname.match(/^\/api\/profiles\/([^/]+)$/);
445
498
  if (profileMatch) {
446
499
  const name = decodeURIComponent(profileMatch[1]);
@@ -1,5 +1,5 @@
1
1
  import { type Server } from 'node:http';
2
- import type { StatusService } from '../services/status-service.js';
2
+ import type { StatusService } from '../services/platform/status-service.js';
3
3
  export interface StartUiServerOptions {
4
4
  cwd?: string;
5
5
  host?: string;