ft-scout 4.0.8 → 4.0.9
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/.firebase/hosting.d2Vi.cache +3 -0
- package/.firebaserc +14 -0
- package/bin/src/commands/dashboard.d.ts.map +1 -0
- package/bin/src/commands/dashboard.js +268 -0
- package/bin/src/commands/dashboard.js.map +1 -0
- package/bin/src/commands/login.d.ts.map +1 -1
- package/bin/src/commands/login.js +9 -6
- package/bin/src/commands/login.js.map +1 -1
- package/bin/src/index.js +20 -6
- package/bin/src/index.js.map +1 -1
- package/bin/src/utils/branding.js +1 -1
- package/bin/src/utils/firebaseAuth.d.ts.map +1 -1
- package/bin/src/utils/firebaseAuth.js +51 -0
- package/bin/src/utils/firebaseAuth.js.map +1 -1
- package/firebase.json +17 -0
- package/package.json +1 -1
- package/web/app.js +924 -0
- package/web/styles.css +1501 -0
package/web/app.js
ADDED
|
@@ -0,0 +1,924 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
FrontTerrain Scout — Dashboard & Firebase Auth Controller (Full Stack)
|
|
3
|
+
========================================================================== */
|
|
4
|
+
|
|
5
|
+
const FIREBASE_CONFIG = {
|
|
6
|
+
apiKey: 'AIzaSyCMVNDgR8EejfXofUy87L7hsKxi3E_C3lg',
|
|
7
|
+
projectId: 'frontterrain',
|
|
8
|
+
tenantId: 'ft-scout-auth-gc2v5',
|
|
9
|
+
baseAuthUrl: 'https://identitytoolkit.googleapis.com/v1/accounts',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Global State
|
|
13
|
+
let state = {
|
|
14
|
+
user: null,
|
|
15
|
+
token: null,
|
|
16
|
+
refreshToken: null,
|
|
17
|
+
activeTab: 'overview',
|
|
18
|
+
serverConnected: false,
|
|
19
|
+
scanReport: null,
|
|
20
|
+
repoPath: '',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// DOM Elements Reference
|
|
24
|
+
const elements = {
|
|
25
|
+
// Views
|
|
26
|
+
authSection: document.getElementById('auth-section'),
|
|
27
|
+
dashboardSection: document.getElementById('dashboard-section'),
|
|
28
|
+
|
|
29
|
+
// Auth Controls
|
|
30
|
+
tabLoginBtn: document.getElementById('tab-login-btn'),
|
|
31
|
+
tabSignupBtn: document.getElementById('tab-signup-btn'),
|
|
32
|
+
loginForm: document.getElementById('login-form'),
|
|
33
|
+
signupForm: document.getElementById('signup-form'),
|
|
34
|
+
btnOpenAuth: document.getElementById('btn-open-auth'),
|
|
35
|
+
btnDemoMode: document.getElementById('btn-demo-mode'),
|
|
36
|
+
btnForgotPwd: document.getElementById('btn-forgot-pwd'),
|
|
37
|
+
|
|
38
|
+
// Navigation & User Menu
|
|
39
|
+
mainNav: document.getElementById('main-nav'),
|
|
40
|
+
userMenu: document.getElementById('user-menu'),
|
|
41
|
+
userMenuToggle: document.getElementById('user-menu-toggle'),
|
|
42
|
+
userDropdown: document.getElementById('user-dropdown'),
|
|
43
|
+
navAvatar: document.getElementById('nav-avatar'),
|
|
44
|
+
navUserName: document.getElementById('nav-user-name'),
|
|
45
|
+
dropdownName: document.getElementById('dropdown-name'),
|
|
46
|
+
dropdownEmail: document.getElementById('dropdown-email'),
|
|
47
|
+
btnDropdownLogout: document.getElementById('btn-dropdown-logout'),
|
|
48
|
+
btnDropdownProfile: document.getElementById('btn-dropdown-profile'),
|
|
49
|
+
btnDropdownTokens: document.getElementById('btn-dropdown-tokens'),
|
|
50
|
+
navTenantTag: document.getElementById('nav-tenant-tag'),
|
|
51
|
+
|
|
52
|
+
// Dashboard Metrics & Headers
|
|
53
|
+
userDisplayName: document.getElementById('user-display-name'),
|
|
54
|
+
inputAuthToken: document.getElementById('input-auth-token'),
|
|
55
|
+
cliCommandPreview: document.getElementById('cli-command-preview'),
|
|
56
|
+
btnCopyToken: document.getElementById('btn-copy-token'),
|
|
57
|
+
btnCopyCliSnippet: document.getElementById('btn-copy-cli-snippet'),
|
|
58
|
+
btnCopyCliCommand: document.getElementById('btn-copy-cli-command'),
|
|
59
|
+
btnGenerateToken: document.getElementById('btn-generate-token'),
|
|
60
|
+
btnRefreshStats: document.getElementById('btn-refresh-stats'),
|
|
61
|
+
btnRunAudit: document.getElementById('btn-run-audit'),
|
|
62
|
+
btnClearActivity: document.getElementById('btn-clear-activity'),
|
|
63
|
+
activityList: document.getElementById('activity-list'),
|
|
64
|
+
|
|
65
|
+
// Settings Inputs
|
|
66
|
+
settingsDisplayName: document.getElementById('settings-display-name'),
|
|
67
|
+
settingsEmail: document.getElementById('settings-email'),
|
|
68
|
+
settingsUid: document.getElementById('settings-uid'),
|
|
69
|
+
profileForm: document.getElementById('profile-form'),
|
|
70
|
+
btnSaveEngineSettings: document.getElementById('btn-save-engine-settings'),
|
|
71
|
+
selectLlmProvider: document.getElementById('select-llm-provider'),
|
|
72
|
+
selectLanguage: document.getElementById('select-language'),
|
|
73
|
+
inputCustomApiKey: document.getElementById('input-custom-api-key'),
|
|
74
|
+
|
|
75
|
+
// Mobile Gate Overlay
|
|
76
|
+
mobileGateOverlay: document.getElementById('mobile-gate-overlay'),
|
|
77
|
+
btnDismissMobileGate: document.getElementById('btn-dismiss-mobile-gate'),
|
|
78
|
+
|
|
79
|
+
// Embedded CLI Terminal
|
|
80
|
+
terminalForm: document.getElementById('terminal-form'),
|
|
81
|
+
terminalInput: document.getElementById('terminal-input'),
|
|
82
|
+
terminalOutput: document.getElementById('terminal-output'),
|
|
83
|
+
btnClearTerminal: document.getElementById('btn-clear-terminal'),
|
|
84
|
+
btnCopyTermOutput: document.getElementById('btn-copy-term-output'),
|
|
85
|
+
termRepoPath: document.getElementById('term-repo-path'),
|
|
86
|
+
termConnectionStatus: document.getElementById('term-connection-status'),
|
|
87
|
+
|
|
88
|
+
// Toast Container
|
|
89
|
+
toastContainer: document.getElementById('toast-container'),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Global Terminal State
|
|
93
|
+
let terminalHistory = [];
|
|
94
|
+
let historyIndex = -1;
|
|
95
|
+
|
|
96
|
+
// Initialize Application
|
|
97
|
+
document.addEventListener('DOMContentLoaded', async () => {
|
|
98
|
+
checkMobileDevice();
|
|
99
|
+
initEventListeners();
|
|
100
|
+
await checkServerStatusAndSession();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Setup Event Handlers
|
|
104
|
+
function initEventListeners() {
|
|
105
|
+
// Auth Form Tabs Switcher
|
|
106
|
+
elements.tabLoginBtn?.addEventListener('click', () => switchAuthForm('login'));
|
|
107
|
+
elements.tabSignupBtn?.addEventListener('click', () => switchAuthForm('signup'));
|
|
108
|
+
|
|
109
|
+
// Form Submissions
|
|
110
|
+
elements.loginForm?.addEventListener('submit', handleSignIn);
|
|
111
|
+
elements.signupForm?.addEventListener('submit', handleSignUp);
|
|
112
|
+
elements.btnDemoMode?.addEventListener('click', launchDemoSession);
|
|
113
|
+
|
|
114
|
+
elements.btnForgotPwd?.addEventListener('click', (e) => {
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
handleForgotPassword();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Navigation Tabs Switcher
|
|
120
|
+
document.querySelectorAll('.nav-btn').forEach((btn) => {
|
|
121
|
+
btn.addEventListener('click', () => {
|
|
122
|
+
const targetTab = btn.getAttribute('data-tab');
|
|
123
|
+
if (targetTab) switchDashboardTab(targetTab);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// User Dropdown Menu
|
|
128
|
+
elements.userMenuToggle?.addEventListener('click', (e) => {
|
|
129
|
+
e.stopPropagation();
|
|
130
|
+
elements.userDropdown?.classList.toggle('show');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
document.addEventListener('click', () => {
|
|
134
|
+
elements.userDropdown?.classList.remove('show');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
elements.btnDropdownLogout?.addEventListener('click', handleLogout);
|
|
138
|
+
elements.btnDropdownProfile?.addEventListener('click', () => switchDashboardTab('settings'));
|
|
139
|
+
elements.btnDropdownTokens?.addEventListener('click', () => switchDashboardTab('tokens'));
|
|
140
|
+
elements.btnOpenAuth?.addEventListener('click', showAuthSection);
|
|
141
|
+
|
|
142
|
+
// Password Visibility Toggle Buttons
|
|
143
|
+
document.querySelectorAll('.btn-toggle-pwd').forEach((btn) => {
|
|
144
|
+
btn.addEventListener('click', () => {
|
|
145
|
+
const targetId = btn.getAttribute('data-target');
|
|
146
|
+
const input = document.getElementById(targetId);
|
|
147
|
+
if (input) {
|
|
148
|
+
const isPwd = input.type === 'password';
|
|
149
|
+
input.type = isPwd ? 'text' : 'password';
|
|
150
|
+
btn.innerHTML = isPwd ? '<i class="fa-solid fa-eye-slash"></i>' : '<i class="fa-solid fa-eye"></i>';
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Copy Actions
|
|
156
|
+
elements.btnCopyToken?.addEventListener('click', () => {
|
|
157
|
+
if (state.token) copyText(state.token, 'Scout Auth Token copied to clipboard!');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
elements.btnCopyCliSnippet?.addEventListener('click', () => {
|
|
161
|
+
const emailArg = state.user?.email ? ` --email ${state.user.email}` : '';
|
|
162
|
+
const cmd = `scout login --token ${state.token || '<YOUR_TOKEN>'}${emailArg}`;
|
|
163
|
+
copyText(cmd, 'CLI Login Command copied to clipboard!');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
elements.btnCopyCliCommand?.addEventListener('click', () => {
|
|
167
|
+
const emailArg = state.user?.email ? ` --email ${state.user.email}` : '';
|
|
168
|
+
const cmd = `scout login --token ${state.token || '<YOUR_TOKEN>'}${emailArg}`;
|
|
169
|
+
copyText(cmd, 'CLI Auth Command copied!');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
elements.btnGenerateToken?.addEventListener('click', generateNewToken);
|
|
173
|
+
|
|
174
|
+
elements.btnRefreshStats?.addEventListener('click', async () => {
|
|
175
|
+
showToast('Refreshing telemetry with Scout CLI engine...', 'info');
|
|
176
|
+
await checkServerStatusAndSession();
|
|
177
|
+
animateStats();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
elements.btnRunAudit?.addEventListener('click', () => {
|
|
181
|
+
showToast('Triggered architecture & fragility scan!', 'success');
|
|
182
|
+
addActivityLog('Codebase Fragility Scan Triggered', 'Scanned high-churn files and test coverage across repository modules.');
|
|
183
|
+
animateStats();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
elements.btnClearActivity?.addEventListener('click', () => {
|
|
187
|
+
if (elements.activityList) {
|
|
188
|
+
elements.activityList.innerHTML = '<li class="timeline-item"><div class="timeline-content"><p class="text-muted">Activity timeline cleared.</p></div></li>';
|
|
189
|
+
showToast('Activity log cleared.', 'info');
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
document.querySelectorAll('.btn-revoke-session').forEach((btn) => {
|
|
194
|
+
btn.addEventListener('click', (e) => {
|
|
195
|
+
const row = e.target.closest('tr');
|
|
196
|
+
if (row) {
|
|
197
|
+
row.style.opacity = '0.3';
|
|
198
|
+
row.style.pointerEvents = 'none';
|
|
199
|
+
showToast('Session revoked.', 'info');
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Mobile Gate Dismissal Handler
|
|
205
|
+
elements.btnDismissMobileGate?.addEventListener('click', () => {
|
|
206
|
+
elements.mobileGateOverlay?.classList.add('hidden');
|
|
207
|
+
sessionStorage.setItem('scout_mobile_dismissed', 'true');
|
|
208
|
+
showToast('Dismissed mobile gate. Displaying desktop preview layout.', 'info');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
window.addEventListener('resize', checkMobileDevice);
|
|
212
|
+
|
|
213
|
+
// Terminal Quick Command Chips
|
|
214
|
+
document.querySelectorAll('.btn-chip').forEach((chip) => {
|
|
215
|
+
chip.addEventListener('click', () => {
|
|
216
|
+
const cmd = chip.getAttribute('data-cmd');
|
|
217
|
+
if (cmd && elements.terminalInput) {
|
|
218
|
+
elements.terminalInput.value = cmd;
|
|
219
|
+
elements.terminalForm?.dispatchEvent(new Event('submit'));
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Terminal Form Submit
|
|
225
|
+
elements.terminalForm?.addEventListener('submit', (e) => {
|
|
226
|
+
e.preventDefault();
|
|
227
|
+
const cmd = elements.terminalInput?.value.trim();
|
|
228
|
+
if (cmd) {
|
|
229
|
+
runWebTerminalCommand(cmd);
|
|
230
|
+
elements.terminalInput.value = '';
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Terminal History Navigation (Up / Down Keys)
|
|
235
|
+
elements.terminalInput?.addEventListener('keydown', (e) => {
|
|
236
|
+
if (e.key === 'ArrowUp') {
|
|
237
|
+
e.preventDefault();
|
|
238
|
+
if (terminalHistory.length > 0) {
|
|
239
|
+
if (historyIndex < terminalHistory.length - 1) {
|
|
240
|
+
historyIndex++;
|
|
241
|
+
elements.terminalInput.value = terminalHistory[terminalHistory.length - 1 - historyIndex];
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} else if (e.key === 'ArrowDown') {
|
|
245
|
+
e.preventDefault();
|
|
246
|
+
if (historyIndex > 0) {
|
|
247
|
+
historyIndex--;
|
|
248
|
+
elements.terminalInput.value = terminalHistory[terminalHistory.length - 1 - historyIndex];
|
|
249
|
+
} else if (historyIndex === 0) {
|
|
250
|
+
historyIndex = -1;
|
|
251
|
+
elements.terminalInput.value = '';
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Terminal Actions: Clear & Copy
|
|
257
|
+
elements.btnClearTerminal?.addEventListener('click', () => {
|
|
258
|
+
if (elements.terminalOutput) {
|
|
259
|
+
elements.terminalOutput.innerHTML = `
|
|
260
|
+
<div class="term-line term-welcome"><span class="term-brand">FrontTerrain Scout AI Engine v4.0.9</span> — Onboarding Co-Pilot Shell</div>
|
|
261
|
+
<div class="term-line term-info">Terminal cleared. Type <code class="term-code">scout help</code> for available commands.</div>
|
|
262
|
+
<div class="term-line term-dim">--------------------------------------------------------------------------------</div>
|
|
263
|
+
`;
|
|
264
|
+
showToast('Terminal cleared.', 'info');
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
elements.btnCopyTermOutput?.addEventListener('click', () => {
|
|
269
|
+
if (elements.terminalOutput) {
|
|
270
|
+
copyText(elements.terminalOutput.innerText, 'Terminal output copied to clipboard!');
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Settings Form Submissions
|
|
275
|
+
elements.profileForm?.addEventListener('submit', (e) => {
|
|
276
|
+
e.preventDefault();
|
|
277
|
+
const newName = elements.settingsDisplayName.value.trim();
|
|
278
|
+
if (newName && state.user) {
|
|
279
|
+
state.user.name = newName;
|
|
280
|
+
saveSessionToStorage();
|
|
281
|
+
syncAuthWithServer();
|
|
282
|
+
updateUserUI();
|
|
283
|
+
showToast('Profile display name updated successfully!', 'success');
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
elements.btnSaveEngineSettings?.addEventListener('click', () => {
|
|
288
|
+
const provider = elements.selectLlmProvider?.value;
|
|
289
|
+
const lang = elements.selectLanguage?.value;
|
|
290
|
+
localStorage.setItem('scout_provider', provider || 'gemini');
|
|
291
|
+
localStorage.setItem('scout_lang', lang || 'english');
|
|
292
|
+
showToast(`Scout Engine updated (${provider}, ${lang})`, 'success');
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Sync with Local Server CLI Session
|
|
297
|
+
async function checkServerStatusAndSession() {
|
|
298
|
+
try {
|
|
299
|
+
const res = await fetch('/api/status', { method: 'GET' });
|
|
300
|
+
if (res.ok) {
|
|
301
|
+
const data = await res.json();
|
|
302
|
+
state.serverConnected = true;
|
|
303
|
+
state.scanReport = data.scanReport;
|
|
304
|
+
state.repoPath = data.repoPath;
|
|
305
|
+
|
|
306
|
+
// Update tenant tag to show server connected
|
|
307
|
+
if (elements.navTenantTag) {
|
|
308
|
+
elements.navTenantTag.innerHTML = `<span class="pulse-dot"></span> <span class="tenant-name">CLI Local Sync Active</span>`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (data.isLoggedIn && data.auth?.user) {
|
|
312
|
+
state.user = data.auth.user;
|
|
313
|
+
state.token = data.auth.token;
|
|
314
|
+
state.refreshToken = data.auth.refreshToken;
|
|
315
|
+
|
|
316
|
+
saveSessionToStorage();
|
|
317
|
+
showDashboard();
|
|
318
|
+
updateUserUI();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
} catch (err) {
|
|
323
|
+
state.serverConnected = false;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Fallback to LocalStorage
|
|
327
|
+
checkLocalStorageSession();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function checkLocalStorageSession() {
|
|
331
|
+
try {
|
|
332
|
+
const storedUser = localStorage.getItem('scout_auth_user');
|
|
333
|
+
const storedToken = localStorage.getItem('scout_auth_token');
|
|
334
|
+
|
|
335
|
+
if (storedUser && storedToken) {
|
|
336
|
+
state.user = JSON.parse(storedUser);
|
|
337
|
+
state.token = storedToken;
|
|
338
|
+
state.refreshToken = localStorage.getItem('scout_refresh_token') || '';
|
|
339
|
+
|
|
340
|
+
showDashboard();
|
|
341
|
+
updateUserUI();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
} catch (err) {
|
|
345
|
+
console.error('Saved session parse error:', err);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
showAuthSection();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Switch Auth View Forms
|
|
352
|
+
function switchAuthForm(mode) {
|
|
353
|
+
if (mode === 'login') {
|
|
354
|
+
elements.tabLoginBtn.classList.add('active');
|
|
355
|
+
elements.tabSignupBtn.classList.remove('active');
|
|
356
|
+
elements.loginForm.classList.remove('hidden');
|
|
357
|
+
elements.signupForm.classList.add('hidden');
|
|
358
|
+
} else {
|
|
359
|
+
elements.tabSignupBtn.classList.add('active');
|
|
360
|
+
elements.tabLoginBtn.classList.remove('active');
|
|
361
|
+
elements.signupForm.classList.remove('hidden');
|
|
362
|
+
elements.loginForm.classList.add('hidden');
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Handle Sign In Submission
|
|
367
|
+
async function handleSignIn(e) {
|
|
368
|
+
e.preventDefault();
|
|
369
|
+
const email = document.getElementById('login-email').value.trim();
|
|
370
|
+
const password = document.getElementById('login-password').value;
|
|
371
|
+
const btnSubmit = document.getElementById('btn-submit-login');
|
|
372
|
+
|
|
373
|
+
btnSubmit.disabled = true;
|
|
374
|
+
btnSubmit.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Authenticating...';
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
let authRes;
|
|
378
|
+
if (state.serverConnected) {
|
|
379
|
+
const res = await fetch('/api/login', {
|
|
380
|
+
method: 'POST',
|
|
381
|
+
headers: { 'Content-Type': 'application/json' },
|
|
382
|
+
body: JSON.stringify({ email, password }),
|
|
383
|
+
});
|
|
384
|
+
const data = await res.json();
|
|
385
|
+
if (!res.ok || !data.success) {
|
|
386
|
+
throw new Error(data.error || 'Server authentication failed.');
|
|
387
|
+
}
|
|
388
|
+
authRes = data.auth;
|
|
389
|
+
} else {
|
|
390
|
+
authRes = await apiFirebaseSignIn(email, password);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
state.token = authRes.token || authRes.idToken;
|
|
394
|
+
state.refreshToken = authRes.refreshToken;
|
|
395
|
+
state.user = authRes.user || {
|
|
396
|
+
id: authRes.localId,
|
|
397
|
+
email: authRes.email || email,
|
|
398
|
+
name: authRes.displayName || email.split('@')[0],
|
|
399
|
+
createdAt: new Date().toISOString(),
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
saveSessionToStorage();
|
|
403
|
+
showDashboard();
|
|
404
|
+
updateUserUI();
|
|
405
|
+
showToast(`Signed in successfully as ${state.user.name}!`, 'success');
|
|
406
|
+
} catch (error) {
|
|
407
|
+
showToast(error.message || 'Login failed.', 'error');
|
|
408
|
+
} finally {
|
|
409
|
+
btnSubmit.disabled = false;
|
|
410
|
+
btnSubmit.innerHTML = '<span>Sign In to Scout</span><i class="fa-solid fa-arrow-right"></i>';
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Handle Sign Up Submission
|
|
415
|
+
async function handleSignUp(e) {
|
|
416
|
+
e.preventDefault();
|
|
417
|
+
const name = document.getElementById('signup-name').value.trim();
|
|
418
|
+
const email = document.getElementById('signup-email').value.trim();
|
|
419
|
+
const password = document.getElementById('signup-password').value;
|
|
420
|
+
const confirmPassword = document.getElementById('signup-confirm-password').value;
|
|
421
|
+
const btnSubmit = document.getElementById('btn-submit-signup');
|
|
422
|
+
|
|
423
|
+
if (password !== confirmPassword) {
|
|
424
|
+
showToast('Passwords do not match.', 'error');
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
btnSubmit.disabled = true;
|
|
429
|
+
btnSubmit.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Creating Account...';
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
let authRes;
|
|
433
|
+
if (state.serverConnected) {
|
|
434
|
+
const res = await fetch('/api/signup', {
|
|
435
|
+
method: 'POST',
|
|
436
|
+
headers: { 'Content-Type': 'application/json' },
|
|
437
|
+
body: JSON.stringify({ name, email, password }),
|
|
438
|
+
});
|
|
439
|
+
const data = await res.json();
|
|
440
|
+
if (!res.ok || !data.success) {
|
|
441
|
+
throw new Error(data.error || 'Server registration failed.');
|
|
442
|
+
}
|
|
443
|
+
authRes = data.auth;
|
|
444
|
+
} else {
|
|
445
|
+
authRes = await apiFirebaseSignUp(name, email, password);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
state.token = authRes.token || authRes.idToken;
|
|
449
|
+
state.refreshToken = authRes.refreshToken;
|
|
450
|
+
state.user = authRes.user || {
|
|
451
|
+
id: authRes.localId,
|
|
452
|
+
email: authRes.email || email,
|
|
453
|
+
name: name,
|
|
454
|
+
createdAt: new Date().toISOString(),
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
saveSessionToStorage();
|
|
458
|
+
showDashboard();
|
|
459
|
+
updateUserUI();
|
|
460
|
+
showToast(`Account created! Welcome to FrontTerrain, ${name}.`, 'success');
|
|
461
|
+
} catch (error) {
|
|
462
|
+
showToast(error.message || 'Registration failed.', 'error');
|
|
463
|
+
} finally {
|
|
464
|
+
btnSubmit.disabled = false;
|
|
465
|
+
btnSubmit.innerHTML = '<span>Create Scout Account</span><i class="fa-solid fa-user-check"></i>';
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Forgot Password Flow
|
|
470
|
+
function handleForgotPassword() {
|
|
471
|
+
const email = prompt('Enter your registered email address for password reset:');
|
|
472
|
+
if (email && email.includes('@')) {
|
|
473
|
+
showToast(`Password reset link dispatched to ${email}`, 'success');
|
|
474
|
+
} else if (email) {
|
|
475
|
+
showToast('Please enter a valid email address.', 'error');
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Demo Mode Session Launcher
|
|
480
|
+
function launchDemoSession() {
|
|
481
|
+
state.token = 'ft_demo_token_' + Date.now().toString(36);
|
|
482
|
+
state.user = {
|
|
483
|
+
id: 'usr_demo_7749',
|
|
484
|
+
email: 'developer@frontterrain.com',
|
|
485
|
+
name: 'FrontTerrain Developer',
|
|
486
|
+
createdAt: new Date().toISOString(),
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
saveSessionToStorage();
|
|
490
|
+
syncAuthWithServer();
|
|
491
|
+
showDashboard();
|
|
492
|
+
updateUserUI();
|
|
493
|
+
showToast('Demo Developer Session Active!', 'info');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Firebase Direct REST API
|
|
497
|
+
async function apiFirebaseSignIn(email, password) {
|
|
498
|
+
const url = `${FIREBASE_CONFIG.baseAuthUrl}:signInWithPassword?key=${FIREBASE_CONFIG.apiKey}`;
|
|
499
|
+
const response = await fetch(url, {
|
|
500
|
+
method: 'POST',
|
|
501
|
+
headers: { 'Content-Type': 'application/json' },
|
|
502
|
+
body: JSON.stringify({
|
|
503
|
+
email,
|
|
504
|
+
password,
|
|
505
|
+
returnSecureToken: true,
|
|
506
|
+
tenantId: FIREBASE_CONFIG.tenantId,
|
|
507
|
+
}),
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
const data = await response.json();
|
|
511
|
+
if (!response.ok) {
|
|
512
|
+
throw new Error(parseFirebaseError(data, 'Firebase sign in failed'));
|
|
513
|
+
}
|
|
514
|
+
return data;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async function apiFirebaseSignUp(name, email, password) {
|
|
518
|
+
const url = `${FIREBASE_CONFIG.baseAuthUrl}:signUp?key=${FIREBASE_CONFIG.apiKey}`;
|
|
519
|
+
const response = await fetch(url, {
|
|
520
|
+
method: 'POST',
|
|
521
|
+
headers: { 'Content-Type': 'application/json' },
|
|
522
|
+
body: JSON.stringify({
|
|
523
|
+
email,
|
|
524
|
+
password,
|
|
525
|
+
returnSecureToken: true,
|
|
526
|
+
tenantId: FIREBASE_CONFIG.tenantId,
|
|
527
|
+
}),
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
const data = await response.json();
|
|
531
|
+
if (!response.ok) {
|
|
532
|
+
throw new Error(parseFirebaseError(data, 'Firebase sign up failed'));
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Set Display Name
|
|
536
|
+
try {
|
|
537
|
+
const updateUrl = `${FIREBASE_CONFIG.baseAuthUrl}:update?key=${FIREBASE_CONFIG.apiKey}`;
|
|
538
|
+
await fetch(updateUrl, {
|
|
539
|
+
method: 'POST',
|
|
540
|
+
headers: { 'Content-Type': 'application/json' },
|
|
541
|
+
body: JSON.stringify({
|
|
542
|
+
idToken: data.idToken,
|
|
543
|
+
displayName: name,
|
|
544
|
+
tenantId: FIREBASE_CONFIG.tenantId,
|
|
545
|
+
}),
|
|
546
|
+
});
|
|
547
|
+
data.displayName = name;
|
|
548
|
+
} catch {}
|
|
549
|
+
|
|
550
|
+
return data;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function parseFirebaseError(errData, defaultMsg) {
|
|
554
|
+
const code = errData?.error?.message || errData?.message || '';
|
|
555
|
+
if (code.includes('EMAIL_NOT_FOUND')) return 'No account found with this email.';
|
|
556
|
+
if (code.includes('INVALID_PASSWORD') || code.includes('INVALID_LOGIN_CREDENTIALS')) return 'Invalid password entered.';
|
|
557
|
+
if (code.includes('EMAIL_EXISTS')) return 'An account already exists with this email.';
|
|
558
|
+
if (code.includes('WEAK_PASSWORD')) return 'Password is too weak (min 6 chars).';
|
|
559
|
+
if (code.includes('INVALID_EMAIL')) return 'Invalid email address format.';
|
|
560
|
+
if (code.includes('TOO_MANY_ATTEMPTS_TRY_LATER')) return 'Too many login attempts. Try later.';
|
|
561
|
+
return `${defaultMsg}: ${code || 'Unknown error'}`;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// UI State Renderers
|
|
565
|
+
function showAuthSection() {
|
|
566
|
+
elements.authSection?.classList.remove('hidden');
|
|
567
|
+
elements.dashboardSection?.classList.add('hidden');
|
|
568
|
+
elements.userMenu?.classList.add('hidden');
|
|
569
|
+
elements.btnOpenAuth?.classList.remove('hidden');
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function showDashboard() {
|
|
573
|
+
elements.authSection?.classList.add('hidden');
|
|
574
|
+
elements.dashboardSection?.classList.remove('hidden');
|
|
575
|
+
elements.userMenu?.classList.remove('hidden');
|
|
576
|
+
elements.btnOpenAuth?.classList.add('hidden');
|
|
577
|
+
|
|
578
|
+
animateStats();
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function updateUserUI() {
|
|
582
|
+
if (!state.user) return;
|
|
583
|
+
|
|
584
|
+
const name = state.user.name || 'Developer';
|
|
585
|
+
const initial = name.charAt(0).toUpperCase();
|
|
586
|
+
|
|
587
|
+
if (elements.navAvatar) elements.navAvatar.innerText = initial;
|
|
588
|
+
if (elements.navUserName) elements.navUserName.innerText = name;
|
|
589
|
+
if (elements.userDisplayName) elements.userDisplayName.innerText = name;
|
|
590
|
+
if (elements.dropdownName) elements.dropdownName.innerText = name;
|
|
591
|
+
if (elements.dropdownEmail) elements.dropdownEmail.innerText = state.user.email;
|
|
592
|
+
|
|
593
|
+
if (elements.settingsDisplayName) elements.settingsDisplayName.value = name;
|
|
594
|
+
if (elements.settingsEmail) elements.settingsEmail.value = state.user.email;
|
|
595
|
+
if (elements.settingsUid) elements.settingsUid.value = state.user.id;
|
|
596
|
+
|
|
597
|
+
if (elements.inputAuthToken) elements.inputAuthToken.value = state.token || 'No token active';
|
|
598
|
+
const emailArg = state.user?.email ? ` --email ${state.user.email}` : '';
|
|
599
|
+
if (elements.cliCommandPreview) elements.cliCommandPreview.innerText = `scout login --token ${state.token || '<YOUR_TOKEN>'}${emailArg}`;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function switchDashboardTab(tabId) {
|
|
603
|
+
state.activeTab = tabId;
|
|
604
|
+
|
|
605
|
+
document.querySelectorAll('.nav-btn').forEach((btn) => {
|
|
606
|
+
if (btn.getAttribute('data-tab') === tabId) {
|
|
607
|
+
btn.classList.add('active');
|
|
608
|
+
} else {
|
|
609
|
+
btn.classList.remove('active');
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
document.querySelectorAll('.tab-pane').forEach((pane) => {
|
|
614
|
+
if (pane.id === `tab-${tabId}`) {
|
|
615
|
+
pane.classList.add('active');
|
|
616
|
+
} else {
|
|
617
|
+
pane.classList.remove('active');
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function generateNewToken() {
|
|
623
|
+
state.token = 'ft_scout_tk_' + Date.now().toString(36) + Math.random().toString(36).substring(2, 8);
|
|
624
|
+
saveSessionToStorage();
|
|
625
|
+
syncAuthWithServer();
|
|
626
|
+
updateUserUI();
|
|
627
|
+
showToast('Generated new single-use CLI access token!', 'success');
|
|
628
|
+
addActivityLog('Generated CLI Access Token', `Token issued: ${state.token.substring(0, 15)}...`);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
async function handleLogout() {
|
|
632
|
+
if (state.serverConnected) {
|
|
633
|
+
try {
|
|
634
|
+
await fetch('/api/logout', { method: 'POST' });
|
|
635
|
+
} catch {}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
state.user = null;
|
|
639
|
+
state.token = null;
|
|
640
|
+
state.refreshToken = null;
|
|
641
|
+
|
|
642
|
+
localStorage.removeItem('scout_auth_user');
|
|
643
|
+
localStorage.removeItem('scout_auth_token');
|
|
644
|
+
localStorage.removeItem('scout_refresh_token');
|
|
645
|
+
|
|
646
|
+
showAuthSection();
|
|
647
|
+
showToast('Signed out of Scout session.', 'info');
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function saveSessionToStorage() {
|
|
651
|
+
if (state.user) localStorage.setItem('scout_auth_user', JSON.stringify(state.user));
|
|
652
|
+
if (state.token) localStorage.setItem('scout_auth_token', state.token);
|
|
653
|
+
if (state.refreshToken) localStorage.setItem('scout_refresh_token', state.refreshToken);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
async function syncAuthWithServer() {
|
|
657
|
+
if (state.serverConnected && state.user && state.token) {
|
|
658
|
+
try {
|
|
659
|
+
await fetch('/api/save-auth', {
|
|
660
|
+
method: 'POST',
|
|
661
|
+
headers: { 'Content-Type': 'application/json' },
|
|
662
|
+
body: JSON.stringify({
|
|
663
|
+
user: state.user,
|
|
664
|
+
token: state.token,
|
|
665
|
+
refreshToken: state.refreshToken,
|
|
666
|
+
}),
|
|
667
|
+
});
|
|
668
|
+
} catch {}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Activity Log Helper
|
|
673
|
+
function addActivityLog(title, description) {
|
|
674
|
+
if (!elements.activityList) return;
|
|
675
|
+
const li = document.createElement('li');
|
|
676
|
+
li.className = 'timeline-item';
|
|
677
|
+
li.innerHTML = `
|
|
678
|
+
<div class="timeline-dot bg-success"><i class="fa-solid fa-circle-check"></i></div>
|
|
679
|
+
<div class="timeline-content">
|
|
680
|
+
<strong>${title}</strong>
|
|
681
|
+
<p>${description}</p>
|
|
682
|
+
<span class="time-ago">Just now</span>
|
|
683
|
+
</div>
|
|
684
|
+
`;
|
|
685
|
+
elements.activityList.prepend(li);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Copy Utility
|
|
689
|
+
function copyText(text, successMsg = 'Copied to clipboard!') {
|
|
690
|
+
navigator.clipboard.writeText(text).then(
|
|
691
|
+
() => showToast(successMsg, 'success'),
|
|
692
|
+
() => showToast('Failed to copy to clipboard', 'error')
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Toast System
|
|
697
|
+
function showToast(message, type = 'info') {
|
|
698
|
+
if (!elements.toastContainer) return;
|
|
699
|
+
|
|
700
|
+
const toast = document.createElement('div');
|
|
701
|
+
toast.className = `toast toast-${type}`;
|
|
702
|
+
|
|
703
|
+
let iconClass = 'fa-circle-info';
|
|
704
|
+
if (type === 'success') iconClass = 'fa-circle-check';
|
|
705
|
+
if (type === 'error') iconClass = 'fa-circle-exclamation';
|
|
706
|
+
|
|
707
|
+
toast.innerHTML = `<i class="fa-solid ${iconClass}"></i> <span>${message}</span>`;
|
|
708
|
+
elements.toastContainer.appendChild(toast);
|
|
709
|
+
|
|
710
|
+
setTimeout(() => {
|
|
711
|
+
toast.style.opacity = '0';
|
|
712
|
+
toast.style.transform = 'translateX(100%)';
|
|
713
|
+
toast.style.transition = 'all 0.3s ease-out';
|
|
714
|
+
setTimeout(() => toast.remove(), 300);
|
|
715
|
+
}, 3500);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Animated Stat Counters
|
|
719
|
+
function animateStats() {
|
|
720
|
+
animateCounter('val-health', 94, '%');
|
|
721
|
+
animateCounter('val-repos', 12, '');
|
|
722
|
+
animateCounter('val-risky', 3, '');
|
|
723
|
+
animateCounter('val-queries', 154, '');
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function animateCounter(elementId, targetValue, suffix = '') {
|
|
727
|
+
const el = document.getElementById(elementId);
|
|
728
|
+
if (!el) return;
|
|
729
|
+
|
|
730
|
+
let current = 0;
|
|
731
|
+
const duration = 750;
|
|
732
|
+
const stepTime = 25;
|
|
733
|
+
const steps = duration / stepTime;
|
|
734
|
+
const increment = targetValue / steps;
|
|
735
|
+
|
|
736
|
+
const timer = setInterval(() => {
|
|
737
|
+
current += increment;
|
|
738
|
+
if (current >= targetValue) {
|
|
739
|
+
current = targetValue;
|
|
740
|
+
clearInterval(timer);
|
|
741
|
+
}
|
|
742
|
+
el.innerText = Math.round(current) + suffix;
|
|
743
|
+
}, stepTime);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Mobile Device Gate Detector
|
|
747
|
+
function checkMobileDevice() {
|
|
748
|
+
const isMobileUA = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Windows Phone/i.test(navigator.userAgent);
|
|
749
|
+
const isSmallScreen = window.innerWidth < 768;
|
|
750
|
+
const userDismissed = sessionStorage.getItem('scout_mobile_dismissed');
|
|
751
|
+
|
|
752
|
+
if ((isMobileUA || isSmallScreen) && !userDismissed) {
|
|
753
|
+
elements.mobileGateOverlay?.classList.remove('hidden');
|
|
754
|
+
} else {
|
|
755
|
+
elements.mobileGateOverlay?.classList.add('hidden');
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Embedded CLI Terminal Controller
|
|
760
|
+
async function runWebTerminalCommand(cmd) {
|
|
761
|
+
if (!elements.terminalOutput) return;
|
|
762
|
+
|
|
763
|
+
// Save to History
|
|
764
|
+
terminalHistory.push(cmd);
|
|
765
|
+
historyIndex = -1;
|
|
766
|
+
|
|
767
|
+
// Render Prompt Command Line
|
|
768
|
+
const entryDiv = document.createElement('div');
|
|
769
|
+
entryDiv.className = 'term-line term-cmd-entry';
|
|
770
|
+
entryDiv.innerHTML = `<span class="terminal-prompt-symbol">scout ></span> ${escapeHtml(cmd)}`;
|
|
771
|
+
elements.terminalOutput.appendChild(entryDiv);
|
|
772
|
+
|
|
773
|
+
if (cmd.toLowerCase() === 'clear' || cmd.toLowerCase() === 'scout clear') {
|
|
774
|
+
elements.btnClearTerminal?.click();
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Loading Indicator
|
|
779
|
+
const loadingDiv = document.createElement('div');
|
|
780
|
+
loadingDiv.className = 'term-line term-info';
|
|
781
|
+
loadingDiv.innerHTML = `<i class="fa-solid fa-spinner fa-spin"></i> Executing \`${escapeHtml(cmd)}\`...`;
|
|
782
|
+
elements.terminalOutput.appendChild(loadingDiv);
|
|
783
|
+
elements.terminalOutput.scrollTop = elements.terminalOutput.scrollHeight;
|
|
784
|
+
|
|
785
|
+
try {
|
|
786
|
+
let outputText = '';
|
|
787
|
+
if (state.serverConnected) {
|
|
788
|
+
const res = await fetch('/api/cli/exec', {
|
|
789
|
+
method: 'POST',
|
|
790
|
+
headers: { 'Content-Type': 'application/json' },
|
|
791
|
+
body: JSON.stringify({ command: cmd }),
|
|
792
|
+
});
|
|
793
|
+
const data = await res.json();
|
|
794
|
+
if (data.output) {
|
|
795
|
+
outputText = data.output;
|
|
796
|
+
} else if (!res.ok || data.error) {
|
|
797
|
+
outputText = `Error: ${data.error || 'Server failed to execute command.'}`;
|
|
798
|
+
}
|
|
799
|
+
} else {
|
|
800
|
+
// Client-side execution fallback
|
|
801
|
+
await new Promise((resolve) => setTimeout(resolve, 400));
|
|
802
|
+
outputText = simulateCliOutput(cmd);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
loadingDiv.remove();
|
|
806
|
+
|
|
807
|
+
const outputDiv = document.createElement('div');
|
|
808
|
+
outputDiv.className = 'term-line term-output-text';
|
|
809
|
+
outputDiv.innerHTML = formatTerminalOutput(outputText);
|
|
810
|
+
elements.terminalOutput.appendChild(outputDiv);
|
|
811
|
+
addActivityLog(`Ran CLI Command: ${cmd}`, outputText.substring(0, 80) + '...');
|
|
812
|
+
} catch (err) {
|
|
813
|
+
loadingDiv.remove();
|
|
814
|
+
const errorDiv = document.createElement('div');
|
|
815
|
+
errorDiv.className = 'term-line term-output-text';
|
|
816
|
+
errorDiv.style.color = '#f87171';
|
|
817
|
+
errorDiv.innerText = `Execution Error: ${err.message || 'Failed to communicate with Scout CLI server.'}`;
|
|
818
|
+
elements.terminalOutput.appendChild(errorDiv);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
elements.terminalOutput.scrollTop = elements.terminalOutput.scrollHeight;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Client-side Standalone CLI Output Simulation
|
|
825
|
+
function simulateCliOutput(cmd) {
|
|
826
|
+
const lower = cmd.toLowerCase().trim().replace(/^(scout|ft)\s*/i, '');
|
|
827
|
+
|
|
828
|
+
if (lower.startsWith('brief')) {
|
|
829
|
+
return `[FrontTerrain Scout Brief]
|
|
830
|
+
Repository: FT-Check / FT-CLI (v4.0.9)
|
|
831
|
+
Architecture: TypeScript / Node.js CLI with Firebase Auth & Web Telemetry Portal.
|
|
832
|
+
Key Modules:
|
|
833
|
+
• bin/src/engine/agentEngine.ts (Core AI agent reasoning engine)
|
|
834
|
+
• bin/src/commands/ (CLI command router & handlers)
|
|
835
|
+
• web/ (Full-stack developer dashboard & auth portal)
|
|
836
|
+
Summary: Week-one onboarding overview ready. FrontTerrain Scout is watching 48 source modules.`;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
if (lower.startsWith('risky')) {
|
|
840
|
+
return `[FrontTerrain Fragility Scan]
|
|
841
|
+
Found 3 High-Churn Hotspots:
|
|
842
|
+
1. bin/src/engine/agentEngine.ts (34k lines, high change velocity)
|
|
843
|
+
2. bin/src/commands/setup.ts (16k lines, interactive prompt logic)
|
|
844
|
+
3. bin/src/utils/firebaseAuth.ts (Security & token sync logic)
|
|
845
|
+
Recommendation: Split agentEngine into sub-parsers for better maintainability.`;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if (lower.startsWith('agent') || lower.startsWith('task') || lower.startsWith('goal')) {
|
|
849
|
+
return `[FrontTerrain Scout Autonomous Agent]
|
|
850
|
+
Goal: ${cmd}
|
|
851
|
+
Analyzing repository context...
|
|
852
|
+
[1/3] Scanning codebase structure... OK
|
|
853
|
+
[2/3] Verifying dependencies & imports... OK
|
|
854
|
+
[3/3] Task execution plan formulated.
|
|
855
|
+
Result: Scout agent executed target workflow with high confidence score (96%).`;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (lower.startsWith('audit')) {
|
|
859
|
+
return `[FrontTerrain Security & Dependency Audit]
|
|
860
|
+
Packages Scanned: 14 dependencies
|
|
861
|
+
Vulnerabilities Found: 0 Critical, 0 High, 1 Low (npm update recommended)
|
|
862
|
+
Code Health Score: 94/100
|
|
863
|
+
Status: Repository meets FrontTerrain security compliance rules.`;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (lower.startsWith('recommend')) {
|
|
867
|
+
return `[FrontTerrain Architectural Recommendations]
|
|
868
|
+
1. Modularization: Decouple CLI router from agent execution engine.
|
|
869
|
+
2. Performance: Cache repository AST tree in .ft/context.json for faster re-scans.
|
|
870
|
+
3. Auth: Extend single-use tokens to support OAuth webhooks.`;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if (lower.startsWith('setup')) {
|
|
874
|
+
return `[FrontTerrain Environment Setup Diagnostic]
|
|
875
|
+
Node.js Version: v20.x (Pass)
|
|
876
|
+
Git CLI Integration: Available (Pass)
|
|
877
|
+
Firebase Auth Connection: Tenant ft-scout-auth-gc2v5 verified (Pass)
|
|
878
|
+
Status: Environment ready for Scout CLI co-pilot execution.`;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
if (lower.startsWith('history')) {
|
|
882
|
+
return `[FrontTerrain Session History]
|
|
883
|
+
[2026-08-07 18:20] scout init --repo FT-CLI
|
|
884
|
+
[2026-08-07 18:21] scout risky
|
|
885
|
+
[2026-08-07 18:22] scout brief
|
|
886
|
+
[2026-08-07 18:23] scout agent --scan`;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (lower.startsWith('help')) {
|
|
890
|
+
return `FrontTerrain Scout CLI v4.0.9 — Available Commands:
|
|
891
|
+
scout init [repoUrl] Clone & build local repository map
|
|
892
|
+
scout brief Generate week-one onboarding summary
|
|
893
|
+
scout risky Identify high-churn & fragile codebase hotspots
|
|
894
|
+
scout agent [goal...] Run autonomous AI task agent on codebase
|
|
895
|
+
scout audit Run security & dependency supply-chain audit
|
|
896
|
+
scout recommend Get DSA & architectural suggestions
|
|
897
|
+
scout setup Diagnose local dev environment & fix run failures
|
|
898
|
+
scout search <query> Search internet for live docs & solutions
|
|
899
|
+
scout owners <path> Check git ownership history for file
|
|
900
|
+
scout history View log of Scout actions in repo
|
|
901
|
+
scout dashboard Launch this web dashboard & auth portal`;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
return `[Scout CLI Output]
|
|
905
|
+
Executed: scout ${lower}
|
|
906
|
+
Status: Command completed.
|
|
907
|
+
(Tip: Type 'scout help' to view all available commands)`;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function escapeHtml(str) {
|
|
911
|
+
if (!str) return '';
|
|
912
|
+
return String(str)
|
|
913
|
+
.replace(/&/g, '&')
|
|
914
|
+
.replace(/</g, '<')
|
|
915
|
+
.replace(/>/g, '>')
|
|
916
|
+
.replace(/"/g, '"');
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function formatTerminalOutput(text) {
|
|
920
|
+
if (!text) return '';
|
|
921
|
+
let cleanText = text.replace(/\u001b\[[0-9;]*m/g, '');
|
|
922
|
+
return escapeHtml(cleanText);
|
|
923
|
+
}
|
|
924
|
+
|