brainctl 0.1.23 → 0.1.26
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/services/platform/recent-projects-service.d.ts +16 -0
- package/dist/services/platform/recent-projects-service.js +51 -0
- package/dist/ui/routes.d.ts +2 -0
- package/dist/ui/routes.js +121 -23
- package/dist/web/assets/index-DA6dMqS1.js +63 -0
- package/dist/web/assets/index-DWHJW1mo.css +2 -0
- package/dist/web/assets/index-Dfl6zkjD.js +63 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-DwjP_DgF.js +0 -63
- package/dist/web/assets/index-WpJOVUJR.css +0 -2
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
interface FsLike {
|
|
2
|
+
readFile(p: string): Promise<string>;
|
|
3
|
+
writeFile(p: string, content: string): Promise<void>;
|
|
4
|
+
mkdir(p: string, opts?: {
|
|
5
|
+
recursive: boolean;
|
|
6
|
+
}): Promise<unknown>;
|
|
7
|
+
}
|
|
8
|
+
export interface RecentProjectsService {
|
|
9
|
+
read(): Promise<string[]>;
|
|
10
|
+
addRecent(cwd: string): Promise<string[]>;
|
|
11
|
+
}
|
|
12
|
+
export declare function createRecentProjectsService(deps: {
|
|
13
|
+
filePath: string;
|
|
14
|
+
fs?: FsLike;
|
|
15
|
+
}): RecentProjectsService;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const MAX_RECENTS = 20;
|
|
4
|
+
const FILE_VERSION = 1;
|
|
5
|
+
const defaultFs = {
|
|
6
|
+
readFile: (p) => readFile(p, 'utf8'),
|
|
7
|
+
writeFile: (p, content) => writeFile(p, content, 'utf8'),
|
|
8
|
+
mkdir: (p, opts) => mkdir(p, opts),
|
|
9
|
+
};
|
|
10
|
+
export function createRecentProjectsService(deps) {
|
|
11
|
+
const { filePath, fs: fsImpl = defaultFs } = deps;
|
|
12
|
+
async function readFile_() {
|
|
13
|
+
try {
|
|
14
|
+
const content = await fsImpl.readFile(filePath);
|
|
15
|
+
const parsed = JSON.parse(content);
|
|
16
|
+
if (typeof parsed === 'object' && parsed !== null && 'recents' in parsed) {
|
|
17
|
+
return parsed;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
if (err.code === 'ENOENT') {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function persist(recents) {
|
|
29
|
+
const dir = path.dirname(filePath);
|
|
30
|
+
await fsImpl.mkdir(dir, { recursive: true });
|
|
31
|
+
const data = { version: FILE_VERSION, recents };
|
|
32
|
+
await fsImpl.writeFile(filePath, JSON.stringify(data, null, 2));
|
|
33
|
+
}
|
|
34
|
+
async function read() {
|
|
35
|
+
const data = await readFile_();
|
|
36
|
+
if (!data)
|
|
37
|
+
return [];
|
|
38
|
+
return (data.recents ?? []).filter((e) => typeof e === 'string');
|
|
39
|
+
}
|
|
40
|
+
async function addRecent(cwd) {
|
|
41
|
+
if (!path.isAbsolute(cwd)) {
|
|
42
|
+
throw new Error(`cwd must be an absolute path, got: ${cwd}`);
|
|
43
|
+
}
|
|
44
|
+
const current = await read();
|
|
45
|
+
const deduplicated = current.filter((p) => p !== cwd);
|
|
46
|
+
const updated = [cwd, ...deduplicated].slice(0, MAX_RECENTS);
|
|
47
|
+
await persist(updated);
|
|
48
|
+
return updated;
|
|
49
|
+
}
|
|
50
|
+
return { read, addRecent };
|
|
51
|
+
}
|
package/dist/ui/routes.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { StatusService } from '../services/platform/status-service.js';
|
|
|
3
3
|
export interface UiRouteDependencies {
|
|
4
4
|
cwd: string;
|
|
5
5
|
statusService?: StatusService;
|
|
6
|
+
recentsFilePath?: string;
|
|
7
|
+
claudeJsonPath?: string;
|
|
6
8
|
}
|
|
7
9
|
export type UiRouteHandler = (request: IncomingMessage, response: ServerResponse) => Promise<void>;
|
|
8
10
|
export declare function createUiRouteHandler(dependencies: UiRouteDependencies): UiRouteHandler;
|
package/dist/ui/routes.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
3
3
|
import { BrainctlError, ProfileError, ProfileNotFoundError, ValidationError } from '../errors.js';
|
|
4
4
|
import { createAgentConfigService } from '../services/agent/agent-config-service.js';
|
|
5
5
|
import { createMcpPreflightService } from '../services/platform/mcp-preflight-service.js';
|
|
@@ -11,9 +11,31 @@ import { createSkillPreflightService } from '../services/plugin/skill-preflight-
|
|
|
11
11
|
import { createStatusService } from '../services/platform/status-service.js';
|
|
12
12
|
import { createProfileApplyService, } from '../services/profile/profile-apply-service.js';
|
|
13
13
|
import { createProfileSnapshotService, defaultBackupProfileName, } from '../services/profile/profile-snapshot-service.js';
|
|
14
|
+
import { createRecentProjectsService } from '../services/platform/recent-projects-service.js';
|
|
14
15
|
import path from 'node:path';
|
|
16
|
+
import os from 'node:os';
|
|
15
17
|
import { fileURLToPath } from 'node:url';
|
|
16
18
|
import { spawn } from 'node:child_process';
|
|
19
|
+
function revealInFileManager(targetPath) {
|
|
20
|
+
if (process.platform === 'darwin') {
|
|
21
|
+
spawn('open', ['-R', targetPath], { detached: true, stdio: 'ignore' }).unref();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (process.platform === 'win32') {
|
|
25
|
+
spawn('explorer', [`/select,${targetPath}`], { detached: true, stdio: 'ignore' }).unref();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
spawn('xdg-open', [path.dirname(targetPath)], { detached: true, stdio: 'ignore' }).unref();
|
|
29
|
+
}
|
|
30
|
+
function resolveCwd(req, fallback) {
|
|
31
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
32
|
+
const raw = url.searchParams.get('cwd');
|
|
33
|
+
if (!raw)
|
|
34
|
+
return fallback;
|
|
35
|
+
if (!path.isAbsolute(raw))
|
|
36
|
+
return fallback;
|
|
37
|
+
return raw;
|
|
38
|
+
}
|
|
17
39
|
const uiAssetRoot = resolveUiAssetRoot();
|
|
18
40
|
export function createUiRouteHandler(dependencies) {
|
|
19
41
|
const statusService = dependencies.statusService ?? createStatusService();
|
|
@@ -26,6 +48,19 @@ export function createUiRouteHandler(dependencies) {
|
|
|
26
48
|
const mcpPreflightService = createMcpPreflightService();
|
|
27
49
|
const pluginInstallService = createPluginInstallService();
|
|
28
50
|
const skillPreflightService = createSkillPreflightService();
|
|
51
|
+
const recentsFilePath = dependencies.recentsFilePath ?? path.join(os.homedir(), '.brainctl', 'recents.json');
|
|
52
|
+
const claudeJsonPath = dependencies.claudeJsonPath ?? path.join(os.homedir(), '.claude.json');
|
|
53
|
+
const recentProjectsService = createRecentProjectsService({ filePath: recentsFilePath });
|
|
54
|
+
async function readClaudeProjectPaths() {
|
|
55
|
+
try {
|
|
56
|
+
const raw = await readFile(claudeJsonPath, 'utf8');
|
|
57
|
+
const data = JSON.parse(raw);
|
|
58
|
+
return Object.keys(data.projects ?? {}).sort();
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
29
64
|
return async (request, response) => {
|
|
30
65
|
const url = new URL(request.url ?? '/', 'http://localhost');
|
|
31
66
|
switch (url.pathname) {
|
|
@@ -47,7 +82,7 @@ export function createUiRouteHandler(dependencies) {
|
|
|
47
82
|
if (request.method !== 'GET') {
|
|
48
83
|
return sendJson(response, 405, { error: 'Method not allowed' });
|
|
49
84
|
}
|
|
50
|
-
const configs = await agentConfigService.readAll({ cwd: dependencies.cwd });
|
|
85
|
+
const configs = await agentConfigService.readAll({ cwd: resolveCwd(request, dependencies.cwd) });
|
|
51
86
|
return sendJson(response, 200, configs);
|
|
52
87
|
}
|
|
53
88
|
case '/api/open-folder': {
|
|
@@ -65,13 +100,8 @@ export function createUiRouteHandler(dependencies) {
|
|
|
65
100
|
if (!existsSync(folderPath)) {
|
|
66
101
|
return sendJson(response, 404, { error: `Path not found: ${folderPath}` });
|
|
67
102
|
}
|
|
68
|
-
const opener = process.platform === 'darwin'
|
|
69
|
-
? 'open'
|
|
70
|
-
: process.platform === 'win32'
|
|
71
|
-
? 'explorer'
|
|
72
|
-
: 'xdg-open';
|
|
73
103
|
try {
|
|
74
|
-
|
|
104
|
+
revealInFileManager(folderPath);
|
|
75
105
|
return sendJson(response, 200, { ok: true, path: folderPath });
|
|
76
106
|
}
|
|
77
107
|
catch (error) {
|
|
@@ -167,6 +197,79 @@ export function createUiRouteHandler(dependencies) {
|
|
|
167
197
|
return sendProfileError(response, error);
|
|
168
198
|
}
|
|
169
199
|
}
|
|
200
|
+
case '/api/projects': {
|
|
201
|
+
if (request.method !== 'GET') {
|
|
202
|
+
return sendJson(response, 405, { error: 'Method not allowed' });
|
|
203
|
+
}
|
|
204
|
+
const [claudeProjects, recents] = await Promise.all([
|
|
205
|
+
readClaudeProjectPaths(),
|
|
206
|
+
recentProjectsService.read(),
|
|
207
|
+
]);
|
|
208
|
+
return sendJson(response, 200, {
|
|
209
|
+
current: dependencies.cwd,
|
|
210
|
+
claudeProjects,
|
|
211
|
+
recents,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
case '/api/projects/recent': {
|
|
215
|
+
if (request.method !== 'POST') {
|
|
216
|
+
return sendJson(response, 405, { error: 'Method not allowed' });
|
|
217
|
+
}
|
|
218
|
+
const body = await readJsonBody(request);
|
|
219
|
+
if (!body.ok) {
|
|
220
|
+
return sendJson(response, 400, { error: 'Invalid JSON body' });
|
|
221
|
+
}
|
|
222
|
+
const value = body.value;
|
|
223
|
+
const cwd = typeof value?.cwd === 'string' ? value.cwd : '';
|
|
224
|
+
if (!path.isAbsolute(cwd)) {
|
|
225
|
+
return sendJson(response, 400, { error: 'cwd must be an absolute path' });
|
|
226
|
+
}
|
|
227
|
+
const recents = await recentProjectsService.addRecent(cwd);
|
|
228
|
+
return sendJson(response, 200, { recents });
|
|
229
|
+
}
|
|
230
|
+
case '/api/fs/browse': {
|
|
231
|
+
if (request.method !== 'GET') {
|
|
232
|
+
return sendJson(response, 405, { error: 'Method not allowed' });
|
|
233
|
+
}
|
|
234
|
+
const raw = url.searchParams.get('path');
|
|
235
|
+
const mode = url.searchParams.get('mode') === 'file' ? 'file' : 'dir';
|
|
236
|
+
const startPath = raw && path.isAbsolute(raw) ? raw : os.homedir();
|
|
237
|
+
try {
|
|
238
|
+
const stats = await stat(startPath);
|
|
239
|
+
if (!stats.isDirectory()) {
|
|
240
|
+
return sendJson(response, 400, { error: 'Path is not a directory' });
|
|
241
|
+
}
|
|
242
|
+
const entries = await readdir(startPath, { withFileTypes: true });
|
|
243
|
+
const items = entries
|
|
244
|
+
.filter((entry) => !entry.name.startsWith('.'))
|
|
245
|
+
.filter((entry) => {
|
|
246
|
+
if (mode === 'file')
|
|
247
|
+
return entry.isDirectory() || entry.isFile();
|
|
248
|
+
return entry.isDirectory();
|
|
249
|
+
})
|
|
250
|
+
.map((entry) => ({
|
|
251
|
+
name: entry.name,
|
|
252
|
+
isDir: entry.isDirectory(),
|
|
253
|
+
}))
|
|
254
|
+
.sort((a, b) => {
|
|
255
|
+
if (a.isDir !== b.isDir)
|
|
256
|
+
return a.isDir ? -1 : 1;
|
|
257
|
+
return a.name.localeCompare(b.name);
|
|
258
|
+
});
|
|
259
|
+
const parent = path.dirname(startPath);
|
|
260
|
+
return sendJson(response, 200, {
|
|
261
|
+
path: startPath,
|
|
262
|
+
parent: parent === startPath ? null : parent,
|
|
263
|
+
home: os.homedir(),
|
|
264
|
+
entries: items,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
return sendJson(response, 404, {
|
|
269
|
+
error: error instanceof Error ? error.message : 'Cannot read path',
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
170
273
|
case '/api/profiles/snapshot': {
|
|
171
274
|
if (request.method !== 'POST') {
|
|
172
275
|
return sendJson(response, 405, { error: 'Method not allowed' });
|
|
@@ -204,13 +307,8 @@ export function createUiRouteHandler(dependencies) {
|
|
|
204
307
|
if (!existsSync(folderPath)) {
|
|
205
308
|
return sendJson(response, 404, { error: `Profile folder not found: ${folderPath}` });
|
|
206
309
|
}
|
|
207
|
-
const opener = process.platform === 'darwin'
|
|
208
|
-
? 'open'
|
|
209
|
-
: process.platform === 'win32'
|
|
210
|
-
? 'explorer'
|
|
211
|
-
: 'xdg-open';
|
|
212
310
|
try {
|
|
213
|
-
|
|
311
|
+
revealInFileManager(folderPath);
|
|
214
312
|
return sendJson(response, 200, { ok: true, path: folderPath });
|
|
215
313
|
}
|
|
216
314
|
catch (error) {
|
|
@@ -261,7 +359,7 @@ export function createUiRouteHandler(dependencies) {
|
|
|
261
359
|
return sendJson(response, 400, { error: 'Missing key and MCP payload' });
|
|
262
360
|
}
|
|
263
361
|
const result = await mcpPreflightService.execute({
|
|
264
|
-
cwd: dependencies.cwd,
|
|
362
|
+
cwd: resolveCwd(request, dependencies.cwd),
|
|
265
363
|
agent: agentName,
|
|
266
364
|
key: data.key,
|
|
267
365
|
entry: data.entry,
|
|
@@ -284,7 +382,7 @@ export function createUiRouteHandler(dependencies) {
|
|
|
284
382
|
}
|
|
285
383
|
try {
|
|
286
384
|
await agentConfigService.addMcp({
|
|
287
|
-
cwd: dependencies.cwd,
|
|
385
|
+
cwd: resolveCwd(request, dependencies.cwd),
|
|
288
386
|
agent: agentName,
|
|
289
387
|
key: data.key,
|
|
290
388
|
entry: data.entry,
|
|
@@ -301,7 +399,7 @@ export function createUiRouteHandler(dependencies) {
|
|
|
301
399
|
const scope = url.searchParams.get('scope') === 'project' ? 'project' : 'global';
|
|
302
400
|
try {
|
|
303
401
|
await agentConfigService.removeMcp({
|
|
304
|
-
cwd: dependencies.cwd,
|
|
402
|
+
cwd: resolveCwd(request, dependencies.cwd),
|
|
305
403
|
agent: agentName,
|
|
306
404
|
key: mcpKey,
|
|
307
405
|
scope,
|
|
@@ -400,7 +498,7 @@ export function createUiRouteHandler(dependencies) {
|
|
|
400
498
|
if (!data.name || !data.sourceAgent) {
|
|
401
499
|
return sendJson(response, 400, { error: 'Missing name or sourceAgent' });
|
|
402
500
|
}
|
|
403
|
-
const sourcePlugin = await resolveSourcePlugin(agentConfigService, dependencies.cwd, {
|
|
501
|
+
const sourcePlugin = await resolveSourcePlugin(agentConfigService, resolveCwd(request, dependencies.cwd), {
|
|
404
502
|
sourceAgent: data.sourceAgent,
|
|
405
503
|
name: data.name,
|
|
406
504
|
});
|
|
@@ -410,7 +508,7 @@ export function createUiRouteHandler(dependencies) {
|
|
|
410
508
|
});
|
|
411
509
|
}
|
|
412
510
|
const result = await pluginInstallService.plan({
|
|
413
|
-
cwd: dependencies.cwd,
|
|
511
|
+
cwd: resolveCwd(request, dependencies.cwd),
|
|
414
512
|
targetAgent: agentName,
|
|
415
513
|
sourceAgent: data.sourceAgent,
|
|
416
514
|
plugin: sourcePlugin,
|
|
@@ -430,7 +528,7 @@ export function createUiRouteHandler(dependencies) {
|
|
|
430
528
|
if (!data.name || !data.sourceAgent) {
|
|
431
529
|
return sendJson(response, 400, { error: 'Missing name or sourceAgent' });
|
|
432
530
|
}
|
|
433
|
-
const sourcePlugin = await resolveSourcePlugin(agentConfigService, dependencies.cwd, {
|
|
531
|
+
const sourcePlugin = await resolveSourcePlugin(agentConfigService, resolveCwd(request, dependencies.cwd), {
|
|
434
532
|
sourceAgent: data.sourceAgent,
|
|
435
533
|
name: data.name,
|
|
436
534
|
});
|
|
@@ -441,7 +539,7 @@ export function createUiRouteHandler(dependencies) {
|
|
|
441
539
|
}
|
|
442
540
|
try {
|
|
443
541
|
const result = await pluginInstallService.execute({
|
|
444
|
-
cwd: dependencies.cwd,
|
|
542
|
+
cwd: resolveCwd(request, dependencies.cwd),
|
|
445
543
|
targetAgent: agentName,
|
|
446
544
|
sourceAgent: data.sourceAgent,
|
|
447
545
|
plugin: sourcePlugin,
|
|
@@ -453,7 +551,7 @@ export function createUiRouteHandler(dependencies) {
|
|
|
453
551
|
}
|
|
454
552
|
}
|
|
455
553
|
if (request.method === 'DELETE' && pluginName) {
|
|
456
|
-
const targetPlugin = await resolveTargetPlugin(agentConfigService, dependencies.cwd, {
|
|
554
|
+
const targetPlugin = await resolveTargetPlugin(agentConfigService, resolveCwd(request, dependencies.cwd), {
|
|
457
555
|
targetAgent: agentName,
|
|
458
556
|
name: pluginName,
|
|
459
557
|
});
|
|
@@ -464,7 +562,7 @@ export function createUiRouteHandler(dependencies) {
|
|
|
464
562
|
}
|
|
465
563
|
try {
|
|
466
564
|
const result = await pluginInstallService.remove({
|
|
467
|
-
cwd: dependencies.cwd,
|
|
565
|
+
cwd: resolveCwd(request, dependencies.cwd),
|
|
468
566
|
targetAgent: agentName,
|
|
469
567
|
plugin: targetPlugin,
|
|
470
568
|
});
|