@orbitpanel/cli 0.5.0 → 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,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, 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,23 @@ 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 ? siteLink.orbit_site_id.slice(0, 16) : undefined),
63
+ sessionMinutes,
64
+ gitBranch: (session?.git_branch ?? gitBranch) ?? undefined,
65
+ runtimeMode: runtime,
66
+ }));
105
67
  console.log('');
106
68
  sep();
107
69
  // Auto-prompt auth
@@ -125,7 +87,6 @@ export async function startShell(version) {
125
87
  completer,
126
88
  terminal: true,
127
89
  });
128
- // Ctrl+C and Ctrl+Z to exit
129
90
  rl.on('SIGINT', () => {
130
91
  console.log(`\n\n ${DIM('Alla prossima!')}\n`);
131
92
  rl.close();
@@ -135,6 +96,15 @@ export async function startShell(version) {
135
96
  rl.close();
136
97
  process.exit(0);
137
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
+ };
138
108
  rl.prompt();
139
109
  return new Promise((resolve) => {
140
110
  rl.on('close', () => resolve());
@@ -145,528 +115,218 @@ export async function startShell(version) {
145
115
  rl.prompt();
146
116
  return;
147
117
  }
148
- // Exit
118
+ // --- Command routing (thin dispatcher) ---
149
119
  if (cmd === '/exit' || cmd === '/quit' || cmd === '/q') {
150
120
  console.log(`\n ${DIM('Alla prossima!')}\n`);
151
121
  rl.close();
152
122
  return;
153
123
  }
154
- // Clear
155
124
  if (cmd === '/clear' || cmd === '/cls') {
156
125
  console.clear();
157
126
  rl.prompt();
158
127
  return;
159
128
  }
160
- // Help
161
129
  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('');
130
+ printHelp();
172
131
  rl.prompt();
173
132
  return;
174
133
  }
175
- // Status
176
134
  if (cmd === '/status' || cmd === '/s') {
177
135
  await cmdStatus();
178
136
  rl.prompt();
179
137
  return;
180
138
  }
181
- // Doctor
182
139
  if (cmd === '/doctor' || cmd === '/doc') {
183
140
  await cmdDoctor();
184
141
  rl.prompt();
185
142
  return;
186
143
  }
187
- // Session start
188
144
  if (cmd === '/session start' || cmd === '/ss') {
189
145
  await cmdSessionStart();
146
+ await store.refresh(cwd);
190
147
  rl.prompt();
191
148
  return;
192
149
  }
193
- // Session end
194
150
  if (cmd === '/session end' || cmd === '/se') {
195
151
  await cmdSessionEnd();
152
+ await store.refresh(cwd);
196
153
  rl.prompt();
197
154
  return;
198
155
  }
199
- // Session info
200
156
  if (cmd === '/session' || cmd === '/session info') {
201
157
  await cmdSessionInfo();
202
158
  rl.prompt();
203
159
  return;
204
160
  }
205
- // Note
206
161
  if (cmd.startsWith('/note ') || cmd.startsWith('/n ')) {
207
162
  await cmdNoteAdd(raw.slice(raw.indexOf(' ') + 1));
208
163
  rl.prompt();
209
164
  return;
210
165
  }
211
- // Report
212
166
  if (cmd === '/report' || cmd === '/r') {
213
- await cmdReport();
167
+ await cmdReport(promptConfirm);
168
+ await store.refresh(cwd);
214
169
  rl.prompt();
215
170
  return;
216
171
  }
217
- // List interventions
218
172
  if (cmd === '/list' || cmd === '/ls' || cmd === '/interventions') {
219
173
  await cmdList();
220
174
  rl.prompt();
221
175
  return;
222
176
  }
223
- // Get intervention
224
177
  if (cmd.startsWith('/get ') || cmd.startsWith('/g ')) {
225
178
  await cmdGet(raw.split(' ')[1]);
226
179
  rl.prompt();
227
180
  return;
228
181
  }
229
- // Sites
230
182
  if (cmd === '/sites' || cmd.startsWith('/sites ')) {
231
183
  await cmdSites(cmd.split(' ')[1]);
184
+ await store.refresh(cwd);
232
185
  rl.prompt();
233
186
  return;
234
187
  }
235
- // Auth (device code)
236
188
  if (cmd === '/auth') {
237
189
  await cmdAuth();
190
+ await store.refresh(cwd);
238
191
  rl.prompt();
239
192
  return;
240
193
  }
241
- // Login (token)
242
194
  if (cmd.startsWith('/login ')) {
243
195
  await cmdLogin(raw.split(' ')[1]);
196
+ await store.refresh(cwd);
244
197
  rl.prompt();
245
198
  return;
246
199
  }
247
- // Link
248
200
  if (cmd.startsWith('/link ')) {
249
201
  await cmdLink(raw.split(' ')[1]);
202
+ await store.refresh(cwd);
250
203
  rl.prompt();
251
204
  return;
252
205
  }
253
- // Unknown slash command
254
206
  if (cmd.startsWith('/')) {
255
207
  console.log(`\n ${DIM('Comando sconosciuto:')} ${WHITE(cmd)} ${DIM('— digita /help')}\n`);
256
208
  rl.prompt();
257
209
  return;
258
210
  }
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
- }
211
+ // Free text → AI chat (streaming with state machine + fallback)
212
+ await handleAIChat(raw, store);
282
213
  rl.prompt();
283
214
  });
284
215
  });
285
216
  }
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);
217
+ // --- Help output ---
218
+ function printHelp() {
398
219
  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')}`);
220
+ console.log(` ${sparkle} ${BB('Comandi disponibili')}`);
401
221
  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;
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)}`);
408
225
  }
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
226
  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)}`);
227
+ console.log(` ${DIM('Oppure scrivi in linguaggio naturale per parlare con Orbit AI.')}`);
425
228
  console.log('');
426
229
  }
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');
230
+ // --- AI Chat with streaming + state machine ---
231
+ async function handleAIChat(raw, store) {
232
+ const { config, siteLink } = store.getState();
439
233
  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();
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();
472
237
  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')}`);
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}`);
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;
249
+ }
250
+ case 'thinking': {
251
+ if (spinner.isSpinning)
252
+ spinner.text = DIM('Analisi in corso...');
253
+ break;
254
+ }
255
+ case 'delta': {
256
+ if (spinner.isSpinning) {
257
+ spinner.stop();
258
+ console.log('');
259
+ process.stdout.write(' ');
260
+ }
261
+ const chunk = event.data.content;
262
+ process.stdout.write(chunk.replaceAll('\n', '\n '));
263
+ break;
264
+ }
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));
287
+ }
288
+ else {
289
+ console.log('');
290
+ for (const line of (fallback.response ?? '').split('\n')) {
291
+ console.log(` ${line}`);
292
+ }
293
+ if (fallback.provider_used) {
294
+ console.log(`\n ${DIM(`(${fallback.provider_used})`)}`);
295
+ }
512
296
  }
513
- console.log('');
514
- console.log(` ${DIM(`${result.total} interventi`)}`);
515
297
  }
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))}`);
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}`);
550
305
  }
551
306
  }
552
307
  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)}`);
308
+ if (spinner.isSpinning)
309
+ spinner.stop();
310
+ store.endRequest();
311
+ // Network-level failure → fallback to non-streaming
312
+ try {
313
+ const fallback = await sendChat(raw);
314
+ if (fallback.error) {
588
315
  console.log('');
316
+ console.log(outputBlock('error', fallback.error));
589
317
  }
590
318
  else {
591
- console.log(`\n ${chalk.red('')} Numero non valido. Scegli tra 1 e ${sites.length}\n`);
319
+ console.log('');
320
+ for (const line of (fallback.response ?? '').split('\n')) {
321
+ console.log(` ${line}`);
322
+ }
592
323
  }
593
- return;
594
324
  }
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}`);
325
+ catch (e2) {
326
+ console.log('');
327
+ console.log(outputBlock('error', e2 instanceof Error ? e2.message : String(e2)));
328
+ }
659
329
  }
660
330
  console.log('');
661
331
  }
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
332
  //# sourceMappingURL=shell.js.map