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