tide-commander 1.151.2 → 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 +15 -0
- package/dist/assets/{BossLogsModal-DGb6Dawk.js → BossLogsModal-Dgz-N5gw.js} +1 -1
- package/dist/assets/{BossSpawnModal-bAPCPdSd.js → BossSpawnModal-BK_rYu8s.js} +1 -1
- package/dist/assets/{ControlsModal-Coq__bu-.js → ControlsModal-hC_AiCpk.js} +1 -1
- package/dist/assets/{DockerLogsModal-BQZeOuV9.js → DockerLogsModal-D7ogyaZr.js} +1 -1
- package/dist/assets/{EmbeddedEditor-BtJp9gMt.js → EmbeddedEditor-BFzUOmwz.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-DCbuudZO.js → GmailOAuthSetup-C_Yc5Ypv.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-Cuw3G9nk.js → GoogleOAuthSetup-CefQSOYm.js} +1 -1
- package/dist/assets/{IframeModal-C_PL3LoC.js → IframeModal-BbeuiE04.js} +1 -1
- package/dist/assets/{IntegrationsPanel-UzwNp4gx.js → IntegrationsPanel-xh3TgJLH.js} +2 -2
- package/dist/assets/LogViewerModal-CE2L8nWJ.js +1 -0
- package/dist/assets/{MonitoringModal-DxTy2EgU.js → MonitoringModal-A2SN3uAJ.js} +1 -1
- package/dist/assets/{PM2LogsModal-4hthqpZH.js → PM2LogsModal-DdlhCa8u.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-D5RJraJz.js → RestoreArchivedAreaModal-BvHo-OxE.js} +1 -1
- package/dist/assets/{Scene2DCanvas-BdttXSwh.js → Scene2DCanvas-t6RuUegr.js} +1 -1
- package/dist/assets/{SceneManager-BIk_uwP8.js → SceneManager-TQ2_AvFn.js} +1 -1
- package/dist/assets/{SkillsPanel-rUryywpx.js → SkillsPanel-PjzjcaoG.js} +3 -3
- package/dist/assets/{SlackMultiInstanceSetup-C2rz0yH8.js → SlackMultiInstanceSetup-ms4r32Z8.js} +1 -1
- package/dist/assets/{SpawnModal-DLGHvjww.js → SpawnModal-B1FS8m9h.js} +1 -1
- package/dist/assets/{StatisticsModal-Dp3UcJM-.js → StatisticsModal-mmz8hnD5.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-xAZzPmge.js → SubordinateAssignmentModal-BXoG-Xgd.js} +1 -1
- package/dist/assets/SymbolsNerdFontMono-Regular-BM2yZ5nr.woff2 +0 -0
- package/dist/assets/{TriggerManagerPanel-D1gEAv-W.js → TriggerManagerPanel-ODJyjvY-.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-CWbOnKmv.js → WorkflowEditorPanel-9wtY0Tn3.js} +1 -1
- package/dist/assets/{area-logos-t-kcQpe2.js → area-logos-BLyzmh47.js} +1 -1
- package/dist/assets/{index-Cht8feDm.js → index-1hwiSf7o.js} +1 -1
- package/dist/assets/{index-AhX-UCt7.js → index-CBkh8xcE.js} +1 -1
- package/dist/assets/{index-D56tkDkl.js → index-CN2p2fxH.js} +4 -4
- package/dist/assets/index-CbfoJ_Vc.js +1 -0
- package/dist/assets/{index-D5lCtmXE.js → index-CdbLxNJr.js} +1 -1
- package/dist/assets/{index-BPhmYxbC.js → index-Cf-PINah.js} +3 -3
- package/dist/assets/{index-BzjVLEUP.js → index-Ci2ZKu0n.js} +2 -2
- package/dist/assets/{index-BIjoVXsQ.js → index-D8vFfLs4.js} +3 -3
- package/dist/assets/{index-DDE2P-En.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-C1FuDjQR.js → web-BAkfcL4w.js} +1 -1
- package/dist/assets/{web-CBnEMFyp.js → web-BVIPIXA1.js} +1 -1
- package/dist/assets/{web-1nZKBCf7.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 +19 -0
- package/dist/locales/en/terminal.json +30 -0
- package/dist/src/packages/server/routes/system.js +216 -0
- package/dist/src/packages/server/services/claude-credentials-service.js +329 -0
- package/package.json +1 -1
- package/dist/assets/LogViewerModal-CCJv9FIp.js +0 -1
- package/dist/assets/index-BNzYPyBm.css +0 -1
- package/dist/assets/index-BZtRj8Ya.js +0 -1
- package/dist/assets/main-4YL5DKtG.css +0 -1
- package/dist/assets/main-B-26AddI.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",
|
|
@@ -618,5 +619,23 @@
|
|
|
618
619
|
"close": "Close",
|
|
619
620
|
"retry": "Retry",
|
|
620
621
|
"failed": "App update failed"
|
|
622
|
+
},
|
|
623
|
+
"webBundle": {
|
|
624
|
+
"title": "UI over-the-air",
|
|
625
|
+
"sourceOta": "UI source: synced bundle ({{hash}})",
|
|
626
|
+
"sourceBundled": "UI source: bundled with APK",
|
|
627
|
+
"apkVersion": "APK v{{version}}",
|
|
628
|
+
"serverBundle": "Server UI: v{{version}} ({{hash}})",
|
|
629
|
+
"updateAvailable": "update available",
|
|
630
|
+
"upToDate": "up to date",
|
|
631
|
+
"serverUnavailable": "The server has no built UI bundle to serve (run npm run build on the server)",
|
|
632
|
+
"autoSync": "Sync UI from server automatically",
|
|
633
|
+
"syncNow": "Sync UI now",
|
|
634
|
+
"syncingPct": "Syncing… {{percent}}%",
|
|
635
|
+
"reset": "Reset to bundled UI",
|
|
636
|
+
"bannerSyncing": "Updating UI from server…",
|
|
637
|
+
"bannerSyncingPct": "Updating UI from server… {{percent}}%",
|
|
638
|
+
"bannerFailed": "UI sync failed",
|
|
639
|
+
"close": "Close"
|
|
621
640
|
}
|
|
622
641
|
}
|
|
@@ -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
|
}
|
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
* Endpoints for inspecting / updating the running Tide Commander install.
|
|
4
4
|
*/
|
|
5
5
|
import { Router } from 'express';
|
|
6
|
+
import * as crypto from 'crypto';
|
|
6
7
|
import * as fs from 'fs';
|
|
7
8
|
import * as path from 'path';
|
|
9
|
+
import archiver from 'archiver';
|
|
8
10
|
import { createLogger } from '../utils/logger.js';
|
|
9
11
|
import { fetchLatestNpmVersion, getVersionRelation } from '../../shared/version.js';
|
|
10
12
|
import { endUpdate, getInstallInfo, isAutoUpdateSupported, isUpdateInProgress, runNpmGlobalUpdate, schedulePostUpdateRestart, tryBeginUpdate, } from '../services/self-update-service.js';
|
|
11
13
|
import { getAutoUpdateStatus, setAutoUpdateEnabled } from '../services/auto-update-service.js';
|
|
14
|
+
import { deleteClaudeCredentialProfile, listClaudeCredentialProfiles, renameClaudeCredentialProfile, saveActiveClaudeCredentialProfile, switchClaudeCredentialProfile, } from '../services/claude-credentials-service.js';
|
|
12
15
|
const log = createLogger('SystemRoutes');
|
|
13
16
|
const router = Router();
|
|
14
17
|
const PACKAGE_NAME = 'tide-commander';
|
|
@@ -74,6 +77,128 @@ router.get('/changelog', (_req, res) => {
|
|
|
74
77
|
res.status(500).json({ error: message });
|
|
75
78
|
}
|
|
76
79
|
});
|
|
80
|
+
/**
|
|
81
|
+
* Web bundle (OTA UI sync for the Android app)
|
|
82
|
+
*
|
|
83
|
+
* The APK can swap its WebView assets for a bundle pulled from this server
|
|
84
|
+
* (Capacitor setServerBasePath — Expo-updates style), so UI-only releases
|
|
85
|
+
* reach phones without installing a new APK. These endpoints expose the
|
|
86
|
+
* client build this server serves (installRoot/dist).
|
|
87
|
+
*/
|
|
88
|
+
/** Root of the served client build, or null when no build exists (dev without dist). */
|
|
89
|
+
function getClientDistDir() {
|
|
90
|
+
const installRoot = getInstallInfo().installRoot;
|
|
91
|
+
if (!installRoot)
|
|
92
|
+
return null;
|
|
93
|
+
const dir = path.join(installRoot, 'dist');
|
|
94
|
+
return fs.existsSync(path.join(dir, 'index.html')) ? dir : null;
|
|
95
|
+
}
|
|
96
|
+
// dist/ also holds the compiled server (src/) — never ship that to the WebView.
|
|
97
|
+
const WEB_BUNDLE_EXCLUDED_ROOTS = new Set(['src']);
|
|
98
|
+
function walkClientFiles(root) {
|
|
99
|
+
const files = [];
|
|
100
|
+
const walk = (rel) => {
|
|
101
|
+
for (const entry of fs.readdirSync(path.join(root, rel), { withFileTypes: true })) {
|
|
102
|
+
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
103
|
+
if (!rel && WEB_BUNDLE_EXCLUDED_ROOTS.has(entry.name))
|
|
104
|
+
continue;
|
|
105
|
+
if (entry.isDirectory()) {
|
|
106
|
+
walk(childRel);
|
|
107
|
+
}
|
|
108
|
+
else if (entry.isFile() && !entry.name.endsWith('.map')) {
|
|
109
|
+
files.push({ path: childRel, size: fs.statSync(path.join(root, childRel)).size });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
walk('');
|
|
114
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
115
|
+
return files;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Bundle identity = file list + sizes + full index.html content. Vite
|
|
119
|
+
* content-hashes asset filenames (so index.html captures every JS/CSS
|
|
120
|
+
* change), and sizes catch edits to runtime-fetched files (locales, sw.js).
|
|
121
|
+
*/
|
|
122
|
+
function computeWebBundleInfo() {
|
|
123
|
+
const dir = getClientDistDir();
|
|
124
|
+
if (!dir)
|
|
125
|
+
return null;
|
|
126
|
+
const files = walkClientFiles(dir);
|
|
127
|
+
const indexContent = fs.readFileSync(path.join(dir, 'index.html'), 'utf8');
|
|
128
|
+
const hash = crypto
|
|
129
|
+
.createHash('sha256')
|
|
130
|
+
.update(JSON.stringify(files))
|
|
131
|
+
.update(indexContent)
|
|
132
|
+
.digest('hex')
|
|
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
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
version,
|
|
150
|
+
hash,
|
|
151
|
+
fileCount: files.length,
|
|
152
|
+
totalBytes: files.reduce((sum, f) => sum + f.size, 0),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
router.get('/web-bundle-info', (_req, res) => {
|
|
156
|
+
try {
|
|
157
|
+
const info = computeWebBundleInfo();
|
|
158
|
+
if (!info) {
|
|
159
|
+
res.status(404).json({ error: 'No client build to serve — run npm run build on the server first.' });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
res.json(info);
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
const message = err.message;
|
|
166
|
+
log.error(`Failed to compute web bundle info: ${message}`);
|
|
167
|
+
res.status(500).json({ error: message });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
router.get('/web-bundle', (_req, res) => {
|
|
171
|
+
const dir = getClientDistDir();
|
|
172
|
+
if (!dir) {
|
|
173
|
+
res.status(404).json({ error: 'No client build to serve — run npm run build on the server first.' });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
const info = computeWebBundleInfo();
|
|
178
|
+
res.setHeader('Content-Type', 'application/zip');
|
|
179
|
+
res.setHeader('Content-Disposition', 'attachment; filename="web-bundle.zip"');
|
|
180
|
+
if (info)
|
|
181
|
+
res.setHeader('X-Bundle-Hash', info.hash);
|
|
182
|
+
const archive = archiver('zip', { zlib: { level: 6 } });
|
|
183
|
+
archive.on('error', (err) => {
|
|
184
|
+
log.error(`Web bundle zip failed: ${err.message}`);
|
|
185
|
+
res.destroy(err);
|
|
186
|
+
});
|
|
187
|
+
archive.pipe(res);
|
|
188
|
+
for (const file of walkClientFiles(dir)) {
|
|
189
|
+
archive.file(path.join(dir, file.path), { name: file.path });
|
|
190
|
+
}
|
|
191
|
+
void archive.finalize();
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
const message = err.message;
|
|
195
|
+
log.error(`Failed to stream web bundle: ${message}`);
|
|
196
|
+
if (!res.headersSent)
|
|
197
|
+
res.status(500).json({ error: message });
|
|
198
|
+
else
|
|
199
|
+
res.destroy();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
77
202
|
/**
|
|
78
203
|
* GET /api/system/app-update
|
|
79
204
|
*
|
|
@@ -287,4 +412,95 @@ router.post('/auto-update', (req, res) => {
|
|
|
287
412
|
log.log(`Auto-update ${enabled ? 'enabled' : 'disabled'} via API`);
|
|
288
413
|
res.json(setAutoUpdateEnabled(enabled));
|
|
289
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
|
+
});
|
|
290
506
|
export default router;
|