tide-commander 1.152.0 → 1.153.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/CHANGELOG.md +5 -0
- package/dist/assets/{BossLogsModal-DZWUfQXs.js → BossLogsModal-Dgz-N5gw.js} +1 -1
- package/dist/assets/{BossSpawnModal-7IwzcqE7.js → BossSpawnModal-BK_rYu8s.js} +1 -1
- package/dist/assets/{ControlsModal-D8L1xD-H.js → ControlsModal-hC_AiCpk.js} +1 -1
- package/dist/assets/{DockerLogsModal-_zLe5UFl.js → DockerLogsModal-D7ogyaZr.js} +1 -1
- package/dist/assets/{EmbeddedEditor-gbiFXvt_.js → EmbeddedEditor-BFzUOmwz.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-CcXTV3NP.js → GmailOAuthSetup-C_Yc5Ypv.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-wbETidgv.js → GoogleOAuthSetup-CefQSOYm.js} +1 -1
- package/dist/assets/{IframeModal-a9E5UDBk.js → IframeModal-BbeuiE04.js} +1 -1
- package/dist/assets/{IntegrationsPanel-BbGyluL0.js → IntegrationsPanel-xh3TgJLH.js} +2 -2
- package/dist/assets/LogViewerModal-CE2L8nWJ.js +1 -0
- package/dist/assets/{MonitoringModal-DymqjenY.js → MonitoringModal-A2SN3uAJ.js} +1 -1
- package/dist/assets/{PM2LogsModal-CEaCk-dt.js → PM2LogsModal-DdlhCa8u.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-q_FFv7L6.js → RestoreArchivedAreaModal-BvHo-OxE.js} +1 -1
- package/dist/assets/{Scene2DCanvas-DOvVtiYO.js → Scene2DCanvas-t6RuUegr.js} +1 -1
- package/dist/assets/{SceneManager-D0NdrG8K.js → SceneManager-TQ2_AvFn.js} +1 -1
- package/dist/assets/{SkillsPanel-DpqD60Hy.js → SkillsPanel-PjzjcaoG.js} +1 -1
- package/dist/assets/{SlackMultiInstanceSetup-BdoALd_g.js → SlackMultiInstanceSetup-ms4r32Z8.js} +1 -1
- package/dist/assets/{SpawnModal-DFFFH2kc.js → SpawnModal-B1FS8m9h.js} +1 -1
- package/dist/assets/{StatisticsModal-Dv_rYZDt.js → StatisticsModal-mmz8hnD5.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-CRCCI686.js → SubordinateAssignmentModal-BXoG-Xgd.js} +1 -1
- package/dist/assets/SymbolsNerdFontMono-Regular-BM2yZ5nr.woff2 +0 -0
- package/dist/assets/{TriggerManagerPanel-DBN8IRa-.js → TriggerManagerPanel-ODJyjvY-.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-uQEvNTs2.js → WorkflowEditorPanel-9wtY0Tn3.js} +1 -1
- package/dist/assets/{area-logos-CuVxToGM.js → area-logos-BLyzmh47.js} +1 -1
- package/dist/assets/{index-DKnfLe7S.js → index-1hwiSf7o.js} +1 -1
- package/dist/assets/{index-8Ghh3AQ-.js → index-CBkh8xcE.js} +1 -1
- package/dist/assets/{index-Dwll5Eyj.js → index-CN2p2fxH.js} +1 -1
- package/dist/assets/index-CbfoJ_Vc.js +1 -0
- package/dist/assets/{index-D4iie3Vd.js → index-CdbLxNJr.js} +1 -1
- package/dist/assets/{index-ipuWrg1S.js → index-Cf-PINah.js} +3 -3
- package/dist/assets/{index-HvyFhszc.js → index-Ci2ZKu0n.js} +2 -2
- package/dist/assets/{index-Blc4MwKs.js → index-D8vFfLs4.js} +1 -1
- package/dist/assets/{index-BHM8V4i-.js → index-E5IMeM80.js} +1 -1
- package/dist/assets/index-XI0_ICy6.css +1 -0
- package/dist/assets/main-9TcmAtcq.js +289 -0
- package/dist/assets/main-DP0iv8Le.css +1 -0
- package/dist/assets/{web-1pfPwxBS.js → web-BAkfcL4w.js} +1 -1
- package/dist/assets/{web-D0JSbYeo.js → web-BVIPIXA1.js} +1 -1
- package/dist/assets/{web-DjgiLxvi.js → web-Cz3TYK-R.js} +1 -1
- package/dist/build-info.json +1 -0
- package/dist/index.html +2 -2
- package/dist/locales/en/config.json +1 -0
- package/dist/locales/en/terminal.json +30 -0
- package/dist/src/packages/server/routes/system.js +107 -1
- package/dist/src/packages/server/services/claude-credentials-service.js +329 -0
- package/package.json +1 -1
- package/dist/assets/LogViewerModal-cnMAbNvY.js +0 -1
- package/dist/assets/index-BNzYPyBm.css +0 -1
- package/dist/assets/index-OkenGYC6.js +0 -1
- package/dist/assets/main-DpNJ8pqp.css +0 -1
- package/dist/assets/main-aIpEYDDR.js +0 -289
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{df as t}from"./main-9TcmAtcq.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class o extends t{constructor(){super(),this.handleVisibilityChange=()=>{const e={isActive:document.hidden!==!0};this.notifyListeners("appStateChange",e),document.hidden?this.notifyListeners("pause",null):this.notifyListeners("resume",null)},document.addEventListener("visibilitychange",this.handleVisibilityChange,!1)}exitApp(){throw this.unimplemented("Not implemented on web.")}async getInfo(){throw this.unimplemented("Not implemented on web.")}async getLaunchUrl(){return{url:""}}async getState(){return{isActive:document.hidden!==!0}}async minimizeApp(){throw this.unimplemented("Not implemented on web.")}async toggleBackButtonHandler(){throw this.unimplemented("Not implemented on web.")}async getAppLanguage(){return{value:navigator.language.split("-")[0].toLowerCase()}}}export{o as AppWeb};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{df as a}from"./main-9TcmAtcq.js";import{ImpactStyle as i,NotificationType as r}from"./index-Ci2ZKu0n.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class h extends a{constructor(){super(...arguments),this.selectionStarted=!1}async impact(t){const e=this.patternForImpact(t==null?void 0:t.style);this.vibrateWithPattern(e)}async notification(t){const e=this.patternForNotification(t==null?void 0:t.type);this.vibrateWithPattern(e)}async vibrate(t){const e=(t==null?void 0:t.duration)||300;this.vibrateWithPattern([e])}async selectionStart(){this.selectionStarted=!0}async selectionChanged(){this.selectionStarted&&this.vibrateWithPattern([70])}async selectionEnd(){this.selectionStarted=!1}patternForImpact(t=i.Heavy){return t===i.Medium?[43]:t===i.Light?[20]:[61]}patternForNotification(t=r.Success){return t===r.Warning?[30,40,30,50,60]:t===r.Error?[27,45,50]:[35,65,21]}vibrateWithPattern(t){if(navigator.vibrate)navigator.vibrate(t);else throw this.unavailable("Browser does not support the vibrate API")}}export{h as HapticsWeb};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{df as s}from"./main-9TcmAtcq.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class l extends s{constructor(){super(...arguments),this.pending=[],this.deliveredNotifications=[],this.hasNotificationSupport=()=>{if(!("Notification"in window)||!Notification.requestPermission)return!1;if(Notification.permission!=="granted")try{new Notification("")}catch(i){if(i instanceof Error&&i.name==="TypeError")return!1}return!0}}async getDeliveredNotifications(){const i=[];for(const t of this.deliveredNotifications){const e={title:t.title,id:parseInt(t.tag),body:t.body};i.push(e)}return{notifications:i}}async removeDeliveredNotifications(i){for(const t of i.notifications){const e=this.deliveredNotifications.find(n=>n.tag===String(t.id));e==null||e.close(),this.deliveredNotifications=this.deliveredNotifications.filter(()=>!e)}}async removeAllDeliveredNotifications(){for(const i of this.deliveredNotifications)i.close();this.deliveredNotifications=[]}async createChannel(){throw this.unimplemented("Not implemented on web.")}async deleteChannel(){throw this.unimplemented("Not implemented on web.")}async listChannels(){throw this.unimplemented("Not implemented on web.")}async schedule(i){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");for(const t of i.notifications)this.sendNotification(t);return{notifications:i.notifications.map(t=>({id:t.id}))}}async getPending(){return{notifications:this.pending}}async registerActionTypes(){throw this.unimplemented("Not implemented on web.")}async cancel(i){this.pending=this.pending.filter(t=>!i.notifications.find(e=>e.id===t.id))}async areEnabled(){const{display:i}=await this.checkPermissions();return{value:i==="granted"}}async changeExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async checkExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async requestPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(await Notification.requestPermission())}}async checkPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(Notification.permission)}}transformNotificationPermission(i){switch(i){case"granted":return"granted";case"denied":return"denied";default:return"prompt"}}sendPending(){var i;const t=[],e=new Date().getTime();for(const n of this.pending)!((i=n.schedule)===null||i===void 0)&&i.at&&n.schedule.at.getTime()<=e&&(this.buildNotification(n),t.push(n));this.pending=this.pending.filter(n=>!t.find(o=>o===n))}sendNotification(i){var t;if(!((t=i.schedule)===null||t===void 0)&&t.at){const e=i.schedule.at.getTime()-new Date().getTime();this.pending.push(i),setTimeout(()=>{this.sendPending()},e);return}this.buildNotification(i)}buildNotification(i){const t=new Notification(i.title,{body:i.body,tag:String(i.id)});return t.addEventListener("click",this.onClick.bind(this,i),!1),t.addEventListener("show",this.onShow.bind(this,i),!1),t.addEventListener("close",()=>{this.deliveredNotifications=this.deliveredNotifications.filter(()=>!this)},!1),this.deliveredNotifications.push(t),t}onClick(i){const t={actionId:"tap",notification:i};this.notifyListeners("localNotificationActionPerformed",t)}onShow(i){this.notifyListeners("localNotificationReceived",i)}}export{l as LocalNotificationsWeb};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":"1.153.0"}
|
package/dist/index.html
CHANGED
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
|
|
23
23
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png" />
|
|
24
24
|
<title>Tide Commander</title>
|
|
25
|
-
<script type="module" crossorigin src="/assets/main-
|
|
25
|
+
<script type="module" crossorigin src="/assets/main-9TcmAtcq.js"></script>
|
|
26
26
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react--Eh9ivFN.js">
|
|
27
27
|
<link rel="modulepreload" crossorigin href="/assets/vendor-three-Chj50gSY.js">
|
|
28
|
-
<link rel="stylesheet" crossorigin href="/assets/main-
|
|
28
|
+
<link rel="stylesheet" crossorigin href="/assets/main-DP0iv8Le.css">
|
|
29
29
|
</head>
|
|
30
30
|
<body>
|
|
31
31
|
<div id="app"></div>
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"modelStyle": "Agent Model Style",
|
|
13
13
|
"animations": "Animations",
|
|
14
14
|
"secrets": "Secrets",
|
|
15
|
+
"claudeAccounts": "Claude Accounts",
|
|
15
16
|
"systemPrompt": "System Prompt",
|
|
16
17
|
"whatsapp": "WhatsApp Integration",
|
|
17
18
|
"whatsappNotifications": "WhatsApp Notifications",
|
|
@@ -1294,5 +1294,35 @@
|
|
|
1294
1294
|
"percentUsed": "{{percent}}% used",
|
|
1295
1295
|
"resets": "Resets {{time}}",
|
|
1296
1296
|
"limitsError": "Live limits unavailable: {{message}}"
|
|
1297
|
+
},
|
|
1298
|
+
"credentials": {
|
|
1299
|
+
"title": "Claude accounts",
|
|
1300
|
+
"help": "Named copies of ~/.claude/.credentials.json (e.g. .credentials.david.json). Switch the active account when you hit rate limits — same as renaming the file manually.",
|
|
1301
|
+
"current": "Active: {{name}}",
|
|
1302
|
+
"unsavedActive": "current (not saved as a named profile)",
|
|
1303
|
+
"noActive": "none",
|
|
1304
|
+
"activeBadge": "Active",
|
|
1305
|
+
"invalid": "Invalid",
|
|
1306
|
+
"use": "Use",
|
|
1307
|
+
"useTitle": "Make this the active Claude account",
|
|
1308
|
+
"rename": "Rename profile",
|
|
1309
|
+
"delete": "Delete named profile",
|
|
1310
|
+
"saveActive": "Save active as…",
|
|
1311
|
+
"savePlaceholder": "Profile name (e.g. david)",
|
|
1312
|
+
"nameRequired": "Enter a profile name.",
|
|
1313
|
+
"saved": "Saved active account as \"{{name}}\".",
|
|
1314
|
+
"switched": "Switched active Claude account to \"{{name}}\".",
|
|
1315
|
+
"switchedWithStash": "Saved previous account as \"{{stash}}\", then switched to \"{{name}}\".",
|
|
1316
|
+
"stashRequired": "Name the current account before switching so it is not lost.",
|
|
1317
|
+
"stashPrompt": "Current active credentials are not saved as a named profile. Name them before switching to \"{{name}}\":",
|
|
1318
|
+
"stashPlaceholder": "Name for current account (e.g. previous)",
|
|
1319
|
+
"confirmSwitch": "Save & switch",
|
|
1320
|
+
"confirmOverwrite": "Profile \"{{name}}\" already exists with different credentials. Overwrite?",
|
|
1321
|
+
"confirmDelete": "Delete named profile \"{{name}}\"? The active file is not removed.",
|
|
1322
|
+
"renamePrompt": "Rename profile \"{{name}}\" to:",
|
|
1323
|
+
"renamed": "Renamed \"{{from}}\" → \"{{to}}\".",
|
|
1324
|
+
"deleted": "Deleted profile \"{{name}}\".",
|
|
1325
|
+
"noProfiles": "No named profiles yet. Save the active account with a name (e.g. david, jc) to create one.",
|
|
1326
|
+
"tokenExpires": "token expires {{time}}"
|
|
1297
1327
|
}
|
|
1298
1328
|
}
|
|
@@ -11,6 +11,7 @@ import { createLogger } from '../utils/logger.js';
|
|
|
11
11
|
import { fetchLatestNpmVersion, getVersionRelation } from '../../shared/version.js';
|
|
12
12
|
import { endUpdate, getInstallInfo, isAutoUpdateSupported, isUpdateInProgress, runNpmGlobalUpdate, schedulePostUpdateRestart, tryBeginUpdate, } from '../services/self-update-service.js';
|
|
13
13
|
import { getAutoUpdateStatus, setAutoUpdateEnabled } from '../services/auto-update-service.js';
|
|
14
|
+
import { deleteClaudeCredentialProfile, listClaudeCredentialProfiles, renameClaudeCredentialProfile, saveActiveClaudeCredentialProfile, switchClaudeCredentialProfile, } from '../services/claude-credentials-service.js';
|
|
14
15
|
const log = createLogger('SystemRoutes');
|
|
15
16
|
const router = Router();
|
|
16
17
|
const PACKAGE_NAME = 'tide-commander';
|
|
@@ -130,8 +131,22 @@ function computeWebBundleInfo() {
|
|
|
130
131
|
.update(indexContent)
|
|
131
132
|
.digest('hex')
|
|
132
133
|
.slice(0, 16);
|
|
134
|
+
// The version must describe the dist we SERVE, not the live package.json —
|
|
135
|
+
// package.json moves ahead of the last build (version bumps, dev work), and
|
|
136
|
+
// advertising a version the bundle doesn't contain sends phones into an
|
|
137
|
+
// eternal re-sync loop (pull → still "behind" → pull …). build-info.json is
|
|
138
|
+
// emitted by the vite build; fall back to package.json for older dists.
|
|
139
|
+
let version = getInstallInfo().currentVersion;
|
|
140
|
+
try {
|
|
141
|
+
const buildInfo = JSON.parse(fs.readFileSync(path.join(dir, 'build-info.json'), 'utf8'));
|
|
142
|
+
if (buildInfo.version)
|
|
143
|
+
version = buildInfo.version;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
/* pre-build-info dist — package.json is the best guess */
|
|
147
|
+
}
|
|
133
148
|
return {
|
|
134
|
-
version
|
|
149
|
+
version,
|
|
135
150
|
hash,
|
|
136
151
|
fileCount: files.length,
|
|
137
152
|
totalBytes: files.reduce((sum, f) => sum + f.size, 0),
|
|
@@ -397,4 +412,95 @@ router.post('/auto-update', (req, res) => {
|
|
|
397
412
|
log.log(`Auto-update ${enabled ? 'enabled' : 'disabled'} via API`);
|
|
398
413
|
res.json(setAutoUpdateEnabled(enabled));
|
|
399
414
|
});
|
|
415
|
+
/**
|
|
416
|
+
* Claude CLI OAuth credential profiles (~/.claude/.credentials.json and
|
|
417
|
+
* ~/.claude/.credentials.<name>.json). Used to switch the active Claude
|
|
418
|
+
* account when rate limits are hit without manually renaming files.
|
|
419
|
+
*
|
|
420
|
+
* Tokens are never returned — only non-secret metadata + fingerprints.
|
|
421
|
+
*/
|
|
422
|
+
router.get('/claude-credentials', (_req, res) => {
|
|
423
|
+
try {
|
|
424
|
+
res.json(listClaudeCredentialProfiles());
|
|
425
|
+
}
|
|
426
|
+
catch (err) {
|
|
427
|
+
const message = err.message;
|
|
428
|
+
log.error(`Failed to list Claude credentials: ${message}`);
|
|
429
|
+
res.status(500).json({ error: message });
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
router.post('/claude-credentials/switch', (req, res) => {
|
|
433
|
+
try {
|
|
434
|
+
const name = req.body?.name;
|
|
435
|
+
const stashActiveAs = req.body?.stashActiveAs;
|
|
436
|
+
if (typeof name !== 'string' || !name.trim()) {
|
|
437
|
+
res.status(400).json({ error: 'Body must include { name: string }' });
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (stashActiveAs !== undefined && typeof stashActiveAs !== 'string') {
|
|
441
|
+
res.status(400).json({ error: 'stashActiveAs must be a string when provided' });
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const result = switchClaudeCredentialProfile(name.trim(), {
|
|
445
|
+
stashActiveAs: typeof stashActiveAs === 'string' ? stashActiveAs.trim() : undefined,
|
|
446
|
+
});
|
|
447
|
+
res.json(result);
|
|
448
|
+
}
|
|
449
|
+
catch (err) {
|
|
450
|
+
const message = err.message;
|
|
451
|
+
log.error(`Failed to switch Claude credentials: ${message}`);
|
|
452
|
+
const status = /not found|Invalid|required|already exists|cannot be/i.test(message) ? 400 : 500;
|
|
453
|
+
res.status(status).json({ error: message });
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
router.post('/claude-credentials/save', (req, res) => {
|
|
457
|
+
try {
|
|
458
|
+
const name = req.body?.name;
|
|
459
|
+
const force = Boolean(req.body?.force);
|
|
460
|
+
if (typeof name !== 'string' || !name.trim()) {
|
|
461
|
+
res.status(400).json({ error: 'Body must include { name: string }' });
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
res.json(saveActiveClaudeCredentialProfile(name.trim(), { force }));
|
|
465
|
+
}
|
|
466
|
+
catch (err) {
|
|
467
|
+
const message = err.message;
|
|
468
|
+
log.error(`Failed to save Claude credentials profile: ${message}`);
|
|
469
|
+
const status = /not found|Invalid|already exists|No valid/i.test(message) ? 400 : 500;
|
|
470
|
+
res.status(status).json({ error: message });
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
router.post('/claude-credentials/rename', (req, res) => {
|
|
474
|
+
try {
|
|
475
|
+
const from = req.body?.from;
|
|
476
|
+
const to = req.body?.to;
|
|
477
|
+
if (typeof from !== 'string' || !from.trim() || typeof to !== 'string' || !to.trim()) {
|
|
478
|
+
res.status(400).json({ error: 'Body must include { from: string, to: string }' });
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
res.json(renameClaudeCredentialProfile(from.trim(), to.trim()));
|
|
482
|
+
}
|
|
483
|
+
catch (err) {
|
|
484
|
+
const message = err.message;
|
|
485
|
+
log.error(`Failed to rename Claude credentials profile: ${message}`);
|
|
486
|
+
const status = /not found|Invalid|already exists/i.test(message) ? 400 : 500;
|
|
487
|
+
res.status(status).json({ error: message });
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
router.delete('/claude-credentials/:name', (req, res) => {
|
|
491
|
+
try {
|
|
492
|
+
const name = typeof req.params.name === 'string' ? req.params.name : '';
|
|
493
|
+
if (!name) {
|
|
494
|
+
res.status(400).json({ error: 'Profile name required' });
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
res.json(deleteClaudeCredentialProfile(name));
|
|
498
|
+
}
|
|
499
|
+
catch (err) {
|
|
500
|
+
const message = err.message;
|
|
501
|
+
log.error(`Failed to delete Claude credentials profile: ${message}`);
|
|
502
|
+
const status = /not found|Invalid/i.test(message) ? 400 : 500;
|
|
503
|
+
res.status(status).json({ error: message });
|
|
504
|
+
}
|
|
505
|
+
});
|
|
400
506
|
export default router;
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Credentials Service
|
|
3
|
+
*
|
|
4
|
+
* Manages multiple Claude Code OAuth credential profiles under ~/.claude:
|
|
5
|
+
* - Active (used by CLI + Tide): ~/.claude/.credentials.json
|
|
6
|
+
* - Named profiles: ~/.claude/.credentials.<name>.json
|
|
7
|
+
*
|
|
8
|
+
* Operators keep accounts such as david / jc / felipe as named profiles and
|
|
9
|
+
* promote one to active when the current account hits rate limits. Tokens are
|
|
10
|
+
* never returned to clients — only non-secret metadata + a stable fingerprint.
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as os from 'os';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
import * as crypto from 'crypto';
|
|
16
|
+
import { createLogger } from '../utils/logger.js';
|
|
17
|
+
import { resetClaudeRateLimitCache } from './claude-usage-service.js';
|
|
18
|
+
const log = createLogger('ClaudeCredentials');
|
|
19
|
+
const CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
|
20
|
+
const ACTIVE_FILE = '.credentials.json';
|
|
21
|
+
const NAMED_PREFIX = '.credentials.';
|
|
22
|
+
const NAMED_SUFFIX = '.json';
|
|
23
|
+
/** Overrideable for tests. */
|
|
24
|
+
let claudeDirOverride = null;
|
|
25
|
+
export function setClaudeCredentialsDirForTests(dir) {
|
|
26
|
+
claudeDirOverride = dir;
|
|
27
|
+
}
|
|
28
|
+
function getClaudeDir() {
|
|
29
|
+
return claudeDirOverride ?? CLAUDE_DIR;
|
|
30
|
+
}
|
|
31
|
+
function activePath() {
|
|
32
|
+
return path.join(getClaudeDir(), ACTIVE_FILE);
|
|
33
|
+
}
|
|
34
|
+
function namedPath(name) {
|
|
35
|
+
return path.join(getClaudeDir(), `${NAMED_PREFIX}${name}${NAMED_SUFFIX}`);
|
|
36
|
+
}
|
|
37
|
+
const NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
|
|
38
|
+
export function isValidProfileName(name) {
|
|
39
|
+
if (typeof name !== 'string' || !NAME_RE.test(name))
|
|
40
|
+
return false;
|
|
41
|
+
// Reject names that would collide with the active filename shape or path tricks.
|
|
42
|
+
if (name === 'json' || name.includes('/') || name.includes('\\') || name.includes('..'))
|
|
43
|
+
return false;
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
function fingerprintToken(accessToken) {
|
|
47
|
+
return crypto.createHash('sha256').update(accessToken).digest('hex').slice(0, 12);
|
|
48
|
+
}
|
|
49
|
+
function parseCredentialsFile(filePath) {
|
|
50
|
+
if (!fs.existsSync(filePath))
|
|
51
|
+
return null;
|
|
52
|
+
try {
|
|
53
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
54
|
+
const data = JSON.parse(raw);
|
|
55
|
+
const oauth = data?.claudeAiOauth;
|
|
56
|
+
const accessToken = typeof oauth?.accessToken === 'string' ? oauth.accessToken : '';
|
|
57
|
+
const valid = accessToken.length > 0;
|
|
58
|
+
const scopes = Array.isArray(oauth?.scopes)
|
|
59
|
+
? oauth.scopes.filter((s) => typeof s === 'string')
|
|
60
|
+
: [];
|
|
61
|
+
return {
|
|
62
|
+
raw,
|
|
63
|
+
fingerprint: valid ? fingerprintToken(accessToken) : null,
|
|
64
|
+
subscriptionType: typeof oauth?.subscriptionType === 'string' ? oauth.subscriptionType : null,
|
|
65
|
+
rateLimitTier: typeof oauth?.rateLimitTier === 'string' ? oauth.rateLimitTier : null,
|
|
66
|
+
expiresAt: typeof oauth?.expiresAt === 'number' ? oauth.expiresAt : null,
|
|
67
|
+
refreshTokenExpiresAt: typeof oauth?.refreshTokenExpiresAt === 'number' ? oauth.refreshTokenExpiresAt : null,
|
|
68
|
+
scopes,
|
|
69
|
+
hasMcpOAuth: Boolean(data?.mcpOAuth && typeof data.mcpOAuth === 'object'),
|
|
70
|
+
valid,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
log.warn(`Failed to parse credentials at ${filePath}: ${err}`);
|
|
75
|
+
return {
|
|
76
|
+
raw: '',
|
|
77
|
+
fingerprint: null,
|
|
78
|
+
subscriptionType: null,
|
|
79
|
+
rateLimitTier: null,
|
|
80
|
+
expiresAt: null,
|
|
81
|
+
refreshTokenExpiresAt: null,
|
|
82
|
+
scopes: [],
|
|
83
|
+
hasMcpOAuth: false,
|
|
84
|
+
valid: false,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function fileMtime(filePath) {
|
|
89
|
+
try {
|
|
90
|
+
return fs.statSync(filePath).mtimeMs;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function toMeta(id, name, filePath, parsed, isActive, matchesNamed) {
|
|
97
|
+
return {
|
|
98
|
+
id,
|
|
99
|
+
name,
|
|
100
|
+
isActive,
|
|
101
|
+
path: filePath,
|
|
102
|
+
valid: parsed?.valid ?? false,
|
|
103
|
+
fingerprint: parsed?.fingerprint ?? null,
|
|
104
|
+
subscriptionType: parsed?.subscriptionType ?? null,
|
|
105
|
+
rateLimitTier: parsed?.rateLimitTier ?? null,
|
|
106
|
+
expiresAt: parsed?.expiresAt ?? null,
|
|
107
|
+
refreshTokenExpiresAt: parsed?.refreshTokenExpiresAt ?? null,
|
|
108
|
+
scopes: parsed?.scopes ?? [],
|
|
109
|
+
hasMcpOAuth: parsed?.hasMcpOAuth ?? false,
|
|
110
|
+
mtimeMs: fileMtime(filePath),
|
|
111
|
+
matchesNamed,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function listNamedProfileNames() {
|
|
115
|
+
const dir = getClaudeDir();
|
|
116
|
+
if (!fs.existsSync(dir))
|
|
117
|
+
return [];
|
|
118
|
+
const names = [];
|
|
119
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
120
|
+
if (!entry.startsWith(NAMED_PREFIX) || !entry.endsWith(NAMED_SUFFIX))
|
|
121
|
+
continue;
|
|
122
|
+
if (entry === ACTIVE_FILE)
|
|
123
|
+
continue;
|
|
124
|
+
const name = entry.slice(NAMED_PREFIX.length, entry.length - NAMED_SUFFIX.length);
|
|
125
|
+
if (!name || !isValidProfileName(name))
|
|
126
|
+
continue;
|
|
127
|
+
names.push(name);
|
|
128
|
+
}
|
|
129
|
+
names.sort((a, b) => a.localeCompare(b));
|
|
130
|
+
return names;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* List the active credentials file plus every named profile.
|
|
134
|
+
* Never includes access/refresh tokens.
|
|
135
|
+
*/
|
|
136
|
+
export function listClaudeCredentialProfiles() {
|
|
137
|
+
const dir = getClaudeDir();
|
|
138
|
+
const activeFile = activePath();
|
|
139
|
+
const activeParsed = parseCredentialsFile(activeFile);
|
|
140
|
+
const namedNames = listNamedProfileNames();
|
|
141
|
+
const namedByFp = new Map();
|
|
142
|
+
for (const name of namedNames) {
|
|
143
|
+
const parsed = parseCredentialsFile(namedPath(name));
|
|
144
|
+
if (parsed?.fingerprint)
|
|
145
|
+
namedByFp.set(parsed.fingerprint, name);
|
|
146
|
+
}
|
|
147
|
+
const activeMatchesNamed = activeParsed?.fingerprint ? (namedByFp.get(activeParsed.fingerprint) ?? null) : null;
|
|
148
|
+
const active = fs.existsSync(activeFile)
|
|
149
|
+
? toMeta('active', 'active', activeFile, activeParsed, true, activeMatchesNamed)
|
|
150
|
+
: null;
|
|
151
|
+
const profiles = namedNames.map((name) => {
|
|
152
|
+
const p = namedPath(name);
|
|
153
|
+
const parsed = parseCredentialsFile(p);
|
|
154
|
+
const isActive = Boolean(activeParsed?.fingerprint) &&
|
|
155
|
+
parsed?.fingerprint === activeParsed?.fingerprint;
|
|
156
|
+
return toMeta(name, name, p, parsed, isActive, null);
|
|
157
|
+
});
|
|
158
|
+
return { claudeDir: dir, active, profiles };
|
|
159
|
+
}
|
|
160
|
+
function assertValidName(name) {
|
|
161
|
+
if (!isValidProfileName(name)) {
|
|
162
|
+
throw new Error(`Invalid profile name "${name}". Use 1–64 chars: letters, digits, dot, underscore, hyphen; must start with alphanumeric.`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function ensureClaudeDir() {
|
|
166
|
+
const dir = getClaudeDir();
|
|
167
|
+
if (!fs.existsSync(dir)) {
|
|
168
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/** Atomic write with restrictive permissions (credentials are secrets). */
|
|
172
|
+
function writeCredentialsAtomic(filePath, content) {
|
|
173
|
+
ensureClaudeDir();
|
|
174
|
+
const dir = path.dirname(filePath);
|
|
175
|
+
const tmp = path.join(dir, `.credentials.tmp.${process.pid}.${Date.now()}`);
|
|
176
|
+
fs.writeFileSync(tmp, content, { encoding: 'utf-8', mode: 0o600 });
|
|
177
|
+
fs.renameSync(tmp, filePath);
|
|
178
|
+
try {
|
|
179
|
+
fs.chmodSync(filePath, 0o600);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// best-effort on platforms that ignore mode
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Promote a named profile to the active credentials file (copy model).
|
|
187
|
+
*
|
|
188
|
+
* If the current active file is unique (does not match any named profile),
|
|
189
|
+
* `stashActiveAs` is required so the previous account is not lost.
|
|
190
|
+
* When the active file already matches a named profile, that named file is
|
|
191
|
+
* refreshed from active first (token refresh may have updated it).
|
|
192
|
+
*/
|
|
193
|
+
export function switchClaudeCredentialProfile(name, opts = {}) {
|
|
194
|
+
assertValidName(name);
|
|
195
|
+
const targetFile = namedPath(name);
|
|
196
|
+
if (!fs.existsSync(targetFile)) {
|
|
197
|
+
throw new Error(`Named profile "${name}" not found at ${targetFile}`);
|
|
198
|
+
}
|
|
199
|
+
const targetParsed = parseCredentialsFile(targetFile);
|
|
200
|
+
if (!targetParsed?.valid) {
|
|
201
|
+
throw new Error(`Named profile "${name}" is missing a valid OAuth token`);
|
|
202
|
+
}
|
|
203
|
+
const listing = listClaudeCredentialProfiles();
|
|
204
|
+
const activeFile = activePath();
|
|
205
|
+
let stashedAs = null;
|
|
206
|
+
const previousMatchesNamed = listing.active?.matchesNamed ?? null;
|
|
207
|
+
if (listing.active?.valid && listing.active.fingerprint) {
|
|
208
|
+
if (listing.active.fingerprint === targetParsed.fingerprint) {
|
|
209
|
+
// Already on this account — still refresh active from named in case of drift.
|
|
210
|
+
writeCredentialsAtomic(activeFile, targetParsed.raw);
|
|
211
|
+
resetClaudeRateLimitCache();
|
|
212
|
+
const after = listClaudeCredentialProfiles();
|
|
213
|
+
return {
|
|
214
|
+
ok: true,
|
|
215
|
+
active: after.active,
|
|
216
|
+
profiles: after.profiles,
|
|
217
|
+
stashedAs: null,
|
|
218
|
+
previousMatchesNamed,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
if (!previousMatchesNamed) {
|
|
222
|
+
const stash = opts.stashActiveAs;
|
|
223
|
+
if (!stash) {
|
|
224
|
+
throw new Error('Active credentials are not saved as a named profile. Pass stashActiveAs to keep them before switching.');
|
|
225
|
+
}
|
|
226
|
+
assertValidName(stash);
|
|
227
|
+
if (stash === name) {
|
|
228
|
+
throw new Error('stashActiveAs cannot be the same as the profile you are switching to');
|
|
229
|
+
}
|
|
230
|
+
const stashFile = namedPath(stash);
|
|
231
|
+
if (fs.existsSync(stashFile)) {
|
|
232
|
+
const existing = parseCredentialsFile(stashFile);
|
|
233
|
+
if (existing?.fingerprint && existing.fingerprint !== listing.active.fingerprint) {
|
|
234
|
+
throw new Error(`Profile "${stash}" already exists with different credentials. Choose another name.`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const activeRaw = parseCredentialsFile(activeFile)?.raw;
|
|
238
|
+
if (!activeRaw)
|
|
239
|
+
throw new Error('Failed to read active credentials for stash');
|
|
240
|
+
writeCredentialsAtomic(stashFile, activeRaw);
|
|
241
|
+
stashedAs = stash;
|
|
242
|
+
log.log(`Stashed previous active Claude credentials as profile "${stash}"`);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
// Refresh the named copy of the currently-active account (token refresh).
|
|
246
|
+
const matchedRaw = parseCredentialsFile(activeFile)?.raw;
|
|
247
|
+
if (matchedRaw) {
|
|
248
|
+
writeCredentialsAtomic(namedPath(previousMatchesNamed), matchedRaw);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
writeCredentialsAtomic(activeFile, targetParsed.raw);
|
|
253
|
+
resetClaudeRateLimitCache();
|
|
254
|
+
log.log(`Switched active Claude credentials to profile "${name}"`);
|
|
255
|
+
const after = listClaudeCredentialProfiles();
|
|
256
|
+
if (!after.active)
|
|
257
|
+
throw new Error('Active credentials missing after switch');
|
|
258
|
+
return {
|
|
259
|
+
ok: true,
|
|
260
|
+
active: after.active,
|
|
261
|
+
profiles: after.profiles,
|
|
262
|
+
stashedAs,
|
|
263
|
+
previousMatchesNamed,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Save (copy) the current active credentials as a named profile.
|
|
268
|
+
* Overwrites an existing named profile only when force=true or fingerprints match.
|
|
269
|
+
*/
|
|
270
|
+
export function saveActiveClaudeCredentialProfile(name, opts = {}) {
|
|
271
|
+
assertValidName(name);
|
|
272
|
+
const activeFile = activePath();
|
|
273
|
+
const activeParsed = parseCredentialsFile(activeFile);
|
|
274
|
+
if (!activeParsed?.valid || !activeParsed.raw) {
|
|
275
|
+
throw new Error('No valid active Claude credentials to save');
|
|
276
|
+
}
|
|
277
|
+
const dest = namedPath(name);
|
|
278
|
+
if (fs.existsSync(dest) && !opts.force) {
|
|
279
|
+
const existing = parseCredentialsFile(dest);
|
|
280
|
+
if (existing?.fingerprint && existing.fingerprint !== activeParsed.fingerprint) {
|
|
281
|
+
throw new Error(`Profile "${name}" already exists with different credentials. Pass force=true to overwrite.`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
writeCredentialsAtomic(dest, activeParsed.raw);
|
|
285
|
+
log.log(`Saved active Claude credentials as profile "${name}"`);
|
|
286
|
+
return listClaudeCredentialProfiles();
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Rename a named profile file.
|
|
290
|
+
*/
|
|
291
|
+
export function renameClaudeCredentialProfile(from, to) {
|
|
292
|
+
assertValidName(from);
|
|
293
|
+
assertValidName(to);
|
|
294
|
+
if (from === to)
|
|
295
|
+
return listClaudeCredentialProfiles();
|
|
296
|
+
const src = namedPath(from);
|
|
297
|
+
const dest = namedPath(to);
|
|
298
|
+
if (!fs.existsSync(src))
|
|
299
|
+
throw new Error(`Named profile "${from}" not found`);
|
|
300
|
+
if (fs.existsSync(dest))
|
|
301
|
+
throw new Error(`Profile "${to}" already exists`);
|
|
302
|
+
fs.renameSync(src, dest);
|
|
303
|
+
try {
|
|
304
|
+
fs.chmodSync(dest, 0o600);
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
// ignore
|
|
308
|
+
}
|
|
309
|
+
log.log(`Renamed Claude credentials profile "${from}" → "${to}"`);
|
|
310
|
+
return listClaudeCredentialProfiles();
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Delete a named profile. Refuses to delete when it is the only copy of the
|
|
314
|
+
* currently active credentials (would leave no backup).
|
|
315
|
+
*/
|
|
316
|
+
export function deleteClaudeCredentialProfile(name) {
|
|
317
|
+
assertValidName(name);
|
|
318
|
+
const file = namedPath(name);
|
|
319
|
+
if (!fs.existsSync(file))
|
|
320
|
+
throw new Error(`Named profile "${name}" not found`);
|
|
321
|
+
const listing = listClaudeCredentialProfiles();
|
|
322
|
+
if (listing.active?.matchesNamed === name) {
|
|
323
|
+
// Active still has the tokens; deleting the named backup is allowed but
|
|
324
|
+
// means active becomes "unsaved". That is intentional (user asked to delete).
|
|
325
|
+
}
|
|
326
|
+
fs.unlinkSync(file);
|
|
327
|
+
log.log(`Deleted Claude credentials profile "${name}"`);
|
|
328
|
+
return listClaudeCredentialProfiles();
|
|
329
|
+
}
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{u as me,r as l,n as ge,j as e,I as xe,o as fe}from"./main-aIpEYDDR.js";function je({isOpen:E,onClose:L,onMinimize:P,title:z,icon:U,lines:c,isStreaming:B,onClear:J,emptyMessage:F,streamingIndicatorLabel:X,headerInfo:Y,isLoading:V,extraToolbar:Z,extraFooter:O,modalClassName:ee}){const{t:a}=me(["terminal","common"]),[u,$]=l.useState(""),[h,w]=l.useState(!1),[f,S]=l.useState(!1),[M,I]=l.useState(""),[k,p]=l.useState(!0),[R,te]=l.useState(!0),[se,W]=l.useState(null),[r,le]=l.useState([]),[m,j]=l.useState(0),[v,ne]=l.useState(!1),[T,re]=l.useState(5),[Q,C]=l.useState(null),[y,ae]=l.useState(!0),o=l.useRef(null),D=l.useRef(null),A=l.useRef(null),H=l.useRef(!1),_=l.useRef(0),K=l.useMemo(()=>c.map(t=>t.text),[c]),b=l.useMemo(()=>{if(!u)return C(null),null;try{const t=v?u:u.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),s=new RegExp(t,"gi");return C(null),s}catch(t){return C(t.message),null}},[u,v]),N=l.useMemo(()=>{if(!b)return[];const t=[];for(let s=0;s<K.length;s++)b.test(K[s])&&t.push(s),b.lastIndex=0;return t},[K,b]);l.useEffect(()=>{le(N),m>=N.length&&j(Math.max(0,N.length-1))},[N]);const oe=l.useMemo(()=>{const t=c.map((i,x)=>({entry:i,originalIndex:x}));if(!u||!b||y)return t;if(N.length===0)return[];const s=new Set;for(const i of N){const x=Math.max(0,i-T),de=Math.min(c.length-1,i+T);for(let G=x;G<=de;G++)s.add(G)}const n=Array.from(s).sort((i,x)=>i-x),d=[];for(let i=0;i<n.length;i++){const x=n[i];i>0&&x-n[i-1]>1&&d.push({entry:{text:"",lineNumber:-1},originalIndex:-1,isSeparator:!0}),d.push({entry:c[x],originalIndex:x})}return d},[c,u,b,N,T,y]);l.useEffect(()=>{k&&o.current&&c.length>_.current&&(H.current=!0,requestAnimationFrame(()=>{o.current&&(o.current.scrollTop=o.current.scrollHeight),setTimeout(()=>{H.current=!1},50)})),_.current=c.length},[c.length,k]),l.useEffect(()=>{h&&D.current&&D.current.focus()},[h]),l.useEffect(()=>{f&&A.current&&A.current.focus()},[f]);const g=l.useCallback(t=>{if(!o.current)return;const n=t*18;o.current.scrollTop=n-100,W(t),setTimeout(()=>W(null),2e3)},[]);l.useEffect(()=>{if(!E)return;const t=s=>{if(s.key==="Escape"){s.preventDefault(),s.stopPropagation(),s.stopImmediatePropagation(),h?w(!1):f?S(!1):L();return}if(s.key==="/"&&!h&&!f&&(s.preventDefault(),w(!0)),s.key==="g"&&s.ctrlKey&&(s.preventDefault(),S(!0)),s.key==="End"&&s.ctrlKey&&o.current&&(o.current.scrollTop=o.current.scrollHeight),s.key==="Home"&&s.ctrlKey&&o.current&&(o.current.scrollTop=0),(s.key==="F3"||s.key==="Enter"&&h)&&r.length>0)if(s.preventDefault(),s.shiftKey){const n=(m-1+r.length)%r.length;j(n),g(r[n])}else{const n=(m+1)%r.length;j(n),g(r[n])}if(!h&&!f&&r.length>0){if(s.key==="n"&&!s.ctrlKey){s.preventDefault();const n=(m+1)%r.length;j(n),g(r[n])}else if(s.key==="N"){s.preventDefault();const n=(m-1+r.length)%r.length;j(n),g(r[n])}}!h&&!f&&o.current&&(s.key==="j"?(s.preventDefault(),o.current.scrollTop+=60,p(!1)):s.key==="k"?(s.preventDefault(),o.current.scrollTop-=60,p(!1)):s.key==="G"&&!s.ctrlKey?(s.preventDefault(),o.current.scrollTop=o.current.scrollHeight,p(!0)):s.key==="g"&&!s.ctrlKey&&(s.preventDefault(),o.current.scrollTop=0,p(!1)))};return window.addEventListener("keydown",t,{capture:!0}),()=>window.removeEventListener("keydown",t,{capture:!0})},[E,h,f,r,m,L,g]);const q=l.useCallback(()=>{const t=parseInt(M,10);!isNaN(t)&&t>0&&t<=c.length&&(g(t-1),p(!1)),S(!1),I("")},[M,c.length,g]),ce=l.useCallback(()=>{if(H.current||!o.current)return;const{scrollTop:t,scrollHeight:s,clientHeight:n}=o.current,d=s-t-n<50;!d&&k&&p(!1),d&&!k&&p(!0)},[k]),ie=l.useCallback(t=>{if(!u||!b)return t;const s=v?u:u.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");try{const n=new RegExp(`(${s})`,"gi");return t.replace(n,'<mark class="search-highlight">$1</mark>')}catch{return t}},[u,b,v]),{handleMouseDown:ue,handleClick:he}=ge(L);return E?e.jsx("div",{className:"pm2-logs-modal-overlay",onMouseDown:ue,onClick:he,children:e.jsxs("div",{className:`pm2-logs-modal ${ee||""}`,children:[e.jsxs("div",{className:"pm2-logs-modal-header",children:[e.jsxs("div",{className:"header-left",children:[e.jsx("span",{className:"modal-icon",children:U}),e.jsx("span",{className:"modal-title",children:z}),B&&e.jsxs("span",{className:"streaming-indicator",title:"Live streaming",children:[e.jsx("span",{className:"pulse"}),X||a("terminal:logs.live")]}),Y]}),e.jsxs("div",{className:"header-right",children:[e.jsx("span",{className:"line-count",children:a("terminal:logs.lines",{count:c.length})}),P&&e.jsx("button",{className:"modal-close",onClick:P,title:"Minimize — dock below the terminal input",children:e.jsx(xe,{name:"arrow-down",size:14})}),e.jsx("button",{className:"modal-close",onClick:L,title:a("terminal:logs.closeEsc"),children:"×"})]})]}),e.jsxs("div",{className:"pm2-logs-modal-toolbar",children:[e.jsxs("div",{className:"toolbar-left",children:[e.jsxs("button",{className:`toolbar-btn ${h?"active":""}`,onClick:()=>w(!h),title:a("terminal:logs.searchShortcut"),children:["🔍 ",a("terminal:logs.search")]}),e.jsxs("button",{className:"toolbar-btn",onClick:()=>S(!0),title:a("terminal:logs.goToLineShortcut"),children:["⏬ ",a("terminal:logs.goToLine")]}),Z,e.jsxs("button",{className:`toolbar-btn ${R?"active":""}`,onClick:()=>te(!R),title:a("terminal:logs.toggleWrap"),children:["↩ ",a("terminal:logs.wrap")]}),e.jsxs("button",{className:`toolbar-btn ${k?"active":""}`,onClick:()=>p(!k),title:a("terminal:logs.autoScrollToBottom"),children:["↓ ",a("terminal:logs.auto")]})]}),e.jsx("div",{className:"toolbar-right",children:e.jsxs("button",{className:"toolbar-btn danger",onClick:J,title:a("terminal:logs.clear"),children:["🗑 ",a("terminal:logs.clear")]})})]}),h&&e.jsxs("div",{className:"pm2-logs-search-bar",children:[e.jsx("input",{ref:D,type:"text",placeholder:a(v?"terminal:logs.regexPlaceholder":"terminal:logs.searchPlaceholder"),value:u,onChange:t=>{$(t.target.value),j(0)},onKeyDown:t=>{t.key==="Escape"&&(w(!1),$(""),C(null))}}),e.jsx("button",{className:`toolbar-btn small ${v?"active":""}`,onClick:()=>ne(!v),title:"Toggle regex mode",children:".*"}),e.jsx("button",{className:`toolbar-btn small ${y?"active":""}`,onClick:()=>ae(!y),title:y?"Show all lines (highlight matches)":"Show only context around matches",children:y?"All":"Ctx"}),e.jsxs("div",{className:`context-control ${y?"hidden":""}`,title:"Lines of context around matches",children:[e.jsx("label",{children:"±"}),e.jsx("input",{type:"number",min:"0",max:"100",value:T,onChange:t=>re(Math.max(0,parseInt(t.target.value)||0)),className:"context-input"})]}),e.jsx("button",{className:"toolbar-btn small",onClick:()=>{if(r.length===0)return;const t=(m-1+r.length)%r.length;j(t),g(r[t])},disabled:r.length===0,title:"Previous match (Shift+F3)",children:"▲"}),e.jsx("button",{className:"toolbar-btn small",onClick:()=>{if(r.length===0)return;const t=(m+1)%r.length;j(t),g(r[t])},disabled:r.length===0,title:"Next match (F3)",children:"▼"}),u&&e.jsx("span",{className:"match-count",children:Q?e.jsx("span",{className:"regex-error",title:Q,children:a("terminal:logs.invalidRegex")}):r.length>0?`${m+1}/${r.length}`:a("terminal:logs.noMatches")}),e.jsx("button",{className:"search-close",onClick:()=>{w(!1),$(""),C(null)},children:"×"})]}),f&&e.jsxs("div",{className:"pm2-logs-goto-line",children:[e.jsxs("label",{children:[a("terminal:logs.goToLineLabel"),":"]}),e.jsx("input",{ref:A,type:"number",min:"1",max:c.length,placeholder:`1-${c.length}`,value:M,onChange:t=>I(t.target.value),onKeyDown:t=>{t.key==="Enter"?q():t.key==="Escape"&&(S(!1),I(""))}}),e.jsx("button",{onClick:q,children:a("terminal:logs.go")}),e.jsx("button",{onClick:()=>S(!1),children:a("common:buttons.cancel")})]}),e.jsx("div",{ref:o,className:`pm2-logs-content ${R?"wrap":"nowrap"}`,onScroll:ce,children:c.length===0?e.jsx("div",{className:"logs-empty",children:V||B?e.jsxs("div",{className:"logs-loading",children:[e.jsx("span",{className:"logs-loading-spinner","aria-hidden":"true"}),e.jsx("span",{children:F||a(V?"terminal:logs.loadingLogs":"terminal:logs.waitingForLogs")})]}):e.jsx("span",{children:F||a("terminal:logs.noLogs")})}):e.jsx("div",{className:"logs-lines",children:oe.map((t,s)=>{if("isSeparator"in t&&t.isSeparator)return e.jsxs("div",{className:"log-separator",children:[e.jsx("span",{className:"separator-line"}),e.jsx("span",{className:"separator-label",children:"…"}),e.jsx("span",{className:"separator-line"})]},`sep-${s}`);const{entry:n,originalIndex:d}=t,i=u&&N.includes(d);return e.jsxs("div",{className:`log-line ${se===d?"highlighted":""} ${r[m]===d?"current-match":""} ${i?"match-line":""} ${n.isError?"error-line":""}`,children:[e.jsx("span",{className:"line-number",children:n.lineNumber}),n.sourceLabel&&e.jsxs("span",{className:"boss-log-source",style:{color:n.sourceColor},children:["[",n.sourceLabel,"]"]}),e.jsx("span",{className:"line-content",dangerouslySetInnerHTML:{__html:ie(fe(n.text))}})]},d)})})}),e.jsxs("div",{className:"pm2-logs-modal-footer",children:[e.jsxs("span",{className:"shortcut",children:[e.jsx("kbd",{children:"j"}),"/",e.jsx("kbd",{children:"k"})," Scroll"]}),e.jsxs("span",{className:"shortcut",children:[e.jsx("kbd",{children:"g"}),"/",e.jsx("kbd",{children:"G"})," Top/Bottom"]}),e.jsxs("span",{className:"shortcut",children:[e.jsx("kbd",{children:"/"})," Search"]}),e.jsxs("span",{className:"shortcut",children:[e.jsx("kbd",{children:"n"}),"/",e.jsx("kbd",{children:"N"})," Next/Prev match"]}),e.jsxs("span",{className:"shortcut",children:[e.jsx("kbd",{children:"Ctrl+G"})," Go to line"]}),e.jsxs("span",{className:"shortcut",children:[e.jsx("kbd",{children:"Esc"})," Close"]}),O]})]})}):null}export{je as L};
|