brainctl 0.1.19 → 0.1.21
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/commands/mcp.js +38 -25
- package/dist/services/agent/agent-asset-installer.d.ts +3 -3
- package/dist/services/agent/agent-asset-installer.js +49 -3
- package/dist/services/profile/portable-profile-pack-service.js +4 -4
- package/dist/services/profile/profile-apply-service.js +21 -4
- package/dist/services/profile/profile-import-service.js +1 -1
- package/dist/services/profile/profile-service.d.ts +5 -0
- package/dist/services/profile/profile-service.js +48 -5
- package/dist/types.d.ts +1 -1
- package/dist/ui/routes.js +53 -0
- package/dist/web/assets/index-BWIFiVAz.js +63 -0
- package/dist/web/assets/index-CdsjWk34.css +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +3 -1
- package/dist/web/assets/index-CGmTbSgk.js +0 -63
- package/dist/web/assets/index-EIVU5Woh.css +0 -2
package/dist/commands/mcp.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { startMcpServer } from '../mcp-server.js';
|
|
3
3
|
import { createUpdateCheckService } from '../services/platform/update-check-service.js';
|
|
4
4
|
export function registerMcpCommand(program) {
|
|
@@ -6,39 +6,52 @@ export function registerMcpCommand(program) {
|
|
|
6
6
|
.command('mcp')
|
|
7
7
|
.description('Start the brainctl MCP server (stdio transport)')
|
|
8
8
|
.action(async () => {
|
|
9
|
+
killPriorMcpServers();
|
|
10
|
+
process.stdin.on('end', () => process.exit(0));
|
|
11
|
+
process.stdin.on('close', () => process.exit(0));
|
|
12
|
+
await startMcpServer({ cwd: process.cwd() });
|
|
9
13
|
if (!process.env.BRAINCTL_NO_UPDATE_CHECK) {
|
|
10
|
-
|
|
14
|
+
// Fire-and-forget after the server is up so cold-start isn't blocked
|
|
15
|
+
// on a network round-trip (or `npm install brainctl@latest`).
|
|
16
|
+
void notifyIfOutdated();
|
|
11
17
|
}
|
|
12
|
-
await startMcpServer({ cwd: process.cwd() });
|
|
13
18
|
});
|
|
14
19
|
}
|
|
15
|
-
|
|
20
|
+
function killPriorMcpServers() {
|
|
21
|
+
const self = process.pid;
|
|
22
|
+
const ppid = process.ppid;
|
|
16
23
|
try {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const result = await service.selfUpdate();
|
|
22
|
-
if (result.success) {
|
|
23
|
-
// Re-exec with the updated binary
|
|
24
|
-
const child = spawn(process.execPath, process.argv.slice(1), {
|
|
25
|
-
stdio: 'inherit',
|
|
26
|
-
});
|
|
27
|
-
await new Promise((resolve) => {
|
|
28
|
-
child.on('exit', (code) => {
|
|
29
|
-
process.exit(code ?? 0);
|
|
30
|
-
});
|
|
31
|
-
child.on('error', () => {
|
|
32
|
-
resolve(); // fall through to current version
|
|
33
|
-
});
|
|
34
|
-
});
|
|
24
|
+
const result = spawnSync('pgrep', ['-f', 'brainctl/dist/cli\\.js mcp'], {
|
|
25
|
+
encoding: 'utf8',
|
|
26
|
+
});
|
|
27
|
+
if (result.status !== 0 || !result.stdout)
|
|
35
28
|
return;
|
|
29
|
+
const pids = result.stdout
|
|
30
|
+
.split('\n')
|
|
31
|
+
.map((line) => Number.parseInt(line.trim(), 10))
|
|
32
|
+
.filter((pid) => Number.isFinite(pid) && pid !== self && pid !== ppid);
|
|
33
|
+
for (const pid of pids) {
|
|
34
|
+
try {
|
|
35
|
+
process.kill(pid, 'SIGTERM');
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// already gone
|
|
39
|
+
}
|
|
36
40
|
}
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// pgrep unavailable — skip
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function notifyIfOutdated() {
|
|
47
|
+
try {
|
|
48
|
+
const service = createUpdateCheckService();
|
|
49
|
+
const check = await service.check();
|
|
50
|
+
if (check.isOutdated) {
|
|
51
|
+
process.stderr.write(`brainctl: a newer version is available (${check.latest}). Run \`npm i -g brainctl@latest\` to update.\n`);
|
|
39
52
|
}
|
|
40
53
|
}
|
|
41
54
|
catch {
|
|
42
|
-
// Update check failed
|
|
55
|
+
// Update check failed — stay silent
|
|
43
56
|
}
|
|
44
57
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { PortablePluginSnapshot, PortableUserSkillSnapshot } from '../../types.js';
|
|
2
|
-
export declare function installPlugin(sourceDir: string, plugin: PortablePluginSnapshot): Promise<void>;
|
|
3
|
-
export declare function installUserSkill(sourceDir: string, skill: PortableUserSkillSnapshot): Promise<void>;
|
|
1
|
+
import type { AgentName, PortablePluginSnapshot, PortableUserSkillSnapshot } from '../../types.js';
|
|
2
|
+
export declare function installPlugin(sourceDir: string, plugin: PortablePluginSnapshot, targetAgent?: AgentName): Promise<void>;
|
|
3
|
+
export declare function installUserSkill(sourceDir: string, skill: PortableUserSkillSnapshot, targetAgent?: PortableUserSkillSnapshot['agent']): Promise<void>;
|
|
@@ -2,14 +2,22 @@ import { copyFile, cp, mkdir, readFile, rename, rm, stat, writeFile } from 'node
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { ProfileError } from '../../errors.js';
|
|
5
|
+
import { defaultReadInstalledPluginBundle, isAgentInstallableOnTarget, isCommandInstallableOnTarget, } from '../plugin/plugin-install-bundle.js';
|
|
6
|
+
import { defaultCopySkillDirectory, defaultInstallAgent, defaultInstallCommand, } from '../plugin/plugin-install-fs.js';
|
|
7
|
+
import { writeManagedPluginInstall } from '../sync/managed-plugin-registry.js';
|
|
5
8
|
import { formatTimestamp } from '../sync/agent-writer.js';
|
|
6
|
-
export async function installPlugin(sourceDir, plugin) {
|
|
9
|
+
export async function installPlugin(sourceDir, plugin, targetAgent) {
|
|
7
10
|
try {
|
|
8
11
|
await stat(sourceDir);
|
|
9
12
|
}
|
|
10
13
|
catch {
|
|
11
14
|
throw new ProfileError(`Bundled plugin "${plugin.name}" source missing at ${sourceDir}.`);
|
|
12
15
|
}
|
|
16
|
+
const target = targetAgent ?? plugin.agent;
|
|
17
|
+
if (target !== plugin.agent) {
|
|
18
|
+
await installPluginCrossAgent(sourceDir, plugin, target);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
13
21
|
if (plugin.agent === 'gemini') {
|
|
14
22
|
return;
|
|
15
23
|
}
|
|
@@ -34,14 +42,52 @@ export async function installPlugin(sourceDir, plugin) {
|
|
|
34
42
|
});
|
|
35
43
|
}
|
|
36
44
|
}
|
|
37
|
-
|
|
45
|
+
async function installPluginCrossAgent(sourceDir, plugin, targetAgent) {
|
|
46
|
+
const bundle = await defaultReadInstalledPluginBundle(sourceDir);
|
|
47
|
+
for (const skillName of bundle.skills) {
|
|
48
|
+
await defaultCopySkillDirectory({
|
|
49
|
+
sourceInstallPath: sourceDir,
|
|
50
|
+
skillName,
|
|
51
|
+
targetAgent,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const installedAgents = [];
|
|
55
|
+
if (isAgentInstallableOnTarget(targetAgent)) {
|
|
56
|
+
for (const agent of bundle.agents) {
|
|
57
|
+
await defaultInstallAgent({ targetAgent, agent });
|
|
58
|
+
installedAgents.push(agent.name);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const installedCommands = [];
|
|
62
|
+
if (isCommandInstallableOnTarget(targetAgent)) {
|
|
63
|
+
for (const command of bundle.commands) {
|
|
64
|
+
await defaultInstallCommand({ targetAgent, command });
|
|
65
|
+
installedCommands.push(command.name);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
await writeManagedPluginInstall({
|
|
69
|
+
agent: targetAgent,
|
|
70
|
+
plugin: {
|
|
71
|
+
name: plugin.name,
|
|
72
|
+
kind: 'plugin',
|
|
73
|
+
managed: true,
|
|
74
|
+
source: plugin.source,
|
|
75
|
+
pluginSkills: bundle.skills,
|
|
76
|
+
pluginMcps: Object.keys(bundle.mcps),
|
|
77
|
+
pluginAgents: installedAgents,
|
|
78
|
+
pluginCommands: installedCommands,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
export async function installUserSkill(sourceDir, skill, targetAgent) {
|
|
38
83
|
try {
|
|
39
84
|
await stat(sourceDir);
|
|
40
85
|
}
|
|
41
86
|
catch {
|
|
42
87
|
throw new ProfileError(`Bundled user skill "${skill.name}" source missing at ${sourceDir}.`);
|
|
43
88
|
}
|
|
44
|
-
const
|
|
89
|
+
const agent = targetAgent ?? skill.agent;
|
|
90
|
+
const targetDir = path.join(homedir(), `.${agent}`, 'skills', skill.name);
|
|
45
91
|
await rm(targetDir, { recursive: true, force: true });
|
|
46
92
|
await mkdir(path.dirname(targetDir), { recursive: true });
|
|
47
93
|
await cp(sourceDir, targetDir, { recursive: true });
|
|
@@ -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(
|
|
209
|
-
const archivePath = `plugins/${
|
|
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/${
|
|
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 ?
|
|
288
|
+
schemaVersion: hasExtras ? 3 : 1,
|
|
289
289
|
profileName: profile.name,
|
|
290
290
|
createdBy: {
|
|
291
291
|
tool: 'brainctl',
|
|
@@ -59,15 +59,32 @@ export function createProfileApplyService(deps = {}) {
|
|
|
59
59
|
mcpResult = { configPath: '', backedUpTo: null };
|
|
60
60
|
}
|
|
61
61
|
const pluginsInstalled = [];
|
|
62
|
-
|
|
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
|
-
|
|
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({
|
|
@@ -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) {
|
|
@@ -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;
|
|
@@ -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
|
|
72
|
-
|
|
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,
|
|
75
|
-
throw new ProfileError(`Profile "${
|
|
79
|
+
(await pathExists(legacyProfileFile(cwd, trimmed)))) {
|
|
80
|
+
throw new ProfileError(`Profile "${trimmed}" already exists.`);
|
|
76
81
|
}
|
|
77
82
|
const scaffold = {
|
|
78
|
-
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);
|
package/dist/types.d.ts
CHANGED
package/dist/ui/routes.js
CHANGED
|
@@ -13,6 +13,7 @@ import { createProfileApplyService, } from '../services/profile/profile-apply-se
|
|
|
13
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]);
|