orquesta-cli 0.1.20 → 0.1.22
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/core/config/providers.js +6 -5
- package/dist/core/llm/llm-client.js +9 -0
- package/dist/core/routing-state.d.ts +4 -0
- package/dist/core/routing-state.js +8 -0
- package/dist/core/slash-command-handler.js +40 -2
- package/dist/orquesta/config-sync.js +13 -5
- package/dist/orquesta/connection.js +1 -1
- package/dist/orquesta/hook-init.js +1 -1
- package/dist/ui/TodoPanel.d.ts +4 -0
- package/dist/ui/TodoPanel.js +25 -3
- package/dist/ui/components/LLMSetupWizard.js +1 -1
- package/dist/ui/components/PlanExecuteApp.js +4 -2
- package/dist/ui/components/StatusBar.d.ts +5 -0
- package/dist/ui/components/StatusBar.js +11 -1
- package/dist/ui/hooks/useBatutaUsage.d.ts +8 -0
- package/dist/ui/hooks/useBatutaUsage.js +46 -0
- package/package.json +1 -1
- package/dist/core/git-auto-updater.d.ts +0 -58
- package/dist/core/git-auto-updater.js +0 -374
|
@@ -30,10 +30,10 @@ export const PROVIDERS = [
|
|
|
30
30
|
openaiCompatible: false,
|
|
31
31
|
extraHeaders: { 'anthropic-version': '2023-06-01' },
|
|
32
32
|
knownModels: [
|
|
33
|
+
{ id: 'claude-opus-4-7', name: 'Claude Opus 4.7', maxTokens: 200000, capabilities: ['vision', 'tools', 'extended_thinking', 'streaming'] },
|
|
33
34
|
{ id: 'claude-opus-4-6', name: 'Claude Opus 4.6', maxTokens: 200000, capabilities: ['vision', 'tools', 'extended_thinking', 'streaming'] },
|
|
34
35
|
{ id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', maxTokens: 200000, capabilities: ['vision', 'tools', 'extended_thinking', 'streaming'] },
|
|
35
36
|
{ id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5', maxTokens: 200000, capabilities: ['vision', 'tools', 'streaming'] },
|
|
36
|
-
{ id: 'claude-sonnet-4-5-20250514', name: 'Claude Sonnet 4.5', maxTokens: 200000, capabilities: ['vision', 'tools', 'extended_thinking', 'streaming'] },
|
|
37
37
|
],
|
|
38
38
|
},
|
|
39
39
|
{
|
|
@@ -188,7 +188,7 @@ export const PROVIDERS = [
|
|
|
188
188
|
{
|
|
189
189
|
id: 'batuta',
|
|
190
190
|
name: 'Batuta (Orquesta)',
|
|
191
|
-
baseUrl: 'https://
|
|
191
|
+
baseUrl: 'https://getorquesta.com/api/v1',
|
|
192
192
|
authMethod: 'bearer',
|
|
193
193
|
envVars: ['BATUTA_API_KEY'],
|
|
194
194
|
requiresApiKey: true,
|
|
@@ -196,11 +196,12 @@ export const PROVIDERS = [
|
|
|
196
196
|
modelsEndpoint: '/models',
|
|
197
197
|
openaiCompatible: true,
|
|
198
198
|
knownModels: [
|
|
199
|
-
{ id: '
|
|
200
|
-
{ id: '
|
|
199
|
+
{ id: 'batuta-auto', name: 'Batuta Auto (smart routing)', maxTokens: 200000, capabilities: ['vision', 'tools', 'extended_thinking', 'streaming'] },
|
|
200
|
+
{ id: 'claude-opus-4-7', name: 'Claude Opus 4.7', maxTokens: 200000, capabilities: ['vision', 'tools', 'extended_thinking', 'streaming'] },
|
|
201
201
|
{ id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', maxTokens: 200000, capabilities: ['vision', 'tools', 'extended_thinking', 'streaming'] },
|
|
202
|
-
{ id: 'claude-opus-4-6', name: 'Claude Opus 4.6', maxTokens: 200000, capabilities: ['vision', 'tools', 'extended_thinking', 'streaming'] },
|
|
203
202
|
{ id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5', maxTokens: 200000, capabilities: ['vision', 'tools', 'streaming'] },
|
|
203
|
+
{ id: 'gpt-4o', name: 'GPT-4o', maxTokens: 128000, capabilities: ['vision', 'tools', 'json_mode', 'streaming'] },
|
|
204
|
+
{ id: 'gpt-4o-mini', name: 'GPT-4o Mini', maxTokens: 128000, capabilities: ['vision', 'tools', 'json_mode', 'streaming'] },
|
|
204
205
|
{ id: 'deepseek-chat', name: 'DeepSeek Chat', maxTokens: 128000, capabilities: ['tools', 'json_mode', 'streaming'] },
|
|
205
206
|
{ id: 'deepseek-reasoner', name: 'DeepSeek Reasoner', maxTokens: 64000, capabilities: ['tools', 'streaming'] },
|
|
206
207
|
{ id: 'gemini-2.0-flash', name: 'Gemini 2.0 Flash', maxTokens: 1048576, capabilities: ['vision', 'tools', 'json_mode', 'streaming'] },
|
|
@@ -5,6 +5,13 @@ import { NetworkError, APIError, TimeoutError, ConnectionError, } from '../../er
|
|
|
5
5
|
import { LLMError, TokenLimitError, RateLimitError, ContextLengthError, } from '../../errors/llm.js';
|
|
6
6
|
import { logger, isLLMLogEnabled } from '../../utils/logger.js';
|
|
7
7
|
import { usageTracker } from '../usage-tracker.js';
|
|
8
|
+
import { getForcedTier } from '../routing-state.js';
|
|
9
|
+
function buildPerRequestHeaders() {
|
|
10
|
+
const tier = getForcedTier();
|
|
11
|
+
if (!tier)
|
|
12
|
+
return undefined;
|
|
13
|
+
return { 'X-Batuta-Force-Tier': tier };
|
|
14
|
+
}
|
|
8
15
|
export class LLMClient {
|
|
9
16
|
axiosInstance;
|
|
10
17
|
baseUrl;
|
|
@@ -111,6 +118,7 @@ export class LLMClient {
|
|
|
111
118
|
this.currentAbortController = new AbortController();
|
|
112
119
|
const response = await this.axiosInstance.post(url, requestBody, {
|
|
113
120
|
signal: this.currentAbortController.signal,
|
|
121
|
+
headers: buildPerRequestHeaders(),
|
|
114
122
|
});
|
|
115
123
|
this.currentAbortController = null;
|
|
116
124
|
const elapsed = logger.endTimer('llm-api-call');
|
|
@@ -270,6 +278,7 @@ export class LLMClient {
|
|
|
270
278
|
const response = await this.axiosInstance.post(url, requestBody, {
|
|
271
279
|
responseType: 'stream',
|
|
272
280
|
signal: this.currentAbortController.signal,
|
|
281
|
+
headers: buildPerRequestHeaders(),
|
|
273
282
|
});
|
|
274
283
|
logger.debug('Streaming response started', { status: response.status });
|
|
275
284
|
const stream = response.data;
|
|
@@ -3,6 +3,7 @@ import { usageTracker } from './usage-tracker.js';
|
|
|
3
3
|
import { logger } from '../utils/logger.js';
|
|
4
4
|
import { fullSync } from '../orquesta/config-sync.js';
|
|
5
5
|
import { configManager } from './config/config-manager.js';
|
|
6
|
+
import { getForcedTier, setForcedTier } from './routing-state.js';
|
|
6
7
|
export async function executeSlashCommand(command, context) {
|
|
7
8
|
const trimmedCommand = command.trim();
|
|
8
9
|
logger.enter('executeSlashCommand', { command: trimmedCommand });
|
|
@@ -131,6 +132,42 @@ export async function executeSlashCommand(command, context) {
|
|
|
131
132
|
},
|
|
132
133
|
};
|
|
133
134
|
}
|
|
135
|
+
if (trimmedCommand === '/route' || trimmedCommand.startsWith('/route ')) {
|
|
136
|
+
const arg = trimmedCommand.slice('/route'.length).trim().toLowerCase();
|
|
137
|
+
const currentModel = configManager.getCurrentModel();
|
|
138
|
+
const currentModelId = currentModel?.id ?? null;
|
|
139
|
+
const onBatutaAuto = currentModelId === 'batuta-auto';
|
|
140
|
+
let body;
|
|
141
|
+
if (!arg) {
|
|
142
|
+
const pinned = getForcedTier();
|
|
143
|
+
body = pinned
|
|
144
|
+
? `Batuta Auto tier pinned to: ${pinned}.\n\nUse /route auto to let the classifier decide again.`
|
|
145
|
+
: `Batuta Auto: classifier-driven (default).\n\nUsage: /route [auto|fast|balanced|premium]\n fast → Haiku 4.5 (cheapest, simple Q&A)\n balanced → Sonnet 4.6 (code, day-to-day)\n premium → Opus 4.7 (reasoning, long context)\n auto → let the classifier decide per prompt`;
|
|
146
|
+
}
|
|
147
|
+
else if (arg === 'auto' || arg === 'reset' || arg === 'clear') {
|
|
148
|
+
setForcedTier(null);
|
|
149
|
+
body = `Batuta Auto: pin cleared. Classifier will choose tier per prompt.`;
|
|
150
|
+
}
|
|
151
|
+
else if (arg === 'fast' || arg === 'balanced' || arg === 'premium') {
|
|
152
|
+
setForcedTier(arg);
|
|
153
|
+
body = onBatutaAuto
|
|
154
|
+
? `Batuta Auto: pinned to ${arg}. Every prompt this session routes to ${arg === 'fast' ? 'Haiku 4.5' : arg === 'balanced' ? 'Sonnet 4.6' : 'Opus 4.7'}.`
|
|
155
|
+
: `Pin set to ${arg}, but the active model is "${currentModelId ?? 'none'}", not "batuta-auto" — the pin only takes effect when you switch to batuta-auto (use /model).`;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
body = `Unknown tier: "${arg}". Use one of: auto, fast, balanced, premium.`;
|
|
159
|
+
}
|
|
160
|
+
const updatedMessages = [
|
|
161
|
+
...context.messages,
|
|
162
|
+
{ role: 'assistant', content: body },
|
|
163
|
+
];
|
|
164
|
+
context.setMessages(updatedMessages);
|
|
165
|
+
return {
|
|
166
|
+
handled: true,
|
|
167
|
+
shouldContinue: false,
|
|
168
|
+
updatedContext: { messages: updatedMessages },
|
|
169
|
+
};
|
|
170
|
+
}
|
|
134
171
|
if (trimmedCommand === '/usage') {
|
|
135
172
|
const usageMessage = usageTracker.formatUsageDisplay();
|
|
136
173
|
const updatedMessages = [
|
|
@@ -150,7 +187,7 @@ export async function executeSlashCommand(command, context) {
|
|
|
150
187
|
logger.flow('Sync command received');
|
|
151
188
|
const orquestaConfig = configManager.getOrquestaConfig();
|
|
152
189
|
if (!orquestaConfig?.token) {
|
|
153
|
-
const notConnectedMessage = `❌ Not connected to Orquesta.\n\nTo sync configurations, first connect using an Orquesta CLI token:\n1. Go to https://
|
|
190
|
+
const notConnectedMessage = `❌ Not connected to Orquesta.\n\nTo sync configurations, first connect using an Orquesta CLI token:\n1. Go to https://getorquesta.com/dashboard/orquesta-cli\n2. Generate a CLI token\n3. Run orquesta-cli again and enter your token`;
|
|
154
191
|
const updatedMessages = [
|
|
155
192
|
...context.messages,
|
|
156
193
|
{ role: 'assistant', content: notConnectedMessage },
|
|
@@ -229,7 +266,7 @@ export async function executeSlashCommand(command, context) {
|
|
|
229
266
|
logger.flow('Project command received');
|
|
230
267
|
const orquestaConfig = configManager.getOrquestaConfig();
|
|
231
268
|
if (!orquestaConfig?.token) {
|
|
232
|
-
const notConnectedMessage = `❌ Not connected to Orquesta.\n\nTo switch projects, first connect using an Orquesta CLI token:\n1. Go to https://
|
|
269
|
+
const notConnectedMessage = `❌ Not connected to Orquesta.\n\nTo switch projects, first connect using an Orquesta CLI token:\n1. Go to https://getorquesta.com/dashboard/orquesta-cli\n2. Generate a CLI token\n3. Restart with: orquesta --token <your-token>`;
|
|
233
270
|
const updatedMessages = [
|
|
234
271
|
...context.messages,
|
|
235
272
|
{ role: 'assistant', content: notConnectedMessage },
|
|
@@ -276,6 +313,7 @@ Available commands:
|
|
|
276
313
|
/tool - Enable/disable optional tools (Browser, Background)
|
|
277
314
|
/load - Load a saved session
|
|
278
315
|
/usage - Show token usage statistics
|
|
316
|
+
/route - Pin Batuta Auto tier (fast/balanced/premium/auto)
|
|
279
317
|
/sync - Bidirectional sync with Orquesta dashboard (pull & push LLM configs)
|
|
280
318
|
|
|
281
319
|
CLI Options (restart required):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { configManager } from '../core/config/config-manager.js';
|
|
2
2
|
import { logger } from '../utils/logger.js';
|
|
3
|
-
const ORQUESTA_API = process.env['ORQUESTA_API_URL'] || 'https://
|
|
3
|
+
const ORQUESTA_API = process.env['ORQUESTA_API_URL'] || 'https://getorquesta.com';
|
|
4
4
|
export async function fetchOrquestaProjects(token) {
|
|
5
5
|
try {
|
|
6
6
|
const response = await fetch(`${ORQUESTA_API}/api/orquesta-cli/projects`, {
|
|
@@ -83,16 +83,24 @@ export async function syncOrquestaConfigs() {
|
|
|
83
83
|
const endpoints = data.endpoints || [];
|
|
84
84
|
const hasBatuta = endpoints.some((e) => e.provider === 'batuta' || e.id === 'batuta-proxy');
|
|
85
85
|
if (!hasBatuta) {
|
|
86
|
+
const healthy = { enabled: true, healthStatus: 'healthy', lastHealthCheck: new Date() };
|
|
86
87
|
endpoints.push({
|
|
87
88
|
id: 'batuta-proxy',
|
|
88
|
-
name: 'Batuta
|
|
89
|
+
name: 'Batuta',
|
|
89
90
|
baseUrl: `${ORQUESTA_API}/api/v1`,
|
|
90
91
|
apiKey: orquestaConfig.token,
|
|
91
92
|
provider: 'batuta',
|
|
92
93
|
models: [
|
|
93
|
-
{ id: '
|
|
94
|
-
{ id: '
|
|
95
|
-
{ id: '
|
|
94
|
+
{ id: 'batuta-auto', name: 'Batuta Auto (smart routing)', maxTokens: 200000, ...healthy },
|
|
95
|
+
{ id: 'claude-opus-4-7', name: 'Claude Opus 4.7', maxTokens: 200000, ...healthy },
|
|
96
|
+
{ id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', maxTokens: 200000, ...healthy },
|
|
97
|
+
{ id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5', maxTokens: 200000, ...healthy },
|
|
98
|
+
{ id: 'gpt-4o', name: 'GPT-4o', maxTokens: 128000, ...healthy },
|
|
99
|
+
{ id: 'gpt-4o-mini', name: 'GPT-4o Mini', maxTokens: 128000, ...healthy },
|
|
100
|
+
{ id: 'deepseek-chat', name: 'DeepSeek Chat', maxTokens: 128000, ...healthy },
|
|
101
|
+
{ id: 'deepseek-reasoner', name: 'DeepSeek Reasoner', maxTokens: 64000, ...healthy },
|
|
102
|
+
{ id: 'gemini-2.0-pro', name: 'Gemini 2.0 Pro', maxTokens: 1048576, ...healthy },
|
|
103
|
+
{ id: 'grok-4', name: 'Grok 4', maxTokens: 131072, ...healthy },
|
|
96
104
|
],
|
|
97
105
|
createdAt: new Date(),
|
|
98
106
|
updatedAt: new Date(),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { io } from 'socket.io-client';
|
|
2
2
|
import * as os from 'os';
|
|
3
|
-
const ORQUESTA_API = process.env['ORQUESTA_API_URL'] || 'https://
|
|
3
|
+
const ORQUESTA_API = process.env['ORQUESTA_API_URL'] || 'https://getorquesta.com';
|
|
4
4
|
const ORQUESTA_WS = process.env['ORQUESTA_WS_URL'] || 'wss://ws.orquesta.live';
|
|
5
5
|
export class OrquestaConnection {
|
|
6
6
|
token;
|
|
@@ -31,7 +31,7 @@ function resolveAgentBin() {
|
|
|
31
31
|
console.warn(' \x1b[33m npm install -g orquesta-agent\x1b[0m\n');
|
|
32
32
|
return 'orquesta-agent';
|
|
33
33
|
}
|
|
34
|
-
export async function initHooks(token, apiUrl = 'https://
|
|
34
|
+
export async function initHooks(token, apiUrl = 'https://getorquesta.com') {
|
|
35
35
|
const cwd = process.cwd();
|
|
36
36
|
console.log('\n Initializing Orquesta hook integration...\n');
|
|
37
37
|
const agentBin = resolveAgentBin();
|
package/dist/ui/TodoPanel.d.ts
CHANGED
|
@@ -9,6 +9,10 @@ export declare const TodoPanel: React.FC<TodoPanelProps>;
|
|
|
9
9
|
export declare const TodoStatusBar: React.FC<{
|
|
10
10
|
todos: TodoItem[];
|
|
11
11
|
projectName?: string;
|
|
12
|
+
batutaUsage?: {
|
|
13
|
+
tokensUsed: number;
|
|
14
|
+
tokenLimit: number;
|
|
15
|
+
} | null;
|
|
12
16
|
}>;
|
|
13
17
|
export default TodoPanel;
|
|
14
18
|
//# sourceMappingURL=TodoPanel.d.ts.map
|
package/dist/ui/TodoPanel.js
CHANGED
|
@@ -100,12 +100,31 @@ export const TodoPanel = React.memo(({ todos, currentTodoId, isProcessing = fals
|
|
|
100
100
|
}
|
|
101
101
|
return true;
|
|
102
102
|
});
|
|
103
|
-
export const TodoStatusBar = React.memo(({ todos, projectName }) => {
|
|
103
|
+
export const TodoStatusBar = React.memo(({ todos, projectName, batutaUsage }) => {
|
|
104
104
|
useEffect(() => {
|
|
105
105
|
logger.debug('TodoStatusBar rendered', { todoCount: todos.length });
|
|
106
106
|
}, [todos.length]);
|
|
107
|
+
const batutaIndicator = batutaUsage && batutaUsage.tokenLimit > 0 ? (() => {
|
|
108
|
+
const pct = (batutaUsage.tokensUsed / batutaUsage.tokenLimit) * 100;
|
|
109
|
+
const color = pct > 90 ? 'red' : pct > 70 ? 'yellow' : 'greenBright';
|
|
110
|
+
const fmt = (n) => n < 1000 ? `${n}` : n < 1_000_000 ? `${(n / 1000).toFixed(1)}k` : `${(n / 1_000_000).toFixed(2)}M`;
|
|
111
|
+
return (React.createElement(React.Fragment, null,
|
|
112
|
+
React.createElement(Text, { color: "gray" }, "Batuta "),
|
|
113
|
+
React.createElement(Text, { color: color },
|
|
114
|
+
fmt(batutaUsage.tokensUsed),
|
|
115
|
+
"/",
|
|
116
|
+
fmt(batutaUsage.tokenLimit))));
|
|
117
|
+
})() : null;
|
|
107
118
|
if (todos.length === 0) {
|
|
108
|
-
|
|
119
|
+
if (!batutaIndicator)
|
|
120
|
+
return null;
|
|
121
|
+
return (React.createElement(Box, null,
|
|
122
|
+
projectName && (React.createElement(React.Fragment, null,
|
|
123
|
+
React.createElement(Text, { color: "magenta" },
|
|
124
|
+
"\uD83D\uDCC1 ",
|
|
125
|
+
projectName),
|
|
126
|
+
React.createElement(Text, { color: "white" }, " \u2502 "))),
|
|
127
|
+
batutaIndicator));
|
|
109
128
|
}
|
|
110
129
|
const completedCount = todos.filter(t => t.status === 'completed').length;
|
|
111
130
|
const inProgressCount = todos.filter(t => t.status === 'in_progress').length;
|
|
@@ -134,7 +153,10 @@ export const TodoStatusBar = React.memo(({ todos, projectName }) => {
|
|
|
134
153
|
currentTodo.title.length > 30 ? '...' : ''))),
|
|
135
154
|
inProgressCount === 0 && completedCount === todos.length && (React.createElement(React.Fragment, null,
|
|
136
155
|
React.createElement(Text, { color: "white" }, " \u2502 "),
|
|
137
|
-
React.createElement(Text, { color: "greenBright" }, "Done")))
|
|
156
|
+
React.createElement(Text, { color: "greenBright" }, "Done"))),
|
|
157
|
+
batutaIndicator && (React.createElement(React.Fragment, null,
|
|
158
|
+
React.createElement(Text, { color: "white" }, " \u2502 "),
|
|
159
|
+
batutaIndicator))));
|
|
138
160
|
});
|
|
139
161
|
export default TodoPanel;
|
|
140
162
|
//# sourceMappingURL=TodoPanel.js.map
|
|
@@ -419,7 +419,7 @@ export const LLMSetupWizard = ({ onComplete, onSkip }) => {
|
|
|
419
419
|
React.createElement(Box, { paddingX: 1, marginBottom: 1, flexDirection: "column" },
|
|
420
420
|
React.createElement(Text, { color: "gray" }, "Enter your Orquesta CLI token to sync LLM configurations."),
|
|
421
421
|
React.createElement(Text, { color: "gray" }, "Get your token from: "),
|
|
422
|
-
React.createElement(Text, { color: "cyan" }, "https://
|
|
422
|
+
React.createElement(Text, { color: "cyan" }, "https://getorquesta.com/dashboard/orquesta-cli")),
|
|
423
423
|
React.createElement(Box, { paddingX: 1, marginY: 1 },
|
|
424
424
|
React.createElement(Text, { color: "yellow" }, "Token: "),
|
|
425
425
|
React.createElement(TextInput, { value: orquestaToken, onChange: setOrquestaToken, placeholder: "oclt_xxxxxxxxxxxx" })),
|
|
@@ -8,6 +8,7 @@ import { getShellConfig, isNativeWindows } from '../../utils/platform-utils.js';
|
|
|
8
8
|
import { CustomTextInput } from './CustomTextInput.js';
|
|
9
9
|
import { LLMClient, createLLMClient } from '../../core/llm/llm-client.js';
|
|
10
10
|
import { TodoPanel, TodoStatusBar } from '../TodoPanel.js';
|
|
11
|
+
import { useBatutaUsage } from '../hooks/useBatutaUsage.js';
|
|
11
12
|
import { sessionManager } from '../../core/session/session-manager.js';
|
|
12
13
|
import { initializeDocsDirectory, setDocsSearchProgressCallback } from '../../agents/docs-search/index.js';
|
|
13
14
|
import { DocsSearchProgress } from './DocsSearchProgress.js';
|
|
@@ -96,6 +97,7 @@ export const PlanExecuteApp = ({ llmClient: initialLlmClient, modelInfo }) => {
|
|
|
96
97
|
const [subActivities, setSubActivities] = useState([]);
|
|
97
98
|
const [sessionTokens, setSessionTokens] = useState(0);
|
|
98
99
|
const [sessionElapsed, setSessionElapsed] = useState(0);
|
|
100
|
+
const batutaUsage = useBatutaUsage();
|
|
99
101
|
const [currentToolName, setCurrentToolName] = useState(null);
|
|
100
102
|
const [showSessionBrowser, setShowSessionBrowser] = useState(false);
|
|
101
103
|
const [showSettings, setShowSettings] = useState(false);
|
|
@@ -1339,9 +1341,9 @@ export const PlanExecuteApp = ({ llmClient: initialLlmClient, modelInfo }) => {
|
|
|
1339
1341
|
React.createElement(Text, { color: "cyan" }, currentModelInfo.model),
|
|
1340
1342
|
React.createElement(Text, { color: "gray" }, " \u2502 "),
|
|
1341
1343
|
React.createElement(Text, { color: "gray" }, shortenPath(process.cwd())),
|
|
1342
|
-
planExecutionState.todos.length > 0 && (React.createElement(React.Fragment, null,
|
|
1344
|
+
(planExecutionState.todos.length > 0 || batutaUsage) && (React.createElement(React.Fragment, null,
|
|
1343
1345
|
React.createElement(Text, { color: "gray" }, " \u2502 "),
|
|
1344
|
-
React.createElement(TodoStatusBar, { todos: planExecutionState.todos, projectName: configManager.getOrquestaConfig()?.projectName })))),
|
|
1346
|
+
React.createElement(TodoStatusBar, { todos: planExecutionState.todos, projectName: configManager.getOrquestaConfig()?.projectName, batutaUsage: batutaUsage })))),
|
|
1345
1347
|
React.createElement(Text, { color: "gray", dimColor: true }, "Tab: mode \u2502 /help")))),
|
|
1346
1348
|
showLogFiles && (React.createElement(Box, { marginTop: 0 },
|
|
1347
1349
|
React.createElement(LogBrowser, { onClose: () => setShowLogFiles(false) })))));
|
|
@@ -19,6 +19,11 @@ export interface StatusBarProps {
|
|
|
19
19
|
healthStatus?: 'healthy' | 'unhealthy' | 'checking' | 'unknown';
|
|
20
20
|
currentActivity?: string;
|
|
21
21
|
sessionElapsedSeconds?: number;
|
|
22
|
+
batutaUsage?: {
|
|
23
|
+
tokensUsed: number;
|
|
24
|
+
tokenLimit: number;
|
|
25
|
+
tier: 'llm' | 'llm_team' | null;
|
|
26
|
+
} | null;
|
|
22
27
|
}
|
|
23
28
|
export declare const StatusBar: React.FC<StatusBarProps>;
|
|
24
29
|
export default StatusBar;
|
|
@@ -62,7 +62,7 @@ function formatElapsedTime(seconds) {
|
|
|
62
62
|
const secs = seconds % 60;
|
|
63
63
|
return `${mins}m ${secs}s`;
|
|
64
64
|
}
|
|
65
|
-
export const StatusBar = ({ model, endpoint: _endpoint, projectName, organizationName: _organizationName, workingDirectory, status = 'idle', message: _message, messageCount = 0, sessionTokens = 0, contextUsage, contextRemainingPercent, todoCount, todoCompleted, healthStatus, currentActivity, sessionElapsedSeconds, }) => {
|
|
65
|
+
export const StatusBar = ({ model, endpoint: _endpoint, projectName, organizationName: _organizationName, workingDirectory, status = 'idle', message: _message, messageCount = 0, sessionTokens = 0, contextUsage, contextRemainingPercent, todoCount, todoCompleted, healthStatus, currentActivity, sessionElapsedSeconds, batutaUsage, }) => {
|
|
66
66
|
void _endpoint;
|
|
67
67
|
void _message;
|
|
68
68
|
useEffect(() => {
|
|
@@ -149,6 +149,16 @@ export const StatusBar = ({ model, endpoint: _endpoint, projectName, organizatio
|
|
|
149
149
|
React.createElement(Text, { color: "cyan" },
|
|
150
150
|
"\u26A1 ",
|
|
151
151
|
formatTokens(sessionTokens)))),
|
|
152
|
+
batutaUsage && batutaUsage.tokenLimit > 0 && (() => {
|
|
153
|
+
const pct = (batutaUsage.tokensUsed / batutaUsage.tokenLimit) * 100;
|
|
154
|
+
const color = pct > 90 ? 'red' : pct > 70 ? 'yellow' : 'green';
|
|
155
|
+
return (React.createElement(Box, { marginRight: 2 },
|
|
156
|
+
React.createElement(Text, { color: "gray" }, "Batuta "),
|
|
157
|
+
React.createElement(Text, { color: color },
|
|
158
|
+
formatTokens(batutaUsage.tokensUsed),
|
|
159
|
+
"/",
|
|
160
|
+
formatTokens(batutaUsage.tokenLimit))));
|
|
161
|
+
})(),
|
|
152
162
|
contextUsage && contextUsage.current > 0 && (React.createElement(Box, { marginRight: 2 },
|
|
153
163
|
React.createElement(Text, { color: "gray" }, "CTX "),
|
|
154
164
|
React.createElement(ContextMiniBar, { current: contextUsage.current, max: contextUsage.max }))),
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { configManager } from '../../core/config/config-manager.js';
|
|
3
|
+
const ORQUESTA_API = process.env['ORQUESTA_API_URL'] || 'https://getorquesta.com';
|
|
4
|
+
const POLL_INTERVAL_MS = 60_000;
|
|
5
|
+
export function useBatutaUsage() {
|
|
6
|
+
const [usage, setUsage] = useState(null);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
let cancelled = false;
|
|
9
|
+
async function tick() {
|
|
10
|
+
const endpoint = configManager.getCurrentEndpoint();
|
|
11
|
+
const orquestaConfig = configManager.getOrquestaConfig();
|
|
12
|
+
const onBatuta = endpoint?.provider === 'batuta' || endpoint?.id === 'batuta-proxy';
|
|
13
|
+
if (!onBatuta || !orquestaConfig?.token) {
|
|
14
|
+
if (!cancelled)
|
|
15
|
+
setUsage(null);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetch(`${ORQUESTA_API}/api/billing`, {
|
|
20
|
+
headers: { Authorization: `Bearer ${orquestaConfig.token}` },
|
|
21
|
+
});
|
|
22
|
+
if (!res.ok || cancelled)
|
|
23
|
+
return;
|
|
24
|
+
const data = await res.json();
|
|
25
|
+
if (data.batutaLlm && !cancelled) {
|
|
26
|
+
setUsage({
|
|
27
|
+
tokensUsed: data.batutaLlm.tokensUsed ?? 0,
|
|
28
|
+
tokenLimit: data.batutaLlm.tokenLimit ?? 0,
|
|
29
|
+
tier: data.batutaLlm.tier ?? null,
|
|
30
|
+
enabled: data.batutaLlm.enabled ?? false,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
tick();
|
|
38
|
+
const interval = setInterval(tick, POLL_INTERVAL_MS);
|
|
39
|
+
return () => {
|
|
40
|
+
cancelled = true;
|
|
41
|
+
clearInterval(interval);
|
|
42
|
+
};
|
|
43
|
+
}, []);
|
|
44
|
+
return usage;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=useBatutaUsage.js.map
|
package/package.json
CHANGED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
export type UpdateStatus = {
|
|
2
|
-
type: 'checking';
|
|
3
|
-
} | {
|
|
4
|
-
type: 'no_update';
|
|
5
|
-
} | {
|
|
6
|
-
type: 'first_run';
|
|
7
|
-
step: number;
|
|
8
|
-
totalSteps: number;
|
|
9
|
-
message: string;
|
|
10
|
-
} | {
|
|
11
|
-
type: 'updating';
|
|
12
|
-
step: number;
|
|
13
|
-
totalSteps: number;
|
|
14
|
-
message: string;
|
|
15
|
-
} | {
|
|
16
|
-
type: 'complete';
|
|
17
|
-
needsRestart: boolean;
|
|
18
|
-
message: string;
|
|
19
|
-
} | {
|
|
20
|
-
type: 'error';
|
|
21
|
-
message: string;
|
|
22
|
-
} | {
|
|
23
|
-
type: 'skipped';
|
|
24
|
-
reason: string;
|
|
25
|
-
};
|
|
26
|
-
export type StatusCallback = (status: UpdateStatus) => void;
|
|
27
|
-
export declare class GitAutoUpdater {
|
|
28
|
-
private repoUrl;
|
|
29
|
-
private repoDir;
|
|
30
|
-
private commitFile;
|
|
31
|
-
private enabled;
|
|
32
|
-
private onStatus;
|
|
33
|
-
constructor(options?: {
|
|
34
|
-
repoUrl?: string;
|
|
35
|
-
enabled?: boolean;
|
|
36
|
-
onStatus?: StatusCallback;
|
|
37
|
-
});
|
|
38
|
-
setStatusCallback(callback: StatusCallback): void;
|
|
39
|
-
private emitStatus;
|
|
40
|
-
run(options?: {
|
|
41
|
-
noUpdate?: boolean;
|
|
42
|
-
}): Promise<boolean>;
|
|
43
|
-
private runBinaryMode;
|
|
44
|
-
private getRemoteCommit;
|
|
45
|
-
private getSavedCommit;
|
|
46
|
-
private saveCommit;
|
|
47
|
-
private updateBinary;
|
|
48
|
-
private initialSetup;
|
|
49
|
-
private pullAndUpdate;
|
|
50
|
-
private freshClone;
|
|
51
|
-
private rebuildAndLink;
|
|
52
|
-
private copyBinariesInternal;
|
|
53
|
-
private ensurePathConfigured;
|
|
54
|
-
private unlinkNpm;
|
|
55
|
-
private cleanupRepo;
|
|
56
|
-
}
|
|
57
|
-
export default GitAutoUpdater;
|
|
58
|
-
//# sourceMappingURL=git-auto-updater.d.ts.map
|
|
@@ -1,374 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
|
-
import fs, { createReadStream, createWriteStream } from 'fs';
|
|
3
|
-
import { rm, copyFile, chmod } from 'fs/promises';
|
|
4
|
-
import { pipeline } from 'stream/promises';
|
|
5
|
-
import path from 'path';
|
|
6
|
-
import os from 'os';
|
|
7
|
-
import zlib from 'zlib';
|
|
8
|
-
import { logger } from '../utils/logger.js';
|
|
9
|
-
function isRunningAsBinary() {
|
|
10
|
-
const execPath = process.execPath;
|
|
11
|
-
const isNodeRuntime = execPath.includes('node') || execPath.includes('nodejs');
|
|
12
|
-
return !isNodeRuntime;
|
|
13
|
-
}
|
|
14
|
-
function execAsync(command, options = {}) {
|
|
15
|
-
return new Promise((resolve, reject) => {
|
|
16
|
-
const [cmd, ...args] = command.split(' ');
|
|
17
|
-
const child = spawn(cmd, args, {
|
|
18
|
-
cwd: options.cwd,
|
|
19
|
-
shell: true,
|
|
20
|
-
stdio: 'pipe',
|
|
21
|
-
});
|
|
22
|
-
let stdout = '';
|
|
23
|
-
let stderr = '';
|
|
24
|
-
child.stdout?.on('data', (data) => {
|
|
25
|
-
stdout += data.toString();
|
|
26
|
-
});
|
|
27
|
-
child.stderr?.on('data', (data) => {
|
|
28
|
-
stderr += data.toString();
|
|
29
|
-
});
|
|
30
|
-
child.on('close', (code) => {
|
|
31
|
-
if (code === 0) {
|
|
32
|
-
resolve({ stdout, stderr });
|
|
33
|
-
}
|
|
34
|
-
else {
|
|
35
|
-
const error = new Error(`Command failed: ${command}`);
|
|
36
|
-
error.stdout = stdout;
|
|
37
|
-
error.stderr = stderr;
|
|
38
|
-
error.code = code;
|
|
39
|
-
reject(error);
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
child.on('error', (err) => {
|
|
43
|
-
reject(err);
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
export class GitAutoUpdater {
|
|
48
|
-
repoUrl = 'https://github.com/A2G-Dev-Space/Local-CLI.git';
|
|
49
|
-
repoDir;
|
|
50
|
-
commitFile;
|
|
51
|
-
enabled = true;
|
|
52
|
-
onStatus = null;
|
|
53
|
-
constructor(options) {
|
|
54
|
-
this.repoDir = path.join(os.homedir(), '.local-cli', 'repo');
|
|
55
|
-
this.commitFile = path.join(os.homedir(), '.local-cli', 'current-commit');
|
|
56
|
-
if (options?.repoUrl) {
|
|
57
|
-
this.repoUrl = options.repoUrl;
|
|
58
|
-
}
|
|
59
|
-
if (options?.enabled !== undefined) {
|
|
60
|
-
this.enabled = options.enabled;
|
|
61
|
-
}
|
|
62
|
-
if (options?.onStatus) {
|
|
63
|
-
this.onStatus = options.onStatus;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
setStatusCallback(callback) {
|
|
67
|
-
this.onStatus = callback;
|
|
68
|
-
}
|
|
69
|
-
emitStatus(status) {
|
|
70
|
-
if (this.onStatus) {
|
|
71
|
-
this.onStatus(status);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
async run(options = {}) {
|
|
75
|
-
logger.enter('GitAutoUpdater.run', {
|
|
76
|
-
noUpdate: options.noUpdate,
|
|
77
|
-
enabled: this.enabled,
|
|
78
|
-
repoDir: this.repoDir
|
|
79
|
-
});
|
|
80
|
-
if (options.noUpdate || !this.enabled) {
|
|
81
|
-
logger.flow('Git auto-update disabled - skipping');
|
|
82
|
-
this.emitStatus({ type: 'skipped', reason: 'disabled' });
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
this.emitStatus({ type: 'checking' });
|
|
86
|
-
try {
|
|
87
|
-
if (isRunningAsBinary() && fs.existsSync(this.repoDir)) {
|
|
88
|
-
logger.flow('Cleaning up leftover repo from previous version');
|
|
89
|
-
await this.cleanupRepo();
|
|
90
|
-
}
|
|
91
|
-
if (isRunningAsBinary()) {
|
|
92
|
-
return await this.runBinaryMode();
|
|
93
|
-
}
|
|
94
|
-
logger.flow('Checking repository directory');
|
|
95
|
-
if (!fs.existsSync(this.repoDir)) {
|
|
96
|
-
logger.flow('First run detected - need initial setup');
|
|
97
|
-
return await this.initialSetup();
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
return await this.pullAndUpdate();
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
catch (error) {
|
|
104
|
-
logger.error('Git auto-update failed', error);
|
|
105
|
-
this.emitStatus({ type: 'error', message: 'Auto-update failed, continuing with current version' });
|
|
106
|
-
}
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
|
-
async runBinaryMode() {
|
|
110
|
-
logger.flow('Running in binary mode - using ls-remote for update check');
|
|
111
|
-
try {
|
|
112
|
-
const remoteCommit = await this.getRemoteCommit();
|
|
113
|
-
if (!remoteCommit) {
|
|
114
|
-
logger.error('Failed to get remote commit');
|
|
115
|
-
this.emitStatus({ type: 'error', message: 'Failed to check for updates' });
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
const savedCommit = this.getSavedCommit();
|
|
119
|
-
logger.debug('Version check', { remote: remoteCommit.slice(0, 7), saved: savedCommit?.slice(0, 7) || 'none' });
|
|
120
|
-
if (savedCommit === remoteCommit) {
|
|
121
|
-
logger.flow('Already up to date');
|
|
122
|
-
this.emitStatus({ type: 'no_update' });
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
125
|
-
logger.flow('Update available, starting update process');
|
|
126
|
-
return await this.updateBinary(remoteCommit);
|
|
127
|
-
}
|
|
128
|
-
catch (error) {
|
|
129
|
-
logger.error('Binary mode update failed', error);
|
|
130
|
-
this.emitStatus({ type: 'error', message: 'Update check failed' });
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
async getRemoteCommit() {
|
|
135
|
-
try {
|
|
136
|
-
const result = await execAsync(`git ls-remote ${this.repoUrl} refs/heads/main`);
|
|
137
|
-
const match = result.stdout.match(/^([a-f0-9]+)/);
|
|
138
|
-
return match && match[1] ? match[1] : null;
|
|
139
|
-
}
|
|
140
|
-
catch (error) {
|
|
141
|
-
logger.error('Failed to get remote commit via ls-remote', error);
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
getSavedCommit() {
|
|
146
|
-
try {
|
|
147
|
-
if (fs.existsSync(this.commitFile)) {
|
|
148
|
-
return fs.readFileSync(this.commitFile, 'utf-8').trim();
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
catch (error) {
|
|
152
|
-
logger.debug('Failed to read saved commit: ' + (error instanceof Error ? error.message : String(error)));
|
|
153
|
-
}
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
saveCommit(commit) {
|
|
157
|
-
try {
|
|
158
|
-
const dir = path.dirname(this.commitFile);
|
|
159
|
-
if (!fs.existsSync(dir)) {
|
|
160
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
161
|
-
}
|
|
162
|
-
fs.writeFileSync(this.commitFile, commit);
|
|
163
|
-
logger.debug('Saved commit hash: ' + commit.slice(0, 7));
|
|
164
|
-
}
|
|
165
|
-
catch (error) {
|
|
166
|
-
logger.debug('Failed to save commit: ' + (error instanceof Error ? error.message : String(error)));
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
async updateBinary(remoteCommit) {
|
|
170
|
-
const totalSteps = 3;
|
|
171
|
-
const isFirstRun = !this.getSavedCommit();
|
|
172
|
-
try {
|
|
173
|
-
const statusType = isFirstRun ? 'first_run' : 'updating';
|
|
174
|
-
this.emitStatus({ type: statusType, step: 1, totalSteps, message: 'Downloading update...' });
|
|
175
|
-
const parentDir = path.dirname(this.repoDir);
|
|
176
|
-
if (!fs.existsSync(parentDir)) {
|
|
177
|
-
fs.mkdirSync(parentDir, { recursive: true });
|
|
178
|
-
}
|
|
179
|
-
await execAsync(`git clone --depth 1 ${this.repoUrl} ${this.repoDir}`);
|
|
180
|
-
this.emitStatus({ type: statusType, step: 2, totalSteps, message: 'Installing update...' });
|
|
181
|
-
const success = await this.copyBinariesInternal();
|
|
182
|
-
if (!success) {
|
|
183
|
-
return false;
|
|
184
|
-
}
|
|
185
|
-
this.emitStatus({ type: statusType, step: 3, totalSteps, message: 'Finalizing...' });
|
|
186
|
-
await this.cleanupRepo();
|
|
187
|
-
this.saveCommit(remoteCommit);
|
|
188
|
-
const shell = process.env['SHELL'] || '/bin/bash';
|
|
189
|
-
const rcFile = shell.includes('zsh') ? '~/.zshrc' : '~/.bashrc';
|
|
190
|
-
const restartMsg = isFirstRun
|
|
191
|
-
? `Setup complete! Run: source ${rcFile} && lcli`
|
|
192
|
-
: 'Update complete! Please restart.';
|
|
193
|
-
this.emitStatus({ type: 'complete', needsRestart: true, message: restartMsg });
|
|
194
|
-
return true;
|
|
195
|
-
}
|
|
196
|
-
catch (error) {
|
|
197
|
-
logger.error('Binary update failed', error);
|
|
198
|
-
await this.cleanupRepo();
|
|
199
|
-
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
200
|
-
this.emitStatus({ type: 'error', message: `Update failed: ${message}` });
|
|
201
|
-
return false;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
async initialSetup() {
|
|
205
|
-
logger.enter('initialSetup', {
|
|
206
|
-
repoDir: this.repoDir,
|
|
207
|
-
repoUrl: this.repoUrl
|
|
208
|
-
});
|
|
209
|
-
const totalSteps = 4;
|
|
210
|
-
try {
|
|
211
|
-
this.emitStatus({ type: 'first_run', step: 1, totalSteps, message: 'Cloning repository...' });
|
|
212
|
-
const parentDir = path.dirname(this.repoDir);
|
|
213
|
-
if (!fs.existsSync(parentDir)) {
|
|
214
|
-
fs.mkdirSync(parentDir, { recursive: true });
|
|
215
|
-
}
|
|
216
|
-
await execAsync(`git clone ${this.repoUrl} ${this.repoDir}`);
|
|
217
|
-
this.emitStatus({ type: 'first_run', step: 2, totalSteps, message: 'Installing dependencies...' });
|
|
218
|
-
await execAsync('npm install', { cwd: this.repoDir });
|
|
219
|
-
this.emitStatus({ type: 'first_run', step: 3, totalSteps, message: 'Building project...' });
|
|
220
|
-
await execAsync('npm run build', { cwd: this.repoDir });
|
|
221
|
-
this.emitStatus({ type: 'first_run', step: 4, totalSteps, message: 'Creating global link...' });
|
|
222
|
-
await execAsync('npm link', { cwd: this.repoDir });
|
|
223
|
-
this.emitStatus({ type: 'complete', needsRestart: true, message: 'Setup complete! Please restart.' });
|
|
224
|
-
return true;
|
|
225
|
-
}
|
|
226
|
-
catch (error) {
|
|
227
|
-
logger.error('Initial setup failed', error);
|
|
228
|
-
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
229
|
-
this.emitStatus({ type: 'error', message: `Setup failed: ${message}` });
|
|
230
|
-
return false;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
async pullAndUpdate() {
|
|
234
|
-
logger.debug('Checking for updates', { repoDir: this.repoDir });
|
|
235
|
-
try {
|
|
236
|
-
await execAsync('git fetch origin main', { cwd: this.repoDir });
|
|
237
|
-
const currentResult = await execAsync('git rev-parse HEAD', { cwd: this.repoDir });
|
|
238
|
-
const latestResult = await execAsync('git rev-parse origin/main', { cwd: this.repoDir });
|
|
239
|
-
const currentCommit = currentResult.stdout.trim();
|
|
240
|
-
const latestCommit = latestResult.stdout.trim();
|
|
241
|
-
if (currentCommit === latestCommit) {
|
|
242
|
-
logger.debug('Already up to date, no rebuild needed');
|
|
243
|
-
this.emitStatus({ type: 'no_update' });
|
|
244
|
-
return false;
|
|
245
|
-
}
|
|
246
|
-
logger.debug('Resetting to latest commit...', { from: currentCommit.slice(0, 7), to: latestCommit.slice(0, 7) });
|
|
247
|
-
await execAsync('git reset --hard origin/main', { cwd: this.repoDir });
|
|
248
|
-
return await this.rebuildAndLink();
|
|
249
|
-
}
|
|
250
|
-
catch (error) {
|
|
251
|
-
logger.error('Pull/reset failed, attempting fresh clone', error);
|
|
252
|
-
return await this.freshClone();
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
async freshClone() {
|
|
256
|
-
logger.flow('Performing fresh clone');
|
|
257
|
-
try {
|
|
258
|
-
this.emitStatus({ type: 'updating', step: 1, totalSteps: 4, message: 'Removing old repository...' });
|
|
259
|
-
await rm(this.repoDir, { recursive: true, force: true });
|
|
260
|
-
return await this.initialSetup();
|
|
261
|
-
}
|
|
262
|
-
catch (error) {
|
|
263
|
-
logger.error('Fresh clone failed', error);
|
|
264
|
-
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
265
|
-
this.emitStatus({ type: 'error', message: `Fresh clone failed: ${message}` });
|
|
266
|
-
return false;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
async rebuildAndLink() {
|
|
270
|
-
const totalSteps = 3;
|
|
271
|
-
try {
|
|
272
|
-
this.emitStatus({ type: 'updating', step: 1, totalSteps, message: 'Updating dependencies...' });
|
|
273
|
-
await execAsync('npm install', { cwd: this.repoDir });
|
|
274
|
-
this.emitStatus({ type: 'updating', step: 2, totalSteps, message: 'Building project...' });
|
|
275
|
-
await execAsync('npm run build', { cwd: this.repoDir });
|
|
276
|
-
this.emitStatus({ type: 'updating', step: 3, totalSteps, message: 'Updating global link...' });
|
|
277
|
-
await execAsync('npm link', { cwd: this.repoDir });
|
|
278
|
-
this.emitStatus({ type: 'complete', needsRestart: true, message: 'Update complete! Please restart.' });
|
|
279
|
-
return true;
|
|
280
|
-
}
|
|
281
|
-
catch (buildError) {
|
|
282
|
-
logger.error('Build/link failed', buildError);
|
|
283
|
-
const message = buildError instanceof Error ? buildError.message : 'Unknown error';
|
|
284
|
-
this.emitStatus({ type: 'error', message: `Build failed: ${message}` });
|
|
285
|
-
return false;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
async copyBinariesInternal() {
|
|
289
|
-
try {
|
|
290
|
-
const repoBinDir = path.join(this.repoDir, 'bin');
|
|
291
|
-
const installDir = path.join(os.homedir(), '.local', 'bin');
|
|
292
|
-
const lcliGzSrc = path.join(repoBinDir, 'lcli.gz');
|
|
293
|
-
const yogaSrc = path.join(repoBinDir, 'yoga.wasm');
|
|
294
|
-
const lcliDest = path.join(installDir, 'lcli');
|
|
295
|
-
const yogaDest = path.join(installDir, 'yoga.wasm');
|
|
296
|
-
if (!fs.existsSync(lcliGzSrc)) {
|
|
297
|
-
this.emitStatus({ type: 'error', message: 'Binary not found in repository (bin/lcli.gz)' });
|
|
298
|
-
return false;
|
|
299
|
-
}
|
|
300
|
-
if (!fs.existsSync(installDir)) {
|
|
301
|
-
fs.mkdirSync(installDir, { recursive: true });
|
|
302
|
-
}
|
|
303
|
-
await rm(lcliDest, { force: true });
|
|
304
|
-
await pipeline(createReadStream(lcliGzSrc), zlib.createGunzip(), createWriteStream(lcliDest));
|
|
305
|
-
await chmod(lcliDest, 0o755);
|
|
306
|
-
if (fs.existsSync(yogaSrc)) {
|
|
307
|
-
await copyFile(yogaSrc, yogaDest);
|
|
308
|
-
}
|
|
309
|
-
await this.ensurePathConfigured(installDir);
|
|
310
|
-
await this.unlinkNpm();
|
|
311
|
-
logger.debug('Binaries copied successfully');
|
|
312
|
-
return true;
|
|
313
|
-
}
|
|
314
|
-
catch (error) {
|
|
315
|
-
logger.error('Copy binaries internal failed', error);
|
|
316
|
-
return false;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
async ensurePathConfigured(binDir) {
|
|
320
|
-
const pathExport = `export PATH="${binDir}:$PATH"`;
|
|
321
|
-
const marker = '# local-cli binary';
|
|
322
|
-
const shell = process.env['SHELL'] || '/bin/bash';
|
|
323
|
-
let rcFile;
|
|
324
|
-
if (shell.includes('zsh')) {
|
|
325
|
-
rcFile = path.join(os.homedir(), '.zshrc');
|
|
326
|
-
}
|
|
327
|
-
else {
|
|
328
|
-
rcFile = path.join(os.homedir(), '.bashrc');
|
|
329
|
-
}
|
|
330
|
-
try {
|
|
331
|
-
let content = '';
|
|
332
|
-
if (fs.existsSync(rcFile)) {
|
|
333
|
-
content = fs.readFileSync(rcFile, 'utf-8');
|
|
334
|
-
}
|
|
335
|
-
if (content.includes(marker)) {
|
|
336
|
-
logger.debug('PATH already configured (marker found)');
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
const currentPath = process.env['PATH'] || '';
|
|
340
|
-
if (currentPath.split(':').includes(binDir)) {
|
|
341
|
-
logger.debug('PATH already contains binDir, adding marker for future reference');
|
|
342
|
-
fs.appendFileSync(rcFile, `\n${marker}\n# PATH already configured elsewhere\n`);
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
const addition = `\n${marker}\n${pathExport}\n`;
|
|
346
|
-
fs.appendFileSync(rcFile, addition);
|
|
347
|
-
logger.debug('PATH configuration added to ' + rcFile);
|
|
348
|
-
}
|
|
349
|
-
catch (error) {
|
|
350
|
-
logger.debug('Failed to configure PATH: ' + (error instanceof Error ? error.message : String(error)));
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
async unlinkNpm() {
|
|
354
|
-
try {
|
|
355
|
-
await execAsync('npm unlink -g local-cli');
|
|
356
|
-
}
|
|
357
|
-
catch (error) {
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
async cleanupRepo() {
|
|
361
|
-
try {
|
|
362
|
-
if (fs.existsSync(this.repoDir)) {
|
|
363
|
-
logger.debug('Cleaning up repo directory to prevent source code exposure');
|
|
364
|
-
await rm(this.repoDir, { recursive: true, force: true });
|
|
365
|
-
logger.debug('Repo directory removed successfully');
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
catch (error) {
|
|
369
|
-
logger.debug('Failed to cleanup repo: ' + (error instanceof Error ? error.message : String(error)));
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
export default GitAutoUpdater;
|
|
374
|
-
//# sourceMappingURL=git-auto-updater.js.map
|