@fenglimg/fabric-server 1.7.0 → 1.8.0-rc.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/{chunk-PTFSYO4Y.js → chunk-E3BHIUIW.js} +986 -166
- package/dist/{http-6LFZLHCN.js → http-MEFXOG3L.js} +82 -83
- package/dist/index.d.ts +147 -3
- package/dist/index.js +329 -140
- package/dist/static/assets/index-C-ba4ih0.js +10 -0
- package/dist/static/index.html +1 -1
- package/package.json +10 -4
- package/dist/static/assets/index-DEc8gkD_.js +0 -10
package/dist/index.js
CHANGED
|
@@ -3,180 +3,290 @@ import {
|
|
|
3
3
|
EVENT_LEDGER_PATH,
|
|
4
4
|
LEDGER_PATH,
|
|
5
5
|
LEGACY_LEDGER_PATH,
|
|
6
|
-
RULE_SECTION_NAMES,
|
|
7
6
|
buildRuleMeta,
|
|
8
7
|
computeRuleTestIndex,
|
|
9
8
|
computeRulesBasedAgentsMeta,
|
|
10
9
|
deriveRuleMetaLayer,
|
|
11
10
|
deriveRuleMetaTopologyType,
|
|
11
|
+
ensureRulesFresh,
|
|
12
|
+
flushAndSyncEventLedger,
|
|
12
13
|
getEventLedgerPath,
|
|
13
14
|
getLedgerPath,
|
|
14
15
|
getLegacyLedgerPath,
|
|
15
16
|
getRuleSections,
|
|
16
17
|
isSameRuleTestIndex,
|
|
17
18
|
planContext,
|
|
19
|
+
reconcileRules,
|
|
18
20
|
resolveProjectRoot,
|
|
19
21
|
runDoctorFix,
|
|
20
22
|
runDoctorReport,
|
|
21
23
|
stableStringify,
|
|
22
24
|
writeRuleMeta
|
|
23
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-E3BHIUIW.js";
|
|
24
26
|
|
|
25
27
|
// src/index.ts
|
|
28
|
+
import { existsSync as existsSync2 } from "fs";
|
|
26
29
|
import { readFile } from "fs/promises";
|
|
27
|
-
import { join, resolve } from "path";
|
|
30
|
+
import { join as join2, resolve } from "path";
|
|
28
31
|
import { fileURLToPath } from "url";
|
|
29
32
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
30
33
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
31
34
|
|
|
35
|
+
// src/services/in-flight-tracker.ts
|
|
36
|
+
function createInFlightTracker() {
|
|
37
|
+
const active = /* @__PURE__ */ new Map();
|
|
38
|
+
let resolveDrained = null;
|
|
39
|
+
return {
|
|
40
|
+
enter(id) {
|
|
41
|
+
active.set(id, Date.now());
|
|
42
|
+
},
|
|
43
|
+
exit(id) {
|
|
44
|
+
active.delete(id);
|
|
45
|
+
if (active.size === 0 && resolveDrained) {
|
|
46
|
+
resolveDrained();
|
|
47
|
+
resolveDrained = null;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
drain(deadlineMs) {
|
|
51
|
+
const startedWith = active.size;
|
|
52
|
+
if (startedWith === 0) return Promise.resolve({ drained: 0, timed_out: 0 });
|
|
53
|
+
return new Promise((resolve2) => {
|
|
54
|
+
const timer = setTimeout(() => {
|
|
55
|
+
const drainedCount = startedWith - active.size;
|
|
56
|
+
resolveDrained = null;
|
|
57
|
+
resolve2({ drained: drainedCount, timed_out: active.size });
|
|
58
|
+
}, deadlineMs);
|
|
59
|
+
resolveDrained = () => {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
resolve2({ drained: startedWith, timed_out: 0 });
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
size() {
|
|
66
|
+
return active.size;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
32
71
|
// src/tools/plan-context.ts
|
|
33
|
-
import {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
extension: z.string(),
|
|
62
|
-
inferred_domain: z.array(z.string()),
|
|
63
|
-
known_tech: z.array(z.string()),
|
|
64
|
-
user_intent: z.string(),
|
|
65
|
-
intent_tokens: z.array(z.string()),
|
|
66
|
-
impact_hints: z.array(z.string()),
|
|
67
|
-
detected_entities: z.array(z.string())
|
|
68
|
-
});
|
|
69
|
-
var selectionPolicySchema = z.object({
|
|
70
|
-
required_levels: z.tuple([z.literal("L0"), z.literal("L2")]),
|
|
71
|
-
ai_selectable_levels: z.tuple([z.literal("L1")]),
|
|
72
|
-
final_fetch_rule: z.literal("required_stable_ids + ai_selected_l1_stable_ids")
|
|
73
|
-
});
|
|
74
|
-
var outputSchema = z.object({
|
|
75
|
-
revision_hash: z.string(),
|
|
76
|
-
stale: z.boolean(),
|
|
77
|
-
selection_token: z.string(),
|
|
78
|
-
entries: z.array(
|
|
79
|
-
z.object({
|
|
80
|
-
path: z.string(),
|
|
81
|
-
requirement_profile: requirementProfileSchema,
|
|
82
|
-
description_index: z.array(descriptionIndexItemSchema),
|
|
83
|
-
required_stable_ids: z.array(z.string()),
|
|
84
|
-
ai_selectable_stable_ids: z.array(z.string()),
|
|
85
|
-
initial_selected_stable_ids: z.array(z.string()),
|
|
86
|
-
selection_policy: selectionPolicySchema
|
|
87
|
-
})
|
|
88
|
-
),
|
|
89
|
-
shared: z.object({
|
|
90
|
-
required_stable_ids: z.array(z.string()),
|
|
91
|
-
ai_selectable_stable_ids: z.array(z.string()),
|
|
92
|
-
description_index: z.array(descriptionIndexItemSchema),
|
|
93
|
-
preflight_diagnostics: z.array(
|
|
94
|
-
z.object({
|
|
95
|
-
code: z.literal("missing_description"),
|
|
96
|
-
severity: z.literal("warn"),
|
|
97
|
-
message: z.string(),
|
|
98
|
-
stable_ids: z.array(z.string()).optional(),
|
|
99
|
-
path: z.string().optional()
|
|
100
|
-
})
|
|
101
|
-
)
|
|
102
|
-
})
|
|
103
|
-
});
|
|
104
|
-
function registerPlanContext(server) {
|
|
72
|
+
import { randomUUID } from "crypto";
|
|
73
|
+
import {
|
|
74
|
+
planContextAnnotations,
|
|
75
|
+
planContextInputSchema,
|
|
76
|
+
planContextOutputSchema
|
|
77
|
+
} from "@fenglimg/fabric-shared/schemas/api-contracts";
|
|
78
|
+
import { enforcePayloadLimit } from "@fenglimg/fabric-shared/node/mcp-payload-guard";
|
|
79
|
+
|
|
80
|
+
// src/config-loader.ts
|
|
81
|
+
import { existsSync, readFileSync } from "fs";
|
|
82
|
+
import { join } from "path";
|
|
83
|
+
function readFabricConfig(projectRoot) {
|
|
84
|
+
const configPath = join(projectRoot, "fabric.config.json");
|
|
85
|
+
if (!existsSync(configPath)) {
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
const parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
89
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
90
|
+
throw new Error(`Expected object in ${configPath}`);
|
|
91
|
+
}
|
|
92
|
+
return parsed;
|
|
93
|
+
}
|
|
94
|
+
function readPayloadLimits(projectRoot) {
|
|
95
|
+
return readFabricConfig(projectRoot).mcpPayloadLimits;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/tools/plan-context.ts
|
|
99
|
+
function registerPlanContext(server, tracker) {
|
|
105
100
|
server.registerTool(
|
|
106
101
|
"fab_plan_context",
|
|
107
102
|
{
|
|
108
103
|
description: "Use during plan or architecture phases to build a neutral Fabric rule description index and selection token before fetching rule sections.",
|
|
109
|
-
inputSchema,
|
|
110
|
-
outputSchema,
|
|
111
|
-
annotations:
|
|
104
|
+
inputSchema: planContextInputSchema,
|
|
105
|
+
outputSchema: planContextOutputSchema,
|
|
106
|
+
annotations: planContextAnnotations
|
|
112
107
|
},
|
|
113
108
|
async ({ paths, intent, known_tech, detected_entities, client_hash, correlation_id, session_id }) => {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
109
|
+
const requestId = randomUUID();
|
|
110
|
+
tracker?.enter(requestId);
|
|
111
|
+
try {
|
|
112
|
+
const projectRoot = resolveProjectRoot();
|
|
113
|
+
const syncReport = await ensureRulesFresh(projectRoot);
|
|
114
|
+
const result = await planContext(projectRoot, {
|
|
115
|
+
paths,
|
|
116
|
+
intent,
|
|
117
|
+
known_tech,
|
|
118
|
+
detected_entities,
|
|
119
|
+
client_hash,
|
|
120
|
+
correlation_id,
|
|
121
|
+
session_id
|
|
122
|
+
});
|
|
123
|
+
const response = {
|
|
124
|
+
...result,
|
|
125
|
+
warnings: [...syncReport.warnings]
|
|
126
|
+
};
|
|
127
|
+
const payloadLimits = readPayloadLimits(projectRoot);
|
|
128
|
+
const serialized = JSON.stringify(response);
|
|
129
|
+
const guardResult = enforcePayloadLimit(serialized, payloadLimits);
|
|
130
|
+
if (guardResult.warning) {
|
|
131
|
+
response.warnings = [
|
|
132
|
+
...response.warnings,
|
|
133
|
+
{
|
|
134
|
+
code: guardResult.warning.code,
|
|
135
|
+
file: "<response>",
|
|
136
|
+
action_hint: "Consider narrowing the request scope to reduce response size"
|
|
137
|
+
}
|
|
138
|
+
];
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
142
|
+
structuredContent: response
|
|
143
|
+
};
|
|
144
|
+
} finally {
|
|
145
|
+
tracker?.exit(requestId);
|
|
146
|
+
}
|
|
128
147
|
}
|
|
129
148
|
);
|
|
130
149
|
}
|
|
131
150
|
|
|
132
151
|
// src/tools/rule-sections.ts
|
|
133
|
-
import {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
};
|
|
142
|
-
var outputSchema2 = z2.object({
|
|
143
|
-
revision_hash: z2.string(),
|
|
144
|
-
precedence: z2.tuple([z2.literal("L2"), z2.literal("L1"), z2.literal("L0")]),
|
|
145
|
-
selected_stable_ids: z2.array(z2.string()),
|
|
146
|
-
rules: z2.array(
|
|
147
|
-
z2.object({
|
|
148
|
-
stable_id: z2.string(),
|
|
149
|
-
level: z2.enum(["L0", "L1", "L2"]),
|
|
150
|
-
path: z2.string(),
|
|
151
|
-
sections: z2.record(z2.string())
|
|
152
|
-
})
|
|
153
|
-
),
|
|
154
|
-
diagnostics: z2.array(
|
|
155
|
-
z2.object({
|
|
156
|
-
code: z2.literal("missing_section"),
|
|
157
|
-
severity: z2.literal("warn"),
|
|
158
|
-
stable_id: z2.string(),
|
|
159
|
-
section: z2.enum(RULE_SECTION_NAMES),
|
|
160
|
-
message: z2.string()
|
|
161
|
-
})
|
|
162
|
-
)
|
|
163
|
-
});
|
|
164
|
-
function registerRuleSections(server) {
|
|
152
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
153
|
+
import {
|
|
154
|
+
ruleSectionsAnnotations,
|
|
155
|
+
ruleSectionsInputSchema,
|
|
156
|
+
ruleSectionsOutputSchema
|
|
157
|
+
} from "@fenglimg/fabric-shared/schemas/api-contracts";
|
|
158
|
+
import { enforcePayloadLimit as enforcePayloadLimit2 } from "@fenglimg/fabric-shared/node/mcp-payload-guard";
|
|
159
|
+
function registerRuleSections(server, tracker) {
|
|
165
160
|
server.registerTool(
|
|
166
161
|
"fab_get_rule_sections",
|
|
167
162
|
{
|
|
168
163
|
description: "Fetch structured Fabric rule sections after fab_plan_context. Required L0/L2 rules are merged with AI-selected L1 rules server-side.",
|
|
169
|
-
inputSchema:
|
|
170
|
-
outputSchema:
|
|
171
|
-
annotations:
|
|
164
|
+
inputSchema: ruleSectionsInputSchema,
|
|
165
|
+
outputSchema: ruleSectionsOutputSchema,
|
|
166
|
+
annotations: ruleSectionsAnnotations
|
|
172
167
|
},
|
|
173
168
|
async (input) => {
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
169
|
+
const requestId = randomUUID2();
|
|
170
|
+
tracker?.enter(requestId);
|
|
171
|
+
try {
|
|
172
|
+
const projectRoot = resolveProjectRoot();
|
|
173
|
+
const syncReport = await ensureRulesFresh(projectRoot);
|
|
174
|
+
const result = await getRuleSections(projectRoot, input);
|
|
175
|
+
const response = {
|
|
176
|
+
...result,
|
|
177
|
+
warnings: [...syncReport.warnings]
|
|
178
|
+
};
|
|
179
|
+
const payloadLimits = readPayloadLimits(projectRoot);
|
|
180
|
+
const serialized = JSON.stringify(response);
|
|
181
|
+
const guardResult = enforcePayloadLimit2(serialized, payloadLimits);
|
|
182
|
+
if (guardResult.warning) {
|
|
183
|
+
response.warnings = [
|
|
184
|
+
...response.warnings,
|
|
185
|
+
{
|
|
186
|
+
code: guardResult.warning.code,
|
|
187
|
+
file: "<response>",
|
|
188
|
+
action_hint: "Consider narrowing the request scope to reduce response size"
|
|
189
|
+
}
|
|
190
|
+
];
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
194
|
+
structuredContent: response
|
|
195
|
+
};
|
|
196
|
+
} finally {
|
|
197
|
+
tracker?.exit(requestId);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/services/serve-lock.ts
|
|
204
|
+
import fs from "fs";
|
|
205
|
+
import path from "path";
|
|
206
|
+
import { IOFabricError } from "@fenglimg/fabric-shared/errors";
|
|
207
|
+
var LOCK_FILENAME = ".serve.lock";
|
|
208
|
+
var ServeLockHeldError = class extends IOFabricError {
|
|
209
|
+
code = "SERVE_LOCK_HELD";
|
|
210
|
+
httpStatus = 423;
|
|
211
|
+
};
|
|
212
|
+
function lockPath(projectRoot) {
|
|
213
|
+
return path.join(projectRoot, ".fabric", LOCK_FILENAME);
|
|
214
|
+
}
|
|
215
|
+
function isAlive(pid) {
|
|
216
|
+
try {
|
|
217
|
+
process.kill(pid, 0);
|
|
218
|
+
return true;
|
|
219
|
+
} catch (e) {
|
|
220
|
+
const err = e;
|
|
221
|
+
if (err.code === "ESRCH") return false;
|
|
222
|
+
if (err.code === "EPERM") return true;
|
|
223
|
+
throw e;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function acquireLock(projectRoot, opts) {
|
|
227
|
+
const p = lockPath(projectRoot);
|
|
228
|
+
if (fs.existsSync(p)) {
|
|
229
|
+
let state = null;
|
|
230
|
+
try {
|
|
231
|
+
state = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
232
|
+
} catch {
|
|
233
|
+
}
|
|
234
|
+
if (state && state.pid && state.pid !== process.pid && isAlive(state.pid) && !opts?.force) {
|
|
235
|
+
throw new ServeLockHeldError(
|
|
236
|
+
`serve lock held by live PID ${state.pid}`,
|
|
237
|
+
{
|
|
238
|
+
actionHint: `Stop the other process (PID ${state.pid}) or run with --force to override`,
|
|
239
|
+
details: state
|
|
240
|
+
}
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
if (state && state.pid && !isAlive(state.pid)) {
|
|
244
|
+
process.stderr.write(`[serve-lock] stale lock from PID ${state.pid} \u2014 overwriting
|
|
245
|
+
`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
249
|
+
fs.writeFileSync(
|
|
250
|
+
p,
|
|
251
|
+
JSON.stringify({ pid: process.pid, acquiredAt: Date.now(), host: process.env.HOSTNAME })
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
function releaseLock(projectRoot) {
|
|
255
|
+
const p = lockPath(projectRoot);
|
|
256
|
+
try {
|
|
257
|
+
if (fs.existsSync(p)) {
|
|
258
|
+
const state = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
259
|
+
if (state.pid === process.pid) {
|
|
260
|
+
fs.unlinkSync(p);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
} catch {
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function readLockState(projectRoot) {
|
|
267
|
+
const p = lockPath(projectRoot);
|
|
268
|
+
if (!fs.existsSync(p)) return null;
|
|
269
|
+
try {
|
|
270
|
+
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
271
|
+
} catch {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function checkLockOrThrow(projectRoot, opts) {
|
|
276
|
+
const state = readLockState(projectRoot);
|
|
277
|
+
if (state === null) return;
|
|
278
|
+
if (state.pid === process.pid) return;
|
|
279
|
+
if (!isAlive(state.pid)) {
|
|
280
|
+
process.stderr.write(`[serve-lock] stale lock from PID ${state.pid} \u2014 ignoring
|
|
281
|
+
`);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (opts?.force) return;
|
|
285
|
+
throw new ServeLockHeldError(
|
|
286
|
+
`serve lock held by live PID ${state.pid}`,
|
|
287
|
+
{
|
|
288
|
+
actionHint: `Stop the other serve process (PID ${state.pid}) before running this command, or pass --force to override`,
|
|
289
|
+
details: state
|
|
180
290
|
}
|
|
181
291
|
);
|
|
182
292
|
}
|
|
@@ -192,13 +302,20 @@ function formatError(error) {
|
|
|
192
302
|
}
|
|
193
303
|
return `Unknown error: ${String(error)}`;
|
|
194
304
|
}
|
|
195
|
-
function
|
|
305
|
+
function formatPreexistingRootMessage(projectRoot) {
|
|
306
|
+
const preexisting = [];
|
|
307
|
+
if (existsSync2(join2(projectRoot, "CLAUDE.md"))) preexisting.push("CLAUDE.md");
|
|
308
|
+
if (existsSync2(join2(projectRoot, "AGENTS.md"))) preexisting.push("AGENTS.md");
|
|
309
|
+
if (preexisting.length === 0) return null;
|
|
310
|
+
return `[startup] info: detected ${preexisting.join(", ")} at project root. Note: Fabric serves rules from .fabric/rules/ via MCP \u2014 root markdown files are not auto-loaded into the AI context.`;
|
|
311
|
+
}
|
|
312
|
+
function createFabricServer(tracker) {
|
|
196
313
|
const server = new McpServer({
|
|
197
314
|
name: "fabric-context-server",
|
|
198
|
-
version: "1.
|
|
315
|
+
version: "1.8.0-rc.1"
|
|
199
316
|
});
|
|
200
|
-
registerPlanContext(server);
|
|
201
|
-
registerRuleSections(server);
|
|
317
|
+
registerPlanContext(server, tracker);
|
|
318
|
+
registerRuleSections(server, tracker);
|
|
202
319
|
server.registerResource(
|
|
203
320
|
"bootstrap README",
|
|
204
321
|
AGENTS_MD_RESOURCE_URI,
|
|
@@ -208,7 +325,7 @@ function createFabricServer() {
|
|
|
208
325
|
},
|
|
209
326
|
async (_uri) => {
|
|
210
327
|
const projectRoot = process.env.FABRIC_PROJECT_ROOT ?? process.cwd();
|
|
211
|
-
const content = await readFile(
|
|
328
|
+
const content = await readFile(join2(projectRoot, ".fabric", "bootstrap", "README.md"), "utf8");
|
|
212
329
|
return {
|
|
213
330
|
contents: [
|
|
214
331
|
{
|
|
@@ -223,12 +340,73 @@ function createFabricServer() {
|
|
|
223
340
|
return server;
|
|
224
341
|
}
|
|
225
342
|
async function startStdioServer() {
|
|
226
|
-
const
|
|
343
|
+
const tracker = createInFlightTracker();
|
|
344
|
+
const projectRoot = resolveProjectRoot();
|
|
345
|
+
const syncStart = Date.now();
|
|
346
|
+
const reconcileResult = await reconcileRules(projectRoot, { trigger: "startup" });
|
|
347
|
+
const syncDurationMs = Date.now() - syncStart;
|
|
348
|
+
process.stderr.write(
|
|
349
|
+
`[startup] rule sync: status=${reconcileResult.status}, events=${reconcileResult.events.length}, ${syncDurationMs}ms
|
|
350
|
+
`
|
|
351
|
+
);
|
|
352
|
+
const rootMsg = formatPreexistingRootMessage(projectRoot);
|
|
353
|
+
if (rootMsg !== null) {
|
|
354
|
+
process.stderr.write(`${rootMsg}
|
|
355
|
+
`);
|
|
356
|
+
}
|
|
357
|
+
const server = createFabricServer(tracker);
|
|
227
358
|
const transport = new StdioServerTransport();
|
|
228
359
|
await server.connect(transport);
|
|
360
|
+
const closeServer = async () => {
|
|
361
|
+
await server.close();
|
|
362
|
+
};
|
|
363
|
+
process.on(
|
|
364
|
+
"SIGINT",
|
|
365
|
+
createShutdownHandler({ signal: "SIGINT", tracker, projectRoot, closeServer })
|
|
366
|
+
);
|
|
367
|
+
process.on(
|
|
368
|
+
"SIGTERM",
|
|
369
|
+
createShutdownHandler({ signal: "SIGTERM", tracker, projectRoot, closeServer })
|
|
370
|
+
);
|
|
371
|
+
process.on(
|
|
372
|
+
"SIGHUP",
|
|
373
|
+
createShutdownHandler({ signal: "SIGHUP", tracker, projectRoot, closeServer })
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
function createShutdownHandler(deps) {
|
|
377
|
+
const exit = deps.exit ?? ((code) => process.exit(code));
|
|
378
|
+
const deadlineMs = deps.drainDeadlineMs ?? 5e3;
|
|
379
|
+
let invoked = false;
|
|
380
|
+
return () => {
|
|
381
|
+
void (async () => {
|
|
382
|
+
if (invoked) {
|
|
383
|
+
process.stderr.write(`
|
|
384
|
+
[shutdown] ${deps.signal} repeated \u2014 forcing exit(1)
|
|
385
|
+
`);
|
|
386
|
+
exit(1);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
invoked = true;
|
|
390
|
+
process.stderr.write(
|
|
391
|
+
`
|
|
392
|
+
[shutdown] ${deps.signal} received \u2014 draining ${deps.tracker.size()} requests (${deadlineMs / 1e3}s deadline)
|
|
393
|
+
`
|
|
394
|
+
);
|
|
395
|
+
const result = await deps.tracker.drain(deadlineMs);
|
|
396
|
+
process.stderr.write(`[shutdown] drained ${result.drained}, timed_out ${result.timed_out}
|
|
397
|
+
`);
|
|
398
|
+
flushAndSyncEventLedger(deps.projectRoot);
|
|
399
|
+
process.stderr.write("[shutdown] ledger fsynced; closing server\n");
|
|
400
|
+
try {
|
|
401
|
+
await deps.closeServer();
|
|
402
|
+
} catch {
|
|
403
|
+
}
|
|
404
|
+
exit(0);
|
|
405
|
+
})();
|
|
406
|
+
};
|
|
229
407
|
}
|
|
230
408
|
async function startHttpServer(options) {
|
|
231
|
-
const { createFabricHttpApp } = await import("./http-
|
|
409
|
+
const { createFabricHttpApp } = await import("./http-MEFXOG3L.js");
|
|
232
410
|
const { port, projectRoot, host = "127.0.0.1", authToken, dashboardDistPath, dev } = options;
|
|
233
411
|
const app = createFabricHttpApp({ projectRoot, host, authToken, dashboardDistPath, dev });
|
|
234
412
|
return await new Promise((resolveServer, rejectServer) => {
|
|
@@ -258,16 +436,27 @@ export {
|
|
|
258
436
|
EVENT_LEDGER_PATH,
|
|
259
437
|
LEDGER_PATH,
|
|
260
438
|
LEGACY_LEDGER_PATH,
|
|
439
|
+
ServeLockHeldError,
|
|
440
|
+
acquireLock,
|
|
261
441
|
buildRuleMeta,
|
|
442
|
+
checkLockOrThrow,
|
|
262
443
|
computeRuleTestIndex,
|
|
263
444
|
computeRulesBasedAgentsMeta,
|
|
264
445
|
createFabricServer,
|
|
446
|
+
createInFlightTracker,
|
|
447
|
+
createShutdownHandler,
|
|
265
448
|
deriveRuleMetaLayer,
|
|
266
449
|
deriveRuleMetaTopologyType,
|
|
450
|
+
ensureRulesFresh,
|
|
451
|
+
flushAndSyncEventLedger,
|
|
452
|
+
formatPreexistingRootMessage,
|
|
267
453
|
getEventLedgerPath,
|
|
268
454
|
getLedgerPath,
|
|
269
455
|
getLegacyLedgerPath,
|
|
270
456
|
isSameRuleTestIndex,
|
|
457
|
+
readLockState,
|
|
458
|
+
reconcileRules,
|
|
459
|
+
releaseLock,
|
|
271
460
|
runDoctorFix,
|
|
272
461
|
runDoctorReport,
|
|
273
462
|
stableStringify,
|