minovative-mind-cli 2.3.1 → 2.3.3
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/README.md +5 -3
- package/dist/services/agent/slashCommands.js +82 -17
- package/dist/services/agent/types.d.ts +1 -0
- package/dist/services/agent.d.ts +1 -0
- package/dist/services/agent.js +19 -1
- package/dist/services/chatHistoryService.d.ts +2 -0
- package/dist/services/proxyClient.d.ts +2 -0
- package/dist/services/proxyClient.js +5 -2
- package/dist/services/workspaceRegistry.d.ts +27 -5
- package/dist/services/workspaceRegistry.js +61 -12
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,11 +121,13 @@ Hot-swap during a session using `/models`:
|
|
|
121
121
|
|
|
122
122
|
---
|
|
123
123
|
|
|
124
|
-
## 🌐 Multi-Workspace & Cross-Repo Support
|
|
124
|
+
## 🌐 Multi-Workspace & Cross-Repo Support (Master & Sub-Workspaces)
|
|
125
125
|
|
|
126
|
-
Minovative Mind CLI doesn't restrict you to a single repository. You can
|
|
126
|
+
Minovative Mind CLI doesn't restrict you to a single repository. You can logically group multiple external repositories into **Master Workspaces** (Profiles) containing dedicated **Sub-Workspaces** (mapped to short aliases like `@backend` or `@frontend`).
|
|
127
127
|
|
|
128
|
-
|
|
128
|
+
The Context Agent and Thread Agents operate strictly within the boundaries of the active Master Workspace, ignoring other profiles to keep the context window highly targeted and memory-efficient.
|
|
129
|
+
|
|
130
|
+
By prefixing file paths with `@alias/` (e.g., `@backend/src/api.ts` and `@frontend/src/App.tsx`), the agents can investigate, refactor, and coordinate changes across your entire nested stack in a single prompt. The terminal highlights cross-workspace actions with a blue `[alias]` visual tag. Use the `/workspaces` command to create profiles, link sub-workspaces, or switch active environments.
|
|
129
131
|
|
|
130
132
|
---
|
|
131
133
|
|
|
@@ -11,7 +11,7 @@ import { chatHistoryService } from '../chatHistoryService.js';
|
|
|
11
11
|
import { printLogo, brandBg, brandFg } from '../../utils/logo.js';
|
|
12
12
|
import { readPaste } from '../../utils/paste.js';
|
|
13
13
|
import { setApprovalMode, getApprovalMode, isSubAgentsEnabled, setSubAgentsEnabled } from '../agent-tools.js';
|
|
14
|
-
import { ProxyChatSession, setGlobalActiveModel, getGlobalActiveModel
|
|
14
|
+
import { ProxyChatSession, setGlobalActiveModel, getGlobalActiveModel } from '../ai.js';
|
|
15
15
|
import { GEMINI_MODELS } from '../../utils/config.js';
|
|
16
16
|
const execAsync = promisify(exec);
|
|
17
17
|
/**
|
|
@@ -134,7 +134,24 @@ export async function handleSlashCommand(command, context) {
|
|
|
134
134
|
const prevUsage = context.chatSessionState?.previousUsageMetadata;
|
|
135
135
|
const currentModel = chat.getModel();
|
|
136
136
|
const globalModel = getGlobalActiveModel();
|
|
137
|
-
const
|
|
137
|
+
const modelUsageCounts = context.chatSessionState?.modelUsageCounts || {};
|
|
138
|
+
const modelsUsed = Object.keys(modelUsageCounts);
|
|
139
|
+
let modelStatsStr = '';
|
|
140
|
+
if (modelsUsed.length > 0) {
|
|
141
|
+
const totalTurns = Object.values(modelUsageCounts).reduce((a, b) => a + b, 0);
|
|
142
|
+
const parts = modelsUsed.map((m) => {
|
|
143
|
+
const percent = Math.round((modelUsageCounts[m] / totalTurns) * 100);
|
|
144
|
+
return `${m} ${percent}%`;
|
|
145
|
+
});
|
|
146
|
+
modelStatsStr = parts.join(', ');
|
|
147
|
+
}
|
|
148
|
+
let displayModel = '';
|
|
149
|
+
if (globalModel === 'auto') {
|
|
150
|
+
displayModel = modelStatsStr ? `Auto (${modelStatsStr})` : `Auto (Last turn: ${currentModel})`;
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
displayModel = modelStatsStr || currentModel;
|
|
154
|
+
}
|
|
138
155
|
const autoApprove = getApprovalMode() === 'skip-all' ? 'Enabled' : 'Disabled';
|
|
139
156
|
const subAgents = isSubAgentsEnabled() ? 'Enabled' : 'Disabled';
|
|
140
157
|
const planMode = context.isPlanMode ? 'Enabled' : 'Disabled';
|
|
@@ -405,6 +422,7 @@ export async function handleSlashCommand(command, context) {
|
|
|
405
422
|
chatSessionState.totalOutputTokens = session.totalOutputTokens || 0;
|
|
406
423
|
chatSessionState.latestUsageMetadata = session.latestUsageMetadata;
|
|
407
424
|
chatSessionState.previousUsageMetadata = session.previousUsageMetadata;
|
|
425
|
+
chatSessionState.modelUsageCounts = session.modelUsageCounts || {};
|
|
408
426
|
chat.setSessionInfo(session.id, workspaceRoot);
|
|
409
427
|
}
|
|
410
428
|
process.stdout.write('\x1B[2J\x1B[3J\x1B[H'); // Hard clear screen and scrollback
|
|
@@ -495,7 +513,9 @@ export async function handleSlashCommand(command, context) {
|
|
|
495
513
|
p.log.info(`${pc.dim('Model:')} ${pc.cyan(session.modelName)}`);
|
|
496
514
|
}
|
|
497
515
|
if (session.totalTokens !== undefined) {
|
|
498
|
-
const inputStr = session.totalInputTokens
|
|
516
|
+
const inputStr = session.totalInputTokens
|
|
517
|
+
? ` (Input: ${session.totalInputTokens.toLocaleString()}, Output: ${(session.totalOutputTokens || 0).toLocaleString()})`
|
|
518
|
+
: '';
|
|
499
519
|
p.log.info(`${pc.dim('Session Token Usage:')} ${pc.cyan(session.totalTokens.toLocaleString() + ' total tokens')}${pc.dim(inputStr)}`);
|
|
500
520
|
}
|
|
501
521
|
if (session.gitBranch) {
|
|
@@ -537,9 +557,9 @@ export async function handleSlashCommand(command, context) {
|
|
|
537
557
|
while (true) {
|
|
538
558
|
const allRoots = workspaceRegistry.getAllRoots(workspaceRoot);
|
|
539
559
|
const options = [];
|
|
560
|
+
const hasProfiles = workspaceRegistry.hasProfiles();
|
|
540
561
|
options.push({ value: 'add', label: 'Add Workspace' });
|
|
541
|
-
|
|
542
|
-
if (externalRoots.length > 0) {
|
|
562
|
+
if (hasProfiles) {
|
|
543
563
|
options.push({ value: 'edit', label: 'Edit Workspace' });
|
|
544
564
|
options.push({ value: 'remove', label: 'Remove Workspace' });
|
|
545
565
|
options.push({ value: 'list', label: 'List Workspaces' });
|
|
@@ -553,6 +573,40 @@ export async function handleSlashCommand(command, context) {
|
|
|
553
573
|
break;
|
|
554
574
|
}
|
|
555
575
|
if (action === 'add') {
|
|
576
|
+
const type = await p['select']({
|
|
577
|
+
message: 'What type of workspace would you like to add?',
|
|
578
|
+
options: [
|
|
579
|
+
{ value: 'master', label: 'Master Workspace (Create)' },
|
|
580
|
+
{ value: 'sub', label: 'Sub-workspace', hint: 'Requires an existing Master Workspace' },
|
|
581
|
+
],
|
|
582
|
+
});
|
|
583
|
+
if (p.isCancel(type))
|
|
584
|
+
continue;
|
|
585
|
+
let profileStr;
|
|
586
|
+
if (type === 'master') {
|
|
587
|
+
profileStr = (await p['text']({
|
|
588
|
+
message: 'Enter profile name (e.g. work, personal):',
|
|
589
|
+
validate: (val) => {
|
|
590
|
+
if (!val)
|
|
591
|
+
return 'Profile name is required';
|
|
592
|
+
},
|
|
593
|
+
}));
|
|
594
|
+
if (p.isCancel(profileStr))
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
else {
|
|
598
|
+
const profiles = workspaceRegistry.listProfiles();
|
|
599
|
+
if (profiles.length === 0) {
|
|
600
|
+
p.log.error('No master workspaces (profiles) exist. Please create one first.');
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
profileStr = (await p['select']({
|
|
604
|
+
message: 'Select a Master Workspace (For Sub-Workspaces):',
|
|
605
|
+
options: profiles.map((p) => ({ value: p.name, label: p.name })),
|
|
606
|
+
}));
|
|
607
|
+
if (p.isCancel(profileStr))
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
556
610
|
const aliasStr = await p['text']({
|
|
557
611
|
message: 'Enter a short alias (e.g. backend, ui):',
|
|
558
612
|
validate: (val) => {
|
|
@@ -567,7 +621,7 @@ export async function handleSlashCommand(command, context) {
|
|
|
567
621
|
if (p.isCancel(aliasStr))
|
|
568
622
|
continue;
|
|
569
623
|
const rootPathStr = await p['text']({
|
|
570
|
-
message: 'Enter the absolute path to the workspace root
|
|
624
|
+
message: 'Enter the absolute path to the workspace root:',
|
|
571
625
|
validate: (val) => {
|
|
572
626
|
if (!val)
|
|
573
627
|
return 'Path is required';
|
|
@@ -586,17 +640,17 @@ export async function handleSlashCommand(command, context) {
|
|
|
586
640
|
p.log.error('Path is not a directory.');
|
|
587
641
|
continue;
|
|
588
642
|
}
|
|
589
|
-
workspaceRegistry.register(aliasStr, cleanRootPathStr);
|
|
590
|
-
p.log.success(`Added @${aliasStr} -> ${cleanRootPathStr}`);
|
|
643
|
+
workspaceRegistry.register(profileStr, aliasStr, cleanRootPathStr);
|
|
644
|
+
p.log.success(`Added @${aliasStr} -> ${cleanRootPathStr} (Profile: ${profileStr})`);
|
|
591
645
|
}
|
|
592
646
|
catch (e) {
|
|
593
|
-
p.log.error(`Invalid path or directory does not exist: ${cleanRootPathStr}`);
|
|
647
|
+
p.log.error(e.message || `Invalid path or directory does not exist: ${cleanRootPathStr}`);
|
|
594
648
|
}
|
|
595
649
|
}
|
|
596
650
|
else if (action === 'edit') {
|
|
597
|
-
const editOptions =
|
|
651
|
+
const editOptions = workspaceRegistry.list().map((r) => ({
|
|
598
652
|
value: r.alias,
|
|
599
|
-
label: `@${r.alias} -> ${r.
|
|
653
|
+
label: `@${r.alias} -> ${r.absolutePath}`,
|
|
600
654
|
}));
|
|
601
655
|
editOptions.push({ value: 'cancel', label: 'Cancel' });
|
|
602
656
|
const aliasToEdit = await p['select']({
|
|
@@ -608,6 +662,16 @@ export async function handleSlashCommand(command, context) {
|
|
|
608
662
|
const ws = workspaceRegistry.get(aliasToEdit);
|
|
609
663
|
if (!ws)
|
|
610
664
|
continue;
|
|
665
|
+
const newProfileStr = await p['text']({
|
|
666
|
+
message: `Enter new profile name (current: ${ws.profile}):`,
|
|
667
|
+
initialValue: ws.profile,
|
|
668
|
+
validate: (val) => {
|
|
669
|
+
if (!val)
|
|
670
|
+
return 'Profile name is required';
|
|
671
|
+
},
|
|
672
|
+
});
|
|
673
|
+
if (p.isCancel(newProfileStr))
|
|
674
|
+
continue;
|
|
611
675
|
const newAliasStr = await p['text']({
|
|
612
676
|
message: `Enter new alias (current: ${ws.alias}):`,
|
|
613
677
|
initialValue: ws.alias,
|
|
@@ -645,22 +709,22 @@ export async function handleSlashCommand(command, context) {
|
|
|
645
709
|
}
|
|
646
710
|
// Remove old alias first to avoid duplicate alias error, or to clean up
|
|
647
711
|
workspaceRegistry.unregister(ws.alias);
|
|
648
|
-
workspaceRegistry.register(newAliasStr, cleanNewRootPathStr);
|
|
649
|
-
p.log.success(`Updated @${newAliasStr} -> ${cleanNewRootPathStr}`);
|
|
712
|
+
workspaceRegistry.register(newProfileStr, newAliasStr, cleanNewRootPathStr);
|
|
713
|
+
p.log.success(`Updated @${newAliasStr} -> ${cleanNewRootPathStr} (Profile: ${newProfileStr})`);
|
|
650
714
|
}
|
|
651
715
|
catch (e) {
|
|
652
716
|
// If register failed, try to rollback
|
|
653
717
|
p.log.error(`Failed to update workspace: ${e instanceof Error ? e.message : String(e)}`);
|
|
654
718
|
try {
|
|
655
|
-
workspaceRegistry.register(ws.alias, ws.absolutePath);
|
|
719
|
+
workspaceRegistry.register(ws.profile, ws.alias, ws.absolutePath);
|
|
656
720
|
}
|
|
657
721
|
catch { }
|
|
658
722
|
}
|
|
659
723
|
}
|
|
660
724
|
else if (action === 'remove') {
|
|
661
|
-
const removeOptions =
|
|
725
|
+
const removeOptions = workspaceRegistry.list().map((r) => ({
|
|
662
726
|
value: r.alias,
|
|
663
|
-
label: `@${r.alias} -> ${r.
|
|
727
|
+
label: `@${r.alias} -> ${r.absolutePath}`,
|
|
664
728
|
}));
|
|
665
729
|
removeOptions.push({ value: 'cancel', label: 'Cancel' });
|
|
666
730
|
const aliasToRemove = await p['select']({
|
|
@@ -674,8 +738,9 @@ export async function handleSlashCommand(command, context) {
|
|
|
674
738
|
}
|
|
675
739
|
else if (action === 'list') {
|
|
676
740
|
for (const { alias, root } of allRoots) {
|
|
741
|
+
const ws = alias ? workspaceRegistry.get(alias) : null;
|
|
677
742
|
if (alias) {
|
|
678
|
-
p.log.step(`${pc.blue(`@${alias}`)} -> ${pc.dim(root)}`);
|
|
743
|
+
p.log.step(`${pc.blue(`@${alias}`)} [${ws?.profile || 'default'}] -> ${pc.dim(root)}`);
|
|
679
744
|
}
|
|
680
745
|
else {
|
|
681
746
|
p.log.step(`${pc.cyan('(primary)')} -> ${pc.dim(root)}`);
|
package/dist/services/agent.d.ts
CHANGED
|
@@ -57,6 +57,7 @@ export declare function executeSingleTurn(workspaceRoot: string, userInput: stri
|
|
|
57
57
|
totalOutputTokens: number;
|
|
58
58
|
latestUsageMetadata?: any;
|
|
59
59
|
previousUsageMetadata?: any;
|
|
60
|
+
modelUsageCounts?: Record<string, number>;
|
|
60
61
|
}, isPlanMode: boolean, cachedContextResult?: any): Promise<{
|
|
61
62
|
planModeReturn?: string;
|
|
62
63
|
contextResult?: any;
|
package/dist/services/agent.js
CHANGED
|
@@ -86,7 +86,7 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
86
86
|
chatHistoryService.init(workspaceRoot);
|
|
87
87
|
const chat = createSharedChatSession();
|
|
88
88
|
const inputHandler = new AsyncInputHandler();
|
|
89
|
-
const chatSessionState = { id: crypto.randomUUID(), title: '', totalTokens: 0, totalInputTokens: 0, totalOutputTokens: 0 };
|
|
89
|
+
const chatSessionState = { id: crypto.randomUUID(), title: '', totalTokens: 0, totalInputTokens: 0, totalOutputTokens: 0, modelUsageCounts: {} };
|
|
90
90
|
chat.setSessionInfo(chatSessionState.id, workspaceRoot);
|
|
91
91
|
const sessionInputHistory = [];
|
|
92
92
|
let isRawPasteMode = false;
|
|
@@ -115,6 +115,9 @@ export async function startAgentLoop(workspaceRoot, version) {
|
|
|
115
115
|
chatSessionState.totalTokens = 0;
|
|
116
116
|
chatSessionState.totalInputTokens = 0;
|
|
117
117
|
chatSessionState.totalOutputTokens = 0;
|
|
118
|
+
chatSessionState.modelUsageCounts = {};
|
|
119
|
+
chatSessionState.latestUsageMetadata = undefined;
|
|
120
|
+
chatSessionState.previousUsageMetadata = undefined;
|
|
118
121
|
chat.setSessionInfo(chatSessionState.id, workspaceRoot);
|
|
119
122
|
}
|
|
120
123
|
inputHandler.stop();
|
|
@@ -377,6 +380,12 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
|
|
|
377
380
|
}
|
|
378
381
|
// Print Usage Stats
|
|
379
382
|
const usage = getAndResetTurnUsage();
|
|
383
|
+
chatSessionState.modelUsageCounts = chatSessionState.modelUsageCounts || {};
|
|
384
|
+
if (usage.modelsUsed) {
|
|
385
|
+
for (const [m, count] of Object.entries(usage.modelsUsed)) {
|
|
386
|
+
chatSessionState.modelUsageCounts[m] = (chatSessionState.modelUsageCounts[m] || 0) + count;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
380
389
|
chatSessionState.previousUsageMetadata = chatSessionState.latestUsageMetadata;
|
|
381
390
|
chatSessionState.latestUsageMetadata = usage;
|
|
382
391
|
chatSessionState.totalTokens += usage.totalTokenCount;
|
|
@@ -433,6 +442,7 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
|
|
|
433
442
|
subAgents: isSubAgentsEnabled(),
|
|
434
443
|
latestUsageMetadata: chatSessionState.latestUsageMetadata,
|
|
435
444
|
previousUsageMetadata: chatSessionState.previousUsageMetadata,
|
|
445
|
+
modelUsageCounts: chatSessionState.modelUsageCounts,
|
|
436
446
|
})
|
|
437
447
|
.catch((e) => debugLog('Failed to auto-save session: ' + e));
|
|
438
448
|
}
|
|
@@ -511,6 +521,12 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
|
|
|
511
521
|
changeLogger.markComplete();
|
|
512
522
|
}
|
|
513
523
|
if (usage) {
|
|
524
|
+
chatSessionState.modelUsageCounts = chatSessionState.modelUsageCounts || {};
|
|
525
|
+
if (usage.modelsUsed) {
|
|
526
|
+
for (const [m, count] of Object.entries(usage.modelsUsed)) {
|
|
527
|
+
chatSessionState.modelUsageCounts[m] = (chatSessionState.modelUsageCounts[m] || 0) + count;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
514
530
|
chatSessionState.previousUsageMetadata = chatSessionState.latestUsageMetadata;
|
|
515
531
|
chatSessionState.latestUsageMetadata = usage;
|
|
516
532
|
chatSessionState.totalTokens += usage.totalTokenCount || 0;
|
|
@@ -541,6 +557,7 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
|
|
|
541
557
|
subAgents: isSubAgentsEnabled(),
|
|
542
558
|
latestUsageMetadata: chatSessionState.latestUsageMetadata,
|
|
543
559
|
previousUsageMetadata: chatSessionState.previousUsageMetadata,
|
|
560
|
+
modelUsageCounts: chatSessionState.modelUsageCounts,
|
|
544
561
|
});
|
|
545
562
|
}
|
|
546
563
|
catch (e) {
|
|
@@ -564,6 +581,7 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
|
|
|
564
581
|
subAgents: isSubAgentsEnabled(),
|
|
565
582
|
latestUsageMetadata: chatSessionState.latestUsageMetadata,
|
|
566
583
|
previousUsageMetadata: chatSessionState.previousUsageMetadata,
|
|
584
|
+
modelUsageCounts: chatSessionState.modelUsageCounts,
|
|
567
585
|
})
|
|
568
586
|
.catch((e) => debugLog('Failed to auto-save session: ' + e));
|
|
569
587
|
}
|
|
@@ -35,6 +35,8 @@ export interface ChatSessionData {
|
|
|
35
35
|
latestUsageMetadata?: any;
|
|
36
36
|
/** Optional metadata about the token usage from the turn prior to the last turn. */
|
|
37
37
|
previousUsageMetadata?: any;
|
|
38
|
+
/** Optional map tracking the number of turns each model was used during the session. */
|
|
39
|
+
modelUsageCounts?: Record<string, number>;
|
|
38
40
|
}
|
|
39
41
|
/**
|
|
40
42
|
* Service responsible for managing the persistence, retrieval, and deletion of chat session histories.
|
|
@@ -21,6 +21,7 @@ export declare function getAndResetTurnUsage(): {
|
|
|
21
21
|
totalTokenCount: number;
|
|
22
22
|
creditsUsed: number;
|
|
23
23
|
remainingBalance: number | undefined;
|
|
24
|
+
modelsUsed: Record<string, number>;
|
|
24
25
|
};
|
|
25
26
|
export declare function peekTurnUsage(): {
|
|
26
27
|
promptTokens: number;
|
|
@@ -29,6 +30,7 @@ export declare function peekTurnUsage(): {
|
|
|
29
30
|
totalTokenCount: number;
|
|
30
31
|
creditsUsed: number;
|
|
31
32
|
remainingBalance: number | undefined;
|
|
33
|
+
modelsUsed: Record<string, number>;
|
|
32
34
|
};
|
|
33
35
|
/**
|
|
34
36
|
* Client service interacting directly with the serverless Gemini proxy endpoint.
|
|
@@ -39,7 +39,8 @@ let globalSessionAccumulatedUsage = {
|
|
|
39
39
|
cachedTokens: 0,
|
|
40
40
|
totalTokenCount: 0,
|
|
41
41
|
creditsUsed: 0,
|
|
42
|
-
remainingBalance: undefined
|
|
42
|
+
remainingBalance: undefined,
|
|
43
|
+
modelsUsed: {}
|
|
43
44
|
};
|
|
44
45
|
export function getAndResetTurnUsage() {
|
|
45
46
|
const current = { ...globalSessionAccumulatedUsage };
|
|
@@ -49,7 +50,8 @@ export function getAndResetTurnUsage() {
|
|
|
49
50
|
cachedTokens: 0,
|
|
50
51
|
totalTokenCount: 0,
|
|
51
52
|
creditsUsed: 0,
|
|
52
|
-
remainingBalance: undefined
|
|
53
|
+
remainingBalance: undefined,
|
|
54
|
+
modelsUsed: {}
|
|
53
55
|
};
|
|
54
56
|
return current;
|
|
55
57
|
}
|
|
@@ -190,6 +192,7 @@ export class ProxyClient {
|
|
|
190
192
|
if (data.usage.remainingBalance !== undefined) {
|
|
191
193
|
globalSessionAccumulatedUsage.remainingBalance = data.usage.remainingBalance;
|
|
192
194
|
}
|
|
195
|
+
globalSessionAccumulatedUsage.modelsUsed[modelName] = (globalSessionAccumulatedUsage.modelsUsed[modelName] || 0) + 1;
|
|
193
196
|
}
|
|
194
197
|
if (data.groundingMetadata) {
|
|
195
198
|
groundingMetadata = data.groundingMetadata;
|
|
@@ -8,6 +8,17 @@ export interface RegisteredWorkspace {
|
|
|
8
8
|
absolutePath: string;
|
|
9
9
|
/** Unix epoch timestamp (ms) when this workspace was registered. */
|
|
10
10
|
registeredAt: number;
|
|
11
|
+
/** The profile (main workspace) this workspace belongs to. */
|
|
12
|
+
profile: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Represents a Profile (Main Workspace) that groups multiple registered workspaces.
|
|
16
|
+
*/
|
|
17
|
+
export interface Profile {
|
|
18
|
+
/** Unique name of the profile. */
|
|
19
|
+
name: string;
|
|
20
|
+
/** Unix epoch timestamp (ms) when this profile was created. */
|
|
21
|
+
createdAt: number;
|
|
11
22
|
}
|
|
12
23
|
/**
|
|
13
24
|
* Result of resolving an `@alias/relative/path` string against the workspace registry.
|
|
@@ -39,6 +50,8 @@ export interface ResolvedWorkspacePath {
|
|
|
39
50
|
declare class WorkspaceRegistry {
|
|
40
51
|
/** In-memory map of alias → registered workspace. */
|
|
41
52
|
private workspaces;
|
|
53
|
+
/** In-memory map of profile name → Profile. */
|
|
54
|
+
private profiles;
|
|
42
55
|
/** Whether the registry has been loaded from disk. */
|
|
43
56
|
private initialized;
|
|
44
57
|
/**
|
|
@@ -47,8 +60,9 @@ declare class WorkspaceRegistry {
|
|
|
47
60
|
*/
|
|
48
61
|
init(): void;
|
|
49
62
|
/**
|
|
50
|
-
* Registers a new external workspace root with the given alias.
|
|
63
|
+
* Registers a new external workspace root with the given alias under a profile.
|
|
51
64
|
*
|
|
65
|
+
* @param profile - The profile (main workspace) name.
|
|
52
66
|
* @param alias - Short identifier for the workspace (e.g., "backend").
|
|
53
67
|
* Must be lowercase alphanumeric with hyphens/underscores, 1–30 chars.
|
|
54
68
|
* @param absolutePath - Absolute path to the workspace root directory.
|
|
@@ -56,7 +70,7 @@ declare class WorkspaceRegistry {
|
|
|
56
70
|
* @returns The created `RegisteredWorkspace`, or throws on validation failure.
|
|
57
71
|
* @throws Error if the alias is invalid, the path doesn't exist, or the alias is already taken.
|
|
58
72
|
*/
|
|
59
|
-
register(alias: string, absolutePath: string): RegisteredWorkspace;
|
|
73
|
+
register(profile: string, alias: string, absolutePath: string): RegisteredWorkspace;
|
|
60
74
|
/**
|
|
61
75
|
* Removes a registered workspace by alias.
|
|
62
76
|
*
|
|
@@ -69,13 +83,21 @@ declare class WorkspaceRegistry {
|
|
|
69
83
|
*/
|
|
70
84
|
list(): RegisteredWorkspace[];
|
|
71
85
|
/**
|
|
72
|
-
* Returns
|
|
86
|
+
* Returns all profiles (main workspaces).
|
|
73
87
|
*/
|
|
74
|
-
|
|
88
|
+
listProfiles(): Profile[];
|
|
75
89
|
/**
|
|
76
|
-
* Checks whether any
|
|
90
|
+
* Checks whether any profiles (main workspaces) exist.
|
|
91
|
+
*/
|
|
92
|
+
hasProfiles(): boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Checks whether any workspaces exist.
|
|
77
95
|
*/
|
|
78
96
|
hasWorkspaces(): boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Returns all registered workspaces for a given profile.
|
|
99
|
+
*/
|
|
100
|
+
getWorkspacesByProfile(profile: string): RegisteredWorkspace[];
|
|
79
101
|
/**
|
|
80
102
|
* Looks up a workspace by alias.
|
|
81
103
|
*
|
|
@@ -24,6 +24,8 @@ const REGISTRY_FILE = path.join(GLOBAL_CONFIG_DIR, 'workspaces.json');
|
|
|
24
24
|
class WorkspaceRegistry {
|
|
25
25
|
/** In-memory map of alias → registered workspace. */
|
|
26
26
|
workspaces = new Map();
|
|
27
|
+
/** In-memory map of profile name → Profile. */
|
|
28
|
+
profiles = new Map();
|
|
27
29
|
/** Whether the registry has been loaded from disk. */
|
|
28
30
|
initialized = false;
|
|
29
31
|
/**
|
|
@@ -37,8 +39,9 @@ class WorkspaceRegistry {
|
|
|
37
39
|
this.loadFromDisk();
|
|
38
40
|
}
|
|
39
41
|
/**
|
|
40
|
-
* Registers a new external workspace root with the given alias.
|
|
42
|
+
* Registers a new external workspace root with the given alias under a profile.
|
|
41
43
|
*
|
|
44
|
+
* @param profile - The profile (main workspace) name.
|
|
42
45
|
* @param alias - Short identifier for the workspace (e.g., "backend").
|
|
43
46
|
* Must be lowercase alphanumeric with hyphens/underscores, 1–30 chars.
|
|
44
47
|
* @param absolutePath - Absolute path to the workspace root directory.
|
|
@@ -46,8 +49,16 @@ class WorkspaceRegistry {
|
|
|
46
49
|
* @returns The created `RegisteredWorkspace`, or throws on validation failure.
|
|
47
50
|
* @throws Error if the alias is invalid, the path doesn't exist, or the alias is already taken.
|
|
48
51
|
*/
|
|
49
|
-
register(alias, absolutePath) {
|
|
52
|
+
register(profile, alias, absolutePath) {
|
|
53
|
+
const normalizedProfile = profile.toLowerCase().trim();
|
|
50
54
|
const normalizedAlias = alias.toLowerCase().trim();
|
|
55
|
+
// Ensure profile exists
|
|
56
|
+
if (!this.profiles.has(normalizedProfile)) {
|
|
57
|
+
this.profiles.set(normalizedProfile, {
|
|
58
|
+
name: normalizedProfile,
|
|
59
|
+
createdAt: Date.now(),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
51
62
|
// Validate alias format
|
|
52
63
|
if (!ALIAS_PATTERN.test(normalizedAlias)) {
|
|
53
64
|
throw new Error(`Invalid workspace alias "${normalizedAlias}". Must be 1-30 characters, ` +
|
|
@@ -85,10 +96,11 @@ class WorkspaceRegistry {
|
|
|
85
96
|
alias: normalizedAlias,
|
|
86
97
|
absolutePath: normalizedPath,
|
|
87
98
|
registeredAt: Date.now(),
|
|
99
|
+
profile: normalizedProfile,
|
|
88
100
|
};
|
|
89
101
|
this.workspaces.set(normalizedAlias, workspace);
|
|
90
102
|
this.saveToDisk();
|
|
91
|
-
debugLog(`Registered workspace: @${normalizedAlias} → ${normalizedPath}`);
|
|
103
|
+
debugLog(`Registered workspace: @${normalizedAlias} → ${normalizedPath} (Profile: ${normalizedProfile})`);
|
|
92
104
|
return workspace;
|
|
93
105
|
}
|
|
94
106
|
/**
|
|
@@ -113,17 +125,29 @@ class WorkspaceRegistry {
|
|
|
113
125
|
return Array.from(this.workspaces.values()).sort((a, b) => a.alias.localeCompare(b.alias));
|
|
114
126
|
}
|
|
115
127
|
/**
|
|
116
|
-
* Returns
|
|
128
|
+
* Returns all profiles (main workspaces).
|
|
117
129
|
*/
|
|
118
|
-
|
|
119
|
-
return this.
|
|
130
|
+
listProfiles() {
|
|
131
|
+
return Array.from(this.profiles.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
120
132
|
}
|
|
121
133
|
/**
|
|
122
|
-
* Checks whether any
|
|
134
|
+
* Checks whether any profiles (main workspaces) exist.
|
|
135
|
+
*/
|
|
136
|
+
hasProfiles() {
|
|
137
|
+
return this.profiles.size > 0;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Checks whether any workspaces exist.
|
|
123
141
|
*/
|
|
124
142
|
hasWorkspaces() {
|
|
125
143
|
return this.workspaces.size > 0;
|
|
126
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Returns all registered workspaces for a given profile.
|
|
147
|
+
*/
|
|
148
|
+
getWorkspacesByProfile(profile) {
|
|
149
|
+
return this.list().filter((ws) => ws.profile === profile.toLowerCase().trim());
|
|
150
|
+
}
|
|
127
151
|
/**
|
|
128
152
|
* Looks up a workspace by alias.
|
|
129
153
|
*
|
|
@@ -216,12 +240,31 @@ class WorkspaceRegistry {
|
|
|
216
240
|
}
|
|
217
241
|
const raw = fs.readFileSync(REGISTRY_FILE, 'utf-8');
|
|
218
242
|
const data = JSON.parse(raw);
|
|
219
|
-
if (!Array.isArray(data)) {
|
|
243
|
+
if (!data || !Array.isArray(data.workspaces)) {
|
|
244
|
+
// Fallback for old schema
|
|
245
|
+
const oldData = JSON.parse(raw);
|
|
246
|
+
if (Array.isArray(oldData)) {
|
|
247
|
+
for (const entry of oldData) {
|
|
248
|
+
if (typeof entry.alias === 'string' &&
|
|
249
|
+
typeof entry.absolutePath === 'string' &&
|
|
250
|
+
ALIAS_PATTERN.test(entry.alias)) {
|
|
251
|
+
if (fs.existsSync(entry.absolutePath)) {
|
|
252
|
+
const profile = 'default';
|
|
253
|
+
this.workspaces.set(entry.alias, { ...entry, profile });
|
|
254
|
+
if (!this.profiles.has(profile)) {
|
|
255
|
+
this.profiles.set(profile, { name: profile, createdAt: Date.now() });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
debugLog(`Migrated ${this.workspaces.size} workspace(s) from old schema.`);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
220
263
|
debugLog('Workspace registry file has invalid format, starting fresh.');
|
|
221
264
|
return;
|
|
222
265
|
}
|
|
223
266
|
// Validate each entry and skip invalid ones
|
|
224
|
-
for (const entry of data) {
|
|
267
|
+
for (const entry of data.workspaces) {
|
|
225
268
|
if (typeof entry.alias === 'string' &&
|
|
226
269
|
typeof entry.absolutePath === 'string' &&
|
|
227
270
|
ALIAS_PATTERN.test(entry.alias)) {
|
|
@@ -234,7 +277,10 @@ class WorkspaceRegistry {
|
|
|
234
277
|
}
|
|
235
278
|
}
|
|
236
279
|
}
|
|
237
|
-
|
|
280
|
+
for (const profile of data.profiles) {
|
|
281
|
+
this.profiles.set(profile.name, profile);
|
|
282
|
+
}
|
|
283
|
+
debugLog(`Loaded ${this.workspaces.size} workspace(s) and ${this.profiles.size} profile(s) from global registry.`);
|
|
238
284
|
}
|
|
239
285
|
catch (err) {
|
|
240
286
|
debugLog(`Failed to load workspace registry: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -250,12 +296,15 @@ class WorkspaceRegistry {
|
|
|
250
296
|
if (!fs.existsSync(GLOBAL_CONFIG_DIR)) {
|
|
251
297
|
fs.mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true });
|
|
252
298
|
}
|
|
253
|
-
const data =
|
|
299
|
+
const data = {
|
|
300
|
+
workspaces: Array.from(this.workspaces.values()),
|
|
301
|
+
profiles: Array.from(this.profiles.values()),
|
|
302
|
+
};
|
|
254
303
|
const tempPath = `${REGISTRY_FILE}.${Date.now()}.tmp`;
|
|
255
304
|
fs.writeFileSync(tempPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
256
305
|
// Atomic rename to prevent corruption
|
|
257
306
|
fs.renameSync(tempPath, REGISTRY_FILE);
|
|
258
|
-
debugLog(`Saved ${data.length} workspace(s) to global registry.`);
|
|
307
|
+
debugLog(`Saved ${data.workspaces.length} workspace(s) and ${data.profiles.length} profile(s) to global registry.`);
|
|
259
308
|
}
|
|
260
309
|
catch (err) {
|
|
261
310
|
debugLog(`Failed to save workspace registry: ${err instanceof Error ? err.message : String(err)}`);
|
package/oclif.manifest.json
CHANGED
package/package.json
CHANGED