@orbitpanel/cli 0.4.5 → 0.6.0

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/lib/shell.js CHANGED
@@ -1,656 +1,332 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
1
  /**
3
2
  * Interactive REPL shell for @orbitpanel/cli.
4
- * Built with React + Ink same stack as Claude Code, Gemini CLI, Codex CLI.
3
+ * Thin orchestration layerdelegates to shell-commands.ts and shell-render.ts.
4
+ * Pure readline + chalk — no Ink/React dependency.
5
5
  */
6
- import React, { useState, useEffect } from 'react';
7
- import { render, Box, Text, useApp, useInput } from 'ink';
8
- import TextInput from 'ink-text-input';
9
- import Spinner from 'ink-spinner';
10
- import gradient from 'gradient-string';
11
- import { loadConfig, DEFAULT_API_URL } from './config.js';
12
- import { OrbitClient } from './client.js';
13
- import { isAuthenticated, requestDeviceCode, pollDeviceCode, saveDeviceAuth, openBrowser } from './device-auth.js';
6
+ import chalk from 'chalk';
7
+ import ora from 'ora';
8
+ import { createInterface } from 'node:readline';
9
+ import { loadConfig } from './config.js';
14
10
  import { loadSiteLink } from './site.js';
15
- import { loadActiveSession, createSession, addNote, endSession, } from './session.js';
16
- import { getGitBranch, getGitCommitHead, getGitDiffStat } from './git.js';
17
- import { buildReportPayload } from './report-builder.js';
11
+ import { loadActiveSession } from './session.js';
12
+ import { getGitBranch } from './git.js';
18
13
  import { sendChat } from './ai-chat.js';
19
- const orbitGrad = gradient(['#2563EB', '#3B82F6', '#60A5FA', '#0D9488']);
20
- /** All available slash commands with descriptions. */
21
- const SLASH_COMMANDS = [
22
- { cmd: '/status', alias: '/s', desc: 'panoramica completa' },
23
- { cmd: '/doctor', alias: '/doc', desc: 'verifica configurazione' },
24
- { cmd: '/session start', alias: '/ss', desc: 'avvia sessione di lavoro' },
25
- { cmd: '/session end', alias: '/se', desc: 'chiudi sessione' },
26
- { cmd: '/session', alias: '', desc: 'info sessione attiva' },
27
- { cmd: '/note', alias: '/n', desc: 'aggiungi nota (+ testo)' },
28
- { cmd: '/report', alias: '/r', desc: 'invia report a Orbit' },
29
- { cmd: '/list', alias: '/ls', desc: 'lista interventi' },
30
- { cmd: '/get', alias: '/g', desc: 'dettaglio intervento (+ id)' },
31
- { cmd: '/sites', alias: '', desc: 'lista siti e seleziona' },
32
- { cmd: '/auth', alias: '', desc: 'accedi con browser (consigliato)' },
33
- { cmd: '/login', alias: '', desc: 'configura token (+ token)' },
34
- { cmd: '/link', alias: '', desc: 'collega directory (+ site_id)' },
35
- { cmd: '/clear', alias: '', desc: 'pulisci schermo' },
36
- { cmd: '/help', alias: '/h', desc: 'lista comandi' },
37
- { cmd: '/exit', alias: '/q', desc: 'esci' },
38
- ];
39
- /** Filter slash commands matching current input. */
40
- function getCommandSuggestions(input) {
41
- if (!input.startsWith('/'))
42
- return [];
43
- const query = input.toLowerCase();
44
- if (query === '/')
45
- return SLASH_COMMANDS;
46
- return SLASH_COMMANDS.filter(c => c.cmd.startsWith(query) || (c.alias && c.alias.startsWith(query)));
14
+ import { OrbitStore } from '../state/store.js';
15
+ import { sendChatStream } from './ai-stream.js';
16
+ import { StreamStateMachine } from './stream-state.js';
17
+ import { outputBlock } from './ui.js';
18
+ import { B, BL, BB, YELLOW, DIM, WHITE, orbitGrad, sparkle, logoLines, SLASH_COMMANDS, sep, } from './shell-render.js';
19
+ import { statusLine } from './ui.js';
20
+ import { cmdStatus, cmdDoctor, cmdSessionStart, cmdSessionEnd, cmdSessionInfo, cmdNoteAdd, cmdReport, cmdList, cmdGet, cmdSites, cmdAuth, cmdLogin, cmdLink, } from './shell-commands.js';
21
+ // Default IO adapter bridges store to real filesystem modules
22
+ const defaultStoreIO = {
23
+ loadConfig,
24
+ loadSiteLink: (cwd) => loadSiteLink(cwd),
25
+ loadActiveSession: (siteId) => loadActiveSession(siteId),
26
+ getGitBranch,
27
+ };
28
+ function sleep(ms) {
29
+ return new Promise(r => setTimeout(r, ms));
47
30
  }
48
- /** Autocomplete suggestions component with arrow key navigation. */
49
- function CommandSuggestions({ input, selectedIndex }) {
50
- const suggestions = getCommandSuggestions(input);
51
- if (suggestions.length === 0)
52
- return null;
53
- return (_jsx(Box, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: suggestions.map((s, i) => {
54
- const isSelected = i === selectedIndex;
55
- return (_jsxs(Box, { children: [_jsx(Text, { color: isSelected ? '#2563EB' : undefined, children: isSelected ? '❯ ' : ' ' }), _jsx(Text, { color: isSelected ? '#2563EB' : undefined, bold: isSelected, children: s.cmd.padEnd(18) }), s.alias ? _jsx(Text, { dimColor: true, children: s.alias.padEnd(6) }) : _jsx(Text, { children: ' ' }), _jsx(Text, { dimColor: true, children: s.desc })] }, i));
56
- }) }));
31
+ // Tab-completion handler
32
+ function completer(line) {
33
+ if (!line.startsWith('/'))
34
+ return [[], line];
35
+ const hits = SLASH_COMMANDS
36
+ .filter(c => c.cmd.startsWith(line.toLowerCase()) || (c.alias && c.alias.startsWith(line.toLowerCase())))
37
+ .map(c => c.cmd);
38
+ return [hits.length ? hits : SLASH_COMMANDS.map(c => c.cmd), line];
57
39
  }
58
- const logoLines = [
59
- ' ██████╗ ██████╗ ██████╗ ██╗████████╗',
60
- ' ██╔═══██╗██╔══██╗██╔══██╗██║╚══██╔══╝',
61
- ' ██║ ██║██████╔╝██████╔╝██║ ██║ ',
62
- ' ██║ ██║██╔══██╗██╔══██╗██║ ██║ ',
63
- ' ╚██████╔╝██║ ██║██████╔╝██║ ██║ ',
64
- ' ╚═════╝ ╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝ ',
65
- ];
66
- function Header({ version }) {
67
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [logoLines.map((line, i) => (_jsx(Text, { children: orbitGrad(line) }, i))), _jsx(Text, { children: '' }), _jsxs(Text, { children: [_jsx(Text, { backgroundColor: "#2563EB", color: "white", bold: true, children: ' CLI ' }), _jsx(Text, { backgroundColor: "#1E293B", color: "#60A5FA", children: ` v${version} ` }), _jsx(Text, { dimColor: true, children: " \u00B7 " }), _jsx(Text, { color: "#60A5FA", children: "Gestione Orbit Panel AI-powered" })] })] }));
68
- }
69
- function StatusBar() {
70
- const [status, setStatus] = useState([]);
71
- useEffect(() => {
72
- (async () => {
73
- const config = await loadConfig();
74
- const tokenEntry = config.default_site ? config.tokens[config.default_site] : undefined;
75
- const siteLink = await loadSiteLink(process.cwd());
76
- const session = siteLink ? await loadActiveSession(siteLink.orbit_site_id) : null;
77
- const parts = [];
78
- parts.push(tokenEntry ? '● Connesso' : '○ Non connesso');
79
- if (siteLink)
80
- parts.push(siteLink.final_url ?? siteLink.orbit_site_id.slice(0, 16));
81
- if (session) {
82
- const elapsed = Math.round((Date.now() - new Date(session.started_at).getTime()) / 60_000);
83
- parts.push(`● Sessione ${elapsed}min`);
84
- if (session.git_branch)
85
- parts.push(session.git_branch);
86
- }
87
- setStatus(parts);
88
- })();
89
- }, []);
90
- if (status.length === 0)
91
- return null;
92
- return (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { children: " " }), status.map((s, i) => (_jsxs(React.Fragment, { children: [i > 0 && _jsx(Text, { dimColor: true, children: " \u2502 " }), _jsx(Text, { color: s.startsWith('') ? '#22C55E' : s.startsWith('○') ? '#EF4444' : '#60A5FA', children: s })] }, i)))] }));
93
- }
94
- function OutputArea({ lines }) {
95
- return (_jsx(Box, { flexDirection: "column", children: lines.map((line, i) => (_jsx(Text, { color: line.color, dimColor: line.dim, bold: line.bold, children: line.text }, i))) }));
96
- }
97
- function Shell({ version }) {
98
- const { exit } = useApp();
99
- const [input, setInput] = useState('');
100
- const [output, setOutput] = useState([]);
101
- const [loading, setLoading] = useState(false);
102
- const [loadingText, setLoadingText] = useState('');
103
- const [history, setHistory] = useState([]);
104
- const [selectedIndex, setSelectedIndex] = useState(-1);
105
- const addOutput = (...lines) => {
106
- setOutput(prev => [...prev, ...lines]);
40
+ export async function startShell(version) {
41
+ // Header with line-by-line animation
42
+ console.clear();
43
+ console.log('');
44
+ for (const line of logoLines) {
45
+ console.log(orbitGrad(line));
46
+ await sleep(50);
47
+ }
48
+ console.log('');
49
+ console.log(` ${chalk.bgRgb(37, 99, 235).white.bold(' CLI ')} ${chalk.bgHex('#1E293B').hex('#60A5FA')(` v${version} `)} ${DIM('·')} ${BL('Gestione Orbit Panel AI-powered')}`);
50
+ // Initialize centralized state
51
+ const cwd = process.cwd();
52
+ const store = new OrbitStore(defaultStoreIO);
53
+ await store.init(cwd);
54
+ const { config, tokenEntry, siteLink, session, gitBranch, runtime } = store.getState();
55
+ // Status bar (using centralized statusLine renderer)
56
+ const sessionMinutes = session
57
+ ? Math.round((Date.now() - new Date(session.started_at).getTime()) / 60_000)
58
+ : undefined;
59
+ console.log('');
60
+ console.log(statusLine({
61
+ connected: !!tokenEntry,
62
+ siteUrl: siteLink?.final_url ?? (siteLink ? siteLink.orbit_site_id.slice(0, 16) : undefined),
63
+ sessionMinutes,
64
+ gitBranch: (session?.git_branch ?? gitBranch) ?? undefined,
65
+ runtimeMode: runtime,
66
+ }));
67
+ console.log('');
68
+ sep();
69
+ // Auto-prompt auth
70
+ if (!tokenEntry) {
71
+ console.log('');
72
+ console.log(` ${YELLOW('Non sei autenticato.')}`);
73
+ console.log('');
74
+ console.log(` ${BL('/auth')} accedi con il browser (consigliato)`);
75
+ console.log(` ${DIM('/login <t>')} inserisci un token MCP manualmente`);
76
+ console.log('');
77
+ }
78
+ else {
79
+ console.log(` ${DIM('Digita /help per la lista comandi, o scrivi in linguaggio naturale.')}`);
80
+ }
81
+ console.log('');
82
+ // REPL
83
+ const rl = createInterface({
84
+ input: process.stdin,
85
+ output: process.stdout,
86
+ prompt: ` ${B('orbit')} ${DIM('›')} `,
87
+ completer,
88
+ terminal: true,
89
+ });
90
+ rl.on('SIGINT', () => {
91
+ console.log(`\n\n ${DIM('Alla prossima!')}\n`);
92
+ rl.close();
93
+ });
94
+ process.on('SIGTSTP', () => {
95
+ console.log(`\n\n ${DIM('Alla prossima!')}\n`);
96
+ rl.close();
97
+ process.exit(0);
98
+ });
99
+ // Prompt function for risk confirmations (uses the active readline)
100
+ const promptConfirm = (message) => {
101
+ return new Promise((resolve) => {
102
+ rl.question(message, (answer) => {
103
+ const a = answer.trim().toLowerCase();
104
+ resolve(a === 's' || a === 'si' || a === 'y' || a === 'yes');
105
+ });
106
+ });
107
107
  };
108
- const clearOutput = () => setOutput([]);
109
- // Auto-prompt auth on first launch if not authenticated
110
- useEffect(() => {
111
- (async () => {
112
- const authed = await isAuthenticated();
113
- if (!authed) {
114
- addOutput({ text: '' }, { text: ' Non sei autenticato.', color: '#EAB308' }, { text: '' }, { text: ' Scegli come accedere:', dim: false }, { text: ' /auth accedi con il browser (consigliato)', color: '#60A5FA' }, { text: ' /login <t> inserisci un token MCP manualmente', dim: true }, { text: '' });
115
- }
116
- })();
117
- }, []);
118
- // Get current suggestions for arrow navigation
119
- const suggestions = getCommandSuggestions(input);
120
- const showSuggestions = input.startsWith('/') && suggestions.length > 0 && !loading;
121
- useInput((ch, key) => {
122
- if (key.ctrl && ch === 'c') {
123
- addOutput({ text: '', dim: true }, { text: ' Alla prossima!', dim: true }, { text: '' });
124
- setTimeout(() => exit(), 100);
125
- return;
126
- }
127
- // Arrow navigation in suggestions
128
- if (showSuggestions) {
129
- if (key.downArrow) {
130
- setSelectedIndex(prev => (prev + 1) % suggestions.length);
108
+ rl.prompt();
109
+ return new Promise((resolve) => {
110
+ rl.on('close', () => resolve());
111
+ rl.on('line', async (input) => {
112
+ const raw = input.trim();
113
+ const cmd = raw.toLowerCase();
114
+ if (!cmd) {
115
+ rl.prompt();
131
116
  return;
132
117
  }
133
- if (key.upArrow) {
134
- setSelectedIndex(prev => prev <= 0 ? suggestions.length - 1 : prev - 1);
118
+ // --- Command routing (thin dispatcher) ---
119
+ if (cmd === '/exit' || cmd === '/quit' || cmd === '/q') {
120
+ console.log(`\n ${DIM('Alla prossima!')}\n`);
121
+ rl.close();
135
122
  return;
136
123
  }
137
- // Right arrow or Tab on selected item = autocomplete without submit
138
- if ((key.rightArrow || key.tab) && selectedIndex >= 0 && selectedIndex < suggestions.length) {
139
- const selected = suggestions[selectedIndex];
140
- const needsArg = ['/note', '/get', '/login', '/link', '/auth'].indexOf(selected.cmd) === -1 ? false : ['/note', '/get', '/login', '/link'].includes(selected.cmd);
141
- setInput(selected.cmd + (needsArg ? ' ' : ''));
142
- setSelectedIndex(-1);
124
+ if (cmd === '/clear' || cmd === '/cls') {
125
+ console.clear();
126
+ rl.prompt();
143
127
  return;
144
128
  }
145
- // Return on selected = autocomplete and submit (or fill if needs arg)
146
- if (key.return && selectedIndex >= 0 && selectedIndex < suggestions.length) {
147
- const selected = suggestions[selectedIndex];
148
- const needsArg = ['/note', '/get', '/login', '/link'].includes(selected.cmd);
149
- if (needsArg) {
150
- // Autocomplete but don't submit — needs argument
151
- setInput(selected.cmd + ' ');
152
- setSelectedIndex(-1);
153
- return;
154
- }
155
- // Submit directly
156
- setInput(selected.cmd);
157
- setSelectedIndex(-1);
158
- // Don't return — let TextInput handle the submit
159
- }
160
- }
161
- // Reset selection when typing
162
- if (!key.downArrow && !key.upArrow && !key.tab && !key.return) {
163
- setSelectedIndex(-1);
164
- }
165
- });
166
- const handleSubmit = async (value) => {
167
- const raw = value.trim();
168
- setInput('');
169
- if (!raw)
170
- return;
171
- setHistory(prev => [...prev, raw]);
172
- const cmd = raw.toLowerCase();
173
- // Exit
174
- if (cmd === '/exit' || cmd === '/quit' || cmd === '/q') {
175
- addOutput({ text: '' }, { text: ' Alla prossima!', dim: true }, { text: '' });
176
- setTimeout(() => exit(), 100);
177
- return;
178
- }
179
- // Clear
180
- if (cmd === '/clear' || cmd === '/cls') {
181
- clearOutput();
182
- return;
183
- }
184
- // Help / List
185
- if (cmd === '/help' || cmd === '/h') {
186
- const helpLines = [
187
- { text: '' },
188
- { text: ' ✦ Comandi disponibili', color: '#60A5FA', bold: true },
189
- { text: '' },
190
- ];
191
- for (const c of SLASH_COMMANDS) {
192
- const alias = c.alias ? ` ${c.alias}` : '';
193
- helpLines.push({ text: ` ${c.cmd.padEnd(18)}${alias.padEnd(8)}${c.desc}` });
194
- }
195
- helpLines.push({ text: '' });
196
- addOutput(...helpLines);
197
- return;
198
- }
199
- // Status
200
- if (cmd === '/status' || cmd === '/s') {
201
- setLoading(true);
202
- setLoadingText('Caricamento stato...');
203
- try {
204
- const config = await loadConfig();
205
- const client = getClient(config);
206
- const lines = [{ text: '' }, { text: ' ✦ Stato', color: '#2563EB', bold: true }, { text: '' }];
207
- if (!client) {
208
- lines.push({ text: ' ✗ Token non configurato', color: '#EF4444' });
209
- lines.push({ text: ' Esegui: /login <token>', dim: true });
210
- }
211
- else {
212
- const t = config.tokens[config.default_site];
213
- const v = await client.validateToken();
214
- lines.push({ text: ` ✓ Token ${t.token.slice(0, 15)}...`, color: v.valid ? '#22C55E' : '#EF4444' });
215
- lines.push({ text: ` ✓ API ${v.valid ? 'raggiungibile' : v.error}`, color: v.valid ? '#22C55E' : '#EF4444' });
216
- const siteLink = await loadSiteLink(process.cwd());
217
- if (siteLink)
218
- lines.push({ text: ` ✓ Sito ${siteLink.final_url ?? siteLink.orbit_site_id}`, color: '#60A5FA' });
219
- const session = siteLink ? await loadActiveSession(siteLink.orbit_site_id) : null;
220
- if (session) {
221
- const elapsed = Math.round((Date.now() - new Date(session.started_at).getTime()) / 60_000);
222
- lines.push({ text: ` ✓ Sessione attiva (${elapsed}min · ${session.notes.length} note)`, color: '#22C55E' });
223
- }
224
- if (v.valid) {
225
- try {
226
- const stats = await client.getStats();
227
- const total = stats.total_interventions ?? 0;
228
- lines.push({ text: '' });
229
- lines.push({ text: ` ${total} interventi totali`, color: '#60A5FA' });
230
- }
231
- catch { }
232
- }
233
- }
234
- lines.push({ text: '' });
235
- addOutput(...lines);
236
- }
237
- finally {
238
- setLoading(false);
239
- }
240
- return;
241
- }
242
- // Doctor
243
- if (cmd === '/doctor' || cmd === '/doc') {
244
- setLoading(true);
245
- setLoadingText('Diagnostica...');
246
- try {
247
- const config = await loadConfig();
248
- const lines = [{ text: '' }, { text: ' ✦ Diagnostica', color: '#2563EB', bold: true }, { text: '' }];
249
- const t = config.default_site ? config.tokens[config.default_site] : undefined;
250
- lines.push({ text: ` ${t ? '✓' : '!'} Token ${t ? t.token.slice(0, 15) + '...' : 'non configurato'}`, color: t ? '#22C55E' : '#EAB308' });
251
- if (t) {
252
- const c = new OrbitClient(t.token, config.api_url);
253
- const start = Date.now();
254
- const v = await c.validateToken();
255
- lines.push({ text: ` ${v.valid ? '✓' : '✗'} API ${v.valid ? `raggiungibile (${Date.now() - start}ms)` : v.error}`, color: v.valid ? '#22C55E' : '#EF4444' });
256
- }
257
- const site = await loadSiteLink(process.cwd());
258
- lines.push({ text: ` ${site ? '✓' : '!'} Sito ${site ? '.orbit.json presente' : 'non collegato'}`, color: site ? '#22C55E' : '#EAB308' });
259
- const branch = await getGitBranch();
260
- lines.push({ text: ` ${branch ? '✓' : '!'} Git ${branch ?? 'nessun repository'}`, color: branch ? '#22C55E' : '#EAB308' });
261
- const allOk = !!(t && site && branch);
262
- lines.push({ text: '' });
263
- lines.push({ text: allOk ? ' ✓ Tutto ok — pronto per lavorare' : ' ! Attenzione — vedi sopra', color: allOk ? '#22C55E' : '#EAB308', bold: true });
264
- lines.push({ text: '' });
265
- addOutput(...lines);
266
- }
267
- finally {
268
- setLoading(false);
269
- }
270
- return;
271
- }
272
- // Session start
273
- if (cmd === '/session start' || cmd === '/ss') {
274
- setLoading(true);
275
- setLoadingText('Avvio sessione...');
276
- try {
277
- const siteLink = await loadSiteLink(process.cwd());
278
- if (!siteLink) {
279
- addOutput({ text: '' }, { text: ' ✗ Nessun sito collegato. Esegui: /link <site_id>', color: '#EF4444' }, { text: '' });
280
- return;
281
- }
282
- const existing = await loadActiveSession(siteLink.orbit_site_id);
283
- if (existing) {
284
- addOutput({ text: '' }, { text: ` ! Sessione gia attiva (${existing.id}). Chiudi con: /session end`, color: '#EAB308' }, { text: '' });
285
- return;
286
- }
287
- const branch = await getGitBranch();
288
- const commit = await getGitCommitHead();
289
- const s = await createSession(siteLink.orbit_site_id, { git_branch: branch ?? undefined, git_commit: commit ?? undefined });
290
- addOutput({ text: '' }, { text: ' ✦ Sessione avviata', color: '#2563EB', bold: true }, { text: '' }, { text: ` ID ${s.id}`, color: '#60A5FA' }, ...(branch ? [{ text: ` Branch ${branch}` }] : []), ...(commit ? [{ text: ` Commit ${commit}`, color: '#60A5FA' }] : []), { text: ` Inizio ${new Date().toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' })}` }, { text: '' });
291
- }
292
- finally {
293
- setLoading(false);
294
- }
295
- return;
296
- }
297
- // Session end
298
- if (cmd === '/session end' || cmd === '/se') {
299
- setLoading(true);
300
- setLoadingText('Chiusura sessione...');
301
- try {
302
- const siteLink = await loadSiteLink(process.cwd());
303
- if (!siteLink) {
304
- addOutput({ text: ' ✗ Nessun sito collegato', color: '#EF4444' });
305
- return;
306
- }
307
- const session = await loadActiveSession(siteLink.orbit_site_id);
308
- if (!session) {
309
- addOutput({ text: ' Nessuna sessione attiva', dim: true });
310
- return;
311
- }
312
- const commitEnd = await getGitCommitHead();
313
- await endSession(session.id, { git_commit_end: commitEnd ?? undefined });
314
- const elapsed = Math.round((Date.now() - new Date(session.started_at).getTime()) / 60_000);
315
- addOutput({ text: '' }, { text: ` ✓ Sessione chiusa (${elapsed}min · ${session.notes.length} note)`, color: '#22C55E', bold: true }, { text: ' Invia il report con: /report', dim: true }, { text: '' });
316
- }
317
- finally {
318
- setLoading(false);
319
- }
320
- return;
321
- }
322
- // Session info
323
- if (cmd === '/session' || cmd === '/session info') {
324
- const siteLink = await loadSiteLink(process.cwd());
325
- if (!siteLink) {
326
- addOutput({ text: ' Nessun sito collegato', dim: true });
129
+ if (cmd === '/help' || cmd === '/h') {
130
+ printHelp();
131
+ rl.prompt();
327
132
  return;
328
133
  }
329
- const session = siteLink ? await loadActiveSession(siteLink.orbit_site_id) : null;
330
- if (!session) {
331
- addOutput({ text: ' Nessuna sessione attiva', dim: true });
134
+ if (cmd === '/status' || cmd === '/s') {
135
+ await cmdStatus();
136
+ rl.prompt();
332
137
  return;
333
138
  }
334
- const elapsed = Math.round((Date.now() - new Date(session.started_at).getTime()) / 60_000);
335
- const lines = [
336
- { text: '' },
337
- { text: ' ✦ Sessione attiva', color: '#2563EB', bold: true },
338
- { text: '' },
339
- { text: ` ID ${session.id}`, color: '#60A5FA' },
340
- { text: ` Durata ${elapsed} min` },
341
- ];
342
- if (session.git_branch)
343
- lines.push({ text: ` Branch ${session.git_branch}`, color: '#60A5FA' });
344
- lines.push({ text: ` Note ${session.notes.length}` });
345
- for (const n of session.notes.slice(-5)) {
346
- lines.push({ text: ` · ${n.text}` });
139
+ if (cmd === '/doctor' || cmd === '/doc') {
140
+ await cmdDoctor();
141
+ rl.prompt();
142
+ return;
347
143
  }
348
- lines.push({ text: '' });
349
- addOutput(...lines);
350
- return;
351
- }
352
- // Note
353
- if (cmd.startsWith('/note ') || cmd.startsWith('/n ')) {
354
- const text = raw.slice(raw.indexOf(' ') + 1);
355
- const siteLink = await loadSiteLink(process.cwd());
356
- if (!siteLink) {
357
- addOutput({ text: ' ✗ Nessun sito collegato', color: '#EF4444' });
144
+ if (cmd === '/session start' || cmd === '/ss') {
145
+ await cmdSessionStart();
146
+ await store.refresh(cwd);
147
+ rl.prompt();
358
148
  return;
359
149
  }
360
- const session = await loadActiveSession(siteLink.orbit_site_id);
361
- if (!session) {
362
- addOutput({ text: ' Nessuna sessione attiva. /session start', dim: true });
150
+ if (cmd === '/session end' || cmd === '/se') {
151
+ await cmdSessionEnd();
152
+ await store.refresh(cwd);
153
+ rl.prompt();
363
154
  return;
364
155
  }
365
- await addNote(session.id, text, 'medium');
366
- addOutput({ text: '' }, { text: ` ✓ Nota aggiunta (${session.notes.length + 1} totali)`, color: '#22C55E', bold: true }, { text: ` │ ${text}`, color: '#2563EB' }, { text: '' });
367
- return;
368
- }
369
- // Report
370
- if (cmd === '/report' || cmd === '/r') {
371
- setLoading(true);
372
- setLoadingText('Invio report a Orbit...');
373
- try {
374
- const config = await loadConfig();
375
- const client = getClient(config);
376
- if (!client) {
377
- addOutput({ text: ' ✗ Token non configurato', color: '#EF4444' });
378
- return;
379
- }
380
- const siteLink = await loadSiteLink(process.cwd());
381
- if (!siteLink) {
382
- addOutput({ text: ' ✗ Nessun sito collegato', color: '#EF4444' });
383
- return;
384
- }
385
- let session = await loadActiveSession(siteLink.orbit_site_id);
386
- const commitEnd = await getGitCommitHead();
387
- let filesChanged = 0;
388
- if (session?.git_commit_start && commitEnd) {
389
- const diff = await getGitDiffStat(session.git_commit_start, commitEnd);
390
- filesChanged = diff.files_changed;
391
- }
392
- if (session && session.status === 'active') {
393
- session = await endSession(session.id, { git_commit_end: commitEnd ?? undefined });
394
- }
395
- const payload = buildReportPayload({
396
- session: session ?? { id: 'no-session', site_id: siteLink.orbit_site_id, started_at: new Date().toISOString(), notes: [], status: 'completed' },
397
- git_commit_end: commitEnd ?? undefined, files_changed: filesChanged,
398
- });
399
- payload.site_id = siteLink.orbit_site_id;
400
- const result = await client.createIntervention(payload);
401
- addOutput({ text: '' }, { text: ` ✓ Report inviato ${String(result.id).slice(0, 8)}`, color: '#22C55E', bold: true }, { text: '' });
156
+ if (cmd === '/session' || cmd === '/session info') {
157
+ await cmdSessionInfo();
158
+ rl.prompt();
159
+ return;
402
160
  }
403
- catch (err) {
404
- addOutput({ text: ` ✗ Invio fallito: ${err instanceof Error ? err.message : String(err)}`, color: '#EF4444' });
161
+ if (cmd.startsWith('/note ') || cmd.startsWith('/n ')) {
162
+ await cmdNoteAdd(raw.slice(raw.indexOf(' ') + 1));
163
+ rl.prompt();
164
+ return;
405
165
  }
406
- finally {
407
- setLoading(false);
166
+ if (cmd === '/report' || cmd === '/r') {
167
+ await cmdReport(promptConfirm);
168
+ await store.refresh(cwd);
169
+ rl.prompt();
170
+ return;
408
171
  }
409
- return;
410
- }
411
- // List interventions
412
- if (cmd === '/list' || cmd === '/ls' || cmd === '/interventions') {
413
- setLoading(true);
414
- setLoadingText('Caricamento interventi...');
415
- try {
416
- const config = await loadConfig();
417
- const client = getClient(config);
418
- if (!client) {
419
- addOutput({ text: ' ✗ Token non configurato', color: '#EF4444' });
420
- return;
421
- }
422
- const siteLink = await loadSiteLink(process.cwd());
423
- const filters = { limit: 10, sort: '-created_at' };
424
- if (siteLink)
425
- filters.site_id = siteLink.orbit_site_id;
426
- const result = await client.listInterventions(filters);
427
- const lines = [{ text: '' }, { text: ' ✦ Interventi', color: '#2563EB', bold: true }, { text: '' }];
428
- if (result.items.length === 0) {
429
- lines.push({ text: ' Nessun intervento trovato', dim: true });
430
- }
431
- else {
432
- for (const item of result.items) {
433
- const id = String(item.id ?? '').slice(0, 8);
434
- const t = String(item.title ?? '').slice(0, 35).padEnd(35);
435
- const s = item.status;
436
- const sIcon = s === 'completed' ? '●' : s === 'in_progress' ? '●' : '●';
437
- const sColor = s === 'completed' ? '#22C55E' : s === 'in_progress' ? '#EAB308' : '#60A5FA';
438
- const sLabel = s === 'completed' ? 'completato' : s === 'in_progress' ? 'in corso' : s === 'verified' ? 'verificato' : 'pianificato';
439
- const date = String(item.created_at ?? '').slice(5, 10);
440
- const origin = item.origin === 'cli' ? 'CLI' : String(item.origin);
441
- lines.push({ text: ` ${sIcon} ${id} ${t} ${sLabel.padEnd(12)} ${origin.padEnd(6)} ${date}`, color: sColor });
442
- }
443
- lines.push({ text: '' });
444
- lines.push({ text: ` ${result.total} interventi`, dim: true });
445
- }
446
- lines.push({ text: '' });
447
- addOutput(...lines);
172
+ if (cmd === '/list' || cmd === '/ls' || cmd === '/interventions') {
173
+ await cmdList();
174
+ rl.prompt();
175
+ return;
448
176
  }
449
- catch (err) {
450
- addOutput({ text: ` ✗ ${err instanceof Error ? err.message : String(err)}`, color: '#EF4444' });
177
+ if (cmd.startsWith('/get ') || cmd.startsWith('/g ')) {
178
+ await cmdGet(raw.split(' ')[1]);
179
+ rl.prompt();
180
+ return;
451
181
  }
452
- finally {
453
- setLoading(false);
182
+ if (cmd === '/sites' || cmd.startsWith('/sites ')) {
183
+ await cmdSites(cmd.split(' ')[1]);
184
+ await store.refresh(cwd);
185
+ rl.prompt();
186
+ return;
454
187
  }
455
- return;
456
- }
457
- // Get intervention
458
- if (cmd.startsWith('/get ') || cmd.startsWith('/g ')) {
459
- const id = raw.split(' ')[1];
460
- if (!id) {
461
- addOutput({ text: ' Uso: /get <intervention_id>', dim: true });
188
+ if (cmd === '/auth') {
189
+ await cmdAuth();
190
+ await store.refresh(cwd);
191
+ rl.prompt();
462
192
  return;
463
193
  }
464
- setLoading(true);
465
- setLoadingText('Caricamento...');
466
- try {
467
- const config = await loadConfig();
468
- const client = getClient(config);
469
- if (!client) {
470
- addOutput({ text: ' ✗ Token non configurato', color: '#EF4444' });
471
- return;
472
- }
473
- const item = await client.getIntervention(id);
474
- addOutput({ text: '' }, { text: ` ✦ ${String(item.title)}`, color: '#2563EB', bold: true }, { text: '' }, { text: ` ID ${String(item.id)}`, color: '#60A5FA' }, { text: ` Stato ${String(item.status)}` }, { text: ` Priorita ${String(item.priority)}` }, { text: ` Origin ${item.origin === 'cli' ? 'CLI' : String(item.origin)}`, color: item.origin === 'cli' ? '#2563EB' : undefined }, ...(item.type_tag ? [{ text: ` Tipo ${String(item.type_tag)}`, color: '#60A5FA' }] : []), { text: ` Creato ${String(item.created_at)}`, dim: true }, { text: '' });
194
+ if (cmd.startsWith('/login ')) {
195
+ await cmdLogin(raw.split(' ')[1]);
196
+ await store.refresh(cwd);
197
+ rl.prompt();
198
+ return;
475
199
  }
476
- catch (err) {
477
- addOutput({ text: ` ✗ ${err instanceof Error ? err.message : String(err)}`, color: '#EF4444' });
200
+ if (cmd.startsWith('/link ')) {
201
+ await cmdLink(raw.split(' ')[1]);
202
+ await store.refresh(cwd);
203
+ rl.prompt();
204
+ return;
478
205
  }
479
- finally {
480
- setLoading(false);
206
+ if (cmd.startsWith('/')) {
207
+ console.log(`\n ${DIM('Comando sconosciuto:')} ${WHITE(cmd)} ${DIM('— digita /help')}\n`);
208
+ rl.prompt();
209
+ return;
481
210
  }
482
- return;
483
- }
484
- // List and select sites
485
- if (cmd === '/sites' || cmd.startsWith('/sites ')) {
486
- setLoading(true);
487
- setLoadingText('Caricamento siti...');
488
- try {
489
- const config = await loadConfig();
490
- const client = getClient(config);
491
- if (!client) {
492
- setLoading(false);
493
- addOutput({ text: ' ✗ Non autenticato. Usa /auth', color: '#EF4444' });
494
- return;
211
+ // Free text → AI chat (streaming with state machine + fallback)
212
+ await handleAIChat(raw, store);
213
+ rl.prompt();
214
+ });
215
+ });
216
+ }
217
+ // --- Help output ---
218
+ function printHelp() {
219
+ console.log('');
220
+ console.log(` ${sparkle} ${BB('Comandi disponibili')}`);
221
+ console.log('');
222
+ for (const c of SLASH_COMMANDS) {
223
+ const alias = c.alias ? DIM(` ${c.alias}`) : '';
224
+ console.log(` ${WHITE(c.cmd.padEnd(18))}${alias.padEnd(12)}${DIM(c.desc)}`);
225
+ }
226
+ console.log('');
227
+ console.log(` ${DIM('Oppure scrivi in linguaggio naturale per parlare con Orbit AI.')}`);
228
+ console.log('');
229
+ }
230
+ // --- AI Chat with streaming + state machine ---
231
+ async function handleAIChat(raw, store) {
232
+ const { config, siteLink } = store.getState();
233
+ console.log('');
234
+ console.log(` ${sparkle} ${BB('Orbit AI')}`);
235
+ const spinner = ora({ text: DIM('Recupero contesto...'), color: 'blue', indent: 2 }).start();
236
+ const sm = new StreamStateMachine();
237
+ try {
238
+ store.startRequest();
239
+ const result = await sendChatStream(raw, (event) => {
240
+ // State machine gates event processing
241
+ if (!sm.transition(event.type))
242
+ return;
243
+ switch (event.type) {
244
+ case 'status': {
245
+ const msg = (event.data.message ?? event.data.phase ?? '');
246
+ if (spinner.isSpinning)
247
+ spinner.text = DIM(msg);
248
+ break;
495
249
  }
496
- const sites = await client.listSites();
497
- setLoading(false);
498
- if (!Array.isArray(sites) || sites.length === 0) {
499
- addOutput({ text: '' }, { text: ' Nessun sito trovato.', dim: true }, { text: '' });
500
- return;
250
+ case 'thinking': {
251
+ if (spinner.isSpinning)
252
+ spinner.text = DIM('Analisi in corso...');
253
+ break;
501
254
  }
502
- // If a number is passed, select that site
503
- const selectNum = cmd.split(' ')[1];
504
- if (selectNum && /^\d+$/.test(selectNum)) {
505
- const idx = parseInt(selectNum, 10) - 1;
506
- if (idx >= 0 && idx < sites.length) {
507
- const site = sites[idx];
508
- const siteId = String(site.id);
509
- const domain = String(site.domain ?? site.home_url ?? siteId);
510
- const { saveSiteLink } = await import('./site.js');
511
- await saveSiteLink(process.cwd(), {
512
- orbit_site_id: siteId,
513
- platform: 'wordpress',
514
- final_url: String(site.home_url ?? site.domain ?? ''),
515
- });
516
- addOutput({ text: '' }, { text: ` ✓ Sito selezionato: ${domain}`, color: '#22C55E', bold: true }, { text: ` ID: ${siteId}`, dim: true }, { text: '' });
517
- }
518
- else {
519
- addOutput({ text: ` ✗ Numero non valido. Scegli tra 1 e ${sites.length}`, color: '#EF4444' });
255
+ case 'delta': {
256
+ if (spinner.isSpinning) {
257
+ spinner.stop();
258
+ console.log('');
259
+ process.stdout.write(' ');
520
260
  }
521
- return;
261
+ const chunk = event.data.content;
262
+ process.stdout.write(chunk.replaceAll('\n', '\n '));
263
+ break;
522
264
  }
523
- // Show list with numbers
524
- const lines = [
525
- { text: '' },
526
- { text: ' ✦ Siti disponibili', color: '#2563EB', bold: true },
527
- { text: '' },
528
- ];
529
- sites.forEach((site, i) => {
530
- const num = String(i + 1).padStart(2);
531
- const domain = String(site.domain ?? site.home_url ?? '—').slice(0, 35).padEnd(35);
532
- const status = site.status;
533
- const statusIcon = status === 'active' ? '●' : '○';
534
- const statusColor = status === 'active' ? '#22C55E' : '#EF4444';
535
- const platform = String(site.wp_version ? `WP ${site.wp_version}` : 'wordpress').padEnd(10);
536
- const id = String(site.id ?? '').slice(0, 8);
537
- lines.push({
538
- text: ` ${num}. ${statusIcon} ${domain} ${platform} ${id}`,
539
- color: statusColor,
540
- });
541
- });
542
- lines.push({ text: '' });
543
- lines.push({ text: ' Seleziona con: /sites <numero>', dim: true });
544
- lines.push({ text: '' });
545
- addOutput(...lines);
546
- }
547
- catch (err) {
548
- setLoading(false);
549
- addOutput({ text: ` ✗ ${err instanceof Error ? err.message : String(err)}`, color: '#EF4444' });
550
- }
551
- return;
552
- }
553
- // Auth via browser (Device Code Flow)
554
- if (cmd === '/auth') {
555
- setLoading(true);
556
- setLoadingText('Generazione codice...');
557
- try {
558
- const config = await loadConfig();
559
- const apiUrl = config.api_url || DEFAULT_API_URL;
560
- const deviceCode = await requestDeviceCode(apiUrl);
561
- setLoading(false);
562
- addOutput({ text: '' }, { text: ' ✦ Accesso con browser', color: '#2563EB', bold: true }, { text: '' }, { text: ` Apri questo URL nel browser:`, dim: false }, { text: ` ${deviceCode.verification_url}`, color: '#60A5FA' }, { text: '' }, { text: ` Codice: ${deviceCode.user_code}`, color: '#2563EB', bold: true }, { text: '' }, { text: ' In attesa di autorizzazione...', dim: true });
563
- // Open browser automatically
564
- await openBrowser(deviceCode.verification_url);
565
- // Poll for authorization
566
- setLoading(true);
567
- setLoadingText('In attesa di autorizzazione dal browser...');
568
- const result = await pollDeviceCode(apiUrl, deviceCode.device_code);
569
- await saveDeviceAuth(result);
570
- setLoading(false);
571
- addOutput({ text: '' }, { text: ` ✓ Autenticato come ${result.user_email ?? 'utente'}`, color: '#22C55E', bold: true }, { text: ' Token salvato in ~/.orbit/config.json', dim: true }, { text: '' });
572
- }
573
- catch (err) {
574
- setLoading(false);
575
- addOutput({ text: ` ✗ Auth fallita: ${err instanceof Error ? err.message : String(err)}`, color: '#EF4444' });
576
- }
577
- return;
578
- }
579
- // Login with token
580
- if (cmd.startsWith('/login ')) {
581
- const token = raw.split(' ')[1];
582
- if (!token?.startsWith('orbit_mcp_')) {
583
- addOutput({ text: ' Uso: /login orbit_mcp_xxx', dim: true });
584
- return;
265
+ case 'error': {
266
+ if (spinner.isSpinning)
267
+ spinner.stop();
268
+ console.log(`\n ${chalk.red('')} ${event.data.message}`);
269
+ break;
270
+ }
271
+ case 'done':
272
+ if (spinner.isSpinning)
273
+ spinner.stop();
274
+ break;
275
+ }
276
+ }, config, siteLink);
277
+ if (spinner.isSpinning)
278
+ spinner.stop();
279
+ store.endRequest();
280
+ const contentStarted = sm.current === 'generating' || sm.current === 'done';
281
+ // Fallback: stream failed before any content → try non-streaming
282
+ if (result.error && !contentStarted) {
283
+ const fallback = await sendChat(raw);
284
+ if (fallback.error) {
285
+ console.log('');
286
+ console.log(outputBlock('error', fallback.error));
585
287
  }
586
- setLoading(true);
587
- setLoadingText('Validazione token...');
588
- try {
589
- const client = new OrbitClient(token, 'https://api.orbit.principi.it');
590
- const v = await client.validateToken();
591
- if (v.valid) {
592
- const { setToken } = await import('./config.js');
593
- await setToken('default', { token, label: 'default', site_id: 'default', created_at: new Date().toISOString() });
594
- addOutput({ text: '' }, { text: ' ✓ Autenticazione riuscita', color: '#22C55E', bold: true }, { text: '' });
288
+ else {
289
+ console.log('');
290
+ for (const line of (fallback.response ?? '').split('\n')) {
291
+ console.log(` ${line}`);
595
292
  }
596
- else {
597
- addOutput({ text: ` ✗ Token non valido: ${v.error}`, color: '#EF4444' });
293
+ if (fallback.provider_used) {
294
+ console.log(`\n ${DIM(`(${fallback.provider_used})`)}`);
598
295
  }
599
296
  }
600
- finally {
601
- setLoading(false);
602
- }
603
- return;
604
- }
605
- // Link
606
- if (cmd.startsWith('/link ')) {
607
- const siteId = raw.split(' ')[1];
608
- if (!siteId) {
609
- addOutput({ text: ' Uso: /link <site_id>', dim: true });
610
- return;
611
- }
612
- const { saveSiteLink, detectPlatform } = await import('./site.js');
613
- const platform = await detectPlatform(process.cwd());
614
- await saveSiteLink(process.cwd(), { orbit_site_id: siteId, platform });
615
- addOutput({ text: '' }, { text: ` ✓ Sito collegato (${platform})`, color: '#22C55E', bold: true }, { text: '' });
616
- return;
617
297
  }
618
- // Unknown command
619
- if (cmd.startsWith('/')) {
620
- addOutput({ text: '' }, { text: ` Comando sconosciuto: ${cmd} — digita /help`, dim: true }, { text: '' });
621
- return;
298
+ else {
299
+ if (contentStarted)
300
+ process.stdout.write('\n');
301
+ if (result.provider)
302
+ console.log(` ${DIM(`(${result.provider})`)}`);
303
+ if (result.error)
304
+ console.log(` ${chalk.red('✗')} ${result.error}`);
622
305
  }
623
- // Free text → AI chat
624
- setLoading(true);
625
- setLoadingText('Orbit AI sta pensando...');
306
+ }
307
+ catch (err) {
308
+ if (spinner.isSpinning)
309
+ spinner.stop();
310
+ store.endRequest();
311
+ // Network-level failure → fallback to non-streaming
626
312
  try {
627
- const chatResult = await sendChat(raw);
628
- setLoading(false);
629
- if (chatResult.error) {
630
- addOutput({ text: '' }, { text: ` ✗ ${chatResult.error}`, color: '#EF4444' }, { text: '' });
313
+ const fallback = await sendChat(raw);
314
+ if (fallback.error) {
315
+ console.log('');
316
+ console.log(outputBlock('error', fallback.error));
631
317
  }
632
318
  else {
633
- const providerInfo = chatResult.provider_used ? ` (${chatResult.provider_used})` : '';
634
- addOutput({ text: '' }, { text: ` ✦ Orbit AI${providerInfo}`, color: '#2563EB', bold: true }, { text: '' },
635
- // Split response into lines for proper rendering
636
- ...(chatResult.response ?? '').split('\n').map(line => ({ text: ` ${line}` })), { text: '' });
319
+ console.log('');
320
+ for (const line of (fallback.response ?? '').split('\n')) {
321
+ console.log(` ${line}`);
322
+ }
637
323
  }
638
324
  }
639
- catch (err) {
640
- setLoading(false);
641
- addOutput({ text: ` ✗ ${err instanceof Error ? err.message : String(err)}`, color: '#EF4444' });
325
+ catch (e2) {
326
+ console.log('');
327
+ console.log(outputBlock('error', e2 instanceof Error ? e2.message : String(e2)));
642
328
  }
643
- };
644
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { version: version }), _jsx(Text, { children: orbitGrad(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') }), _jsx(StatusBar, {}), _jsx(Text, { children: orbitGrad(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') }), _jsx(Text, { dimColor: true, children: " Digita /help per la lista comandi." }), _jsx(Text, { children: '' }), _jsx(OutputArea, { lines: output }), showSuggestions && (_jsx(CommandSuggestions, { input: input, selectedIndex: selectedIndex })), loading ? (_jsxs(Box, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: "#2563EB", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", loadingText] })] })) : (_jsxs(Box, { children: [_jsx(Text, { color: "#2563EB", bold: true, children: " orbit" }), _jsx(Text, { dimColor: true, children: " \u203A " }), _jsx(TextInput, { value: input, onChange: setInput, onSubmit: handleSubmit })] }))] }));
645
- }
646
- function getClient(config) {
647
- const t = config.default_site ? config.tokens[config.default_site] : undefined;
648
- if (!t)
649
- return null;
650
- return new OrbitClient(t.token, config.api_url);
651
- }
652
- export async function startShell(version) {
653
- const { waitUntilExit } = render(_jsx(Shell, { version: version }));
654
- await waitUntilExit();
329
+ }
330
+ console.log('');
655
331
  }
656
332
  //# sourceMappingURL=shell.js.map