@axhub/genie 0.2.8 → 0.2.10
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/LICENSE +21 -675
- package/dist/api-docs.html +2 -2
- package/dist/assets/App-CYCCsgwf.js +264 -0
- package/dist/assets/ReviewApp-0srHIXwb.js +1 -0
- package/dist/assets/{_basePickBy-CqJbRZ9y.js → _basePickBy-DVVb07UV.js} +1 -1
- package/dist/assets/{_baseUniq-BS8YH8jO.js → _baseUniq-BtbziL5G.js} +1 -1
- package/dist/assets/{arc-BBmKEN-S.js → arc-BsCC8yBD.js} +1 -1
- package/dist/assets/{architectureDiagram-2XIMDMQ5-N5lcb82R.js → architectureDiagram-2XIMDMQ5-woFp6eNI.js} +1 -1
- package/dist/assets/{blockDiagram-WCTKOSBZ-DTMwHuLn.js → blockDiagram-WCTKOSBZ-ya8VAc2k.js} +1 -1
- package/dist/assets/{c4Diagram-IC4MRINW-BTKlkXI9.js → c4Diagram-IC4MRINW-CY1dZmIZ.js} +1 -1
- package/dist/assets/channel-BMhScXFe.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-DUdoTxAc.js → chunk-4BX2VUAB-CR1lAd74.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-Bm_92xe4.js → chunk-55IACEB6-CP98WcFC.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-CGW0g62g.js → chunk-FMBD7UC4-D9c7ijAB.js} +1 -1
- package/dist/assets/{chunk-JSJVCQXG-DYkTH3w1.js → chunk-JSJVCQXG-DQAGYOn-.js} +1 -1
- package/dist/assets/{chunk-KX2RTZJC-C9oTlISU.js → chunk-KX2RTZJC-BbTXiDq7.js} +1 -1
- package/dist/assets/{chunk-NQ4KR5QH-CM50ygWP.js → chunk-NQ4KR5QH-BI6AX0dr.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-7dzpYeNJ.js → chunk-QZHKN3VN-DB3V2Ifo.js} +1 -1
- package/dist/assets/{chunk-WL4C6EOR-Cm9nQrsr.js → chunk-WL4C6EOR-DhzTthv6.js} +1 -1
- package/dist/assets/classDiagram-VBA2DB6C-CMIxlWcT.js +1 -0
- package/dist/assets/classDiagram-v2-RAHNMMFH-CMIxlWcT.js +1 -0
- package/dist/assets/clone-BPqOt4r3.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-Ccp_p0JZ.js → cose-bilkent-S5V4N54A-BQ09ZE2j.js} +1 -1
- package/dist/assets/{dagre-KLK3FWXG-fBwTLUp9.js → dagre-KLK3FWXG-Dc2ueD_R.js} +1 -1
- package/dist/assets/{diagram-E7M64L7V-CeNVmFUp.js → diagram-E7M64L7V-DP-LsQoL.js} +1 -1
- package/dist/assets/{diagram-IFDJBPK2-CtavyLGa.js → diagram-IFDJBPK2-Cg6r42cB.js} +1 -1
- package/dist/assets/{diagram-P4PSJMXO-CpQTjQwc.js → diagram-P4PSJMXO-aHsfoUZE.js} +1 -1
- package/dist/assets/{erDiagram-INFDFZHY-B8R5vwhd.js → erDiagram-INFDFZHY-qBXJ4aAz.js} +1 -1
- package/dist/assets/{flowDiagram-PKNHOUZH-BvkVVwIQ.js → flowDiagram-PKNHOUZH-D_13emJM.js} +1 -1
- package/dist/assets/{ganttDiagram-A5KZAMGK-DOu3hSNa.js → ganttDiagram-A5KZAMGK-BvIcOLwz.js} +1 -1
- package/dist/assets/{gitGraphDiagram-K3NZZRJ6-C7zT67YE.js → gitGraphDiagram-K3NZZRJ6-ad0vvNcU.js} +1 -1
- package/dist/assets/{graph-D11wiwHo.js → graph-CeJCMjan.js} +1 -1
- package/dist/assets/{highlighted-body-TPN3WLV5-Babpthg-.js → highlighted-body-TPN3WLV5-B_novwSz.js} +1 -1
- package/dist/assets/index-C514cLyb.js +2 -0
- package/dist/assets/index-h1DBl_g3.css +1 -0
- package/dist/assets/{infoDiagram-LFFYTUFH-BmA7IpQG.js → infoDiagram-LFFYTUFH-lOxAqb3m.js} +1 -1
- package/dist/assets/{ishikawaDiagram-PHBUUO56-BEquZd3E.js → ishikawaDiagram-PHBUUO56-DIr-51gj.js} +1 -1
- package/dist/assets/{journeyDiagram-4ABVD52K-BfemGz7f.js → journeyDiagram-4ABVD52K-CYcIW0ZU.js} +1 -1
- package/dist/assets/{kanban-definition-K7BYSVSG-CWja3mln.js → kanban-definition-K7BYSVSG-C1ZK616a.js} +1 -1
- package/dist/assets/{layout-BLUNf-PJ.js → layout-CI2RM-v6.js} +1 -1
- package/dist/assets/{linear-DukIV_Xv.js → linear-DE7bISck.js} +1 -1
- package/dist/assets/{mermaid-O7DHMXV3-SgtM28qI.js → mermaid-O7DHMXV3-XxAJo8EK.js} +6 -6
- package/dist/assets/{mindmap-definition-YRQLILUH-4UjqXITU.js → mindmap-definition-YRQLILUH-Dz6EFjmn.js} +1 -1
- package/dist/assets/{pieDiagram-SKSYHLDU-8AxqJd0M.js → pieDiagram-SKSYHLDU-DPpEzUed.js} +1 -1
- package/dist/assets/{quadrantDiagram-337W2JSQ-D60m8V8r.js → quadrantDiagram-337W2JSQ-xdoXNet7.js} +1 -1
- package/dist/assets/{requirementDiagram-Z7DCOOCP-zqh9jBVf.js → requirementDiagram-Z7DCOOCP-DUq8H3CL.js} +1 -1
- package/dist/assets/{sankeyDiagram-WA2Y5GQK-CDZILTLI.js → sankeyDiagram-WA2Y5GQK-CmqEUxRu.js} +1 -1
- package/dist/assets/{sequenceDiagram-2WXFIKYE-7BReFd0L.js → sequenceDiagram-2WXFIKYE-DhtXRNiH.js} +1 -1
- package/dist/assets/{stateDiagram-RAJIS63D-HPTVdIG4.js → stateDiagram-RAJIS63D-Dj0HOlbN.js} +1 -1
- package/dist/assets/stateDiagram-v2-FVOUBMTO-C9utf5gv.js +1 -0
- package/dist/assets/{timeline-definition-YZTLITO2-CTVllFgr.js → timeline-definition-YZTLITO2-DUuJzZB5.js} +1 -1
- package/dist/assets/{treemap-KZPCXAKY-BtyxboJZ.js → treemap-KZPCXAKY-DpYBQ0qr.js} +1 -1
- package/dist/assets/vendor-codemirror-CMHSJ_9p.js +9 -0
- package/dist/assets/{vendor-react-Cpt6D04s.js → vendor-react-xmA_f8ig.js} +1 -1
- package/dist/assets/{vennDiagram-LZ73GAT5-D96ZI6Mg.js → vennDiagram-LZ73GAT5-DpePUyOd.js} +1 -1
- package/dist/assets/{xychartDiagram-JWTSCODW-eRk-39YO.js → xychartDiagram-JWTSCODW-Cfp1I4_U.js} +1 -1
- package/dist/index.html +5 -5
- package/package.json +8 -7
- package/server/acp-runtime/client.js +129 -16
- package/server/acp-runtime/index.js +54 -0
- package/server/acp-runtime/registry.js +2 -2
- package/server/acp-runtime/session-store.js +79 -5
- package/server/cli.js +55 -10
- package/server/database/db.js +20 -0
- package/server/external-agent/service.js +24 -6
- package/server/external-agent/ws.js +540 -27
- package/server/index.js +112 -151
- package/server/lan-access/core.js +79 -0
- package/server/lan-access/state.js +102 -0
- package/server/middleware/auth.js +57 -14
- package/server/projects.js +930 -667
- package/server/routes/auth.js +24 -4
- package/server/routes/cli-auth.js +21 -25
- package/server/routes/codex.js +84 -298
- package/server/routes/commands.js +322 -407
- package/server/routes/lan-access.js +231 -0
- package/server/routes/projects.js +154 -158
- package/server/routes/session-core.js +160 -91
- package/server/routes/settings.js +113 -99
- package/server/session-core/eventStore.js +60 -20
- package/server/session-core/providerAdapters.js +75 -38
- package/server/session-core/runtimeState.js +8 -0
- package/server/session-core/sessionListMerge.js +47 -0
- package/shared/conversationEvents.js +174 -15
- package/shared/modelConstants.js +79 -99
- package/dist/assets/App-CTKZtqB1.js +0 -460
- package/dist/assets/ReviewApp-DM6BNAzR.js +0 -1
- package/dist/assets/channel-1oJBvF-0.js +0 -1
- package/dist/assets/classDiagram-VBA2DB6C-d5TeKFM4.js +0 -1
- package/dist/assets/classDiagram-v2-RAHNMMFH-d5TeKFM4.js +0 -1
- package/dist/assets/clone-CinxIlEu.js +0 -1
- package/dist/assets/index-DFxzgWoO.js +0 -2
- package/dist/assets/index-YCFGDVKw.css +0 -1
- package/dist/assets/stateDiagram-v2-FVOUBMTO-DTUf5_gC.js +0 -1
- package/dist/assets/vendor-codemirror-Dz7_EqNA.js +0 -39
- package/server/_legacy-providers/README.md +0 -30
- package/server/_legacy-providers/claude-sdk.js +0 -956
- package/server/_legacy-providers/gemini-cli.js +0 -368
- package/server/_legacy-providers/openai-codex.js +0 -705
- package/server/_legacy-providers/opencode-cli.js +0 -674
- package/server/routes/git.js +0 -1110
- package/server/routes/mcp-utils.js +0 -48
- package/server/routes/mcp.js +0 -536
- package/server/routes/taskmaster.js +0 -1963
- package/server/utils/mcp-detector.js +0 -198
- package/server/utils/taskmaster-websocket.js +0 -129
package/server/index.js
CHANGED
|
@@ -96,7 +96,7 @@ async function getPty() {
|
|
|
96
96
|
import fetch from 'node-fetch';
|
|
97
97
|
import mime from 'mime-types';
|
|
98
98
|
|
|
99
|
-
import { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache, clearProviderSessionLookupCaches, getGeminiSessionMessages } from './projects.js';
|
|
99
|
+
import { getProjects, getProjectsList, getProjectDetails, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache, clearProviderSessionLookupCaches, getGeminiSessionMessages } from './projects.js';
|
|
100
100
|
import {
|
|
101
101
|
executeAgentPrompt,
|
|
102
102
|
getActiveAgentSessions,
|
|
@@ -104,11 +104,7 @@ import {
|
|
|
104
104
|
resolveAgentPermission,
|
|
105
105
|
setAgentSessionMode
|
|
106
106
|
} from './acp-runtime/index.js';
|
|
107
|
-
import gitRoutes from './routes/git.js';
|
|
108
107
|
import authRoutes from './routes/auth.js';
|
|
109
|
-
import mcpRoutes from './routes/mcp.js';
|
|
110
|
-
import taskmasterRoutes from './routes/taskmaster.js';
|
|
111
|
-
import mcpUtilsRoutes from './routes/mcp-utils.js';
|
|
112
108
|
import commandsRoutes from './routes/commands.js';
|
|
113
109
|
import settingsRoutes from './routes/settings.js';
|
|
114
110
|
import channelsRoutes from './routes/channels.js';
|
|
@@ -127,13 +123,14 @@ import {
|
|
|
127
123
|
} from './utils/workspaceRoots.js';
|
|
128
124
|
import cliAuthRoutes, { detectProviderInstallationStatus } from './routes/cli-auth.js';
|
|
129
125
|
import ccConnectRoutes from './routes/cc-connect.js';
|
|
126
|
+
import { lanAccessApiRoutes, lanAccessPageRoutes } from './routes/lan-access.js';
|
|
130
127
|
import userRoutes from './routes/user.js';
|
|
131
128
|
import codexRoutes from './routes/codex.js';
|
|
132
129
|
import opencodeRoutes from './routes/opencode.js';
|
|
133
130
|
import sessionCoreRoutes from './routes/session-core.js';
|
|
134
131
|
import { parseCodexTokenCountInfo } from './utils/codexTokenUsage.js';
|
|
135
132
|
import { getConfiguredDefaultProjectPath, resolveWorkingDirectory } from './utils/defaultWorkingDirectory.js';
|
|
136
|
-
import { initializeDatabase } from './database/db.js';
|
|
133
|
+
import { initializeDatabase, userDb } from './database/db.js';
|
|
137
134
|
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
|
138
135
|
import { IS_PLATFORM } from './constants/config.js';
|
|
139
136
|
import { getChannelManager } from './channels/index.js';
|
|
@@ -333,8 +330,8 @@ async function setupProjectsWatcher() {
|
|
|
333
330
|
clearProjectDirectoryCache();
|
|
334
331
|
clearProviderSessionLookupCaches();
|
|
335
332
|
|
|
336
|
-
// Get updated projects list
|
|
337
|
-
const updatedProjects = await
|
|
333
|
+
// Get updated lightweight projects list
|
|
334
|
+
const updatedProjects = await getProjectsList(broadcastProgress);
|
|
338
335
|
|
|
339
336
|
// Notify all connected clients about the project changes
|
|
340
337
|
const changedProvider = filePath.startsWith(codexSessionsPath)
|
|
@@ -343,14 +340,21 @@ async function setupProjectsWatcher() {
|
|
|
343
340
|
const changedFileRoot = changedProvider === 'codex'
|
|
344
341
|
? codexSessionsPath
|
|
345
342
|
: claudeProjectsPath;
|
|
343
|
+
const normalizedRelativePath = path.relative(changedFileRoot, filePath);
|
|
344
|
+
const changedPathSegments = normalizedRelativePath.split(path.sep).filter(Boolean);
|
|
345
|
+
const affectedProjectName = changedProvider === 'claude'
|
|
346
|
+
? (changedPathSegments[0] || null)
|
|
347
|
+
: null;
|
|
346
348
|
const updateMessage = JSON.stringify({
|
|
347
349
|
type: 'projects_updated',
|
|
348
350
|
projects: updatedProjects,
|
|
349
351
|
timestamp: new Date().toISOString(),
|
|
350
352
|
changeType: eventType,
|
|
351
|
-
changedFile:
|
|
353
|
+
changedFile: normalizedRelativePath,
|
|
352
354
|
changedProvider,
|
|
353
|
-
changedSessionId: extractSessionIdFromChangedFile(filePath)
|
|
355
|
+
changedSessionId: extractSessionIdFromChangedFile(filePath),
|
|
356
|
+
affectedProjectName,
|
|
357
|
+
requiresDetailsRefresh: Boolean(affectedProjectName)
|
|
354
358
|
});
|
|
355
359
|
|
|
356
360
|
connectedClients.forEach(client => {
|
|
@@ -416,7 +420,7 @@ const wss = new WebSocketServer({
|
|
|
416
420
|
|
|
417
421
|
// Platform mode: always allow internal UI WebSocket connections.
|
|
418
422
|
if (IS_PLATFORM) {
|
|
419
|
-
const user = authenticateWebSocket(null); // Will return first user
|
|
423
|
+
const user = authenticateWebSocket(null, info.req); // Will return first user
|
|
420
424
|
if (!user) {
|
|
421
425
|
console.log('[WARN] Platform mode: No user found in database');
|
|
422
426
|
done(false, 500, 'Platform mode: No user found in database');
|
|
@@ -431,7 +435,7 @@ const wss = new WebSocketServer({
|
|
|
431
435
|
const token = url.searchParams.get('token') ||
|
|
432
436
|
info.req.headers.authorization?.split(' ')[1];
|
|
433
437
|
|
|
434
|
-
const user = authenticateWebSocket(token);
|
|
438
|
+
const user = authenticateWebSocket(token, info.req);
|
|
435
439
|
if (!user) {
|
|
436
440
|
console.log('[WARN] WebSocket authentication failed');
|
|
437
441
|
done(false, 401, 'Unauthorized');
|
|
@@ -490,22 +494,12 @@ app.use('/api', validateApiKey);
|
|
|
490
494
|
|
|
491
495
|
// Authentication routes (public)
|
|
492
496
|
app.use('/api/auth', authRoutes);
|
|
497
|
+
app.use('/api/lan-access', lanAccessApiRoutes);
|
|
498
|
+
app.use('/lan-access', lanAccessPageRoutes);
|
|
493
499
|
|
|
494
500
|
// Projects API Routes (protected)
|
|
495
501
|
app.use('/api/projects', authenticateToken, projectsRoutes);
|
|
496
502
|
|
|
497
|
-
// Git API Routes (protected)
|
|
498
|
-
app.use('/api/git', authenticateToken, gitRoutes);
|
|
499
|
-
|
|
500
|
-
// MCP API Routes (protected)
|
|
501
|
-
app.use('/api/mcp', authenticateToken, mcpRoutes);
|
|
502
|
-
|
|
503
|
-
// TaskMaster API Routes (protected)
|
|
504
|
-
app.use('/api/taskmaster', authenticateToken, taskmasterRoutes);
|
|
505
|
-
|
|
506
|
-
// MCP utilities
|
|
507
|
-
app.use('/api/mcp-utils', authenticateToken, mcpUtilsRoutes);
|
|
508
|
-
|
|
509
503
|
// Commands API Routes (protected)
|
|
510
504
|
app.use('/api/commands', authenticateToken, commandsRoutes);
|
|
511
505
|
|
|
@@ -647,6 +641,7 @@ app.post('/api/system/update', authenticateToken, async (req, res) => {
|
|
|
647
641
|
|
|
648
642
|
app.get('/api/projects', authenticateToken, async (req, res) => {
|
|
649
643
|
try {
|
|
644
|
+
res.setHeader('Deprecation', 'true');
|
|
650
645
|
const projects = await getProjects(broadcastProgress);
|
|
651
646
|
res.json(projects);
|
|
652
647
|
} catch (error) {
|
|
@@ -654,6 +649,27 @@ app.get('/api/projects', authenticateToken, async (req, res) => {
|
|
|
654
649
|
}
|
|
655
650
|
});
|
|
656
651
|
|
|
652
|
+
app.get('/api/projects/list', authenticateToken, async (req, res) => {
|
|
653
|
+
try {
|
|
654
|
+
const projects = await getProjectsList(broadcastProgress);
|
|
655
|
+
res.json(projects);
|
|
656
|
+
} catch (error) {
|
|
657
|
+
res.status(500).json({ error: error.message });
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
app.get('/api/projects/:projectName/details', authenticateToken, async (req, res) => {
|
|
662
|
+
try {
|
|
663
|
+
const project = await getProjectDetails(req.params.projectName);
|
|
664
|
+
res.json(project);
|
|
665
|
+
} catch (error) {
|
|
666
|
+
if (/Project not found/i.test(error.message)) {
|
|
667
|
+
return res.status(404).json({ error: error.message });
|
|
668
|
+
}
|
|
669
|
+
res.status(500).json({ error: error.message });
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
|
|
657
673
|
app.get('/api/projects/:projectName/sessions', authenticateToken, async (req, res) => {
|
|
658
674
|
try {
|
|
659
675
|
const { limit = 5, offset = 0 } = req.query;
|
|
@@ -947,6 +963,65 @@ app.post('/api/create-folder', authenticateToken, async (req, res) => {
|
|
|
947
963
|
}
|
|
948
964
|
});
|
|
949
965
|
|
|
966
|
+
function isWithinProjectRoot(projectRoot, candidatePath) {
|
|
967
|
+
const relativePath = path.relative(projectRoot, candidatePath);
|
|
968
|
+
return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
async function getResolvedProjectRoot(projectName) {
|
|
972
|
+
const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
|
|
973
|
+
if (!projectRoot) {
|
|
974
|
+
const error = new Error('Project not found');
|
|
975
|
+
error.statusCode = 404;
|
|
976
|
+
throw error;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const resolvedRoot = path.resolve(projectRoot);
|
|
980
|
+
await fsPromises.access(resolvedRoot);
|
|
981
|
+
return resolvedRoot;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
async function resolveProjectFilePath(projectName, inputPath) {
|
|
985
|
+
const requestedPath = typeof inputPath === 'string' ? inputPath.trim() : '';
|
|
986
|
+
if (!requestedPath) {
|
|
987
|
+
const error = new Error('Invalid file path');
|
|
988
|
+
error.statusCode = 400;
|
|
989
|
+
throw error;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const projectRoot = await getResolvedProjectRoot(projectName);
|
|
993
|
+
const absoluteTarget = path.isAbsolute(requestedPath)
|
|
994
|
+
? path.resolve(requestedPath)
|
|
995
|
+
: path.resolve(projectRoot, requestedPath);
|
|
996
|
+
|
|
997
|
+
if (!isWithinProjectRoot(projectRoot, absoluteTarget)) {
|
|
998
|
+
const error = new Error('Path must be under project root');
|
|
999
|
+
error.statusCode = 403;
|
|
1000
|
+
throw error;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
return {
|
|
1004
|
+
projectRoot,
|
|
1005
|
+
targetPath: absoluteTarget
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function sendProjectFileError(res, error, missingLabel = 'File not found') {
|
|
1010
|
+
if (error.statusCode) {
|
|
1011
|
+
return res.status(error.statusCode).json({ error: error.message });
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
if (error.code === 'ENOENT') {
|
|
1015
|
+
return res.status(404).json({ error: missingLabel });
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
if (error.code === 'EACCES') {
|
|
1019
|
+
return res.status(403).json({ error: 'Permission denied' });
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
return res.status(500).json({ error: error.message });
|
|
1023
|
+
}
|
|
1024
|
+
|
|
950
1025
|
// Read file content endpoint
|
|
951
1026
|
app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) => {
|
|
952
1027
|
try {
|
|
@@ -955,36 +1030,12 @@ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) =
|
|
|
955
1030
|
|
|
956
1031
|
console.log('[DEBUG] File read request:', projectName, filePath);
|
|
957
1032
|
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
|
|
964
|
-
if (!projectRoot) {
|
|
965
|
-
return res.status(404).json({ error: 'Project not found' });
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
// Handle both absolute and relative paths
|
|
969
|
-
const resolved = path.isAbsolute(filePath)
|
|
970
|
-
? path.resolve(filePath)
|
|
971
|
-
: path.resolve(projectRoot, filePath);
|
|
972
|
-
const normalizedRoot = path.resolve(projectRoot) + path.sep;
|
|
973
|
-
if (!resolved.startsWith(normalizedRoot)) {
|
|
974
|
-
return res.status(403).json({ error: 'Path must be under project root' });
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
const content = await fsPromises.readFile(resolved, 'utf8');
|
|
978
|
-
res.json({ content, path: resolved });
|
|
1033
|
+
const { targetPath } = await resolveProjectFilePath(projectName, filePath);
|
|
1034
|
+
const content = await fsPromises.readFile(targetPath, 'utf8');
|
|
1035
|
+
res.json({ content, path: targetPath });
|
|
979
1036
|
} catch (error) {
|
|
980
1037
|
console.error('Error reading file:', error);
|
|
981
|
-
|
|
982
|
-
res.status(404).json({ error: 'File not found' });
|
|
983
|
-
} else if (error.code === 'EACCES') {
|
|
984
|
-
res.status(403).json({ error: 'Permission denied' });
|
|
985
|
-
} else {
|
|
986
|
-
res.status(500).json({ error: error.message });
|
|
987
|
-
}
|
|
1038
|
+
sendProjectFileError(res, error, 'File not found');
|
|
988
1039
|
}
|
|
989
1040
|
});
|
|
990
1041
|
|
|
@@ -996,35 +1047,15 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
|
|
|
996
1047
|
|
|
997
1048
|
console.log('[DEBUG] Binary file serve request:', projectName, filePath);
|
|
998
1049
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
return res.status(400).json({ error: 'Invalid file path' });
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
|
|
1005
|
-
if (!projectRoot) {
|
|
1006
|
-
return res.status(404).json({ error: 'Project not found' });
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
const resolved = path.resolve(filePath);
|
|
1010
|
-
const normalizedRoot = path.resolve(projectRoot) + path.sep;
|
|
1011
|
-
if (!resolved.startsWith(normalizedRoot)) {
|
|
1012
|
-
return res.status(403).json({ error: 'Path must be under project root' });
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
// Check if file exists
|
|
1016
|
-
try {
|
|
1017
|
-
await fsPromises.access(resolved);
|
|
1018
|
-
} catch (error) {
|
|
1019
|
-
return res.status(404).json({ error: 'File not found' });
|
|
1020
|
-
}
|
|
1050
|
+
const { targetPath } = await resolveProjectFilePath(projectName, filePath);
|
|
1051
|
+
await fsPromises.access(targetPath);
|
|
1021
1052
|
|
|
1022
1053
|
// Get file extension and set appropriate content type
|
|
1023
|
-
const mimeType = mime.lookup(
|
|
1054
|
+
const mimeType = mime.lookup(targetPath) || 'application/octet-stream';
|
|
1024
1055
|
res.setHeader('Content-Type', mimeType);
|
|
1025
1056
|
|
|
1026
1057
|
// Stream the file
|
|
1027
|
-
const fileStream = fs.createReadStream(
|
|
1058
|
+
const fileStream = fs.createReadStream(targetPath);
|
|
1028
1059
|
fileStream.pipe(res);
|
|
1029
1060
|
|
|
1030
1061
|
fileStream.on('error', (error) => {
|
|
@@ -1037,90 +1068,19 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
|
|
|
1037
1068
|
} catch (error) {
|
|
1038
1069
|
console.error('Error serving binary file:', error);
|
|
1039
1070
|
if (!res.headersSent) {
|
|
1040
|
-
res
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
});
|
|
1044
|
-
|
|
1045
|
-
// Save file content endpoint
|
|
1046
|
-
app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) => {
|
|
1047
|
-
try {
|
|
1048
|
-
const { projectName } = req.params;
|
|
1049
|
-
const { filePath, content } = req.body;
|
|
1050
|
-
|
|
1051
|
-
console.log('[DEBUG] File save request:', projectName, filePath);
|
|
1052
|
-
|
|
1053
|
-
// Security: ensure the requested path is inside the project root
|
|
1054
|
-
if (!filePath) {
|
|
1055
|
-
return res.status(400).json({ error: 'Invalid file path' });
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
if (content === undefined) {
|
|
1059
|
-
return res.status(400).json({ error: 'Content is required' });
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
|
|
1063
|
-
if (!projectRoot) {
|
|
1064
|
-
return res.status(404).json({ error: 'Project not found' });
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
// Handle both absolute and relative paths
|
|
1068
|
-
const resolved = path.isAbsolute(filePath)
|
|
1069
|
-
? path.resolve(filePath)
|
|
1070
|
-
: path.resolve(projectRoot, filePath);
|
|
1071
|
-
const normalizedRoot = path.resolve(projectRoot) + path.sep;
|
|
1072
|
-
if (!resolved.startsWith(normalizedRoot)) {
|
|
1073
|
-
return res.status(403).json({ error: 'Path must be under project root' });
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
// Write the new content
|
|
1077
|
-
await fsPromises.writeFile(resolved, content, 'utf8');
|
|
1078
|
-
|
|
1079
|
-
res.json({
|
|
1080
|
-
success: true,
|
|
1081
|
-
path: resolved,
|
|
1082
|
-
message: 'File saved successfully'
|
|
1083
|
-
});
|
|
1084
|
-
} catch (error) {
|
|
1085
|
-
console.error('Error saving file:', error);
|
|
1086
|
-
if (error.code === 'ENOENT') {
|
|
1087
|
-
res.status(404).json({ error: 'File or directory not found' });
|
|
1088
|
-
} else if (error.code === 'EACCES') {
|
|
1089
|
-
res.status(403).json({ error: 'Permission denied' });
|
|
1090
|
-
} else {
|
|
1091
|
-
res.status(500).json({ error: error.message });
|
|
1071
|
+
sendProjectFileError(res, error, 'File not found');
|
|
1092
1072
|
}
|
|
1093
1073
|
}
|
|
1094
1074
|
});
|
|
1095
1075
|
|
|
1096
1076
|
app.get('/api/projects/:projectName/files', authenticateToken, async (req, res) => {
|
|
1097
1077
|
try {
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
// Use extractProjectDirectory to get the actual project path
|
|
1102
|
-
let actualPath;
|
|
1103
|
-
try {
|
|
1104
|
-
actualPath = await extractProjectDirectory(req.params.projectName);
|
|
1105
|
-
} catch (error) {
|
|
1106
|
-
console.error('Error extracting project directory:', error);
|
|
1107
|
-
// Fallback to simple dash replacement
|
|
1108
|
-
actualPath = req.params.projectName.replace(/-/g, '/');
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
// Check if path exists
|
|
1112
|
-
try {
|
|
1113
|
-
await fsPromises.access(actualPath);
|
|
1114
|
-
} catch (e) {
|
|
1115
|
-
return res.status(404).json({ error: `Project path not found: ${actualPath}` });
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
const files = await getFileTree(actualPath, 10, 0, true);
|
|
1119
|
-
const hiddenFiles = files.filter(f => f.name.startsWith('.'));
|
|
1078
|
+
const projectRoot = await getResolvedProjectRoot(req.params.projectName);
|
|
1079
|
+
const files = await getFileTree(projectRoot, 10, 0, true);
|
|
1120
1080
|
res.json(files);
|
|
1121
1081
|
} catch (error) {
|
|
1122
1082
|
console.error('[ERROR] File tree error:', error.message);
|
|
1123
|
-
res
|
|
1083
|
+
sendProjectFileError(res, error, error.statusCode === 404 ? error.message : 'Failed to list files');
|
|
1124
1084
|
}
|
|
1125
1085
|
});
|
|
1126
1086
|
|
|
@@ -2189,6 +2149,7 @@ async function startServer() {
|
|
|
2189
2149
|
try {
|
|
2190
2150
|
// Initialize authentication database
|
|
2191
2151
|
await initializeDatabase();
|
|
2152
|
+
userDb.ensureDefaultUser();
|
|
2192
2153
|
await getChannelManager().initialize();
|
|
2193
2154
|
|
|
2194
2155
|
// Check if running in production mode (dist folder exists)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
|
|
3
|
+
function normalizeHost(value) {
|
|
4
|
+
const raw = String(value || '').trim().toLowerCase();
|
|
5
|
+
if (!raw) {
|
|
6
|
+
return '';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (raw.startsWith('[')) {
|
|
10
|
+
const closingIndex = raw.indexOf(']');
|
|
11
|
+
return closingIndex >= 0 ? raw.slice(1, closingIndex) : raw;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const hostParts = raw.split(':');
|
|
15
|
+
if (hostParts.length === 2) {
|
|
16
|
+
return hostParts[0];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return raw;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeAddress(value) {
|
|
23
|
+
const raw = String(value || '').trim().toLowerCase();
|
|
24
|
+
if (!raw) {
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return raw.replace(/^::ffff:/, '');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isLoopbackHost(host) {
|
|
32
|
+
const normalizedHost = normalizeHost(host);
|
|
33
|
+
return normalizedHost === 'localhost'
|
|
34
|
+
|| normalizedHost === '127.0.0.1'
|
|
35
|
+
|| normalizedHost === '::1';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isLoopbackAddress(address) {
|
|
39
|
+
const normalizedAddress = normalizeAddress(address);
|
|
40
|
+
return normalizedAddress === '127.0.0.1' || normalizedAddress === '::1';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isLoopbackRequest(req) {
|
|
44
|
+
const forwardedHost = String(req?.headers?.['x-forwarded-host'] || '').split(',')[0].trim();
|
|
45
|
+
const host = forwardedHost || req?.headers?.host || req?.hostname || '';
|
|
46
|
+
return isLoopbackHost(host);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isIPv4Family(value) {
|
|
50
|
+
return value === 'IPv4' || value === 4;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function collectLanIPv4Addresses(networkInterfaces = os.networkInterfaces()) {
|
|
54
|
+
const addresses = new Set();
|
|
55
|
+
|
|
56
|
+
for (const entries of Object.values(networkInterfaces || {})) {
|
|
57
|
+
for (const entry of entries || []) {
|
|
58
|
+
if (!entry || entry.internal || !isIPv4Family(entry.family)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const address = String(entry.address || '').trim();
|
|
63
|
+
if (!address) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
addresses.add(address);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return Array.from(addresses).sort((left, right) => left.localeCompare(right));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export {
|
|
75
|
+
collectLanIPv4Addresses,
|
|
76
|
+
isLoopbackAddress,
|
|
77
|
+
isLoopbackHost,
|
|
78
|
+
isLoopbackRequest,
|
|
79
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import bcrypt from 'bcrypt';
|
|
4
|
+
|
|
5
|
+
import { getDataFilePath } from '../database/db.js';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_STATE = Object.freeze({
|
|
8
|
+
passwordHash: null,
|
|
9
|
+
sessionVersion: 0,
|
|
10
|
+
updatedAt: null,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
function getLanAccessStatePath() {
|
|
14
|
+
if (process.env.LAN_ACCESS_STATE_PATH) {
|
|
15
|
+
return process.env.LAN_ACCESS_STATE_PATH;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return path.join(path.dirname(getDataFilePath()), 'lan-access-state.json');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeState(rawState) {
|
|
22
|
+
const source = rawState && typeof rawState === 'object' ? rawState : {};
|
|
23
|
+
return {
|
|
24
|
+
passwordHash: typeof source.passwordHash === 'string' && source.passwordHash.trim() ? source.passwordHash : null,
|
|
25
|
+
sessionVersion: Number.isInteger(source.sessionVersion) && source.sessionVersion >= 0 ? source.sessionVersion : 0,
|
|
26
|
+
updatedAt: typeof source.updatedAt === 'string' ? source.updatedAt : null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function ensureStateDir() {
|
|
31
|
+
const statePath = getLanAccessStatePath();
|
|
32
|
+
const stateDir = path.dirname(statePath);
|
|
33
|
+
if (!fs.existsSync(stateDir)) {
|
|
34
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readLanAccessState() {
|
|
39
|
+
const statePath = getLanAccessStatePath();
|
|
40
|
+
if (!fs.existsSync(statePath)) {
|
|
41
|
+
return { ...DEFAULT_STATE };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
46
|
+
return normalizeState(parsed);
|
|
47
|
+
} catch {
|
|
48
|
+
return { ...DEFAULT_STATE };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function writeLanAccessState(nextState) {
|
|
53
|
+
ensureStateDir();
|
|
54
|
+
const statePath = getLanAccessStatePath();
|
|
55
|
+
const normalized = normalizeState(nextState);
|
|
56
|
+
const tempPath = `${statePath}.tmp`;
|
|
57
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8');
|
|
58
|
+
fs.renameSync(tempPath, statePath);
|
|
59
|
+
return normalized;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function hasLanAccessPassword() {
|
|
63
|
+
return Boolean(readLanAccessState().passwordHash);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getLanSessionVersion() {
|
|
67
|
+
return readLanAccessState().sessionVersion;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function setLanAccessPassword(password) {
|
|
71
|
+
const trimmedPassword = String(password || '').trim();
|
|
72
|
+
if (!trimmedPassword) {
|
|
73
|
+
throw new Error('Access password is required');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const currentState = readLanAccessState();
|
|
77
|
+
const passwordHash = await bcrypt.hash(trimmedPassword, 12);
|
|
78
|
+
return writeLanAccessState({
|
|
79
|
+
passwordHash,
|
|
80
|
+
sessionVersion: currentState.sessionVersion + 1,
|
|
81
|
+
updatedAt: new Date().toISOString(),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function verifyLanAccessPassword(password) {
|
|
86
|
+
const state = readLanAccessState();
|
|
87
|
+
if (!state.passwordHash) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return bcrypt.compare(String(password || ''), state.passwordHash);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export {
|
|
95
|
+
getLanAccessStatePath,
|
|
96
|
+
getLanSessionVersion,
|
|
97
|
+
hasLanAccessPassword,
|
|
98
|
+
readLanAccessState,
|
|
99
|
+
setLanAccessPassword,
|
|
100
|
+
verifyLanAccessPassword,
|
|
101
|
+
writeLanAccessState,
|
|
102
|
+
};
|