maxsimcli 4.6.0 → 4.7.1
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/dist/assets/CHANGELOG.md +39 -0
- package/dist/cli.cjs +470 -961
- package/dist/cli.cjs.map +1 -1
- package/dist/{core-RRjCSt0G.cjs → core-D5zUr9cb.cjs} +4 -3
- package/dist/core-D5zUr9cb.cjs.map +1 -0
- package/dist/install.cjs +3 -195
- package/dist/install.cjs.map +1 -1
- package/dist/mcp-server.cjs +2853 -217
- package/dist/mcp-server.cjs.map +1 -1
- package/dist/{skills-MYlMkYNt.cjs → skills-CjFWZIGM.cjs} +6 -6
- package/dist/{skills-MYlMkYNt.cjs.map → skills-CjFWZIGM.cjs.map} +1 -1
- package/package.json +1 -7
- package/dist/.tsbuildinfo +0 -1
- package/dist/assets/dashboard/client/assets/index-C199D4Eb.css +0 -32
- package/dist/assets/dashboard/client/assets/index-nAXJLp0_.js +0 -233
- package/dist/assets/dashboard/client/index.html +0 -19
- package/dist/assets/dashboard/server.js +0 -78813
- package/dist/backend/index.d.ts +0 -4
- package/dist/backend/index.d.ts.map +0 -1
- package/dist/backend/index.js +0 -12
- package/dist/backend/index.js.map +0 -1
- package/dist/backend/lifecycle.d.ts +0 -13
- package/dist/backend/lifecycle.d.ts.map +0 -1
- package/dist/backend/lifecycle.js +0 -168
- package/dist/backend/lifecycle.js.map +0 -1
- package/dist/backend/server.d.ts +0 -13
- package/dist/backend/server.d.ts.map +0 -1
- package/dist/backend/server.js +0 -1013
- package/dist/backend/server.js.map +0 -1
- package/dist/backend/terminal.d.ts +0 -49
- package/dist/backend/terminal.d.ts.map +0 -1
- package/dist/backend/terminal.js +0 -209
- package/dist/backend/terminal.js.map +0 -1
- package/dist/backend/types.d.ts +0 -77
- package/dist/backend/types.d.ts.map +0 -1
- package/dist/backend/types.js +0 -6
- package/dist/backend/types.js.map +0 -1
- package/dist/backend-server.cjs +0 -80672
- package/dist/backend-server.cjs.map +0 -1
- package/dist/backend-server.d.cts +0 -2
- package/dist/backend-server.d.ts +0 -11
- package/dist/backend-server.d.ts.map +0 -1
- package/dist/backend-server.js +0 -43
- package/dist/backend-server.js.map +0 -1
- package/dist/cli.d.cts +0 -2
- package/dist/cli.d.ts +0 -7
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -510
- package/dist/cli.js.map +0 -1
- package/dist/core/artefakte.d.ts +0 -12
- package/dist/core/artefakte.d.ts.map +0 -1
- package/dist/core/artefakte.js +0 -152
- package/dist/core/artefakte.js.map +0 -1
- package/dist/core/commands.d.ts +0 -26
- package/dist/core/commands.d.ts.map +0 -1
- package/dist/core/commands.js +0 -550
- package/dist/core/commands.js.map +0 -1
- package/dist/core/config.d.ts +0 -10
- package/dist/core/config.d.ts.map +0 -1
- package/dist/core/config.js +0 -143
- package/dist/core/config.js.map +0 -1
- package/dist/core/context-loader.d.ts +0 -21
- package/dist/core/context-loader.d.ts.map +0 -1
- package/dist/core/context-loader.js +0 -212
- package/dist/core/context-loader.js.map +0 -1
- package/dist/core/core.d.ts +0 -91
- package/dist/core/core.d.ts.map +0 -1
- package/dist/core/core.js +0 -823
- package/dist/core/core.js.map +0 -1
- package/dist/core/dashboard-launcher.d.ts +0 -56
- package/dist/core/dashboard-launcher.d.ts.map +0 -1
- package/dist/core/dashboard-launcher.js +0 -246
- package/dist/core/dashboard-launcher.js.map +0 -1
- package/dist/core/drift.d.ts +0 -37
- package/dist/core/drift.d.ts.map +0 -1
- package/dist/core/drift.js +0 -213
- package/dist/core/drift.js.map +0 -1
- package/dist/core/frontmatter.d.ts +0 -33
- package/dist/core/frontmatter.d.ts.map +0 -1
- package/dist/core/frontmatter.js +0 -193
- package/dist/core/frontmatter.js.map +0 -1
- package/dist/core/index.d.ts +0 -28
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/index.js +0 -189
- package/dist/core/index.js.map +0 -1
- package/dist/core/init.d.ts +0 -287
- package/dist/core/init.d.ts.map +0 -1
- package/dist/core/init.js +0 -816
- package/dist/core/init.js.map +0 -1
- package/dist/core/milestone.d.ts +0 -9
- package/dist/core/milestone.d.ts.map +0 -1
- package/dist/core/milestone.js +0 -230
- package/dist/core/milestone.js.map +0 -1
- package/dist/core/phase.d.ts +0 -53
- package/dist/core/phase.d.ts.map +0 -1
- package/dist/core/phase.js +0 -891
- package/dist/core/phase.js.map +0 -1
- package/dist/core/roadmap.d.ts +0 -10
- package/dist/core/roadmap.d.ts.map +0 -1
- package/dist/core/roadmap.js +0 -165
- package/dist/core/roadmap.js.map +0 -1
- package/dist/core/skills.d.ts +0 -20
- package/dist/core/skills.d.ts.map +0 -1
- package/dist/core/skills.js +0 -144
- package/dist/core/skills.js.map +0 -1
- package/dist/core/start.d.ts +0 -15
- package/dist/core/start.d.ts.map +0 -1
- package/dist/core/start.js +0 -80
- package/dist/core/start.js.map +0 -1
- package/dist/core/state.d.ts +0 -32
- package/dist/core/state.d.ts.map +0 -1
- package/dist/core/state.js +0 -582
- package/dist/core/state.js.map +0 -1
- package/dist/core/template.d.ts +0 -30
- package/dist/core/template.d.ts.map +0 -1
- package/dist/core/template.js +0 -223
- package/dist/core/template.js.map +0 -1
- package/dist/core/types.d.ts +0 -519
- package/dist/core/types.d.ts.map +0 -1
- package/dist/core/types.js +0 -60
- package/dist/core/types.js.map +0 -1
- package/dist/core/verify.d.ts +0 -128
- package/dist/core/verify.d.ts.map +0 -1
- package/dist/core/verify.js +0 -754
- package/dist/core/verify.js.map +0 -1
- package/dist/core-RRjCSt0G.cjs.map +0 -1
- package/dist/esm-iIOBzmdz.cjs +0 -1561
- package/dist/esm-iIOBzmdz.cjs.map +0 -1
- package/dist/hooks/index.d.ts +0 -11
- package/dist/hooks/index.d.ts.map +0 -1
- package/dist/hooks/index.js +0 -18
- package/dist/hooks/index.js.map +0 -1
- package/dist/hooks/maxsim-check-update.d.ts +0 -17
- package/dist/hooks/maxsim-check-update.d.ts.map +0 -1
- package/dist/hooks/maxsim-check-update.js +0 -101
- package/dist/hooks/maxsim-check-update.js.map +0 -1
- package/dist/hooks/maxsim-context-monitor.d.ts +0 -21
- package/dist/hooks/maxsim-context-monitor.d.ts.map +0 -1
- package/dist/hooks/maxsim-context-monitor.js +0 -131
- package/dist/hooks/maxsim-context-monitor.js.map +0 -1
- package/dist/hooks/maxsim-statusline.d.ts +0 -19
- package/dist/hooks/maxsim-statusline.d.ts.map +0 -1
- package/dist/hooks/maxsim-statusline.js +0 -146
- package/dist/hooks/maxsim-statusline.js.map +0 -1
- package/dist/hooks/shared.d.ts +0 -11
- package/dist/hooks/shared.d.ts.map +0 -1
- package/dist/hooks/shared.js +0 -29
- package/dist/hooks/shared.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -3
- package/dist/index.js.map +0 -1
- package/dist/install/adapters.d.ts +0 -6
- package/dist/install/adapters.d.ts.map +0 -1
- package/dist/install/adapters.js +0 -65
- package/dist/install/adapters.js.map +0 -1
- package/dist/install/copy.d.ts +0 -6
- package/dist/install/copy.d.ts.map +0 -1
- package/dist/install/copy.js +0 -71
- package/dist/install/copy.js.map +0 -1
- package/dist/install/dashboard.d.ts +0 -16
- package/dist/install/dashboard.d.ts.map +0 -1
- package/dist/install/dashboard.js +0 -273
- package/dist/install/dashboard.js.map +0 -1
- package/dist/install/hooks.d.ts +0 -31
- package/dist/install/hooks.d.ts.map +0 -1
- package/dist/install/hooks.js +0 -260
- package/dist/install/hooks.js.map +0 -1
- package/dist/install/index.d.ts +0 -2
- package/dist/install/index.d.ts.map +0 -1
- package/dist/install/index.js +0 -534
- package/dist/install/index.js.map +0 -1
- package/dist/install/manifest.d.ts +0 -23
- package/dist/install/manifest.d.ts.map +0 -1
- package/dist/install/manifest.js +0 -133
- package/dist/install/manifest.js.map +0 -1
- package/dist/install/patches.d.ts +0 -10
- package/dist/install/patches.d.ts.map +0 -1
- package/dist/install/patches.js +0 -124
- package/dist/install/patches.js.map +0 -1
- package/dist/install/shared.d.ts +0 -56
- package/dist/install/shared.d.ts.map +0 -1
- package/dist/install/shared.js +0 -181
- package/dist/install/shared.js.map +0 -1
- package/dist/install/uninstall.d.ts +0 -5
- package/dist/install/uninstall.d.ts.map +0 -1
- package/dist/install/uninstall.js +0 -222
- package/dist/install/uninstall.js.map +0 -1
- package/dist/install/utils.d.ts +0 -27
- package/dist/install/utils.d.ts.map +0 -1
- package/dist/install/utils.js +0 -99
- package/dist/install/utils.js.map +0 -1
- package/dist/install.d.cts +0 -2
- package/dist/lifecycle-DxCru7rk.cjs +0 -136
- package/dist/lifecycle-DxCru7rk.cjs.map +0 -1
- package/dist/mcp/config-tools.d.ts +0 -13
- package/dist/mcp/config-tools.d.ts.map +0 -1
- package/dist/mcp/config-tools.js +0 -66
- package/dist/mcp/config-tools.js.map +0 -1
- package/dist/mcp/context-tools.d.ts +0 -13
- package/dist/mcp/context-tools.d.ts.map +0 -1
- package/dist/mcp/context-tools.js +0 -176
- package/dist/mcp/context-tools.js.map +0 -1
- package/dist/mcp/index.d.ts +0 -11
- package/dist/mcp/index.d.ts.map +0 -1
- package/dist/mcp/index.js +0 -26
- package/dist/mcp/index.js.map +0 -1
- package/dist/mcp/phase-tools.d.ts +0 -13
- package/dist/mcp/phase-tools.d.ts.map +0 -1
- package/dist/mcp/phase-tools.js +0 -177
- package/dist/mcp/phase-tools.js.map +0 -1
- package/dist/mcp/roadmap-tools.d.ts +0 -13
- package/dist/mcp/roadmap-tools.d.ts.map +0 -1
- package/dist/mcp/roadmap-tools.js +0 -79
- package/dist/mcp/roadmap-tools.js.map +0 -1
- package/dist/mcp/state-tools.d.ts +0 -13
- package/dist/mcp/state-tools.d.ts.map +0 -1
- package/dist/mcp/state-tools.js +0 -185
- package/dist/mcp/state-tools.js.map +0 -1
- package/dist/mcp/todo-tools.d.ts +0 -13
- package/dist/mcp/todo-tools.d.ts.map +0 -1
- package/dist/mcp/todo-tools.js +0 -143
- package/dist/mcp/todo-tools.js.map +0 -1
- package/dist/mcp/utils.d.ts +0 -27
- package/dist/mcp/utils.d.ts.map +0 -1
- package/dist/mcp/utils.js +0 -82
- package/dist/mcp/utils.js.map +0 -1
- package/dist/mcp-server.d.cts +0 -2
- package/dist/mcp-server.d.ts +0 -12
- package/dist/mcp-server.d.ts.map +0 -1
- package/dist/mcp-server.js +0 -31
- package/dist/mcp-server.js.map +0 -1
- package/dist/server-By0TN-nC.cjs +0 -2995
- package/dist/server-By0TN-nC.cjs.map +0 -1
package/dist/backend/server.js
DELETED
|
@@ -1,1013 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* MAXSIM Backend Server — Unified persistent backend service
|
|
4
|
-
*
|
|
5
|
-
* Consolidates HTTP API, WebSocket, MCP endpoint, terminal management,
|
|
6
|
-
* and file watching into a single per-project process.
|
|
7
|
-
*
|
|
8
|
-
* CRITICAL: Never import output() or error() from core — they call process.exit().
|
|
9
|
-
* CRITICAL: Never write to stdout directly — stdout may be reserved for protocol use.
|
|
10
|
-
* All logging must go to stderr via console.error().
|
|
11
|
-
*/
|
|
12
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
-
if (k2 === undefined) k2 = k;
|
|
14
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
-
}
|
|
18
|
-
Object.defineProperty(o, k2, desc);
|
|
19
|
-
}) : (function(o, m, k, k2) {
|
|
20
|
-
if (k2 === undefined) k2 = k;
|
|
21
|
-
o[k2] = m[k];
|
|
22
|
-
}));
|
|
23
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
-
}) : function(o, v) {
|
|
26
|
-
o["default"] = v;
|
|
27
|
-
});
|
|
28
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
-
var ownKeys = function(o) {
|
|
30
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
-
var ar = [];
|
|
32
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
-
return ar;
|
|
34
|
-
};
|
|
35
|
-
return ownKeys(o);
|
|
36
|
-
};
|
|
37
|
-
return function (mod) {
|
|
38
|
-
if (mod && mod.__esModule) return mod;
|
|
39
|
-
var result = {};
|
|
40
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
-
__setModuleDefault(result, mod);
|
|
42
|
-
return result;
|
|
43
|
-
};
|
|
44
|
-
})();
|
|
45
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
46
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
47
|
-
};
|
|
48
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
-
exports.createBackendServer = createBackendServer;
|
|
50
|
-
const path = __importStar(require("node:path"));
|
|
51
|
-
const fs = __importStar(require("node:fs"));
|
|
52
|
-
const os = __importStar(require("node:os"));
|
|
53
|
-
const node_http_1 = require("node:http");
|
|
54
|
-
const express_1 = __importDefault(require("express"));
|
|
55
|
-
const ws_1 = require("ws");
|
|
56
|
-
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
57
|
-
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
58
|
-
const detect_port_1 = __importDefault(require("detect-port"));
|
|
59
|
-
const index_js_1 = require("../core/index.js");
|
|
60
|
-
const index_js_2 = require("../mcp/index.js");
|
|
61
|
-
const terminal_js_1 = require("./terminal.js");
|
|
62
|
-
// ─── Logging ──────────────────────────────────────────────────────────────
|
|
63
|
-
function log(level, tag, ...args) {
|
|
64
|
-
const ts = new Date().toISOString();
|
|
65
|
-
const msg = args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ');
|
|
66
|
-
console.error(`[${ts}] [${level}] [${tag}] ${msg}`);
|
|
67
|
-
}
|
|
68
|
-
// ─── Path security ─────────────────────────────────────────────────────────
|
|
69
|
-
function isWithinPlanning(cwd, targetPath) {
|
|
70
|
-
const planningDir = path.resolve(cwd, '.planning');
|
|
71
|
-
const resolved = path.resolve(cwd, targetPath);
|
|
72
|
-
return resolved.startsWith(planningDir);
|
|
73
|
-
}
|
|
74
|
-
// ─── Write-suppression for watcher loop prevention ─────────────────────────
|
|
75
|
-
function normalizeFsPath(p) {
|
|
76
|
-
return p.replace(/\\/g, '/');
|
|
77
|
-
}
|
|
78
|
-
// ─── Parsers ───────────────────────────────────────────────────────────────
|
|
79
|
-
function parseRoadmap(cwd) {
|
|
80
|
-
const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
|
|
81
|
-
if (!fs.existsSync(roadmapPath))
|
|
82
|
-
return null;
|
|
83
|
-
const content = fs.readFileSync(roadmapPath, 'utf-8').replace(/\r\n/g, '\n');
|
|
84
|
-
const phasesDir = path.join(cwd, '.planning', 'phases');
|
|
85
|
-
const phasePattern = (0, index_js_1.getPhasePattern)();
|
|
86
|
-
const phases = [];
|
|
87
|
-
let match;
|
|
88
|
-
while ((match = phasePattern.exec(content)) !== null) {
|
|
89
|
-
const phaseNum = match[1];
|
|
90
|
-
const phaseName = match[2].replace(/\(INSERTED\)/i, '').trim();
|
|
91
|
-
const sectionStart = match.index;
|
|
92
|
-
const restOfContent = content.slice(sectionStart);
|
|
93
|
-
const nextHeader = restOfContent.match(/\n#{2,4}\s+Phase\s+\d/i);
|
|
94
|
-
const sectionEnd = nextHeader ? sectionStart + nextHeader.index : content.length;
|
|
95
|
-
const section = content.slice(sectionStart, sectionEnd);
|
|
96
|
-
const goalMatch = section.match(/\*\*Goal(?::\*\*|\*\*:)\s*([^\n]+)/i);
|
|
97
|
-
const goal = goalMatch ? goalMatch[1].trim() : null;
|
|
98
|
-
const dependsMatch = section.match(/\*\*Depends on:\*\*\s*([^\n]+)/i);
|
|
99
|
-
const depends_on = dependsMatch ? dependsMatch[1].trim() : null;
|
|
100
|
-
const normalized = (0, index_js_1.normalizePhaseName)(phaseNum);
|
|
101
|
-
let diskStatus = 'no_directory';
|
|
102
|
-
let planCount = 0;
|
|
103
|
-
let summaryCount = 0;
|
|
104
|
-
let hasContext = false;
|
|
105
|
-
let hasResearch = false;
|
|
106
|
-
try {
|
|
107
|
-
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
108
|
-
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
109
|
-
const dirMatch = dirs.find(d => d.startsWith(normalized + '-') || d === normalized);
|
|
110
|
-
if (dirMatch) {
|
|
111
|
-
const phaseFiles = fs.readdirSync(path.join(phasesDir, dirMatch));
|
|
112
|
-
planCount = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
|
|
113
|
-
summaryCount = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
|
|
114
|
-
hasContext = phaseFiles.some(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
|
|
115
|
-
hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
|
|
116
|
-
if (summaryCount >= planCount && planCount > 0)
|
|
117
|
-
diskStatus = 'complete';
|
|
118
|
-
else if (summaryCount > 0)
|
|
119
|
-
diskStatus = 'partial';
|
|
120
|
-
else if (planCount > 0)
|
|
121
|
-
diskStatus = 'planned';
|
|
122
|
-
else if (hasResearch)
|
|
123
|
-
diskStatus = 'researched';
|
|
124
|
-
else if (hasContext)
|
|
125
|
-
diskStatus = 'discussed';
|
|
126
|
-
else
|
|
127
|
-
diskStatus = 'empty';
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
catch {
|
|
131
|
-
// phases dir may not exist
|
|
132
|
-
}
|
|
133
|
-
const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${phaseNum.replace('.', '\\.')}`, 'i');
|
|
134
|
-
const checkboxMatch = content.match(checkboxPattern);
|
|
135
|
-
const roadmapComplete = checkboxMatch ? checkboxMatch[1] === 'x' : false;
|
|
136
|
-
phases.push({
|
|
137
|
-
number: phaseNum,
|
|
138
|
-
name: phaseName,
|
|
139
|
-
goal,
|
|
140
|
-
depends_on,
|
|
141
|
-
plan_count: planCount,
|
|
142
|
-
summary_count: summaryCount,
|
|
143
|
-
has_context: hasContext,
|
|
144
|
-
has_research: hasResearch,
|
|
145
|
-
disk_status: diskStatus,
|
|
146
|
-
roadmap_complete: roadmapComplete,
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
const milestones = [];
|
|
150
|
-
const milestonePattern = /##\s*(.*v(\d+\.\d+)[^(\n]*)/gi;
|
|
151
|
-
let mMatch;
|
|
152
|
-
while ((mMatch = milestonePattern.exec(content)) !== null) {
|
|
153
|
-
milestones.push({ heading: mMatch[1].trim(), version: 'v' + mMatch[2] });
|
|
154
|
-
}
|
|
155
|
-
const currentPhase = phases.find(p => p.disk_status === 'planned' || p.disk_status === 'partial') || null;
|
|
156
|
-
const nextPhase = phases.find(p => p.disk_status === 'empty' || p.disk_status === 'no_directory' ||
|
|
157
|
-
p.disk_status === 'discussed' || p.disk_status === 'researched') || null;
|
|
158
|
-
const totalPlans = phases.reduce((sum, p) => sum + p.plan_count, 0);
|
|
159
|
-
const totalSummaries = phases.reduce((sum, p) => sum + p.summary_count, 0);
|
|
160
|
-
const completedPhases = phases.filter(p => p.disk_status === 'complete').length;
|
|
161
|
-
return {
|
|
162
|
-
milestones,
|
|
163
|
-
phases,
|
|
164
|
-
phase_count: phases.length,
|
|
165
|
-
completed_phases: completedPhases,
|
|
166
|
-
total_plans: totalPlans,
|
|
167
|
-
total_summaries: totalSummaries,
|
|
168
|
-
progress_percent: totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0,
|
|
169
|
-
current_phase: currentPhase ? currentPhase.number : null,
|
|
170
|
-
next_phase: nextPhase ? nextPhase.number : null,
|
|
171
|
-
missing_phase_details: null,
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
function parseState(cwd) {
|
|
175
|
-
const statePath = path.join(cwd, '.planning', 'STATE.md');
|
|
176
|
-
if (!fs.existsSync(statePath))
|
|
177
|
-
return null;
|
|
178
|
-
const content = fs.readFileSync(statePath, 'utf-8').replace(/\r\n/g, '\n');
|
|
179
|
-
const position = (0, index_js_1.stateExtractField)(content, 'Current Position') || (0, index_js_1.stateExtractField)(content, 'Phase');
|
|
180
|
-
const lastActivity = (0, index_js_1.stateExtractField)(content, 'Last activity') || (0, index_js_1.stateExtractField)(content, 'Last Activity');
|
|
181
|
-
const currentPhase = (0, index_js_1.stateExtractField)(content, 'Current Phase') || (0, index_js_1.stateExtractField)(content, 'Phase');
|
|
182
|
-
const currentPlan = (0, index_js_1.stateExtractField)(content, 'Current Plan') || (0, index_js_1.stateExtractField)(content, 'Plan');
|
|
183
|
-
const status = (0, index_js_1.stateExtractField)(content, 'Status');
|
|
184
|
-
const progress = (0, index_js_1.stateExtractField)(content, 'Progress');
|
|
185
|
-
const decisions = [];
|
|
186
|
-
const decisionsMatch = content.match(/###?\s*Decisions\s*\n([\s\S]*?)(?=\n###?|\n##[^#]|$)/i);
|
|
187
|
-
if (decisionsMatch) {
|
|
188
|
-
const items = decisionsMatch[1].match(/^-\s+(.+)$/gm) || [];
|
|
189
|
-
for (const item of items)
|
|
190
|
-
decisions.push(item.replace(/^-\s+/, '').trim());
|
|
191
|
-
}
|
|
192
|
-
const blockers = [];
|
|
193
|
-
const blockersMatch = content.match(/###?\s*(?:Blockers|Blockers\/Concerns)\s*\n([\s\S]*?)(?=\n###?|\n##[^#]|$)/i);
|
|
194
|
-
if (blockersMatch) {
|
|
195
|
-
const items = blockersMatch[1].match(/^-\s+(.+)$/gm) || [];
|
|
196
|
-
for (const item of items)
|
|
197
|
-
blockers.push(item.replace(/^-\s+/, '').trim());
|
|
198
|
-
}
|
|
199
|
-
return { position, lastActivity, currentPhase, currentPlan, status, progress, decisions, blockers, content };
|
|
200
|
-
}
|
|
201
|
-
function parsePhases(cwd) {
|
|
202
|
-
const phasesDir = path.join(cwd, '.planning', 'phases');
|
|
203
|
-
if (!fs.existsSync(phasesDir))
|
|
204
|
-
return [];
|
|
205
|
-
const phases = [];
|
|
206
|
-
try {
|
|
207
|
-
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
208
|
-
const dirs = entries
|
|
209
|
-
.filter(e => e.isDirectory())
|
|
210
|
-
.map(e => e.name)
|
|
211
|
-
.sort((a, b) => (0, index_js_1.comparePhaseNum)(a, b));
|
|
212
|
-
for (const dir of dirs) {
|
|
213
|
-
const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)?)-?(.*)/i);
|
|
214
|
-
const phaseNum = dm ? dm[1] : dir;
|
|
215
|
-
const phaseName = dm && dm[2] ? dm[2].replace(/-/g, ' ') : '';
|
|
216
|
-
const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
|
|
217
|
-
const planCount = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
|
|
218
|
-
const summaryCount = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
|
|
219
|
-
const hasContext = phaseFiles.some(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
|
|
220
|
-
const hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
|
|
221
|
-
let diskStatus = 'no_directory';
|
|
222
|
-
if (summaryCount >= planCount && planCount > 0)
|
|
223
|
-
diskStatus = 'complete';
|
|
224
|
-
else if (summaryCount > 0)
|
|
225
|
-
diskStatus = 'partial';
|
|
226
|
-
else if (planCount > 0)
|
|
227
|
-
diskStatus = 'planned';
|
|
228
|
-
else if (hasResearch)
|
|
229
|
-
diskStatus = 'researched';
|
|
230
|
-
else if (hasContext)
|
|
231
|
-
diskStatus = 'discussed';
|
|
232
|
-
else
|
|
233
|
-
diskStatus = 'empty';
|
|
234
|
-
phases.push({
|
|
235
|
-
number: phaseNum,
|
|
236
|
-
name: phaseName,
|
|
237
|
-
goal: '',
|
|
238
|
-
dependsOn: [],
|
|
239
|
-
planCount,
|
|
240
|
-
summaryCount,
|
|
241
|
-
diskStatus,
|
|
242
|
-
roadmapComplete: diskStatus === 'complete',
|
|
243
|
-
hasContext,
|
|
244
|
-
hasResearch,
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
catch {
|
|
249
|
-
// phases dir may not exist or be empty
|
|
250
|
-
}
|
|
251
|
-
return phases;
|
|
252
|
-
}
|
|
253
|
-
function parsePhaseDetail(cwd, phaseId) {
|
|
254
|
-
const phasesDir = path.join(cwd, '.planning', 'phases');
|
|
255
|
-
if (!fs.existsSync(phasesDir))
|
|
256
|
-
return null;
|
|
257
|
-
const normalized = (0, index_js_1.normalizePhaseName)(phaseId);
|
|
258
|
-
try {
|
|
259
|
-
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
260
|
-
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
261
|
-
const dirMatch = dirs.find(d => d.startsWith(normalized + '-') || d === normalized);
|
|
262
|
-
if (!dirMatch)
|
|
263
|
-
return null;
|
|
264
|
-
const phaseDir = path.join(phasesDir, dirMatch);
|
|
265
|
-
const phaseFiles = fs.readdirSync(phaseDir);
|
|
266
|
-
const planFileNames = phaseFiles
|
|
267
|
-
.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md')
|
|
268
|
-
.sort();
|
|
269
|
-
const plans = [];
|
|
270
|
-
for (const planFileName of planFileNames) {
|
|
271
|
-
const planPath = path.join(phaseDir, planFileName);
|
|
272
|
-
const content = fs.readFileSync(planPath, 'utf-8').replace(/\r\n/g, '\n');
|
|
273
|
-
const frontmatter = (0, index_js_1.extractFrontmatter)(content);
|
|
274
|
-
const tasks = [];
|
|
275
|
-
const taskRegex = /<task\s+type="([^"]*)"[^>]*>\s*<name>([^<]+)<\/name>([\s\S]*?)<\/task>/g;
|
|
276
|
-
let taskMatch;
|
|
277
|
-
while ((taskMatch = taskRegex.exec(content)) !== null) {
|
|
278
|
-
const taskType = taskMatch[1];
|
|
279
|
-
const taskName = taskMatch[2].trim();
|
|
280
|
-
const taskBody = taskMatch[3];
|
|
281
|
-
const filesMatch = taskBody.match(/<files>([\s\S]*?)<\/files>/);
|
|
282
|
-
const actionMatch = taskBody.match(/<action>([\s\S]*?)<\/action>/);
|
|
283
|
-
const verifyMatch = taskBody.match(/<verify>([\s\S]*?)<\/verify>/);
|
|
284
|
-
const doneMatch = taskBody.match(/<done>([\s\S]*?)<\/done>/);
|
|
285
|
-
const files = filesMatch
|
|
286
|
-
? filesMatch[1].trim().split('\n').map(f => f.trim()).filter(Boolean)
|
|
287
|
-
: [];
|
|
288
|
-
const doneText = doneMatch ? doneMatch[1].trim() : '';
|
|
289
|
-
tasks.push({
|
|
290
|
-
name: taskName,
|
|
291
|
-
type: taskType,
|
|
292
|
-
files,
|
|
293
|
-
action: actionMatch ? actionMatch[1].trim() : '',
|
|
294
|
-
verify: verifyMatch ? verifyMatch[1].trim() : '',
|
|
295
|
-
done: doneText,
|
|
296
|
-
completed: /^\[x\]/i.test(doneText),
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
plans.push({
|
|
300
|
-
path: path.join('.planning', 'phases', dirMatch, planFileName),
|
|
301
|
-
content,
|
|
302
|
-
frontmatter,
|
|
303
|
-
tasks,
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
let context = null;
|
|
307
|
-
const contextFile = phaseFiles.find(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
|
|
308
|
-
if (contextFile)
|
|
309
|
-
context = fs.readFileSync(path.join(phaseDir, contextFile), 'utf-8');
|
|
310
|
-
let research = null;
|
|
311
|
-
const researchFile = phaseFiles.find(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
|
|
312
|
-
if (researchFile)
|
|
313
|
-
research = fs.readFileSync(path.join(phaseDir, researchFile), 'utf-8');
|
|
314
|
-
return { plans, context, research };
|
|
315
|
-
}
|
|
316
|
-
catch {
|
|
317
|
-
return null;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
function parseTodos(cwd) {
|
|
321
|
-
const pendingDir = path.join(cwd, '.planning', 'todos', 'pending');
|
|
322
|
-
const completedDir = path.join(cwd, '.planning', 'todos', 'completed');
|
|
323
|
-
const pending = [];
|
|
324
|
-
const completed = [];
|
|
325
|
-
if (fs.existsSync(pendingDir)) {
|
|
326
|
-
try {
|
|
327
|
-
const files = fs.readdirSync(pendingDir).filter(f => f.endsWith('.md'));
|
|
328
|
-
for (const file of files) {
|
|
329
|
-
try {
|
|
330
|
-
const content = fs.readFileSync(path.join(pendingDir, file), 'utf-8');
|
|
331
|
-
const titleMatch = content.match(/^title:\s*(.+)$/m);
|
|
332
|
-
pending.push({ text: titleMatch ? titleMatch[1].trim() : file.replace('.md', ''), completed: false, file });
|
|
333
|
-
}
|
|
334
|
-
catch { /* skip unreadable */ }
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
catch { /* pending dir may not exist */ }
|
|
338
|
-
}
|
|
339
|
-
if (fs.existsSync(completedDir)) {
|
|
340
|
-
try {
|
|
341
|
-
const files = fs.readdirSync(completedDir).filter(f => f.endsWith('.md'));
|
|
342
|
-
for (const file of files) {
|
|
343
|
-
try {
|
|
344
|
-
const content = fs.readFileSync(path.join(completedDir, file), 'utf-8');
|
|
345
|
-
const titleMatch = content.match(/^title:\s*(.+)$/m);
|
|
346
|
-
completed.push({ text: titleMatch ? titleMatch[1].trim() : file.replace('.md', ''), completed: true, file });
|
|
347
|
-
}
|
|
348
|
-
catch { /* skip unreadable */ }
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
catch { /* completed dir may not exist */ }
|
|
352
|
-
}
|
|
353
|
-
return { pending, completed };
|
|
354
|
-
}
|
|
355
|
-
function parseProject(cwd) {
|
|
356
|
-
const projectPath = path.join(cwd, '.planning', 'PROJECT.md');
|
|
357
|
-
const requirementsPath = path.join(cwd, '.planning', 'REQUIREMENTS.md');
|
|
358
|
-
const project = fs.existsSync(projectPath) ? fs.readFileSync(projectPath, 'utf-8') : null;
|
|
359
|
-
const requirements = fs.existsSync(requirementsPath) ? fs.readFileSync(requirementsPath, 'utf-8') : null;
|
|
360
|
-
return { project, requirements };
|
|
361
|
-
}
|
|
362
|
-
// ─── Server Factory ────────────────────────────────────────────────────────
|
|
363
|
-
function createBackendServer(config) {
|
|
364
|
-
const { projectCwd, host, enableTerminal, enableFileWatcher, enableMcp, logDir, } = config;
|
|
365
|
-
let resolvedPort = config.port;
|
|
366
|
-
const startTime = Date.now();
|
|
367
|
-
let serverReady = false;
|
|
368
|
-
// Logging
|
|
369
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
370
|
-
// ─── Write suppression ─────────────────────────────────────────────────
|
|
371
|
-
const suppressedPaths = new Map();
|
|
372
|
-
const SUPPRESS_TTL_MS = 500;
|
|
373
|
-
function suppressPath(filePath) {
|
|
374
|
-
suppressedPaths.set(normalizeFsPath(filePath), Date.now());
|
|
375
|
-
}
|
|
376
|
-
function isSuppressed(filePath) {
|
|
377
|
-
const normalized = normalizeFsPath(filePath);
|
|
378
|
-
const timestamp = suppressedPaths.get(normalized);
|
|
379
|
-
if (timestamp === undefined)
|
|
380
|
-
return false;
|
|
381
|
-
if (Date.now() - timestamp > SUPPRESS_TTL_MS) {
|
|
382
|
-
suppressedPaths.delete(normalized);
|
|
383
|
-
return false;
|
|
384
|
-
}
|
|
385
|
-
return true;
|
|
386
|
-
}
|
|
387
|
-
// Periodic cleanup of stale suppressed paths
|
|
388
|
-
const cleanupInterval = setInterval(() => {
|
|
389
|
-
const now = Date.now();
|
|
390
|
-
for (const [p, ts] of suppressedPaths.entries()) {
|
|
391
|
-
if (now - ts > SUPPRESS_TTL_MS)
|
|
392
|
-
suppressedPaths.delete(p);
|
|
393
|
-
}
|
|
394
|
-
}, 60_000);
|
|
395
|
-
cleanupInterval.unref();
|
|
396
|
-
// ─── MCP shared state ──────────────────────────────────────────────────
|
|
397
|
-
const questionQueue = [];
|
|
398
|
-
const pendingAnswers = new Map();
|
|
399
|
-
// ─── WebSocket ─────────────────────────────────────────────────────────
|
|
400
|
-
let clientCount = 0;
|
|
401
|
-
const wss = new ws_1.WebSocketServer({ noServer: true });
|
|
402
|
-
wss.on('connection', (ws) => {
|
|
403
|
-
clientCount++;
|
|
404
|
-
log('INFO', 'ws', `Client connected (${clientCount} total)`);
|
|
405
|
-
ws.on('close', () => {
|
|
406
|
-
clientCount--;
|
|
407
|
-
log('INFO', 'ws', `Client disconnected (${clientCount} total)`);
|
|
408
|
-
});
|
|
409
|
-
ws.on('error', (err) => {
|
|
410
|
-
log('ERROR', 'ws', `Client error: ${err.message}`);
|
|
411
|
-
});
|
|
412
|
-
ws.send(JSON.stringify({ type: 'connected', timestamp: Date.now() }));
|
|
413
|
-
// Send queued questions on reconnect
|
|
414
|
-
if (questionQueue.length > 0) {
|
|
415
|
-
ws.send(JSON.stringify({
|
|
416
|
-
type: 'questions-queued',
|
|
417
|
-
questions: questionQueue,
|
|
418
|
-
count: questionQueue.length,
|
|
419
|
-
}));
|
|
420
|
-
}
|
|
421
|
-
});
|
|
422
|
-
function broadcast(message) {
|
|
423
|
-
const data = JSON.stringify(message);
|
|
424
|
-
for (const client of wss.clients) {
|
|
425
|
-
if (client.readyState === ws_1.WebSocket.OPEN) {
|
|
426
|
-
client.send(data);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
// ─── File watcher ──────────────────────────────────────────────────────
|
|
431
|
-
let watcher = null;
|
|
432
|
-
async function setupWatcher() {
|
|
433
|
-
if (!enableFileWatcher)
|
|
434
|
-
return;
|
|
435
|
-
const planningDir = path.join(projectCwd, '.planning');
|
|
436
|
-
if (!fs.existsSync(planningDir)) {
|
|
437
|
-
log('WARN', 'watcher', `.planning/ directory not found at ${planningDir}`);
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
try {
|
|
441
|
-
const chokidar = await import('chokidar');
|
|
442
|
-
const changedPaths = new Set();
|
|
443
|
-
let flushTimer = null;
|
|
444
|
-
function flushChanges() {
|
|
445
|
-
if (changedPaths.size > 0) {
|
|
446
|
-
const changes = Array.from(changedPaths);
|
|
447
|
-
changedPaths.clear();
|
|
448
|
-
log('INFO', 'watcher', `Broadcasting ${changes.length} change(s)`);
|
|
449
|
-
broadcast({ type: 'file-changes', changes, timestamp: Date.now() });
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
function onFileChange(filePath) {
|
|
453
|
-
const normalized = normalizeFsPath(filePath);
|
|
454
|
-
if (isSuppressed(normalized))
|
|
455
|
-
return;
|
|
456
|
-
changedPaths.add(normalized);
|
|
457
|
-
if (flushTimer)
|
|
458
|
-
clearTimeout(flushTimer);
|
|
459
|
-
flushTimer = setTimeout(flushChanges, 500);
|
|
460
|
-
}
|
|
461
|
-
const w = chokidar.watch(planningDir, {
|
|
462
|
-
persistent: true,
|
|
463
|
-
ignoreInitial: true,
|
|
464
|
-
depth: 5,
|
|
465
|
-
});
|
|
466
|
-
w.on('add', onFileChange);
|
|
467
|
-
w.on('change', onFileChange);
|
|
468
|
-
w.on('unlink', onFileChange);
|
|
469
|
-
w.on('error', (err) => {
|
|
470
|
-
log('ERROR', 'watcher', `Error: ${err.message}`);
|
|
471
|
-
});
|
|
472
|
-
watcher = w;
|
|
473
|
-
log('INFO', 'watcher', `Watching ${planningDir}`);
|
|
474
|
-
}
|
|
475
|
-
catch (err) {
|
|
476
|
-
log('ERROR', 'watcher', `Failed to start file watcher: ${err.message}`);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
// ─── Express app ───────────────────────────────────────────────────────
|
|
480
|
-
const app = (0, express_1.default)();
|
|
481
|
-
app.use(express_1.default.json());
|
|
482
|
-
// ── Health ──
|
|
483
|
-
app.get('/api/health', (_req, res) => {
|
|
484
|
-
res.json({
|
|
485
|
-
status: 'ok',
|
|
486
|
-
ready: serverReady,
|
|
487
|
-
port: resolvedPort,
|
|
488
|
-
cwd: projectCwd,
|
|
489
|
-
uptime: (Date.now() - startTime) / 1000,
|
|
490
|
-
pid: process.pid,
|
|
491
|
-
mcpEndpoint: enableMcp ? `http://127.0.0.1:${resolvedPort}/mcp` : null,
|
|
492
|
-
terminalAvailable: enableTerminal && terminal_js_1.PtyManager.getInstance().isAvailable(),
|
|
493
|
-
connectedClients: clientCount,
|
|
494
|
-
});
|
|
495
|
-
});
|
|
496
|
-
app.get('/api/ready', (_req, res) => {
|
|
497
|
-
if (serverReady) {
|
|
498
|
-
return res.json({ ready: true, port: resolvedPort, cwd: projectCwd });
|
|
499
|
-
}
|
|
500
|
-
return res.status(503).json({ ready: false, message: 'Server is starting up' });
|
|
501
|
-
});
|
|
502
|
-
// ── Roadmap ──
|
|
503
|
-
app.get('/api/roadmap', (_req, res) => {
|
|
504
|
-
try {
|
|
505
|
-
const data = parseRoadmap(projectCwd);
|
|
506
|
-
if (!data)
|
|
507
|
-
return res.status(404).json({ error: 'ROADMAP.md not found' });
|
|
508
|
-
return res.json(data);
|
|
509
|
-
}
|
|
510
|
-
catch (err) {
|
|
511
|
-
log('ERROR', 'api', `GET /api/roadmap failed: ${err.message}`);
|
|
512
|
-
return res.status(500).json({ error: 'Internal server error' });
|
|
513
|
-
}
|
|
514
|
-
});
|
|
515
|
-
app.patch('/api/roadmap', (req, res) => {
|
|
516
|
-
try {
|
|
517
|
-
const roadmapPath = path.join(projectCwd, '.planning', 'ROADMAP.md');
|
|
518
|
-
if (!fs.existsSync(roadmapPath))
|
|
519
|
-
return res.status(404).json({ error: 'ROADMAP.md not found' });
|
|
520
|
-
const { phaseNumber, checked } = req.body;
|
|
521
|
-
if (!phaseNumber || checked === undefined) {
|
|
522
|
-
return res.status(400).json({ error: 'phaseNumber and checked are required' });
|
|
523
|
-
}
|
|
524
|
-
let content = fs.readFileSync(roadmapPath, 'utf-8').replace(/\r\n/g, '\n');
|
|
525
|
-
const escapedNum = phaseNumber.replace('.', '\\.');
|
|
526
|
-
const pattern = new RegExp(`(-\\s*\\[)(x| )(\\]\\s*.*Phase\\s+${escapedNum})`, 'i');
|
|
527
|
-
const match = content.match(pattern);
|
|
528
|
-
if (!match)
|
|
529
|
-
return res.status(404).json({ error: `Phase ${phaseNumber} checkbox not found` });
|
|
530
|
-
content = content.replace(pattern, `$1${checked ? 'x' : ' '}$3`);
|
|
531
|
-
suppressPath(roadmapPath);
|
|
532
|
-
fs.writeFileSync(roadmapPath, content, 'utf-8');
|
|
533
|
-
return res.json({ updated: true, phaseNumber, checked });
|
|
534
|
-
}
|
|
535
|
-
catch (err) {
|
|
536
|
-
log('ERROR', 'api', `PATCH /api/roadmap failed: ${err.message}`);
|
|
537
|
-
return res.status(500).json({ error: 'Internal server error' });
|
|
538
|
-
}
|
|
539
|
-
});
|
|
540
|
-
// ── State ──
|
|
541
|
-
app.get('/api/state', (_req, res) => {
|
|
542
|
-
try {
|
|
543
|
-
const data = parseState(projectCwd);
|
|
544
|
-
if (!data)
|
|
545
|
-
return res.status(404).json({ error: 'STATE.md not found' });
|
|
546
|
-
return res.json(data);
|
|
547
|
-
}
|
|
548
|
-
catch (err) {
|
|
549
|
-
log('ERROR', 'api', `GET /api/state failed: ${err.message}`);
|
|
550
|
-
return res.status(500).json({ error: 'Internal server error' });
|
|
551
|
-
}
|
|
552
|
-
});
|
|
553
|
-
app.patch('/api/state', (req, res) => {
|
|
554
|
-
try {
|
|
555
|
-
const statePath = path.join(projectCwd, '.planning', 'STATE.md');
|
|
556
|
-
if (!fs.existsSync(statePath))
|
|
557
|
-
return res.status(404).json({ error: 'STATE.md not found' });
|
|
558
|
-
const { field, value } = req.body;
|
|
559
|
-
if (!field || value === undefined) {
|
|
560
|
-
return res.status(400).json({ error: 'field and value are required' });
|
|
561
|
-
}
|
|
562
|
-
const content = fs.readFileSync(statePath, 'utf-8').replace(/\r\n/g, '\n');
|
|
563
|
-
const updated = (0, index_js_1.stateReplaceField)(content, field, value);
|
|
564
|
-
if (!updated)
|
|
565
|
-
return res.status(404).json({ error: `Field "${field}" not found in STATE.md` });
|
|
566
|
-
suppressPath(statePath);
|
|
567
|
-
fs.writeFileSync(statePath, updated, 'utf-8');
|
|
568
|
-
return res.json({ updated: true, field });
|
|
569
|
-
}
|
|
570
|
-
catch (err) {
|
|
571
|
-
log('ERROR', 'api', `PATCH /api/state failed: ${err.message}`);
|
|
572
|
-
return res.status(500).json({ error: 'Internal server error' });
|
|
573
|
-
}
|
|
574
|
-
});
|
|
575
|
-
// Helper: ensure STATE.md exists
|
|
576
|
-
function ensureStateMd(statePath) {
|
|
577
|
-
if (fs.existsSync(statePath))
|
|
578
|
-
return;
|
|
579
|
-
const planningDir = path.dirname(statePath);
|
|
580
|
-
fs.mkdirSync(planningDir, { recursive: true });
|
|
581
|
-
const template = `# Project State
|
|
582
|
-
|
|
583
|
-
## Current Position
|
|
584
|
-
|
|
585
|
-
Phase: 1
|
|
586
|
-
Status: In progress
|
|
587
|
-
Last activity: ${new Date().toISOString().split('T')[0]} — State file created
|
|
588
|
-
|
|
589
|
-
## Accumulated Context
|
|
590
|
-
|
|
591
|
-
### Decisions
|
|
592
|
-
|
|
593
|
-
None yet.
|
|
594
|
-
|
|
595
|
-
### Blockers/Concerns
|
|
596
|
-
|
|
597
|
-
None yet.
|
|
598
|
-
`;
|
|
599
|
-
fs.writeFileSync(statePath, template, 'utf-8');
|
|
600
|
-
}
|
|
601
|
-
// Helper: append to a STATE.md section
|
|
602
|
-
function appendToStateSection(statePath, sectionPattern, entry, fallbackSection) {
|
|
603
|
-
let content = fs.readFileSync(statePath, 'utf-8').replace(/\r\n/g, '\n');
|
|
604
|
-
const match = content.match(sectionPattern);
|
|
605
|
-
if (match) {
|
|
606
|
-
let sectionBody = match[2];
|
|
607
|
-
sectionBody = sectionBody
|
|
608
|
-
.replace(/None yet\.?\s*\n?/gi, '')
|
|
609
|
-
.replace(/No decisions yet\.?\s*\n?/gi, '')
|
|
610
|
-
.replace(/None\.?\s*\n?/gi, '');
|
|
611
|
-
sectionBody = sectionBody.trimEnd() + '\n' + entry + '\n';
|
|
612
|
-
content = content.replace(sectionPattern, (_m, header) => `${header}${sectionBody}`);
|
|
613
|
-
}
|
|
614
|
-
else {
|
|
615
|
-
content = content.trimEnd() + '\n\n' + fallbackSection + '\n' + entry + '\n';
|
|
616
|
-
}
|
|
617
|
-
suppressPath(statePath);
|
|
618
|
-
fs.writeFileSync(statePath, content, 'utf-8');
|
|
619
|
-
}
|
|
620
|
-
// ── Add Decision ──
|
|
621
|
-
app.post('/api/state/decision', (req, res) => {
|
|
622
|
-
try {
|
|
623
|
-
const statePath = path.join(projectCwd, '.planning', 'STATE.md');
|
|
624
|
-
ensureStateMd(statePath);
|
|
625
|
-
const { phase, text } = req.body;
|
|
626
|
-
if (!text?.trim())
|
|
627
|
-
return res.status(400).json({ error: 'text is required' });
|
|
628
|
-
const phaseLabel = phase?.trim() || '?';
|
|
629
|
-
const entry = `- [Phase ${phaseLabel}]: ${text.trim()}`;
|
|
630
|
-
const sectionPattern = /(###?\s*(?:Decisions|Decisions Made|Accumulated.*Decisions)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
|
|
631
|
-
appendToStateSection(statePath, sectionPattern, entry, '### Decisions');
|
|
632
|
-
return res.json({ added: true, decision: entry });
|
|
633
|
-
}
|
|
634
|
-
catch (err) {
|
|
635
|
-
log('ERROR', 'api', `POST /api/state/decision failed: ${err.message}`);
|
|
636
|
-
return res.status(500).json({ error: 'Internal server error' });
|
|
637
|
-
}
|
|
638
|
-
});
|
|
639
|
-
// ── Add Blocker ──
|
|
640
|
-
app.post('/api/state/blocker', (req, res) => {
|
|
641
|
-
try {
|
|
642
|
-
const statePath = path.join(projectCwd, '.planning', 'STATE.md');
|
|
643
|
-
ensureStateMd(statePath);
|
|
644
|
-
const { text } = req.body;
|
|
645
|
-
if (!text?.trim())
|
|
646
|
-
return res.status(400).json({ error: 'text is required' });
|
|
647
|
-
const entry = `- ${text.trim()}`;
|
|
648
|
-
const sectionPattern = /(###?\s*(?:Blockers|Blockers\/Concerns|Concerns)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
|
|
649
|
-
appendToStateSection(statePath, sectionPattern, entry, '### Blockers/Concerns');
|
|
650
|
-
return res.json({ added: true, blocker: text.trim() });
|
|
651
|
-
}
|
|
652
|
-
catch (err) {
|
|
653
|
-
log('ERROR', 'api', `POST /api/state/blocker failed: ${err.message}`);
|
|
654
|
-
return res.status(500).json({ error: 'Internal server error' });
|
|
655
|
-
}
|
|
656
|
-
});
|
|
657
|
-
// ── Phases ──
|
|
658
|
-
app.get('/api/phases', (_req, res) => {
|
|
659
|
-
try {
|
|
660
|
-
return res.json(parsePhases(projectCwd));
|
|
661
|
-
}
|
|
662
|
-
catch (err) {
|
|
663
|
-
log('ERROR', 'api', `GET /api/phases failed: ${err.message}`);
|
|
664
|
-
return res.status(500).json({ error: 'Internal server error' });
|
|
665
|
-
}
|
|
666
|
-
});
|
|
667
|
-
app.get('/api/phase/:id', (req, res) => {
|
|
668
|
-
try {
|
|
669
|
-
const data = parsePhaseDetail(projectCwd, req.params.id);
|
|
670
|
-
if (!data)
|
|
671
|
-
return res.status(404).json({ error: `Phase ${req.params.id} not found` });
|
|
672
|
-
return res.json(data);
|
|
673
|
-
}
|
|
674
|
-
catch (err) {
|
|
675
|
-
log('ERROR', 'api', `GET /api/phase/:id failed: ${err.message}`);
|
|
676
|
-
return res.status(500).json({ error: 'Internal server error' });
|
|
677
|
-
}
|
|
678
|
-
});
|
|
679
|
-
// ── Todos ──
|
|
680
|
-
app.get('/api/todos', (_req, res) => {
|
|
681
|
-
try {
|
|
682
|
-
return res.json(parseTodos(projectCwd));
|
|
683
|
-
}
|
|
684
|
-
catch (err) {
|
|
685
|
-
log('ERROR', 'api', `GET /api/todos failed: ${err.message}`);
|
|
686
|
-
return res.status(500).json({ error: 'Internal server error' });
|
|
687
|
-
}
|
|
688
|
-
});
|
|
689
|
-
app.post('/api/todos', (req, res) => {
|
|
690
|
-
try {
|
|
691
|
-
const pendingDir = path.join(projectCwd, '.planning', 'todos', 'pending');
|
|
692
|
-
const { text } = req.body;
|
|
693
|
-
if (!text)
|
|
694
|
-
return res.status(400).json({ error: 'text is required' });
|
|
695
|
-
fs.mkdirSync(pendingDir, { recursive: true });
|
|
696
|
-
const timestamp = new Date().toISOString().split('T')[0];
|
|
697
|
-
const slug = text.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 40);
|
|
698
|
-
const filename = `${timestamp}-${slug}.md`;
|
|
699
|
-
const filePath = path.join(pendingDir, filename);
|
|
700
|
-
const content = `title: ${text}\ncreated: ${timestamp}\narea: general\n\n${text}\n`;
|
|
701
|
-
suppressPath(filePath);
|
|
702
|
-
fs.writeFileSync(filePath, content, 'utf-8');
|
|
703
|
-
return res.json({ created: true, file: filename, text });
|
|
704
|
-
}
|
|
705
|
-
catch (err) {
|
|
706
|
-
log('ERROR', 'api', `POST /api/todos failed: ${err.message}`);
|
|
707
|
-
return res.status(500).json({ error: 'Internal server error' });
|
|
708
|
-
}
|
|
709
|
-
});
|
|
710
|
-
app.patch('/api/todos', (req, res) => {
|
|
711
|
-
try {
|
|
712
|
-
const pendingDir = path.join(projectCwd, '.planning', 'todos', 'pending');
|
|
713
|
-
const completedDir = path.join(projectCwd, '.planning', 'todos', 'completed');
|
|
714
|
-
const { file, completed } = req.body;
|
|
715
|
-
if (!file)
|
|
716
|
-
return res.status(400).json({ error: 'file is required' });
|
|
717
|
-
if (file.includes('/') || file.includes('\\') || file.includes('..')) {
|
|
718
|
-
return res.status(400).json({ error: 'Invalid filename' });
|
|
719
|
-
}
|
|
720
|
-
if (completed) {
|
|
721
|
-
const sourcePath = path.join(pendingDir, file);
|
|
722
|
-
if (!fs.existsSync(sourcePath))
|
|
723
|
-
return res.status(404).json({ error: 'Todo not found in pending' });
|
|
724
|
-
fs.mkdirSync(completedDir, { recursive: true });
|
|
725
|
-
const today = new Date().toISOString().split('T')[0];
|
|
726
|
-
let content = fs.readFileSync(sourcePath, 'utf-8');
|
|
727
|
-
content = `completed: ${today}\n` + content;
|
|
728
|
-
const destPath = path.join(completedDir, file);
|
|
729
|
-
suppressPath(sourcePath);
|
|
730
|
-
suppressPath(destPath);
|
|
731
|
-
fs.writeFileSync(destPath, content, 'utf-8');
|
|
732
|
-
fs.unlinkSync(sourcePath);
|
|
733
|
-
return res.json({ completed: true, file, date: today });
|
|
734
|
-
}
|
|
735
|
-
else {
|
|
736
|
-
const sourcePath = path.join(completedDir, file);
|
|
737
|
-
if (!fs.existsSync(sourcePath))
|
|
738
|
-
return res.status(404).json({ error: 'Todo not found in completed' });
|
|
739
|
-
fs.mkdirSync(pendingDir, { recursive: true });
|
|
740
|
-
let content = fs.readFileSync(sourcePath, 'utf-8');
|
|
741
|
-
content = content.replace(/^completed:\s*.+\n/m, '');
|
|
742
|
-
const destPath = path.join(pendingDir, file);
|
|
743
|
-
suppressPath(sourcePath);
|
|
744
|
-
suppressPath(destPath);
|
|
745
|
-
fs.writeFileSync(destPath, content, 'utf-8');
|
|
746
|
-
fs.unlinkSync(sourcePath);
|
|
747
|
-
return res.json({ completed: false, file });
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
catch (err) {
|
|
751
|
-
log('ERROR', 'api', `PATCH /api/todos failed: ${err.message}`);
|
|
752
|
-
return res.status(500).json({ error: 'Internal server error' });
|
|
753
|
-
}
|
|
754
|
-
});
|
|
755
|
-
// ── Project ──
|
|
756
|
-
app.get('/api/project', (_req, res) => {
|
|
757
|
-
try {
|
|
758
|
-
return res.json(parseProject(projectCwd));
|
|
759
|
-
}
|
|
760
|
-
catch (err) {
|
|
761
|
-
log('ERROR', 'api', `GET /api/project failed: ${err.message}`);
|
|
762
|
-
return res.status(500).json({ error: 'Internal server error' });
|
|
763
|
-
}
|
|
764
|
-
});
|
|
765
|
-
// ── Plan file (read) ──
|
|
766
|
-
app.get('/api/plan/*', (req, res) => {
|
|
767
|
-
try {
|
|
768
|
-
const pathSegments = req.params['0'].split('/');
|
|
769
|
-
const relativePath = path.join('.planning', ...pathSegments);
|
|
770
|
-
if (!isWithinPlanning(projectCwd, relativePath)) {
|
|
771
|
-
return res.status(403).json({ error: 'Path traversal not allowed' });
|
|
772
|
-
}
|
|
773
|
-
const fullPath = path.join(projectCwd, relativePath);
|
|
774
|
-
if (!fs.existsSync(fullPath))
|
|
775
|
-
return res.status(404).json({ error: 'File not found' });
|
|
776
|
-
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
777
|
-
return res.json({ path: relativePath, content });
|
|
778
|
-
}
|
|
779
|
-
catch (err) {
|
|
780
|
-
log('ERROR', 'api', `GET /api/plan/* failed: ${err.message}`);
|
|
781
|
-
return res.status(500).json({ error: 'Internal server error' });
|
|
782
|
-
}
|
|
783
|
-
});
|
|
784
|
-
// ── Plan file (write) ──
|
|
785
|
-
app.put('/api/plan/*', (req, res) => {
|
|
786
|
-
try {
|
|
787
|
-
const pathSegments = req.params['0'].split('/');
|
|
788
|
-
const relativePath = path.join('.planning', ...pathSegments);
|
|
789
|
-
if (!isWithinPlanning(projectCwd, relativePath)) {
|
|
790
|
-
return res.status(403).json({ error: 'Path traversal not allowed' });
|
|
791
|
-
}
|
|
792
|
-
const { content } = req.body;
|
|
793
|
-
if (content === undefined)
|
|
794
|
-
return res.status(400).json({ error: 'content is required' });
|
|
795
|
-
const fullPath = path.join(projectCwd, relativePath);
|
|
796
|
-
const dir = path.dirname(fullPath);
|
|
797
|
-
if (!fs.existsSync(dir))
|
|
798
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
799
|
-
suppressPath(fullPath);
|
|
800
|
-
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
801
|
-
return res.json({ written: true, path: relativePath });
|
|
802
|
-
}
|
|
803
|
-
catch (err) {
|
|
804
|
-
log('ERROR', 'api', `PUT /api/plan/* failed: ${err.message}`);
|
|
805
|
-
return res.status(500).json({ error: 'Internal server error' });
|
|
806
|
-
}
|
|
807
|
-
});
|
|
808
|
-
// ── Server info ──
|
|
809
|
-
app.get('/api/server-info', (_req, res) => {
|
|
810
|
-
const localNetworkIp = getLocalNetworkIp();
|
|
811
|
-
return res.json({
|
|
812
|
-
localUrl: `http://127.0.0.1:${resolvedPort}`,
|
|
813
|
-
networkUrl: localNetworkIp ? `http://${localNetworkIp}:${resolvedPort}` : null,
|
|
814
|
-
projectName: path.basename(projectCwd),
|
|
815
|
-
projectCwd,
|
|
816
|
-
});
|
|
817
|
-
});
|
|
818
|
-
// ── Shutdown ──
|
|
819
|
-
let shutdownFn = null;
|
|
820
|
-
app.post('/api/shutdown', (_req, res) => {
|
|
821
|
-
res.json({ shutdown: true });
|
|
822
|
-
setTimeout(() => shutdownFn?.(), 100);
|
|
823
|
-
});
|
|
824
|
-
// ── MCP answer ──
|
|
825
|
-
app.post('/api/mcp-answer', (req, res) => {
|
|
826
|
-
const { questionId, answer } = req.body;
|
|
827
|
-
if (!questionId || !answer)
|
|
828
|
-
return res.status(400).json({ error: 'questionId and answer are required' });
|
|
829
|
-
const resolve = pendingAnswers.get(questionId);
|
|
830
|
-
if (!resolve)
|
|
831
|
-
return res.status(404).json({ error: 'No pending question with that ID' });
|
|
832
|
-
pendingAnswers.delete(questionId);
|
|
833
|
-
resolve(answer);
|
|
834
|
-
return res.json({ answered: true });
|
|
835
|
-
});
|
|
836
|
-
// ── MCP endpoint ──
|
|
837
|
-
if (enableMcp) {
|
|
838
|
-
app.post('/mcp', async (req, res) => {
|
|
839
|
-
const mcpServer = new mcp_js_1.McpServer({ name: 'maxsim-backend', version: '1.0.0' });
|
|
840
|
-
(0, index_js_2.registerAllTools)(mcpServer);
|
|
841
|
-
try {
|
|
842
|
-
const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
843
|
-
await mcpServer.connect(transport);
|
|
844
|
-
await transport.handleRequest(req, res, req.body);
|
|
845
|
-
res.on('close', () => {
|
|
846
|
-
transport.close();
|
|
847
|
-
mcpServer.close();
|
|
848
|
-
});
|
|
849
|
-
}
|
|
850
|
-
catch (error) {
|
|
851
|
-
log('ERROR', 'mcp', `Error handling MCP POST request: ${error}`);
|
|
852
|
-
if (!res.headersSent) {
|
|
853
|
-
res.status(500).json({
|
|
854
|
-
jsonrpc: '2.0',
|
|
855
|
-
error: { code: -32603, message: 'Internal server error' },
|
|
856
|
-
id: null,
|
|
857
|
-
});
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
});
|
|
861
|
-
app.get('/mcp', (_req, res) => {
|
|
862
|
-
res.writeHead(405).end(JSON.stringify({
|
|
863
|
-
jsonrpc: '2.0',
|
|
864
|
-
error: { code: -32000, message: 'Method not allowed.' },
|
|
865
|
-
id: null,
|
|
866
|
-
}));
|
|
867
|
-
});
|
|
868
|
-
app.delete('/mcp', (_req, res) => {
|
|
869
|
-
res.status(200).end();
|
|
870
|
-
});
|
|
871
|
-
}
|
|
872
|
-
// ─── Terminal WebSocket ────────────────────────────────────────────────
|
|
873
|
-
const terminalWss = new ws_1.WebSocketServer({ noServer: true });
|
|
874
|
-
const ptyManager = enableTerminal ? terminal_js_1.PtyManager.getInstance() : null;
|
|
875
|
-
if (ptyManager && !ptyManager.isAvailable()) {
|
|
876
|
-
log('WARN', 'server', 'node-pty not available — terminal features disabled');
|
|
877
|
-
}
|
|
878
|
-
terminalWss.on('connection', (ws) => {
|
|
879
|
-
if (!ptyManager)
|
|
880
|
-
return;
|
|
881
|
-
log('INFO', 'terminal-ws', 'Client connected');
|
|
882
|
-
ptyManager.addClient(ws);
|
|
883
|
-
if (!ptyManager.isAvailable()) {
|
|
884
|
-
ws.send(JSON.stringify({ type: 'unavailable', reason: 'node-pty is not installed' }));
|
|
885
|
-
}
|
|
886
|
-
ws.on('message', (raw) => {
|
|
887
|
-
try {
|
|
888
|
-
const msg = JSON.parse(typeof raw === 'string' ? raw : raw.toString());
|
|
889
|
-
switch (msg.type) {
|
|
890
|
-
case 'input':
|
|
891
|
-
ptyManager.write(msg.data);
|
|
892
|
-
break;
|
|
893
|
-
case 'resize':
|
|
894
|
-
ptyManager.resize(msg.cols, msg.rows);
|
|
895
|
-
break;
|
|
896
|
-
case 'spawn':
|
|
897
|
-
try {
|
|
898
|
-
ptyManager.spawn({
|
|
899
|
-
skipPermissions: !!msg.skipPermissions,
|
|
900
|
-
cwd: projectCwd,
|
|
901
|
-
cols: msg.cols,
|
|
902
|
-
rows: msg.rows,
|
|
903
|
-
});
|
|
904
|
-
}
|
|
905
|
-
catch (err) {
|
|
906
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
907
|
-
ws.send(JSON.stringify({ type: 'output', data: `\r\n\x1b[31mFailed to start terminal: ${errMsg}\x1b[0m\r\n` }));
|
|
908
|
-
}
|
|
909
|
-
break;
|
|
910
|
-
case 'kill':
|
|
911
|
-
ptyManager.kill();
|
|
912
|
-
break;
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
catch (err) {
|
|
916
|
-
log('ERROR', 'terminal-ws', `Message handling error: ${err.message}`);
|
|
917
|
-
}
|
|
918
|
-
});
|
|
919
|
-
ws.on('close', () => {
|
|
920
|
-
log('INFO', 'terminal-ws', 'Client disconnected');
|
|
921
|
-
ptyManager.removeClient(ws);
|
|
922
|
-
});
|
|
923
|
-
ws.on('error', (err) => {
|
|
924
|
-
log('ERROR', 'terminal-ws', `Client error: ${err.message}`);
|
|
925
|
-
});
|
|
926
|
-
});
|
|
927
|
-
// ─── HTTP Server ───────────────────────────────────────────────────────
|
|
928
|
-
const server = (0, node_http_1.createServer)(app);
|
|
929
|
-
// WebSocket upgrade routing
|
|
930
|
-
server.on('upgrade', (req, socket, head) => {
|
|
931
|
-
const url = req.url || '/';
|
|
932
|
-
if (url === '/ws/terminal' || url.startsWith('/ws/terminal?')) {
|
|
933
|
-
terminalWss.handleUpgrade(req, socket, head, (ws) => {
|
|
934
|
-
terminalWss.emit('connection', ws, req);
|
|
935
|
-
});
|
|
936
|
-
}
|
|
937
|
-
else if (url === '/api/ws' || url.startsWith('/api/ws?')) {
|
|
938
|
-
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
939
|
-
wss.emit('connection', ws, req);
|
|
940
|
-
});
|
|
941
|
-
}
|
|
942
|
-
else {
|
|
943
|
-
socket.destroy();
|
|
944
|
-
}
|
|
945
|
-
});
|
|
946
|
-
// ─── Lifecycle methods ─────────────────────────────────────────────────
|
|
947
|
-
async function start() {
|
|
948
|
-
const port = await (0, detect_port_1.default)(config.port);
|
|
949
|
-
resolvedPort = port;
|
|
950
|
-
await setupWatcher();
|
|
951
|
-
return new Promise((resolve) => {
|
|
952
|
-
server.listen(port, host, () => {
|
|
953
|
-
serverReady = true;
|
|
954
|
-
log('INFO', 'server', `Backend ready on ${host}:${port} for ${projectCwd}`);
|
|
955
|
-
if (enableMcp) {
|
|
956
|
-
log('INFO', 'mcp', `MCP endpoint available at http://127.0.0.1:${port}/mcp`);
|
|
957
|
-
}
|
|
958
|
-
resolve();
|
|
959
|
-
});
|
|
960
|
-
});
|
|
961
|
-
}
|
|
962
|
-
async function stop() {
|
|
963
|
-
log('INFO', 'server', 'Shutting down...');
|
|
964
|
-
clearInterval(cleanupInterval);
|
|
965
|
-
if (ptyManager) {
|
|
966
|
-
ptyManager.kill();
|
|
967
|
-
}
|
|
968
|
-
if (watcher) {
|
|
969
|
-
await watcher.close().catch(() => { });
|
|
970
|
-
}
|
|
971
|
-
terminalWss.close(() => { });
|
|
972
|
-
wss.close(() => { });
|
|
973
|
-
return new Promise((resolve) => {
|
|
974
|
-
server.close(() => {
|
|
975
|
-
log('INFO', 'server', 'Server closed');
|
|
976
|
-
resolve();
|
|
977
|
-
});
|
|
978
|
-
});
|
|
979
|
-
}
|
|
980
|
-
shutdownFn = () => {
|
|
981
|
-
stop().then(() => process.exit(0)).catch(() => process.exit(1));
|
|
982
|
-
};
|
|
983
|
-
function getStatus() {
|
|
984
|
-
return {
|
|
985
|
-
status: serverReady ? 'ok' : 'starting',
|
|
986
|
-
ready: serverReady,
|
|
987
|
-
port: resolvedPort,
|
|
988
|
-
cwd: projectCwd,
|
|
989
|
-
uptime: (Date.now() - startTime) / 1000,
|
|
990
|
-
pid: process.pid,
|
|
991
|
-
mcpEndpoint: enableMcp ? `http://127.0.0.1:${resolvedPort}/mcp` : null,
|
|
992
|
-
terminalAvailable: ptyManager?.isAvailable() ?? false,
|
|
993
|
-
connectedClients: clientCount,
|
|
994
|
-
};
|
|
995
|
-
}
|
|
996
|
-
function getPort() {
|
|
997
|
-
return resolvedPort;
|
|
998
|
-
}
|
|
999
|
-
return { start, stop, getStatus, getPort };
|
|
1000
|
-
}
|
|
1001
|
-
// ─── Utility ────────────────────────────────────────────────────────────────
|
|
1002
|
-
function getLocalNetworkIp() {
|
|
1003
|
-
const ifaces = os.networkInterfaces();
|
|
1004
|
-
for (const iface of Object.values(ifaces)) {
|
|
1005
|
-
for (const info of iface ?? []) {
|
|
1006
|
-
if (info.family === 'IPv4' && !info.internal) {
|
|
1007
|
-
return info.address;
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
return null;
|
|
1012
|
-
}
|
|
1013
|
-
//# sourceMappingURL=server.js.map
|