tide-commander 1.181.0 → 1.182.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 +11 -0
- package/README.md +21 -1
- package/dist/assets/{AudioPlayer-BAtnEqGO.js → AudioPlayer-BwhOjJ2c.js} +1 -1
- package/dist/assets/{BossLogsModal-DKPlKlOF.js → BossLogsModal-DAHZQ1pi.js} +1 -1
- package/dist/assets/BossSpawnModal-E7SGuXA2.js +1 -0
- package/dist/assets/ControlsModal-bAaYejMT.js +1 -0
- package/dist/assets/{DockerLogsModal-hn265l_A.js → DockerLogsModal-BJx8N4kK.js} +1 -1
- package/dist/assets/{EmbeddedEditor-Cj5jPNh6.js → EmbeddedEditor-Be-cO4-9.js} +1 -1
- package/dist/assets/{FcStdViewer-CFxBBNQq.js → FcStdViewer-DEwQ08Bb.js} +1 -1
- package/dist/assets/{GcodeViewer-3z7qkN29.js → GcodeViewer-ncf61wWb.js} +1 -1
- package/dist/assets/{GlbViewer-CaOGrb_f.js → GlbViewer-D12gmnGd.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-Bk6jecTI.js → GmailOAuthSetup-C-GvDGQm.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-DVeeuuWe.js → GoogleOAuthSetup-BDjIAgur.js} +1 -1
- package/dist/assets/{IframeModal-wSJPMjiR.js → IframeModal-CCocGfa7.js} +1 -1
- package/dist/assets/{IntegrationsPanel-BorSz1f6.js → IntegrationsPanel-CBpaeiJG.js} +2 -2
- package/dist/assets/{LogViewerModal-Bc_8Tjwp.js → LogViewerModal-CaN7XU8z.js} +1 -1
- package/dist/assets/{MonitoringModal-_K-xK58O.js → MonitoringModal-I3nCNlbU.js} +1 -1
- package/dist/assets/{PM2LogsModal-RP5kh78y.js → PM2LogsModal-Bki_LCvj.js} +1 -1
- package/dist/assets/RestoreArchivedAreaModal-Ciy6t7et.js +1 -0
- package/dist/assets/{Scene2DCanvas-BD8gC5GG.js → Scene2DCanvas-DR56E5p7.js} +1 -1
- package/dist/assets/{SceneManager-B1FWKQrV.js → SceneManager-DKg8ugY_.js} +1 -1
- package/dist/assets/{SkillsPanel-B3sV8qgJ.js → SkillsPanel-C_dxzS6F.js} +2 -2
- package/dist/assets/{SlackMultiInstanceSetup-By9kD-7V.js → SlackMultiInstanceSetup-BwiTMMvL.js} +1 -1
- package/dist/assets/SpawnModal-DUMcIHoz.js +1 -0
- package/dist/assets/{StatisticsModal-k-Zow1Lc.js → StatisticsModal-BKXcBthy.js} +1 -1
- package/dist/assets/{StlViewer-CvwylgqD.js → StlViewer-DjdLvysP.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-BgZoihF2.js → SubordinateAssignmentModal-Rl4cdHpe.js} +1 -1
- package/dist/assets/{ThreeAreaSelector-CBoLZACl.js → ThreeAreaSelector-C-9JJ1Hh.js} +1 -1
- package/dist/assets/{ThreeViewShortcuts-CwWPyQ1I.js → ThreeViewShortcuts-DL0Hmuj0.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-8dCnF4zG.js → TriggerManagerPanel-BtCOoX1g.js} +1 -1
- package/dist/assets/{VideoPlayer-CFfNly_q.js → VideoPlayer-BoTr9gkT.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-DaoqmmDI.js → WorkflowEditorPanel-DlcSaDPD.js} +1 -1
- package/dist/assets/{abnfDiagram-VRR7QNED-AVCJRSGv.js → abnfDiagram-VRR7QNED-CEBRXYsw.js} +1 -1
- package/dist/assets/{arc-BozrtqGC.js → arc-COf9R405.js} +1 -1
- package/dist/assets/{architectureDiagram-ZJ3FMSHR-DIbqkKKH.js → architectureDiagram-ZJ3FMSHR-DGBRfu4B.js} +1 -1
- package/dist/assets/{area-logos-_xssrP4-.js → area-logos-BRuKyQ5J.js} +1 -1
- package/dist/assets/{blockDiagram-677ZJIJ3-cUooA8WG.js → blockDiagram-677ZJIJ3-CN0t4U2k.js} +1 -1
- package/dist/assets/{c4Diagram-LMCZKHZV-BN-pAlnh.js → c4Diagram-LMCZKHZV-Cq0IftW5.js} +1 -1
- package/dist/assets/channel-DF_pUJhw.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-Df3QaYQM.js → chunk-2Q5K7J3B-adR2VoGA.js} +1 -1
- package/dist/assets/{chunk-32BRIVSS-eWsLMUAK.js → chunk-32BRIVSS-B6LCChZq.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-D8L5MdGs.js → chunk-5VM5RSS4-CcH2c7y_.js} +1 -1
- package/dist/assets/{chunk-EX3LRPZG-D9F-m38t.js → chunk-EX3LRPZG-zBWa-mu2.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CTPQZilc.js → chunk-JWPE2WC7-DBQlYiWn.js} +1 -1
- package/dist/assets/{chunk-MOJQB5TN-BI5XLi89.js → chunk-MOJQB5TN-BjbsCplS.js} +1 -1
- package/dist/assets/{chunk-RYQCIY6F-aUw8CA0H.js → chunk-RYQCIY6F-B946ZxlN.js} +1 -1
- package/dist/assets/{chunk-V7JOEXUC-Bvn63xfp.js → chunk-V7JOEXUC-DEeYHnRT.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-DuQCapS-.js → chunk-VR4S4FIN-B_QmrVGt.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY--Q2M75o8.js → chunk-XXDRQBXY-cgYh6YrW.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-CeYNs9YM.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-CeYNs9YM.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-CnOCRlHl.js → cose-bilkent-JH36ORCC-DJUmw0Ip.js} +1 -1
- package/dist/assets/{cynefin-VYW2F7L2-CVOENzFv.js → cynefin-VYW2F7L2-BUxR220i.js} +1 -1
- package/dist/assets/{cynefinDiagram-TSTJHNR4-BLe8BxZL.js → cynefinDiagram-TSTJHNR4-nYR1MwE2.js} +1 -1
- package/dist/assets/{dagre-VKFMJZFB-xtW7I5Nk.js → dagre-VKFMJZFB-A5VPim2l.js} +1 -1
- package/dist/assets/{diagram-FQU43EPY-BIr_QEk-.js → diagram-FQU43EPY-wdgwDoVg.js} +1 -1
- package/dist/assets/{diagram-G47NLZAW-BJd55qXW.js → diagram-G47NLZAW-CylyJQBQ.js} +1 -1
- package/dist/assets/{diagram-NH7WQ7WH-CZsKPct7.js → diagram-NH7WQ7WH-BOF1uKhH.js} +1 -1
- package/dist/assets/{diagram-OA4YK3LP-BaG39QNH.js → diagram-OA4YK3LP-B0B5zmh5.js} +1 -1
- package/dist/assets/{diagram-WEI45ONY-ShUVdCXz.js → diagram-WEI45ONY-vKP2IU9q.js} +1 -1
- package/dist/assets/{ebnfDiagram-CCIWWBDH-T9Sx10gP.js → ebnfDiagram-CCIWWBDH-DM8hIEJM.js} +1 -1
- package/dist/assets/{erDiagram-Q63AITRT-CwT0KHCH.js → erDiagram-Q63AITRT-iuRH3hWX.js} +1 -1
- package/dist/assets/{flowDiagram-23GEKE2U-DpfmSS0V.js → flowDiagram-23GEKE2U-CddCUoGB.js} +1 -1
- package/dist/assets/{ganttDiagram-NO4QXBWP-DFHqNARc.js → ganttDiagram-NO4QXBWP-CIfvQLcu.js} +1 -1
- package/dist/assets/{gitGraphDiagram-IHSO6WYX-DiG1nj1m.js → gitGraphDiagram-IHSO6WYX-CbW0FQci.js} +1 -1
- package/dist/assets/index-B4YklnOy.js +1 -0
- package/dist/assets/{index-CeUo--0k.js → index-BmD7ycmn.js} +1 -1
- package/dist/assets/{index-B8WCuK1N.js → index-CIcFjlRX.js} +2 -2
- package/dist/assets/{index-BMINDCB0.js → index-CRhUiTuD.js} +3 -3
- package/dist/assets/{index-COqQ_1sL.css → index-D1GfjaTN.css} +1 -1
- package/dist/assets/index-DHuT10Pa.js +1 -0
- package/dist/assets/{index-BmmzETKL.js → index-DoJdznbY.js} +5 -5
- package/dist/assets/index-Du8eK_OX.js +1 -0
- package/dist/assets/index-HYcYan1q.js +19 -0
- package/dist/assets/index-HhAZ3SIm.js +2 -0
- package/dist/assets/{infoDiagram-FWYZ7A6U-CT7RBQSN.js → infoDiagram-FWYZ7A6U-C0it9hLc.js} +1 -1
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-Bv_eTPfi.js → ishikawaDiagram-FXEZZL3T-pKZnNgqp.js} +1 -1
- package/dist/assets/{journeyDiagram-5HDEW3XC-BwKya-ih.js → journeyDiagram-5HDEW3XC-Ib9FpXdB.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-VrWxcMn7.js → kanban-definition-HUTT4EX6-aOTfe6f6.js} +1 -1
- package/dist/assets/{linear-BsocX-C-.js → linear-B_ipWlyx.js} +1 -1
- package/dist/assets/main-Bvn8CMZc.css +1 -0
- package/dist/assets/{main-B9DphKOa.js → main-m3NNcAo4.js} +115 -115
- package/dist/assets/{mermaid.core-Uz36XO2-.js → mermaid.core-EY9hyXLz.js} +4 -4
- package/dist/assets/{mindmap-definition-LN4V7U3C-Bv9DlPAr.js → mindmap-definition-LN4V7U3C-B_p1LOAa.js} +1 -1
- package/dist/assets/{pegDiagram-2B236MQR-CRG-2pCa.js → pegDiagram-2B236MQR-BgCd9rWg.js} +1 -1
- package/dist/assets/{pieDiagram-ENE6RG2P-dkVFz981.js → pieDiagram-ENE6RG2P-d8ofNooq.js} +1 -1
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-D6Y-oPfA.js → quadrantDiagram-ABIIQ3AL-27DPJdZF.js} +1 -1
- package/dist/assets/{railroadDiagram-RFXS5EU6-DnBPGcSS.js → railroadDiagram-RFXS5EU6-C7S8pppU.js} +1 -1
- package/dist/assets/{requirementDiagram-TGXJPOKE-BfXBQgDX.js → requirementDiagram-TGXJPOKE-BbTkDbLO.js} +1 -1
- package/dist/assets/{sankeyDiagram-HTMAVEWB-B2plq2xb.js → sankeyDiagram-HTMAVEWB-lU57KWVw.js} +1 -1
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-5HYSSRy3.js → sequenceDiagram-DBY2YBRQ-Bg18P0LT.js} +1 -1
- package/dist/assets/{sizeCapture-X5ZJPWSS-Dhd1V1RM.js → sizeCapture-X5ZJPWSS-FDhyxAiu.js} +1 -1
- package/dist/assets/{stateDiagram-2N3HPSRC-BawpJoTz.js → stateDiagram-2N3HPSRC-pp9KUH2d.js} +1 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-BmoS7QDQ.js +1 -0
- package/dist/assets/{swimlanes-5IMT3BWC-BYunKpK9.js → swimlanes-5IMT3BWC-BCUZKORl.js} +2 -2
- package/dist/assets/swimlanesDiagram-G3AALYLV-Da6QbdiL.js +8 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-BA1n8ny7.js → timeline-definition-FHXFAJF6-BaeOwVlH.js} +1 -1
- package/dist/assets/{vennDiagram-L72KCM5P-DfSgfnDb.js → vennDiagram-L72KCM5P-BkhqU_hz.js} +1 -1
- package/dist/assets/{wardleyDiagram-EHGQE667-Ccum1Sj1.js → wardleyDiagram-EHGQE667-DvPxIC6q.js} +1 -1
- package/dist/assets/{web-24bQZmaT.js → web-BhVPxATx.js} +1 -1
- package/dist/assets/{web-CDfC-TTs.js → web-CR-app3U.js} +1 -1
- package/dist/assets/{web-C4jAxolp.js → web-DOiXCdVs.js} +1 -1
- package/dist/assets/{xychartDiagram-FW5EYKEG-CZKJCuVt.js → xychartDiagram-FW5EYKEG-B9MrBAD_.js} +1 -1
- package/dist/build-info.json +1 -1
- package/dist/index.html +2 -2
- package/dist/locales/de/config.json +5 -0
- package/dist/locales/de/terminal.json +1 -0
- package/dist/locales/en/config.json +5 -0
- package/dist/locales/en/terminal.json +2 -0
- package/dist/locales/es/config.json +5 -0
- package/dist/locales/es/terminal.json +11 -0
- package/dist/locales/fr/config.json +5 -0
- package/dist/locales/fr/terminal.json +1 -0
- package/dist/locales/hi/config.json +5 -0
- package/dist/locales/hi/terminal.json +1 -0
- package/dist/locales/it/config.json +5 -0
- package/dist/locales/it/terminal.json +1 -0
- package/dist/locales/ja/config.json +5 -0
- package/dist/locales/ja/terminal.json +1 -0
- package/dist/locales/pt/config.json +5 -0
- package/dist/locales/pt/terminal.json +1 -0
- package/dist/locales/ru/config.json +5 -0
- package/dist/locales/ru/terminal.json +1 -0
- package/dist/locales/zh-CN/config.json +5 -0
- package/dist/locales/zh-CN/terminal.json +1 -0
- package/dist/src/packages/server/data/builtin-skills/boss-instructions.js +1 -1
- package/dist/src/packages/server/grok/backend.js +7 -10
- package/dist/src/packages/server/index.js +61 -34
- package/dist/src/packages/server/routes/files.js +45 -19
- package/dist/src/packages/server/services/global-file-search.js +281 -0
- package/dist/src/packages/server/websocket/handler.js +16 -6
- package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +7 -0
- package/dist/src/packages/shared/agent-types.js +56 -4
- package/dist/src/packages/shared/file-search.js +49 -0
- package/dist/src/packages/shared/types.js +1 -0
- package/package.json +1 -1
- package/dist/assets/BossSpawnModal-CvFw8BPQ.js +0 -1
- package/dist/assets/ControlsModal-f4JvuTMY.js +0 -1
- package/dist/assets/RestoreArchivedAreaModal-C_cQr4Ay.js +0 -1
- package/dist/assets/SpawnModal-yPfAItLC.js +0 -1
- package/dist/assets/channel-Dz9gemPm.js +0 -1
- package/dist/assets/classDiagram-OUVF2IWQ-D29jX9yn.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-D29jX9yn.js +0 -1
- package/dist/assets/index-B5DZivhj.js +0 -19
- package/dist/assets/index-B_A7-K2O.js +0 -1
- package/dist/assets/index-CzZH0S0F.js +0 -1
- package/dist/assets/index-DHN-Bqnw.js +0 -1
- package/dist/assets/index-Dhfk8xKP.js +0 -2
- package/dist/assets/main-4CZSv3SH.css +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-ChimTXB4.js +0 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-4ZzygVvO.js +0 -8
|
@@ -204,7 +204,7 @@ Spawn ONLY when the user EXPLICITLY requests it ("create an agent", "spawn a deb
|
|
|
204
204
|
"customInstructions": "<optional extra system prompt for this agent>",
|
|
205
205
|
"codexModel": "<optional: only when provider='codex'>",
|
|
206
206
|
"opencodeModel": "<optional: only when provider='opencode'>",
|
|
207
|
-
"grokModel": "<optional: only when provider='grok'>",
|
|
207
|
+
"grokModel": "<optional: only when provider='grok': grok-4.6 | grok-4.5>",
|
|
208
208
|
"piModel": "<optional: only when provider='pi', 'provider/model' pattern>"
|
|
209
209
|
}]
|
|
210
210
|
\\\`\\\`\\\`
|
|
@@ -8,6 +8,7 @@ import { execSync } from 'child_process';
|
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import * as os from 'os';
|
|
10
10
|
import * as path from 'path';
|
|
11
|
+
import { resolveGrokReasoningEffort } from '../../shared/agent-types.js';
|
|
11
12
|
import { GrokJsonEventParser } from './json-event-parser.js';
|
|
12
13
|
import { TIDE_COMMANDER_APPENDED_PROMPT } from '../prompts/tide-commander.js';
|
|
13
14
|
import { getSystemPrompt, isEchoPromptEnabled } from '../services/system-prompt-service.js';
|
|
@@ -114,16 +115,12 @@ export class GrokBackend {
|
|
|
114
115
|
args.push('-m', config.model);
|
|
115
116
|
}
|
|
116
117
|
if (config.effort) {
|
|
117
|
-
// Map Tide effort labels to Grok reasoning-effort values
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
max: 'max',
|
|
124
|
-
};
|
|
125
|
-
const mapped = effortMap[config.effort] || config.effort.toLowerCase();
|
|
126
|
-
args.push('--reasoning-effort', mapped);
|
|
118
|
+
// Map Tide effort labels to Grok reasoning-effort values, clamped to what
|
|
119
|
+
// the selected model supports (grok-4.5 has no xhigh tier; Grok has no max).
|
|
120
|
+
const mapped = resolveGrokReasoningEffort(config.model, config.effort);
|
|
121
|
+
if (mapped) {
|
|
122
|
+
args.push('--reasoning-effort', mapped);
|
|
123
|
+
}
|
|
127
124
|
}
|
|
128
125
|
// Session resume / fork
|
|
129
126
|
if (config.sessionId) {
|
|
@@ -26,6 +26,10 @@ const HOST = process.env.HOST || (process.env.LISTEN_ALL_INTERFACES ? '::' : '12
|
|
|
26
26
|
const HTTPS_ENABLED = process.env.HTTPS === '1';
|
|
27
27
|
const TLS_KEY_PATH = process.env.TLS_KEY_PATH;
|
|
28
28
|
const TLS_CERT_PATH = process.env.TLS_CERT_PATH;
|
|
29
|
+
// When set, TLS is served on this port IN ADDITION to plain HTTP on PORT, so
|
|
30
|
+
// http:// and https:// clients can both connect to the same instance. Takes
|
|
31
|
+
// precedence over HTTPS=1 (which serves TLS on PORT and nothing in the clear).
|
|
32
|
+
const HTTPS_PORT = process.env.HTTPS_PORT ? Number(process.env.HTTPS_PORT) : null;
|
|
29
33
|
const FORCE_SHUTDOWN_TIMEOUT_MS = 4500;
|
|
30
34
|
// ============================================================================
|
|
31
35
|
// Global Error Handlers
|
|
@@ -159,26 +163,48 @@ async function main() {
|
|
|
159
163
|
initClaudeCredentialKeepAlive();
|
|
160
164
|
logger.server.log(`Data directory: ${getDataDir()}`);
|
|
161
165
|
logger.server.log(`Log file: ${getLogFilePath()}`);
|
|
162
|
-
// Create Express app and HTTP server
|
|
166
|
+
// Create Express app and HTTP server(s)
|
|
163
167
|
const app = createApp();
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
168
|
+
const readTlsOptions = (reason) => ({
|
|
169
|
+
key: fs.readFileSync(assertTlsPath(TLS_KEY_PATH, 'TLS_KEY_PATH', reason)),
|
|
170
|
+
cert: fs.readFileSync(assertTlsPath(TLS_CERT_PATH, 'TLS_CERT_PATH', reason)),
|
|
171
|
+
});
|
|
172
|
+
if (HTTPS_PORT && HTTPS_ENABLED) {
|
|
173
|
+
logger.server.warn('HTTPS=1 and HTTPS_PORT are both set; HTTPS_PORT wins '
|
|
174
|
+
+ `(plain HTTP on ${PORT}, TLS on ${HTTPS_PORT})`);
|
|
175
|
+
}
|
|
176
|
+
// Listener set: either a single server (legacy) or HTTP + HTTPS side by side.
|
|
177
|
+
const listeners = HTTPS_PORT
|
|
178
|
+
? [
|
|
179
|
+
{ server: createServer(app), protocol: 'http', port: Number(PORT) },
|
|
180
|
+
{ server: createHttpsServer(readTlsOptions('HTTPS_PORT'), app), protocol: 'https', port: HTTPS_PORT },
|
|
181
|
+
]
|
|
182
|
+
: [{
|
|
183
|
+
server: HTTPS_ENABLED ? createHttpsServer(readTlsOptions('HTTPS=1'), app) : createServer(app),
|
|
184
|
+
protocol: HTTPS_ENABLED ? 'https' : 'http',
|
|
185
|
+
port: Number(PORT),
|
|
186
|
+
}];
|
|
187
|
+
const primary = listeners[0].server;
|
|
170
188
|
const sockets = new Set();
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
189
|
+
for (const { server: listener } of listeners) {
|
|
190
|
+
listener.on('connection', (socket) => {
|
|
191
|
+
sockets.add(socket);
|
|
192
|
+
socket.on('close', () => {
|
|
193
|
+
sockets.delete(socket);
|
|
194
|
+
});
|
|
175
195
|
});
|
|
176
|
-
}
|
|
177
|
-
// Initialize WebSocket
|
|
178
|
-
|
|
196
|
+
}
|
|
197
|
+
// Initialize WebSocket. Every listener shares one WebSocketServer so clients
|
|
198
|
+
// land in the same client set no matter which scheme they arrived over.
|
|
199
|
+
const wss = websocket.init(primary);
|
|
200
|
+
for (const { server: listener } of listeners.slice(1)) {
|
|
201
|
+
websocket.attachServer(listener, wss);
|
|
202
|
+
}
|
|
179
203
|
// Set up terminal WebSocket proxy for ttyd buildings
|
|
180
204
|
// (HTTP proxy is set up in app.ts before API routes)
|
|
181
|
-
|
|
205
|
+
for (const { server: listener } of listeners) {
|
|
206
|
+
setupTerminalWsProxy(listener);
|
|
207
|
+
}
|
|
182
208
|
// Set up skill hot-reload (must be after websocket init to have broadcast available)
|
|
183
209
|
skillService.setupSkillHotReload(agentService, runtimeService, websocket.broadcast);
|
|
184
210
|
// Start PM2 status polling for buildings
|
|
@@ -187,22 +213,23 @@ async function main() {
|
|
|
187
213
|
buildingService.startDockerStatusPolling(websocket.broadcast);
|
|
188
214
|
// Start terminal (ttyd) status polling for buildings
|
|
189
215
|
buildingService.startTerminalStatusPolling(websocket.broadcast);
|
|
190
|
-
// Start server
|
|
191
|
-
server
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
216
|
+
// Start server(s)
|
|
217
|
+
for (const { server: listener, protocol, port } of listeners) {
|
|
218
|
+
listener.on('error', (err) => {
|
|
219
|
+
logger.server.error(`Server listen error (${protocol}:${port}):`, err);
|
|
220
|
+
if (err.code === 'EADDRINUSE') {
|
|
221
|
+
logger.server.error(`Port ${port} is already in use. Exiting.`);
|
|
222
|
+
closeFileLogging();
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
listener.listen(port, HOST, () => {
|
|
227
|
+
const wsProtocol = protocol === 'https' ? 'wss' : 'ws';
|
|
228
|
+
logger.server.log(`Server running on ${protocol}://${HOST}:${port}`);
|
|
229
|
+
logger.server.log(`WebSocket available at ${wsProtocol}://${HOST}:${port}/ws`);
|
|
230
|
+
logger.server.log(`API available at ${protocol}://${HOST}:${port}/api`);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
206
233
|
let isShuttingDown = false;
|
|
207
234
|
const gracefulShutdown = async (signal) => {
|
|
208
235
|
if (isShuttingDown) {
|
|
@@ -241,7 +268,7 @@ async function main() {
|
|
|
241
268
|
wss.clients.forEach((client) => client.terminate());
|
|
242
269
|
await new Promise((resolve) => wss.close(() => resolve()));
|
|
243
270
|
sockets.forEach((socket) => socket.destroy());
|
|
244
|
-
await new Promise((resolve) =>
|
|
271
|
+
await Promise.all(listeners.map(({ server: listener }) => (new Promise((resolve) => listener.close(() => resolve())))));
|
|
245
272
|
clearTimeout(forceShutdownTimer);
|
|
246
273
|
closeFileLogging();
|
|
247
274
|
process.exit(0);
|
|
@@ -256,9 +283,9 @@ async function main() {
|
|
|
256
283
|
process.on('SIGINT', () => { void gracefulShutdown('SIGINT'); });
|
|
257
284
|
process.on('SIGTERM', () => { void gracefulShutdown('SIGTERM'); });
|
|
258
285
|
}
|
|
259
|
-
function assertTlsPath(value, envName) {
|
|
286
|
+
function assertTlsPath(value, envName, reason) {
|
|
260
287
|
if (!value) {
|
|
261
|
-
throw new Error(`${envName} is required when
|
|
288
|
+
throw new Error(`${envName} is required when ${reason} is set`);
|
|
262
289
|
}
|
|
263
290
|
return value;
|
|
264
291
|
}
|
|
@@ -11,6 +11,8 @@ import { pathToFileURL } from 'url';
|
|
|
11
11
|
import { logger } from '../utils/logger.js';
|
|
12
12
|
import { loadAreas } from '../data/index.js';
|
|
13
13
|
import { detectRunnerType, mightBeTestFile, mightBeVitestFile, mightBePhpTestFile } from '../services/test-runner-service.js';
|
|
14
|
+
import { DEFAULT_FILE_SEARCH_EXCLUDE_DIRS, parseExcludeDirNames } from '../../shared/file-search.js';
|
|
15
|
+
import { searchFilesGlobal, FILE_SEARCH_MIN_QUERY } from '../services/global-file-search.js';
|
|
14
16
|
const log = logger.files;
|
|
15
17
|
// Get or create temp directory for tide-commander uploads. Exported so other
|
|
16
18
|
// modules (attachment-downloader, attachment-janitor) put their files under the
|
|
@@ -1201,18 +1203,20 @@ router.post('/paste', (req, res) => {
|
|
|
1201
1203
|
res.status(500).json({ error: err.message });
|
|
1202
1204
|
}
|
|
1203
1205
|
});
|
|
1204
|
-
// Directories
|
|
1205
|
-
// Set for O(1) lookups (
|
|
1206
|
-
|
|
1207
|
-
const SEARCH_SKIP_DIRS = new Set([
|
|
1208
|
-
'node_modules', 'dist', 'build', '.git', '__pycache__',
|
|
1209
|
-
'venv', '.venv', 'target', 'vendor', '.next', '.cache',
|
|
1210
|
-
]);
|
|
1206
|
+
// Directories skipped during file/content search unless the request supplies
|
|
1207
|
+
// `exclude`. Kept as a Set for O(1) lookups (checked once per directory entry).
|
|
1208
|
+
const SEARCH_SKIP_DIRS = new Set(DEFAULT_FILE_SEARCH_EXCLUDE_DIRS);
|
|
1211
1209
|
const MAX_SEARCH_DEPTH = 10;
|
|
1210
|
+
/** Request `exclude` (comma list or repeated) overrides the default skip set. */
|
|
1211
|
+
function skipDirsFromQuery(req) {
|
|
1212
|
+
if (req.query.exclude === undefined)
|
|
1213
|
+
return SEARCH_SKIP_DIRS;
|
|
1214
|
+
return new Set(parseExcludeDirNames(req.query.exclude));
|
|
1215
|
+
}
|
|
1212
1216
|
// Helper function to search files recursively (async / non-blocking).
|
|
1213
1217
|
// Uses fs.promises so a deep tree walk never blocks the event loop, which
|
|
1214
1218
|
// matters on a shared multi-agent server. `queryLower` must be pre-lowercased.
|
|
1215
|
-
async function searchFilesAsync(dirPath, queryLower, results, maxResults, depth = 0) {
|
|
1219
|
+
async function searchFilesAsync(dirPath, queryLower, results, maxResults, depth = 0, skipDirs = SEARCH_SKIP_DIRS) {
|
|
1216
1220
|
if (results.length >= maxResults || depth > MAX_SEARCH_DEPTH)
|
|
1217
1221
|
return;
|
|
1218
1222
|
let entries;
|
|
@@ -1227,8 +1231,8 @@ async function searchFilesAsync(dirPath, queryLower, results, maxResults, depth
|
|
|
1227
1231
|
if (results.length >= maxResults)
|
|
1228
1232
|
break;
|
|
1229
1233
|
const isDir = entry.isDirectory();
|
|
1230
|
-
// Skip
|
|
1231
|
-
if (isDir &&
|
|
1234
|
+
// Skip configured non-essential directories
|
|
1235
|
+
if (isDir && skipDirs.has(entry.name))
|
|
1232
1236
|
continue;
|
|
1233
1237
|
const fullPath = path.join(dirPath, entry.name);
|
|
1234
1238
|
// Check if name matches query (case-insensitive)
|
|
@@ -1255,7 +1259,7 @@ async function searchFilesAsync(dirPath, queryLower, results, maxResults, depth
|
|
|
1255
1259
|
for (const sub of subdirs) {
|
|
1256
1260
|
if (results.length >= maxResults)
|
|
1257
1261
|
break;
|
|
1258
|
-
await searchFilesAsync(sub, queryLower, results, maxResults, depth + 1);
|
|
1262
|
+
await searchFilesAsync(sub, queryLower, results, maxResults, depth + 1, skipDirs);
|
|
1259
1263
|
}
|
|
1260
1264
|
}
|
|
1261
1265
|
// GET /api/files/search - Search for files
|
|
@@ -1277,7 +1281,7 @@ router.get('/search', async (req, res) => {
|
|
|
1277
1281
|
return;
|
|
1278
1282
|
}
|
|
1279
1283
|
const results = [];
|
|
1280
|
-
await searchFilesAsync(dirPath, query.toLowerCase(), results, maxResults);
|
|
1284
|
+
await searchFilesAsync(dirPath, query.toLowerCase(), results, maxResults, 0, skipDirsFromQuery(req));
|
|
1281
1285
|
// Sort: files first (more likely what user wants), then by name
|
|
1282
1286
|
results.sort((a, b) => {
|
|
1283
1287
|
if (a.isDirectory !== b.isDirectory) {
|
|
@@ -1292,6 +1296,27 @@ router.get('/search', async (req, res) => {
|
|
|
1292
1296
|
res.status(500).json({ error: err.message });
|
|
1293
1297
|
}
|
|
1294
1298
|
});
|
|
1299
|
+
// GET /api/files/search-global - Filename search across every area directory
|
|
1300
|
+
router.get('/search-global', async (req, res) => {
|
|
1301
|
+
try {
|
|
1302
|
+
const query = typeof req.query.q === 'string' ? req.query.q : '';
|
|
1303
|
+
if (query.trim().length < FILE_SEARCH_MIN_QUERY) {
|
|
1304
|
+
res.json({ files: [] });
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
const limit = parseInt(String(req.query.limit ?? ''), 10);
|
|
1308
|
+
const files = await searchFilesGlobal({
|
|
1309
|
+
query,
|
|
1310
|
+
exclude: req.query.exclude,
|
|
1311
|
+
limit: Number.isFinite(limit) ? limit : undefined,
|
|
1312
|
+
});
|
|
1313
|
+
res.json({ files });
|
|
1314
|
+
}
|
|
1315
|
+
catch (err) {
|
|
1316
|
+
log.error(' Global file search failed:', err);
|
|
1317
|
+
res.status(500).json({ error: err.message });
|
|
1318
|
+
}
|
|
1319
|
+
});
|
|
1295
1320
|
// Text file extensions for content search
|
|
1296
1321
|
const TEXT_EXTENSIONS = new Set([
|
|
1297
1322
|
'.txt', '.md', '.markdown', '.json', '.yaml', '.yml', '.xml', '.html', '.htm',
|
|
@@ -1311,7 +1336,7 @@ const MAX_CONTENT_FILE_SIZE = 1024 * 1024;
|
|
|
1311
1336
|
// a child process (never blocking the server) and is orders of magnitude faster
|
|
1312
1337
|
// than a JS tree walk. Resolves { ok:false } when rg is missing or errors, so
|
|
1313
1338
|
// the caller can fall back to the pure-JS walker.
|
|
1314
|
-
function searchFileContentsRipgrep(dirPath, query, maxResults) {
|
|
1339
|
+
function searchFileContentsRipgrep(dirPath, query, maxResults, skipDirs = SEARCH_SKIP_DIRS) {
|
|
1315
1340
|
return new Promise((resolve) => {
|
|
1316
1341
|
const args = [
|
|
1317
1342
|
'--json',
|
|
@@ -1323,7 +1348,7 @@ function searchFileContentsRipgrep(dirPath, query, maxResults) {
|
|
|
1323
1348
|
'--hidden', // include dotfiles (.env, .github, ...)
|
|
1324
1349
|
'--no-ignore', // don't honor .gitignore (match old behavior)
|
|
1325
1350
|
'--no-messages', // suppress permission/binary warnings on stderr
|
|
1326
|
-
...[...
|
|
1351
|
+
...[...skipDirs].flatMap((d) => ['--glob', `!**/${d}/**`]),
|
|
1327
1352
|
'--',
|
|
1328
1353
|
query,
|
|
1329
1354
|
dirPath,
|
|
@@ -1400,7 +1425,7 @@ function searchFileContentsRipgrep(dirPath, query, maxResults) {
|
|
|
1400
1425
|
// Fallback content search (async / non-blocking) used only when ripgrep is
|
|
1401
1426
|
// unavailable. Mirrors the old behavior but with fs.promises so it never
|
|
1402
1427
|
// blocks the event loop.
|
|
1403
|
-
async function searchFileContentsAsync(dirPath, queryLower, results, maxResults, depth = 0) {
|
|
1428
|
+
async function searchFileContentsAsync(dirPath, queryLower, results, maxResults, depth = 0, skipDirs = SEARCH_SKIP_DIRS) {
|
|
1404
1429
|
if (results.length >= maxResults || depth > MAX_SEARCH_DEPTH)
|
|
1405
1430
|
return;
|
|
1406
1431
|
let entries;
|
|
@@ -1415,7 +1440,7 @@ async function searchFileContentsAsync(dirPath, queryLower, results, maxResults,
|
|
|
1415
1440
|
if (results.length >= maxResults)
|
|
1416
1441
|
break;
|
|
1417
1442
|
if (entry.isDirectory()) {
|
|
1418
|
-
if (!
|
|
1443
|
+
if (!skipDirs.has(entry.name))
|
|
1419
1444
|
subdirs.push(path.join(dirPath, entry.name));
|
|
1420
1445
|
continue;
|
|
1421
1446
|
}
|
|
@@ -1451,7 +1476,7 @@ async function searchFileContentsAsync(dirPath, queryLower, results, maxResults,
|
|
|
1451
1476
|
for (const sub of subdirs) {
|
|
1452
1477
|
if (results.length >= maxResults)
|
|
1453
1478
|
break;
|
|
1454
|
-
await searchFileContentsAsync(sub, queryLower, results, maxResults, depth + 1);
|
|
1479
|
+
await searchFileContentsAsync(sub, queryLower, results, maxResults, depth + 1, skipDirs);
|
|
1455
1480
|
}
|
|
1456
1481
|
}
|
|
1457
1482
|
// GET /api/files/search-content - Search file contents
|
|
@@ -1477,13 +1502,14 @@ router.get('/search-content', async (req, res) => {
|
|
|
1477
1502
|
return;
|
|
1478
1503
|
}
|
|
1479
1504
|
let results = [];
|
|
1480
|
-
const
|
|
1505
|
+
const skipDirs = skipDirsFromQuery(req);
|
|
1506
|
+
const rg = await searchFileContentsRipgrep(dirPath, query, maxResults, skipDirs);
|
|
1481
1507
|
if (rg.ok) {
|
|
1482
1508
|
results = rg.results;
|
|
1483
1509
|
}
|
|
1484
1510
|
else {
|
|
1485
1511
|
// ripgrep unavailable — fall back to the pure-JS async walker.
|
|
1486
|
-
await searchFileContentsAsync(dirPath, query.toLowerCase(), results, maxResults);
|
|
1512
|
+
await searchFileContentsAsync(dirPath, query.toLowerCase(), results, maxResults, 0, skipDirs);
|
|
1487
1513
|
}
|
|
1488
1514
|
res.json({ results });
|
|
1489
1515
|
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global filename search across every directory configured on areas.
|
|
3
|
+
* Used by Spotlight (GET /api/files/search-global).
|
|
4
|
+
*
|
|
5
|
+
* Prefer ripgrep (`rg --files`) when available; fall back to an async
|
|
6
|
+
* directory walk so the event loop stays unblocked.
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import { spawn } from 'child_process';
|
|
11
|
+
import { loadAreas } from '../data/index.js';
|
|
12
|
+
import { DEFAULT_FILE_SEARCH_EXCLUDE_DIRS, parseExcludeDirNames, } from '../../shared/file-search.js';
|
|
13
|
+
function toPosixSeparators(p) {
|
|
14
|
+
return p.replace(/\\/g, '/');
|
|
15
|
+
}
|
|
16
|
+
export const FILE_SEARCH_MIN_QUERY = 2;
|
|
17
|
+
export const FILE_SEARCH_MAX_RESULTS = 50;
|
|
18
|
+
const MAX_AREA_DIRS = 48;
|
|
19
|
+
const MAX_SEARCH_DEPTH = 12;
|
|
20
|
+
const RG_TIMEOUT_MS = 4000;
|
|
21
|
+
function defaultExcludeSet() {
|
|
22
|
+
return new Set(DEFAULT_FILE_SEARCH_EXCLUDE_DIRS);
|
|
23
|
+
}
|
|
24
|
+
export function excludeDirsFromInput(raw, useDefaultWhenMissing) {
|
|
25
|
+
if (raw === undefined) {
|
|
26
|
+
return useDefaultWhenMissing ? defaultExcludeSet() : new Set();
|
|
27
|
+
}
|
|
28
|
+
return new Set(parseExcludeDirNames(raw));
|
|
29
|
+
}
|
|
30
|
+
function gatherRoots(areas) {
|
|
31
|
+
const roots = [];
|
|
32
|
+
const seenResolved = new Set();
|
|
33
|
+
for (const area of areas) {
|
|
34
|
+
if (area.archived)
|
|
35
|
+
continue;
|
|
36
|
+
const dirs = Array.isArray(area.directories) ? area.directories : [];
|
|
37
|
+
for (const raw of dirs) {
|
|
38
|
+
if (roots.length >= MAX_AREA_DIRS)
|
|
39
|
+
return roots;
|
|
40
|
+
if (typeof raw !== 'string' || !raw.trim())
|
|
41
|
+
continue;
|
|
42
|
+
if (!path.isAbsolute(raw))
|
|
43
|
+
continue;
|
|
44
|
+
let resolved;
|
|
45
|
+
try {
|
|
46
|
+
resolved = path.resolve(raw);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (seenResolved.has(resolved))
|
|
52
|
+
continue;
|
|
53
|
+
let stat;
|
|
54
|
+
try {
|
|
55
|
+
stat = fs.statSync(resolved);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (!stat.isDirectory())
|
|
61
|
+
continue;
|
|
62
|
+
seenResolved.add(resolved);
|
|
63
|
+
const posixRoot = toPosixSeparators(resolved);
|
|
64
|
+
roots.push({
|
|
65
|
+
areaId: area.id,
|
|
66
|
+
areaName: area.name,
|
|
67
|
+
projectName: path.basename(resolved) || posixRoot,
|
|
68
|
+
projectRoot: posixRoot,
|
|
69
|
+
resolved,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return roots;
|
|
74
|
+
}
|
|
75
|
+
function matchRoot(filePath, roots) {
|
|
76
|
+
let best = null;
|
|
77
|
+
for (const root of roots) {
|
|
78
|
+
if (filePath === root.resolved || filePath.startsWith(root.resolved + path.sep)) {
|
|
79
|
+
if (!best || root.resolved.length > best.resolved.length)
|
|
80
|
+
best = root;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return best;
|
|
84
|
+
}
|
|
85
|
+
function toHit(filePath, root) {
|
|
86
|
+
const rel = path.relative(root.resolved, filePath);
|
|
87
|
+
return {
|
|
88
|
+
path: toPosixSeparators(filePath),
|
|
89
|
+
name: path.basename(filePath),
|
|
90
|
+
relativePath: toPosixSeparators(rel || path.basename(filePath)),
|
|
91
|
+
projectName: root.projectName,
|
|
92
|
+
projectRoot: root.projectRoot,
|
|
93
|
+
areaId: root.areaId,
|
|
94
|
+
areaName: root.areaName,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function rankHit(hit, queryLower) {
|
|
98
|
+
const name = hit.name.toLowerCase();
|
|
99
|
+
if (name === queryLower)
|
|
100
|
+
return 0;
|
|
101
|
+
if (name.startsWith(queryLower))
|
|
102
|
+
return 1;
|
|
103
|
+
if (name.includes(queryLower))
|
|
104
|
+
return 2;
|
|
105
|
+
if (hit.relativePath.toLowerCase().includes(queryLower))
|
|
106
|
+
return 3;
|
|
107
|
+
return 4;
|
|
108
|
+
}
|
|
109
|
+
function sortHits(hits, queryLower) {
|
|
110
|
+
return hits.sort((a, b) => {
|
|
111
|
+
const ra = rankHit(a, queryLower);
|
|
112
|
+
const rb = rankHit(b, queryLower);
|
|
113
|
+
if (ra !== rb)
|
|
114
|
+
return ra - rb;
|
|
115
|
+
if (a.name.length !== b.name.length)
|
|
116
|
+
return a.name.length - b.name.length;
|
|
117
|
+
return a.relativePath.length - b.relativePath.length;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function searchOneRootRipgrep(root, query, exclude, maxResults) {
|
|
121
|
+
return new Promise((resolve) => {
|
|
122
|
+
// cwd = project root so exclude globs like `!**/tmp/**` cannot match a
|
|
123
|
+
// host path prefix (e.g. searching a tree that lives under /tmp).
|
|
124
|
+
// Do NOT use --iglob for the query: an include glob overrides exclude
|
|
125
|
+
// globs in rg, so `tmp/Keep.ts` would leak through. Filter the query
|
|
126
|
+
// against streamed paths instead.
|
|
127
|
+
const queryLower = query.toLowerCase();
|
|
128
|
+
const args = [
|
|
129
|
+
'--files',
|
|
130
|
+
'--hidden',
|
|
131
|
+
'--no-ignore',
|
|
132
|
+
'--no-messages',
|
|
133
|
+
'--max-depth',
|
|
134
|
+
String(MAX_SEARCH_DEPTH + 1),
|
|
135
|
+
...[...exclude].flatMap((d) => ['--glob', `!${d}/**`, '--glob', `!**/${d}/**`]),
|
|
136
|
+
'--',
|
|
137
|
+
'.',
|
|
138
|
+
];
|
|
139
|
+
let child;
|
|
140
|
+
try {
|
|
141
|
+
child = spawn('rg', args, { cwd: root, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
resolve({ ok: false, paths: [] });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const paths = [];
|
|
148
|
+
let buffer = '';
|
|
149
|
+
let settled = false;
|
|
150
|
+
const finish = (ok) => {
|
|
151
|
+
if (settled)
|
|
152
|
+
return;
|
|
153
|
+
settled = true;
|
|
154
|
+
clearTimeout(timer);
|
|
155
|
+
resolve({ ok, paths });
|
|
156
|
+
};
|
|
157
|
+
const timer = setTimeout(() => {
|
|
158
|
+
try {
|
|
159
|
+
child.kill();
|
|
160
|
+
}
|
|
161
|
+
catch { /* already gone */ }
|
|
162
|
+
finish(paths.length > 0);
|
|
163
|
+
}, RG_TIMEOUT_MS);
|
|
164
|
+
child.on('error', () => finish(false));
|
|
165
|
+
child.stdout?.setEncoding('utf-8');
|
|
166
|
+
child.stdout?.on('data', (chunk) => {
|
|
167
|
+
buffer += chunk;
|
|
168
|
+
let nl;
|
|
169
|
+
while ((nl = buffer.indexOf('\n')) !== -1) {
|
|
170
|
+
const line = buffer.slice(0, nl);
|
|
171
|
+
buffer = buffer.slice(nl + 1);
|
|
172
|
+
if (!line)
|
|
173
|
+
continue;
|
|
174
|
+
const rel = line.replace(/^\.\//, '');
|
|
175
|
+
const name = path.basename(rel);
|
|
176
|
+
if (!name.toLowerCase().includes(queryLower) && !rel.toLowerCase().includes(queryLower)) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const abs = path.isAbsolute(line) ? line : path.join(root, line);
|
|
180
|
+
paths.push(abs);
|
|
181
|
+
if (paths.length >= maxResults) {
|
|
182
|
+
try {
|
|
183
|
+
child.kill();
|
|
184
|
+
}
|
|
185
|
+
catch { /* already gone */ }
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
child.on('close', (code) => {
|
|
191
|
+
// rg: 0 = matches, 1 = no matches, 2 = error.
|
|
192
|
+
finish(!(code === 2 && paths.length === 0));
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
async function searchFilenamesRipgrep(roots, query, exclude, maxResults) {
|
|
197
|
+
const paths = [];
|
|
198
|
+
let anyOk = false;
|
|
199
|
+
for (const root of roots) {
|
|
200
|
+
if (paths.length >= maxResults)
|
|
201
|
+
break;
|
|
202
|
+
const result = await searchOneRootRipgrep(root.resolved, query, exclude, maxResults - paths.length);
|
|
203
|
+
if (result.ok) {
|
|
204
|
+
anyOk = true;
|
|
205
|
+
paths.push(...result.paths);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { ok: anyOk, paths };
|
|
209
|
+
}
|
|
210
|
+
async function walkForFiles(dirPath, queryLower, exclude, acc, maxResults, depth) {
|
|
211
|
+
if (acc.length >= maxResults || depth > MAX_SEARCH_DEPTH)
|
|
212
|
+
return;
|
|
213
|
+
let entries;
|
|
214
|
+
try {
|
|
215
|
+
entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const subdirs = [];
|
|
221
|
+
for (const entry of entries) {
|
|
222
|
+
if (acc.length >= maxResults)
|
|
223
|
+
return;
|
|
224
|
+
if (entry.isDirectory()) {
|
|
225
|
+
if (exclude.has(entry.name))
|
|
226
|
+
continue;
|
|
227
|
+
subdirs.push(path.join(dirPath, entry.name));
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (!entry.isFile() && !entry.isSymbolicLink())
|
|
231
|
+
continue;
|
|
232
|
+
if (!entry.name.toLowerCase().includes(queryLower))
|
|
233
|
+
continue;
|
|
234
|
+
acc.push(path.join(dirPath, entry.name));
|
|
235
|
+
}
|
|
236
|
+
for (const sub of subdirs) {
|
|
237
|
+
if (acc.length >= maxResults)
|
|
238
|
+
return;
|
|
239
|
+
await walkForFiles(sub, queryLower, exclude, acc, maxResults, depth + 1);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
export async function searchFilesGlobal(opts) {
|
|
243
|
+
const query = (opts.query || '').trim();
|
|
244
|
+
if (query.length < FILE_SEARCH_MIN_QUERY)
|
|
245
|
+
return [];
|
|
246
|
+
const limit = Math.max(1, Math.min(opts.limit ?? FILE_SEARCH_MAX_RESULTS, FILE_SEARCH_MAX_RESULTS));
|
|
247
|
+
const exclude = excludeDirsFromInput(opts.exclude, true);
|
|
248
|
+
const areas = opts.areas ?? loadAreas();
|
|
249
|
+
const roots = gatherRoots(areas);
|
|
250
|
+
if (roots.length === 0)
|
|
251
|
+
return [];
|
|
252
|
+
const queryLower = query.toLowerCase();
|
|
253
|
+
const seen = new Set();
|
|
254
|
+
const hits = [];
|
|
255
|
+
const consume = (filePath) => {
|
|
256
|
+
const resolved = path.resolve(filePath);
|
|
257
|
+
if (seen.has(resolved))
|
|
258
|
+
return;
|
|
259
|
+
const root = matchRoot(resolved, roots);
|
|
260
|
+
if (!root)
|
|
261
|
+
return;
|
|
262
|
+
seen.add(resolved);
|
|
263
|
+
hits.push(toHit(resolved, root));
|
|
264
|
+
};
|
|
265
|
+
const rg = await searchFilenamesRipgrep(roots, query, exclude, limit);
|
|
266
|
+
if (rg.ok) {
|
|
267
|
+
for (const p of rg.paths)
|
|
268
|
+
consume(p);
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
const walked = [];
|
|
272
|
+
for (const root of roots) {
|
|
273
|
+
if (walked.length >= limit * 4)
|
|
274
|
+
break;
|
|
275
|
+
await walkForFiles(root.resolved, queryLower, exclude, walked, limit * 4, 0);
|
|
276
|
+
}
|
|
277
|
+
for (const p of walked)
|
|
278
|
+
consume(p);
|
|
279
|
+
}
|
|
280
|
+
return sortHits(hits, queryLower).slice(0, limit);
|
|
281
|
+
}
|
|
@@ -241,12 +241,15 @@ function handleClientMessage(ws, message) {
|
|
|
241
241
|
// ============================================================================
|
|
242
242
|
// Initialization
|
|
243
243
|
// ============================================================================
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
244
|
+
/**
|
|
245
|
+
* Route /ws upgrades from `server` into `wss`.
|
|
246
|
+
*
|
|
247
|
+
* Split out of init() so that when the server listens on both HTTP and HTTPS,
|
|
248
|
+
* the second listener shares the SAME WebSocketServer — and therefore the same
|
|
249
|
+
* client set and broadcast path. Standing up a second WebSocketServer would
|
|
250
|
+
* silently split clients into two groups, and broadcasts would only reach one.
|
|
251
|
+
*/
|
|
252
|
+
export function attachServer(server, wss) {
|
|
250
253
|
server.on('upgrade', (request, socket, head) => {
|
|
251
254
|
const pathname = request.url?.split('?')[0];
|
|
252
255
|
if (pathname === '/ws') {
|
|
@@ -263,6 +266,13 @@ export function init(server) {
|
|
|
263
266
|
}
|
|
264
267
|
// Other paths (like /api/terminal/*/ws) are handled by the terminal proxy
|
|
265
268
|
});
|
|
269
|
+
}
|
|
270
|
+
export function init(server) {
|
|
271
|
+
// Use noServer mode so we can manually route upgrade events.
|
|
272
|
+
// This allows the terminal proxy to handle /api/terminal/*/ws upgrades
|
|
273
|
+
// without the main WSS intercepting and rejecting them.
|
|
274
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
275
|
+
attachServer(server, wss);
|
|
266
276
|
wss.on('connection', (ws, request) => {
|
|
267
277
|
clients.add(ws);
|
|
268
278
|
setWsClientsCount(clients.size);
|