@orbitpanel/cli 0.5.0 → 0.7.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,68 +1,33 @@
1
1
  /**
2
2
  * Interactive REPL shell for @orbitpanel/cli.
3
+ * Thin orchestration layer — delegates to shell-commands.ts and shell-render.ts.
3
4
  * Pure readline + chalk — no Ink/React dependency.
4
- * Stable, works in all terminals and TTY contexts.
5
5
  */
6
6
  import chalk from 'chalk';
7
- import gradient from 'gradient-string';
8
7
  import ora from 'ora';
9
8
  import { createInterface } from 'node:readline';
10
- import { loadConfig, DEFAULT_API_URL } from './config.js';
11
- import { OrbitClient } from './client.js';
12
- import { requestDeviceCode, pollDeviceCode, saveDeviceAuth, openBrowser } from './device-auth.js';
9
+ import { loadConfig } from './config.js';
13
10
  import { loadSiteLink } from './site.js';
14
- import { loadActiveSession, createSession, addNote, endSession } from './session.js';
15
- import { getGitBranch, getGitCommitHead, getGitDiffStat } from './git.js';
16
- import { buildReportPayload } from './report-builder.js';
11
+ import { loadActiveSession } from './session.js';
12
+ import { getGitBranch } from './git.js';
17
13
  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;
26
- const orbitGrad = gradient(['#2563EB', '#3B82F6', '#60A5FA', '#0D9488']);
27
- const sparkle = BL('✦');
28
- const logoLines = [
29
- ' ██████╗ ██████╗ ██████╗ ██╗████████╗',
30
- ' ██╔═══██╗██╔══██╗██╔══██╗██║╚══██╔══╝',
31
- ' ██║ ██║██████╔╝██████╔╝██║ ██║ ',
32
- ' ██║ ██║██╔══██╗██╔══██╗██║ ██║ ',
33
- ' ╚██████╔╝██║ ██║██████╔╝██║ ██║ ',
34
- ' ╚═════╝ ╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝ ',
35
- ];
36
- const SLASH_COMMANDS = [
37
- { cmd: '/status', alias: '/s', desc: 'panoramica completa' },
38
- { cmd: '/doctor', alias: '/doc', desc: 'verifica configurazione' },
39
- { cmd: '/session start', alias: '/ss', desc: 'avvia sessione di lavoro' },
40
- { cmd: '/session end', alias: '/se', desc: 'chiudi sessione' },
41
- { cmd: '/session', alias: '', desc: 'info sessione attiva' },
42
- { cmd: '/note', alias: '/n', desc: 'aggiungi nota (+ testo)' },
43
- { cmd: '/report', alias: '/r', desc: 'invia report a Orbit' },
44
- { cmd: '/list', alias: '/ls', desc: 'lista interventi' },
45
- { cmd: '/get', alias: '/g', desc: 'dettaglio intervento (+ id)' },
46
- { cmd: '/sites', alias: '', desc: 'lista siti e seleziona' },
47
- { cmd: '/auth', alias: '', desc: 'accedi con browser' },
48
- { cmd: '/login', alias: '', desc: 'configura token (+ token)' },
49
- { cmd: '/link', alias: '', desc: 'collega directory (+ site_id)' },
50
- { cmd: '/clear', alias: '', desc: 'pulisci schermo' },
51
- { cmd: '/help', alias: '/h', desc: 'lista comandi' },
52
- { cmd: '/exit', alias: '/q', desc: 'esci' },
53
- ];
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, cmdHistory, 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
+ };
54
28
  function sleep(ms) {
55
29
  return new Promise(r => setTimeout(r, ms));
56
30
  }
57
- function sep() {
58
- console.log(orbitGrad(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
59
- }
60
- function getClient(config) {
61
- const t = config.default_site ? config.tokens[config.default_site] : undefined;
62
- if (!t)
63
- return null;
64
- return new OrbitClient(t.token, config.api_url);
65
- }
66
31
  // Tab-completion handler
67
32
  function completer(line) {
68
33
  if (!line.startsWith('/'))
@@ -82,26 +47,24 @@ export async function startShell(version) {
82
47
  }
83
48
  console.log('');
84
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')}`);
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
- }
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;
103
59
  console.log('');
104
- console.log(` ${parts.join(` ${DIM('│')} `)}`);
60
+ console.log(statusLine({
61
+ connected: !!tokenEntry,
62
+ siteUrl: siteLink?.final_url ?? siteLink?.name ?? (siteLink ? siteLink.orbit_site_id.slice(0, 16) : undefined),
63
+ environment: siteLink?.environment,
64
+ sessionMinutes,
65
+ gitBranch: (session?.git_branch ?? gitBranch) ?? undefined,
66
+ runtimeMode: runtime,
67
+ }));
105
68
  console.log('');
106
69
  sep();
107
70
  // Auto-prompt auth
@@ -125,7 +88,6 @@ export async function startShell(version) {
125
88
  completer,
126
89
  terminal: true,
127
90
  });
128
- // Ctrl+C and Ctrl+Z to exit
129
91
  rl.on('SIGINT', () => {
130
92
  console.log(`\n\n ${DIM('Alla prossima!')}\n`);
131
93
  rl.close();
@@ -135,6 +97,15 @@ export async function startShell(version) {
135
97
  rl.close();
136
98
  process.exit(0);
137
99
  });
100
+ // Prompt function for risk confirmations (uses the active readline)
101
+ const promptConfirm = (message) => {
102
+ return new Promise((resolve) => {
103
+ rl.question(message, (answer) => {
104
+ const a = answer.trim().toLowerCase();
105
+ resolve(a === 's' || a === 'si' || a === 'y' || a === 'yes');
106
+ });
107
+ });
108
+ };
138
109
  rl.prompt();
139
110
  return new Promise((resolve) => {
140
111
  rl.on('close', () => resolve());
@@ -145,528 +116,223 @@ export async function startShell(version) {
145
116
  rl.prompt();
146
117
  return;
147
118
  }
148
- // Exit
119
+ // --- Command routing (thin dispatcher) ---
149
120
  if (cmd === '/exit' || cmd === '/quit' || cmd === '/q') {
150
121
  console.log(`\n ${DIM('Alla prossima!')}\n`);
151
122
  rl.close();
152
123
  return;
153
124
  }
154
- // Clear
155
125
  if (cmd === '/clear' || cmd === '/cls') {
156
126
  console.clear();
157
127
  rl.prompt();
158
128
  return;
159
129
  }
160
- // Help
161
130
  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)}`);
168
- }
169
- console.log('');
170
- console.log(` ${DIM('Oppure scrivi in linguaggio naturale per parlare con Orbit AI.')}`);
171
- console.log('');
131
+ printHelp();
172
132
  rl.prompt();
173
133
  return;
174
134
  }
175
- // Status
176
135
  if (cmd === '/status' || cmd === '/s') {
177
136
  await cmdStatus();
178
137
  rl.prompt();
179
138
  return;
180
139
  }
181
- // Doctor
182
140
  if (cmd === '/doctor' || cmd === '/doc') {
183
141
  await cmdDoctor();
184
142
  rl.prompt();
185
143
  return;
186
144
  }
187
- // Session start
188
145
  if (cmd === '/session start' || cmd === '/ss') {
189
146
  await cmdSessionStart();
147
+ await store.refresh(cwd);
190
148
  rl.prompt();
191
149
  return;
192
150
  }
193
- // Session end
194
151
  if (cmd === '/session end' || cmd === '/se') {
195
152
  await cmdSessionEnd();
153
+ await store.refresh(cwd);
196
154
  rl.prompt();
197
155
  return;
198
156
  }
199
- // Session info
200
157
  if (cmd === '/session' || cmd === '/session info') {
201
158
  await cmdSessionInfo();
202
159
  rl.prompt();
203
160
  return;
204
161
  }
205
- // Note
206
162
  if (cmd.startsWith('/note ') || cmd.startsWith('/n ')) {
207
163
  await cmdNoteAdd(raw.slice(raw.indexOf(' ') + 1));
208
164
  rl.prompt();
209
165
  return;
210
166
  }
211
- // Report
212
167
  if (cmd === '/report' || cmd === '/r') {
213
- await cmdReport();
168
+ await cmdReport(promptConfirm);
169
+ await store.refresh(cwd);
214
170
  rl.prompt();
215
171
  return;
216
172
  }
217
- // List interventions
218
173
  if (cmd === '/list' || cmd === '/ls' || cmd === '/interventions') {
219
174
  await cmdList();
220
175
  rl.prompt();
221
176
  return;
222
177
  }
223
- // Get intervention
178
+ if (cmd === '/history' || cmd === '/hist') {
179
+ await cmdHistory();
180
+ rl.prompt();
181
+ return;
182
+ }
224
183
  if (cmd.startsWith('/get ') || cmd.startsWith('/g ')) {
225
184
  await cmdGet(raw.split(' ')[1]);
226
185
  rl.prompt();
227
186
  return;
228
187
  }
229
- // Sites
230
188
  if (cmd === '/sites' || cmd.startsWith('/sites ')) {
231
189
  await cmdSites(cmd.split(' ')[1]);
190
+ await store.refresh(cwd);
232
191
  rl.prompt();
233
192
  return;
234
193
  }
235
- // Auth (device code)
236
194
  if (cmd === '/auth') {
237
195
  await cmdAuth();
196
+ await store.refresh(cwd);
238
197
  rl.prompt();
239
198
  return;
240
199
  }
241
- // Login (token)
242
200
  if (cmd.startsWith('/login ')) {
243
201
  await cmdLogin(raw.split(' ')[1]);
202
+ await store.refresh(cwd);
244
203
  rl.prompt();
245
204
  return;
246
205
  }
247
- // Link
248
206
  if (cmd.startsWith('/link ')) {
249
207
  await cmdLink(raw.split(' ')[1]);
208
+ await store.refresh(cwd);
250
209
  rl.prompt();
251
210
  return;
252
211
  }
253
- // Unknown slash command
254
212
  if (cmd.startsWith('/')) {
255
213
  console.log(`\n ${DIM('Comando sconosciuto:')} ${WHITE(cmd)} ${DIM('— digita /help')}\n`);
256
214
  rl.prompt();
257
215
  return;
258
216
  }
259
- // Free text → AI chat
260
- const spinner = ora({ text: 'Orbit AI sta pensando...', color: 'blue', indent: 2 }).start();
261
- try {
262
- const chatResult = await sendChat(raw);
263
- spinner.stop();
264
- if (chatResult.error) {
265
- console.log(`\n ${chalk.red('✗')} ${chatResult.error}\n`);
266
- }
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}`);
274
- }
275
- console.log('');
276
- }
277
- }
278
- catch (err) {
279
- spinner.stop();
280
- console.log(`\n ${chalk.red('✗')} ${err instanceof Error ? err.message : String(err)}\n`);
281
- }
217
+ // Free text → AI chat (streaming with state machine + fallback)
218
+ await handleAIChat(raw, store);
282
219
  rl.prompt();
283
220
  });
284
221
  });
285
222
  }
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);
223
+ // --- Help output ---
224
+ function printHelp() {
415
225
  console.log('');
416
- console.log(` ${sparkle} ${BB('Sessione attiva')}`);
226
+ console.log(` ${sparkle} ${BB('Comandi disponibili')}`);
417
227
  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;
228
+ for (const c of SLASH_COMMANDS) {
229
+ const alias = c.alias ? DIM(` ${c.alias}`) : '';
230
+ console.log(` ${WHITE(c.cmd.padEnd(18))}${alias.padEnd(12)}${DIM(c.desc)}`);
432
231
  }
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
232
  console.log('');
440
- console.log(` ${GREEN('✓')} ${GREEN.bold('Nota aggiunta')} ${DIM(`(${session.notes.length + 1} totali)`)}`);
441
- console.log(` ${B('│')} ${WHITE(text)}`);
233
+ console.log(` ${DIM('Oppure scrivi in linguaggio naturale per parlare con Orbit AI.')}`);
442
234
  console.log('');
443
235
  }
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
- }
236
+ // --- AI Chat with streaming + state machine ---
237
+ async function handleAIChat(raw, store) {
238
+ const { config, siteLink } = store.getState();
479
239
  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();
240
+ console.log(` ${sparkle} ${BB('Orbit AI')}`);
241
+ const spinner = ora({ text: DIM('Recupero contesto...'), color: 'blue', indent: 2 }).start();
242
+ const sm = new StreamStateMachine();
493
243
  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')}`);
501
- }
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}`);
244
+ store.startRequest();
245
+ const result = await sendChatStream(raw, (event) => {
246
+ // State machine gates event processing
247
+ if (!sm.transition(event.type))
248
+ return;
249
+ switch (event.type) {
250
+ case 'status': {
251
+ const msg = (event.data.message ?? event.data.phase ?? '');
252
+ if (spinner.isSpinning)
253
+ spinner.text = DIM(msg);
254
+ break;
255
+ }
256
+ case 'thinking': {
257
+ if (spinner.isSpinning)
258
+ spinner.text = DIM('Analisi in corso...');
259
+ break;
260
+ }
261
+ case 'delta': {
262
+ if (spinner.isSpinning) {
263
+ spinner.stop();
264
+ console.log('');
265
+ process.stdout.write(' ');
266
+ }
267
+ const chunk = event.data.content;
268
+ process.stdout.write(chunk.replaceAll('\n', '\n '));
269
+ break;
270
+ }
271
+ case 'error': {
272
+ if (spinner.isSpinning)
273
+ spinner.stop();
274
+ console.log(`\n ${chalk.red('✗')} ${event.data.message}`);
275
+ break;
276
+ }
277
+ case 'done':
278
+ if (spinner.isSpinning)
279
+ spinner.stop();
280
+ break;
281
+ }
282
+ }, config, siteLink);
283
+ if (spinner.isSpinning)
284
+ spinner.stop();
285
+ store.endRequest();
286
+ const contentStarted = sm.current === 'generating' || sm.current === 'done';
287
+ // Fallback: stream failed before any content → try non-streaming
288
+ if (result.error && !contentStarted) {
289
+ const fallback = await sendChat(raw);
290
+ if (fallback.error) {
291
+ console.log('');
292
+ console.log(outputBlock('error', fallback.error));
293
+ }
294
+ else {
295
+ console.log('');
296
+ for (const line of (fallback.response ?? '').split('\n')) {
297
+ console.log(` ${line}`);
298
+ }
299
+ if (fallback.provider_used) {
300
+ console.log(`\n ${DIM(`(${fallback.provider_used})`)}`);
301
+ }
512
302
  }
513
- console.log('');
514
- console.log(` ${DIM(`${result.total} interventi`)}`);
515
303
  }
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))}`);
304
+ else {
305
+ if (contentStarted)
306
+ process.stdout.write('\n');
307
+ if (result.provider)
308
+ console.log(` ${DIM(`(${result.provider})`)}`);
309
+ if (result.error)
310
+ console.log(` ${chalk.red('✗')} ${result.error}`);
550
311
  }
551
312
  }
552
313
  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`);
570
- return;
571
- }
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)}`);
314
+ if (spinner.isSpinning)
315
+ spinner.stop();
316
+ store.endRequest();
317
+ // Network-level failure → fallback to non-streaming
318
+ try {
319
+ const fallback = await sendChat(raw);
320
+ if (fallback.error) {
588
321
  console.log('');
322
+ console.log(outputBlock('error', fallback.error));
589
323
  }
590
324
  else {
591
- console.log(`\n ${chalk.red('')} Numero non valido. Scegli tra 1 e ${sites.length}\n`);
325
+ console.log('');
326
+ for (const line of (fallback.response ?? '').split('\n')) {
327
+ console.log(` ${line}`);
328
+ }
592
329
  }
593
- return;
594
330
  }
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
- }
615
- }
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
- }
643
- }
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}`);
331
+ catch (e2) {
332
+ console.log('');
333
+ console.log(outputBlock('error', e2 instanceof Error ? e2.message : String(e2)));
334
+ }
659
335
  }
660
336
  console.log('');
661
337
  }
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`);
671
- }
672
338
  //# sourceMappingURL=shell.js.map