fuli-context 0.7.4-beta.3 → 0.7.5
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/README.md +77 -6
- package/README.zh-CN.md +69 -6
- package/compose.graphiti.yml +6 -6
- package/dist/web/assets/AboutPage-ByCUor-j.js +1 -0
- package/dist/web/assets/ConnectionsPage-DkdkZ40Q.js +1 -0
- package/dist/web/assets/KnowledgeEditDialog-DSe3Gwep.js +1 -0
- package/dist/web/assets/KnowledgeInspector-CYAjAb9J.js +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-CV4F54R_.js +1 -0
- package/dist/web/assets/KnowledgePage-vYYXipOi.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-z4u5mXOG.js +4 -0
- package/dist/web/assets/OverviewPage-BWOfeS6k.js +1 -0
- package/dist/web/assets/PersonalProfilePage-c3w4-h7a.js +1 -0
- package/dist/web/assets/PersonalProjectsPage-B7AlLEwf.js +1 -0
- package/dist/web/assets/ProjectAgentsPage-CgftpIPB.css +1 -0
- package/dist/web/assets/ProjectAgentsPage-DTHcL_ox.js +11 -0
- package/dist/web/assets/PublicProjectsPage-DdH0oGB3.js +1 -0
- package/dist/web/assets/ReviewPage-BYIpcIHG.js +1 -0
- package/dist/web/assets/SearchableSelect-Dz13ClQb.js +1 -0
- package/dist/web/assets/SettingsPage-2VkbXgZi.js +1 -0
- package/dist/web/assets/SettingsPage-CwZp2ka4.css +1 -0
- package/dist/web/assets/WritingTastePage-BOPQZuva.js +1 -0
- package/dist/web/assets/{console-D_2zC4lm.js → console-BlAhujsH.js} +1 -1
- package/dist/web/assets/{conversation-launcher-settings-DQmMH2j7.js → conversation-launcher-settings-DTYxiLJt.js} +1 -1
- package/dist/web/assets/i18n-Ci3heuiL.js +9 -0
- package/dist/web/assets/{identity-DmGcY3Y6.js → identity-4_-v6cB7.js} +1 -1
- package/dist/web/assets/index-BnH5mcZx.css +1 -0
- package/dist/web/assets/index-C67Q0JY2.js +2 -0
- package/dist/web/assets/{model-CVoOdRsL.js → model-yG0YqUyZ.js} +1 -1
- package/dist/web/assets/{useApi-CROJJdhE-CBNg4CdH.js → useApi-CROJJdhE-twhdZArA.js} +1 -1
- package/dist/web/assets/vue-router-C-wot_Di.js +1 -0
- package/dist/web/index.html +6 -6
- package/graph-provider/fuli_graph/app.py +8 -0
- package/graph-provider/fuli_graph/collaboration_context.py +69 -6
- package/graph-provider/fuli_graph/knowledge_search.py +84 -5
- package/graph-provider/fuli_graph/model_validation.py +130 -0
- package/graph-provider/fuli_graph/models.py +24 -5
- package/graph-provider/fuli_graph/project_agent_access.py +54 -0
- package/graph-provider/fuli_graph/project_agent_executor_models.py +635 -0
- package/graph-provider/fuli_graph/project_agent_models.py +278 -0
- package/graph-provider/fuli_graph/project_agent_routes.py +597 -0
- package/graph-provider/fuli_graph/project_agent_task_models.py +542 -0
- package/graph-provider/fuli_graph/runtime.py +259 -0
- package/graph-provider/fuli_graph/search_projection.py +3 -0
- package/graph-provider/fuli_graph/store.py +6 -0
- package/graph-provider/fuli_graph/store_knowledge.py +62 -9
- package/graph-provider/fuli_graph/store_project_agent_executor_learning.py +756 -0
- package/graph-provider/fuli_graph/store_project_agent_executor_routing.py +601 -0
- package/graph-provider/fuli_graph/store_project_agent_executors.py +1087 -0
- package/graph-provider/fuli_graph/store_project_agent_task_reads.py +458 -0
- package/graph-provider/fuli_graph/store_project_agent_task_recruitment.py +699 -0
- package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +409 -0
- package/graph-provider/fuli_graph/store_project_agent_tasks.py +1144 -0
- package/graph-provider/fuli_graph/store_project_agents.py +965 -0
- package/graph-provider/pyproject.toml +2 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/capturing-session-knowledge/SKILL.md +39 -0
- package/src/adaptive-runtime/executor-pool.js +127 -0
- package/src/adaptive-runtime/graph-services.js +166 -0
- package/src/adaptive-runtime/lease-client.js +335 -0
- package/src/adaptive-runtime/runtime-broker.js +318 -0
- package/src/adaptive-runtime/settings.js +111 -0
- package/src/adaptive-runtime/state.js +74 -0
- package/src/agent-tools/graph-definitions.js +396 -0
- package/src/agent-tools/graph-handlers.js +61 -0
- package/src/agent-tools/project-agent-definitions.js +307 -0
- package/src/backup/graph-backup.js +289 -0
- package/src/backup/identity-reconciliation.js +208 -0
- package/src/backup/runtime-adapters.js +269 -0
- package/src/cli/command-registry.js +6 -2
- package/src/cli/graph-data-command.js +112 -0
- package/src/cli/local-runtime-command.js +16 -2
- package/src/cli/setup-command.js +19 -3
- package/src/cli/update-command.js +4 -0
- package/src/cli.js +6 -0
- package/src/graphiti/collaboration-preference-projection.js +29 -1
- package/src/graphiti/collaboration-preference-workflow.js +121 -0
- package/src/graphiti/federated-application.js +53 -90
- package/src/graphiti/project-agent-control-plane.js +263 -0
- package/src/graphiti/project-agent-mapping.js +1003 -0
- package/src/graphiti/project-agent-workflows.js +557 -0
- package/src/graphiti/provider-client.js +270 -1
- package/src/http/api-router.js +63 -9
- package/src/http/graph-api-router.js +278 -0
- package/src/local-runtime/lifecycle.js +46 -10
- package/src/mcp/context-resources.js +15 -6
- package/src/mcp/create-mcp-server.js +51 -16
- package/src/mcp/instructions.js +7 -5
- package/src/mcp/runtime.js +17 -5
- package/src/mcp/session-id.js +38 -1
- package/src/mcp/tool-annotations.js +69 -0
- package/src/mcp-server.js +23 -6
- package/src/native-runtime/runtime.js +717 -0
- package/src/server.js +6 -2
- package/src/setup/agent-installation-status.js +7 -1
- package/src/setup/agents.js +3 -1
- package/src/setup/claude-code-config.js +3 -1
- package/src/setup/codex-bootstrap.js +11 -10
- package/src/setup/codex-config.js +7 -1
- package/src/setup/cursor-config.js +3 -1
- package/src/setup/graph-runtime.js +158 -13
- package/src/setup/neo4j-memory-profile.js +43 -0
- package/src/setup/options.js +38 -2
- package/src/setup/paths.js +14 -0
- package/src/setup/setup.js +37 -1
- package/src/system/resource-monitor.js +181 -16
- package/src/system/runtime-settings.js +10 -1
- package/src/system/system-service.js +27 -2
- package/dist/web/assets/AboutPage-Dpf5fRWv.js +0 -1
- package/dist/web/assets/ConnectionsPage-D21aUDCP.js +0 -1
- package/dist/web/assets/KnowledgeEditDialog-u7lpfN3j.js +0 -1
- package/dist/web/assets/KnowledgeInspector-k4xnHtXd.js +0 -1
- package/dist/web/assets/KnowledgeOrganizerPage-Bdj7Rn84.js +0 -1
- package/dist/web/assets/KnowledgePage-UHVAbC1W.js +0 -1
- package/dist/web/assets/KnowledgeWorkspace-Lq3d-MeC.js +0 -4
- package/dist/web/assets/OverviewPage-CycAgA1V.js +0 -1
- package/dist/web/assets/PersonalProfilePage-U9oIV5FT.js +0 -1
- package/dist/web/assets/PersonalProjectsPage-C5RpXKcA.js +0 -1
- package/dist/web/assets/PublicProjectsPage-DiWxm35E.js +0 -1
- package/dist/web/assets/ReviewPage-jxRfGSJK.js +0 -1
- package/dist/web/assets/SearchableSelect-B3-5IPup.js +0 -1
- package/dist/web/assets/SettingsPage-BqfGDOho.js +0 -1
- package/dist/web/assets/SettingsPage-CAfz4qiV.css +0 -1
- package/dist/web/assets/WritingTastePage-BJxhMqJR.js +0 -1
- package/dist/web/assets/i18n-CwqXDI1C.js +0 -5
- package/dist/web/assets/index-IUGLt4lG.js +0 -2
- package/dist/web/assets/index-X2j29j8i.css +0 -1
- package/dist/web/assets/vue-router-CWSsUUio.js +0 -1
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
import { execFile, spawn } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import {
|
|
4
|
+
chmodSync,
|
|
5
|
+
closeSync,
|
|
6
|
+
createReadStream,
|
|
7
|
+
createWriteStream,
|
|
8
|
+
existsSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
openSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
unlinkSync
|
|
13
|
+
} from 'node:fs';
|
|
14
|
+
import {
|
|
15
|
+
mkdir,
|
|
16
|
+
readFile,
|
|
17
|
+
rename,
|
|
18
|
+
rm,
|
|
19
|
+
stat,
|
|
20
|
+
writeFile
|
|
21
|
+
} from 'node:fs/promises';
|
|
22
|
+
import { Readable } from 'node:stream';
|
|
23
|
+
import { pipeline } from 'node:stream/promises';
|
|
24
|
+
import { createConnection } from 'node:net';
|
|
25
|
+
import { dirname, join } from 'node:path';
|
|
26
|
+
|
|
27
|
+
import { FULI_VERSION } from '../package-metadata.js';
|
|
28
|
+
import { readJsonFile, writeJsonFileAtomic } from '../storage/json-file.js';
|
|
29
|
+
import {
|
|
30
|
+
DEFAULT_RUNTIME_SETTINGS,
|
|
31
|
+
managedProviderUrls,
|
|
32
|
+
readRuntimeSettings
|
|
33
|
+
} from '../system/runtime-settings.js';
|
|
34
|
+
|
|
35
|
+
export const NATIVE_NEO4J_VERSION = '5.26.28';
|
|
36
|
+
export const NATIVE_NEO4J_SHA256 =
|
|
37
|
+
'9d4064cdd87627cae376a741c893848c4faa3c4fb980362b6dae541c203e8072';
|
|
38
|
+
export const NATIVE_NEO4J_URL =
|
|
39
|
+
`https://dist.neo4j.org/neo4j-community-${NATIVE_NEO4J_VERSION}-unix.tar.gz`;
|
|
40
|
+
|
|
41
|
+
const MANIFEST_VERSION = 1;
|
|
42
|
+
const PROVIDER_RUNTIME_REVISION = 2;
|
|
43
|
+
const COMMAND_TIMEOUT_MS = 10 * 60_000;
|
|
44
|
+
const DOWNLOAD_TIMEOUT_MS = 30 * 60_000;
|
|
45
|
+
|
|
46
|
+
export function createNativeGraphServices({
|
|
47
|
+
paths,
|
|
48
|
+
runtimeDescriptor = null,
|
|
49
|
+
personalOnly = null,
|
|
50
|
+
readText = (path) => readFile(path, 'utf8'),
|
|
51
|
+
writeText = secureWriteText,
|
|
52
|
+
pathExists = existsSync,
|
|
53
|
+
markInitialized = secureMarker,
|
|
54
|
+
readJson = (path) => readJsonFile(path, null),
|
|
55
|
+
readSettings = readRuntimeSettings,
|
|
56
|
+
run = runCommand,
|
|
57
|
+
waitForDatabase = waitForTcp,
|
|
58
|
+
spawnDatabase = spawnNativeDatabase,
|
|
59
|
+
spawnProvider = spawnNativeProvider,
|
|
60
|
+
readProcessState = (path) => readJsonFile(path, null),
|
|
61
|
+
writeProcessState = writeJsonFileAtomic,
|
|
62
|
+
processMatches = processMatchesCommand,
|
|
63
|
+
stopProcess = stopOwnedProcess,
|
|
64
|
+
fetchImpl = globalThis.fetch,
|
|
65
|
+
wait = delay,
|
|
66
|
+
now = () => new Date()
|
|
67
|
+
}) {
|
|
68
|
+
const descriptor = runtimeDescriptor ?? readJson(paths.nativeRuntimeManifestPath);
|
|
69
|
+
|
|
70
|
+
async function start({ providers: startProviders = true } = {}) {
|
|
71
|
+
assertDescriptor(descriptor);
|
|
72
|
+
const specs = await serviceSpecs();
|
|
73
|
+
const previousState = readProcessState(paths.nativeProcessStatePath) ?? {};
|
|
74
|
+
const databases = {};
|
|
75
|
+
const providers = {};
|
|
76
|
+
try {
|
|
77
|
+
for (const spec of specs) {
|
|
78
|
+
await prepareInstance(spec);
|
|
79
|
+
const existing = previousState.databases?.[spec.id];
|
|
80
|
+
if (
|
|
81
|
+
Number.isInteger(existing?.pid) &&
|
|
82
|
+
typeof existing.command === 'string' &&
|
|
83
|
+
await processMatches(existing.pid, existing.command)
|
|
84
|
+
) {
|
|
85
|
+
databases[spec.id] = existing;
|
|
86
|
+
} else {
|
|
87
|
+
const child = spawnDatabase(databaseSpec(spec));
|
|
88
|
+
if (!Number.isInteger(child?.pid)) {
|
|
89
|
+
throw new Error(`The native ${spec.id} Neo4j process did not return a process ID.`);
|
|
90
|
+
}
|
|
91
|
+
databases[spec.id] = {
|
|
92
|
+
pid: child.pid,
|
|
93
|
+
command: descriptor.neo4jHome,
|
|
94
|
+
startedAt: now().toISOString()
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
persistProcessState(databases, providers);
|
|
98
|
+
await waitForDatabase('127.0.0.1', spec.boltPort);
|
|
99
|
+
}
|
|
100
|
+
if (!startProviders) {
|
|
101
|
+
persistProcessState(databases, providers);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
for (const spec of specs) {
|
|
105
|
+
const existing = previousState.providers?.[spec.id];
|
|
106
|
+
if (
|
|
107
|
+
Number.isInteger(existing?.pid) &&
|
|
108
|
+
typeof existing.command === 'string' &&
|
|
109
|
+
await processMatches(existing.pid, existing.command)
|
|
110
|
+
) {
|
|
111
|
+
providers[spec.id] = existing;
|
|
112
|
+
} else {
|
|
113
|
+
const child = spawnProvider(providerSpec(spec));
|
|
114
|
+
if (!Number.isInteger(child?.pid)) {
|
|
115
|
+
throw new Error(`The native ${spec.id} Provider did not return a process ID.`);
|
|
116
|
+
}
|
|
117
|
+
providers[spec.id] = {
|
|
118
|
+
pid: child.pid,
|
|
119
|
+
command: descriptor.providerPython,
|
|
120
|
+
startedAt: now().toISOString()
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
persistProcessState(databases, providers);
|
|
125
|
+
for (const spec of specs) await waitForProvider(spec.providerUrl, fetchImpl, wait);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
await stopProviderEntries(providers);
|
|
128
|
+
await stopProviderEntries(databases);
|
|
129
|
+
persistProcessState({}, {});
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function stopProviders() {
|
|
135
|
+
const state = readProcessState(paths.nativeProcessStatePath) ?? {};
|
|
136
|
+
await stopProviderEntries(state.providers ?? {});
|
|
137
|
+
writeProcessState(paths.nativeProcessStatePath, {
|
|
138
|
+
...state,
|
|
139
|
+
version: 1,
|
|
140
|
+
mode: 'native',
|
|
141
|
+
databases: state.databases ?? {},
|
|
142
|
+
providers: {},
|
|
143
|
+
updatedAt: now().toISOString()
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function stopDatabases() {
|
|
148
|
+
await stopProviders();
|
|
149
|
+
if (!descriptor) return;
|
|
150
|
+
const state = readProcessState(paths.nativeProcessStatePath) ?? {};
|
|
151
|
+
if (Object.keys(state.databases ?? {}).length > 0) {
|
|
152
|
+
await stopProviderEntries(state.databases);
|
|
153
|
+
} else {
|
|
154
|
+
// Compatibility with native runtimes created before database PIDs were recorded.
|
|
155
|
+
for (const spec of await serviceSpecs()) {
|
|
156
|
+
await run(join(descriptor.neo4jHome, 'bin', 'neo4j'), ['stop'], {
|
|
157
|
+
env: neo4jEnvironment(spec)
|
|
158
|
+
}).catch(() => {});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
persistProcessState({}, {});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function ready() {
|
|
165
|
+
try {
|
|
166
|
+
const specs = await serviceSpecs();
|
|
167
|
+
const checks = await Promise.all(specs.map((spec) => providerReady(
|
|
168
|
+
spec.providerUrl,
|
|
169
|
+
fetchImpl
|
|
170
|
+
)));
|
|
171
|
+
return checks.every(Boolean);
|
|
172
|
+
} catch {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function serviceSpecs() {
|
|
178
|
+
const settings = paths.runtimeSettingsPath
|
|
179
|
+
? readSettings(paths.runtimeSettingsPath)
|
|
180
|
+
: DEFAULT_RUNTIME_SETTINGS;
|
|
181
|
+
const values = parseEnv(await readText(paths.graphEnvPath));
|
|
182
|
+
const includeWorkspace = personalOnly === null
|
|
183
|
+
? managedWorkspaceEnabled(paths, readJson)
|
|
184
|
+
: !personalOnly;
|
|
185
|
+
return nativeInstanceSpecs(paths, settings, values, includeWorkspace);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function prepareInstance(spec) {
|
|
189
|
+
const confPath = join(spec.instanceDir, 'conf', 'neo4j.conf');
|
|
190
|
+
await writeText(confPath, nativeNeo4jConfiguration({
|
|
191
|
+
instanceDir: spec.instanceDir,
|
|
192
|
+
httpPort: spec.httpPort,
|
|
193
|
+
boltPort: spec.boltPort,
|
|
194
|
+
memory: spec.memory
|
|
195
|
+
}));
|
|
196
|
+
const marker = join(spec.instanceDir, '.password-initialized');
|
|
197
|
+
if (pathExists(marker)) return;
|
|
198
|
+
await run(join(descriptor.neo4jHome, 'bin', 'neo4j-admin'), [
|
|
199
|
+
'dbms', 'set-initial-password', spec.password
|
|
200
|
+
], { env: neo4jEnvironment(spec) });
|
|
201
|
+
await markInitialized(marker);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function neo4jEnvironment(spec) {
|
|
205
|
+
return {
|
|
206
|
+
...process.env,
|
|
207
|
+
JAVA_HOME: descriptor.javaHome,
|
|
208
|
+
NEO4J_HOME: descriptor.neo4jHome,
|
|
209
|
+
NEO4J_CONF: join(spec.instanceDir, 'conf')
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function providerSpec(spec) {
|
|
214
|
+
return {
|
|
215
|
+
id: spec.id,
|
|
216
|
+
python: descriptor.providerPython,
|
|
217
|
+
port: spec.providerPort,
|
|
218
|
+
logPath: join(spec.instanceDir, 'logs', 'provider.log'),
|
|
219
|
+
environment: {
|
|
220
|
+
...process.env,
|
|
221
|
+
PYTHONDONTWRITEBYTECODE: '1',
|
|
222
|
+
PYTHONUNBUFFERED: '1',
|
|
223
|
+
GRAPHITI_TELEMETRY_ENABLED: 'false',
|
|
224
|
+
EMBEDDING_DIM: '384',
|
|
225
|
+
FULI_PROVIDER_MODE: spec.providerMode,
|
|
226
|
+
FULI_PROVIDER_ID: spec.providerId,
|
|
227
|
+
FULI_PROVIDER_NAME: spec.providerName,
|
|
228
|
+
FULI_BOOTSTRAP_TOKEN: spec.bootstrapToken,
|
|
229
|
+
...(spec.humanReviewToken
|
|
230
|
+
? { FULI_HUMAN_REVIEW_TOKEN: spec.humanReviewToken }
|
|
231
|
+
: {}),
|
|
232
|
+
...(spec.workflowObservationToken
|
|
233
|
+
? { FULI_WORKFLOW_OBSERVATION_TOKEN: spec.workflowObservationToken }
|
|
234
|
+
: {}),
|
|
235
|
+
FULI_NEO4J_URI: `bolt://127.0.0.1:${spec.boltPort}`,
|
|
236
|
+
FULI_NEO4J_USER: 'neo4j',
|
|
237
|
+
FULI_NEO4J_PASSWORD: spec.password,
|
|
238
|
+
FULI_EMBEDDING_DIM: '384'
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function databaseSpec(spec) {
|
|
244
|
+
return {
|
|
245
|
+
id: spec.id,
|
|
246
|
+
command: join(descriptor.neo4jHome, 'bin', 'neo4j'),
|
|
247
|
+
args: ['console'],
|
|
248
|
+
pidPath: join(spec.instanceDir, 'run', 'neo4j.pid'),
|
|
249
|
+
logPath: join(spec.instanceDir, 'logs', 'neo4j-console.log'),
|
|
250
|
+
environment: neo4jEnvironment(spec)
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function stopProviderEntries(entries) {
|
|
255
|
+
for (const entry of Object.values(entries)) {
|
|
256
|
+
if (!Number.isInteger(entry?.pid) || typeof entry.command !== 'string') continue;
|
|
257
|
+
if (await processMatches(entry.pid, entry.command)) await stopProcess(entry.pid);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function persistProcessState(databases, providers) {
|
|
262
|
+
writeProcessState(paths.nativeProcessStatePath, {
|
|
263
|
+
version: 1,
|
|
264
|
+
mode: 'native',
|
|
265
|
+
databases,
|
|
266
|
+
providers,
|
|
267
|
+
updatedAt: now().toISOString()
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return { start, stopProviders, stopDatabases, ready };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export async function ensureNativeRuntime(input, dependencies = {}) {
|
|
275
|
+
const deps = nativeDependencies(dependencies);
|
|
276
|
+
if (!['darwin', 'linux'].includes(deps.platform)) {
|
|
277
|
+
throw new Error('Native graph mode currently supports macOS and Linux. Use container mode here.');
|
|
278
|
+
}
|
|
279
|
+
const javaHome = await deps.resolveJavaHome(input.env ?? process.env);
|
|
280
|
+
if (!javaHome) throw nativeJavaError(deps.platform);
|
|
281
|
+
const uvCommand = await deps.resolveUvCommand(input.env ?? process.env);
|
|
282
|
+
if (!uvCommand) {
|
|
283
|
+
throw new Error('Native graph mode requires uv with Python 3.12 support. Install uv, then run setup again.');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const providerPython = join(input.paths.nativeProviderVenvPath, 'bin', 'python');
|
|
287
|
+
const expected = {
|
|
288
|
+
version: MANIFEST_VERSION,
|
|
289
|
+
status: 'ready',
|
|
290
|
+
mode: 'native',
|
|
291
|
+
neo4jVersion: NATIVE_NEO4J_VERSION,
|
|
292
|
+
providerVersion: `${FULI_VERSION}+native.${PROVIDER_RUNTIME_REVISION}`,
|
|
293
|
+
javaHome,
|
|
294
|
+
uvCommand,
|
|
295
|
+
neo4jHome: input.paths.nativeNeo4jHome,
|
|
296
|
+
providerPython
|
|
297
|
+
};
|
|
298
|
+
const saved = deps.readManifest(input.paths.nativeRuntimeManifestPath);
|
|
299
|
+
if (manifestReady(saved, expected, input.paths, deps.pathExists)) return saved;
|
|
300
|
+
|
|
301
|
+
deps.onProgress(input, `Installing native Neo4j ${NATIVE_NEO4J_VERSION}…`);
|
|
302
|
+
if (!neo4jReady(input.paths, deps.pathExists)) {
|
|
303
|
+
await deps.installNeo4j({
|
|
304
|
+
paths: input.paths,
|
|
305
|
+
home: input.paths.nativeNeo4jHome,
|
|
306
|
+
url: NATIVE_NEO4J_URL,
|
|
307
|
+
sha256: NATIVE_NEO4J_SHA256
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
if (!deps.pathExists(providerPython) || saved?.providerVersion !== expected.providerVersion) {
|
|
311
|
+
deps.onProgress(input, 'Installing the native Graph Provider…');
|
|
312
|
+
await deps.installProvider({
|
|
313
|
+
uvCommand,
|
|
314
|
+
venvPath: input.paths.nativeProviderVenvPath,
|
|
315
|
+
providerPython,
|
|
316
|
+
providerSource: join(dirname(dirname(input.paths.graphComposePath)), 'graph-provider'),
|
|
317
|
+
packageRoot: dirname(input.paths.graphComposePath)
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
if (!neo4jReady(input.paths, deps.pathExists) || !deps.pathExists(providerPython)) {
|
|
321
|
+
throw new Error('Native graph runtime installation did not produce the required executables.');
|
|
322
|
+
}
|
|
323
|
+
const manifest = { ...expected, installedAt: new Date().toISOString() };
|
|
324
|
+
deps.writeManifest(input.paths.nativeRuntimeManifestPath, manifest);
|
|
325
|
+
return manifest;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function nativeNeo4jConfiguration({
|
|
329
|
+
instanceDir,
|
|
330
|
+
httpPort,
|
|
331
|
+
boltPort,
|
|
332
|
+
memory
|
|
333
|
+
}) {
|
|
334
|
+
return [
|
|
335
|
+
'server.default_listen_address=127.0.0.1',
|
|
336
|
+
'server.default_advertised_address=127.0.0.1',
|
|
337
|
+
`server.http.listen_address=127.0.0.1:${httpPort}`,
|
|
338
|
+
`server.http.advertised_address=127.0.0.1:${httpPort}`,
|
|
339
|
+
`server.bolt.listen_address=127.0.0.1:${boltPort}`,
|
|
340
|
+
`server.bolt.advertised_address=127.0.0.1:${boltPort}`,
|
|
341
|
+
`server.directories.data=${join(instanceDir, 'data')}`,
|
|
342
|
+
`server.directories.logs=${join(instanceDir, 'logs')}`,
|
|
343
|
+
`server.directories.run=${join(instanceDir, 'run')}`,
|
|
344
|
+
`server.directories.transaction.logs.root=${join(instanceDir, 'transactions')}`,
|
|
345
|
+
`server.memory.heap.initial_size=${memory.heapInitial}`,
|
|
346
|
+
`server.memory.heap.max_size=${memory.heapMax}`,
|
|
347
|
+
`server.memory.pagecache.size=${memory.pageCache}`,
|
|
348
|
+
'dbms.security.auth_enabled=true',
|
|
349
|
+
'dbms.usage_report.enabled=false',
|
|
350
|
+
''
|
|
351
|
+
].join('\n');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function manifestReady(saved, expected, paths, pathExists) {
|
|
355
|
+
return saved?.version === expected.version &&
|
|
356
|
+
saved.mode === expected.mode &&
|
|
357
|
+
saved.neo4jVersion === expected.neo4jVersion &&
|
|
358
|
+
saved.providerVersion === expected.providerVersion &&
|
|
359
|
+
saved.javaHome === expected.javaHome &&
|
|
360
|
+
saved.uvCommand === expected.uvCommand &&
|
|
361
|
+
saved.neo4jHome === expected.neo4jHome &&
|
|
362
|
+
saved.providerPython === expected.providerPython &&
|
|
363
|
+
neo4jReady(paths, pathExists) &&
|
|
364
|
+
pathExists(expected.providerPython);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function neo4jReady(paths, pathExists) {
|
|
368
|
+
return pathExists(join(paths.nativeNeo4jHome, 'bin', 'neo4j')) &&
|
|
369
|
+
pathExists(join(paths.nativeNeo4jHome, 'bin', 'neo4j-admin'));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function nativeJavaError(platform) {
|
|
373
|
+
return new Error(platform === 'darwin'
|
|
374
|
+
? 'Native graph mode requires Java 21. Install it with `brew install openjdk@21`, then run setup again.'
|
|
375
|
+
: 'Native graph mode requires Java 21. Install an OpenJDK 21 package, then run setup again.');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function nativeDependencies(overrides) {
|
|
379
|
+
return {
|
|
380
|
+
platform: process.platform,
|
|
381
|
+
pathExists: existsSync,
|
|
382
|
+
readManifest: (path) => readJsonFile(path, null),
|
|
383
|
+
writeManifest: writeJsonFileAtomic,
|
|
384
|
+
resolveJavaHome,
|
|
385
|
+
resolveUvCommand,
|
|
386
|
+
installNeo4j,
|
|
387
|
+
installProvider,
|
|
388
|
+
onProgress: (input, message) => input.onProgress?.(message),
|
|
389
|
+
...overrides
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function resolveJavaHome(env) {
|
|
394
|
+
const configured = nonEmpty(env.FULI_NATIVE_JAVA_HOME) ?? nonEmpty(env.JAVA_HOME);
|
|
395
|
+
if (configured && existsSync(join(configured, 'bin', 'java'))) return configured;
|
|
396
|
+
if (process.platform === 'darwin') {
|
|
397
|
+
const apple = await commandResult('/usr/libexec/java_home', ['-v', '21'], { env });
|
|
398
|
+
const appleHome = nonEmpty(apple.stdout);
|
|
399
|
+
if (apple.status === 0 && appleHome && existsSync(join(appleHome, 'bin', 'java'))) {
|
|
400
|
+
return appleHome;
|
|
401
|
+
}
|
|
402
|
+
const brew = await commandResult('brew', ['--prefix', 'openjdk@21'], { env });
|
|
403
|
+
const brewHome = nonEmpty(brew.stdout);
|
|
404
|
+
if (brew.status === 0 && brewHome && existsSync(join(brewHome, 'bin', 'java'))) {
|
|
405
|
+
return brewHome;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const java = await commandResult('java', ['-version'], { env });
|
|
409
|
+
if (java.status === 0 && /(?:version\s+")?21(?:[.\s"]|$)/i.test(java.stderr + java.stdout)) {
|
|
410
|
+
return dirname(dirname(java.command ?? 'java'));
|
|
411
|
+
}
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function resolveUvCommand(env) {
|
|
416
|
+
const result = await commandResult('uv', ['--version'], { env });
|
|
417
|
+
return result.status === 0 ? 'uv' : null;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function installNeo4j({ paths, home, url, sha256 }) {
|
|
421
|
+
const downloadDir = join(paths.nativeRuntimeDir, 'downloads');
|
|
422
|
+
const archive = join(downloadDir, `neo4j-community-${NATIVE_NEO4J_VERSION}-unix.tar.gz`);
|
|
423
|
+
await mkdir(downloadDir, { recursive: true });
|
|
424
|
+
if (!existsSync(archive) || await sha256File(archive) !== sha256) {
|
|
425
|
+
await downloadFile(url, archive);
|
|
426
|
+
}
|
|
427
|
+
if (await sha256File(archive) !== sha256) {
|
|
428
|
+
throw new Error('The downloaded Neo4j archive failed checksum verification.');
|
|
429
|
+
}
|
|
430
|
+
await rm(home, { recursive: true, force: true });
|
|
431
|
+
await runCommand('tar', ['-xzf', archive, '-C', paths.nativeRuntimeDir]);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function installProvider({ uvCommand, venvPath, providerPython, packageRoot }) {
|
|
435
|
+
await mkdir(dirname(venvPath), { recursive: true });
|
|
436
|
+
if (!existsSync(providerPython)) {
|
|
437
|
+
await runCommand(uvCommand, ['venv', '--python', '3.12', '--seed', venvPath]);
|
|
438
|
+
}
|
|
439
|
+
await runCommand(uvCommand, [
|
|
440
|
+
'pip', 'install', '--python', providerPython, '--upgrade',
|
|
441
|
+
join(packageRoot, 'graph-provider')
|
|
442
|
+
]);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async function downloadFile(url, destination) {
|
|
446
|
+
const partial = `${destination}.partial`;
|
|
447
|
+
let existingBytes = 0;
|
|
448
|
+
try {
|
|
449
|
+
existingBytes = (await stat(partial)).size;
|
|
450
|
+
} catch {
|
|
451
|
+
// No partial download is available to resume.
|
|
452
|
+
}
|
|
453
|
+
const response = await fetch(url, {
|
|
454
|
+
headers: existingBytes > 0 ? { range: `bytes=${existingBytes}-` } : {},
|
|
455
|
+
signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS)
|
|
456
|
+
});
|
|
457
|
+
if (!response.ok || !response.body) {
|
|
458
|
+
throw new Error(`Neo4j download failed with HTTP ${response.status}.`);
|
|
459
|
+
}
|
|
460
|
+
const resumed = existingBytes > 0 && response.status === 206;
|
|
461
|
+
if (!resumed && existingBytes > 0) {
|
|
462
|
+
await rm(partial, { force: true });
|
|
463
|
+
}
|
|
464
|
+
await pipeline(Readable.fromWeb(response.body), createWriteStream(partial, {
|
|
465
|
+
mode: 0o600,
|
|
466
|
+
flags: resumed ? 'a' : 'w'
|
|
467
|
+
}));
|
|
468
|
+
await rename(partial, destination);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async function sha256File(path) {
|
|
472
|
+
const hash = createHash('sha256');
|
|
473
|
+
await pipeline(createReadStream(path), hash);
|
|
474
|
+
return hash.digest('hex');
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function runCommand(command, args, options = {}) {
|
|
478
|
+
return new Promise((resolve, reject) => {
|
|
479
|
+
execFile(command, args, {
|
|
480
|
+
env: options.env ?? process.env,
|
|
481
|
+
timeout: COMMAND_TIMEOUT_MS,
|
|
482
|
+
encoding: 'utf8',
|
|
483
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
484
|
+
windowsHide: true
|
|
485
|
+
}, (error, stdout, stderr) => {
|
|
486
|
+
if (error) {
|
|
487
|
+
reject(new Error(nonEmpty(stderr) ?? nonEmpty(stdout) ?? error.message));
|
|
488
|
+
} else {
|
|
489
|
+
resolve({ stdout, stderr });
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function commandResult(command, args, options = {}) {
|
|
496
|
+
return new Promise((resolve) => {
|
|
497
|
+
execFile(command, args, {
|
|
498
|
+
env: options.env ?? process.env,
|
|
499
|
+
timeout: 10_000,
|
|
500
|
+
encoding: 'utf8',
|
|
501
|
+
windowsHide: true
|
|
502
|
+
}, (error, stdout, stderr) => {
|
|
503
|
+
resolve({ status: error ? 1 : 0, stdout, stderr, command });
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function nonEmpty(value) {
|
|
509
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function nativeInstanceSpecs(paths, settings, values, includeWorkspace) {
|
|
513
|
+
const memory = {
|
|
514
|
+
heapInitial: values.FULI_NEO4J_HEAP_INITIAL_SIZE ?? '128m',
|
|
515
|
+
heapMax: values.FULI_NEO4J_HEAP_MAX_SIZE ?? '256m',
|
|
516
|
+
pageCache: values.FULI_NEO4J_PAGECACHE_SIZE ?? '64m'
|
|
517
|
+
};
|
|
518
|
+
const urls = managedProviderUrls(settings);
|
|
519
|
+
const personal = {
|
|
520
|
+
id: 'personal',
|
|
521
|
+
instanceDir: paths.nativePersonalDir,
|
|
522
|
+
httpPort: settings.ports.personalNeo4jHttp,
|
|
523
|
+
boltPort: settings.ports.personalNeo4jBolt,
|
|
524
|
+
providerPort: settings.ports.personalProvider,
|
|
525
|
+
providerUrl: urls.personal,
|
|
526
|
+
providerMode: 'personal',
|
|
527
|
+
providerId: 'local-personal',
|
|
528
|
+
providerName: 'Fuli Personal Provider',
|
|
529
|
+
password: requiredEnv(values, 'FULI_PERSONAL_NEO4J_PASSWORD'),
|
|
530
|
+
bootstrapToken: requiredEnv(values, 'FULI_PERSONAL_BOOTSTRAP_TOKEN'),
|
|
531
|
+
humanReviewToken: values.FULI_PERSONAL_HUMAN_REVIEW_TOKEN,
|
|
532
|
+
workflowObservationToken: values.FULI_PERSONAL_WORKFLOW_OBSERVATION_TOKEN,
|
|
533
|
+
memory
|
|
534
|
+
};
|
|
535
|
+
if (!includeWorkspace) return [personal];
|
|
536
|
+
return [personal, {
|
|
537
|
+
id: 'workspace',
|
|
538
|
+
instanceDir: paths.nativeWorkspaceDir,
|
|
539
|
+
httpPort: settings.ports.workspaceNeo4jHttp,
|
|
540
|
+
boltPort: settings.ports.workspaceNeo4jBolt,
|
|
541
|
+
providerPort: settings.ports.workspaceProvider,
|
|
542
|
+
providerUrl: urls.workspace,
|
|
543
|
+
providerMode: 'workspace',
|
|
544
|
+
providerId: 'development-workspace',
|
|
545
|
+
providerName: 'Fuli Workspace Provider',
|
|
546
|
+
password: requiredEnv(values, 'FULI_WORKSPACE_NEO4J_PASSWORD'),
|
|
547
|
+
bootstrapToken: requiredEnv(values, 'FULI_WORKSPACE_BOOTSTRAP_TOKEN'),
|
|
548
|
+
humanReviewToken: null,
|
|
549
|
+
workflowObservationToken: null,
|
|
550
|
+
memory
|
|
551
|
+
}];
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function managedWorkspaceEnabled(paths, readJson) {
|
|
555
|
+
const state = readJson(paths.graphRuntimeStatePath) ?? {};
|
|
556
|
+
if (state.managedProviders?.includes('development-workspace')) return true;
|
|
557
|
+
const config = readJson(paths.graphRuntimeConfigPath) ?? {};
|
|
558
|
+
return (config.workspaces ?? []).some((workspace) =>
|
|
559
|
+
workspace?.managedDevelopment === true);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function parseEnv(body) {
|
|
563
|
+
const values = {};
|
|
564
|
+
for (const line of String(body).split(/\r?\n/)) {
|
|
565
|
+
const trimmed = line.trim();
|
|
566
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
567
|
+
const separator = trimmed.indexOf('=');
|
|
568
|
+
if (separator <= 0) continue;
|
|
569
|
+
values[trimmed.slice(0, separator)] = trimmed.slice(separator + 1);
|
|
570
|
+
}
|
|
571
|
+
return values;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function requiredEnv(values, name) {
|
|
575
|
+
const value = nonEmpty(values[name]);
|
|
576
|
+
if (!value) throw new Error(`Native graph runtime is missing ${name}. Run setup again.`);
|
|
577
|
+
return value;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function assertDescriptor(descriptor) {
|
|
581
|
+
if (
|
|
582
|
+
descriptor?.status !== 'ready' ||
|
|
583
|
+
descriptor.mode !== 'native' ||
|
|
584
|
+
!nonEmpty(descriptor.javaHome) ||
|
|
585
|
+
!nonEmpty(descriptor.neo4jHome) ||
|
|
586
|
+
!nonEmpty(descriptor.providerPython)
|
|
587
|
+
) {
|
|
588
|
+
throw new Error('The native graph runtime is not installed. Run setup in native mode first.');
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
async function secureWriteText(path, body) {
|
|
593
|
+
await mkdir(dirname(path), { recursive: true });
|
|
594
|
+
await writeFile(path, body, { mode: 0o600 });
|
|
595
|
+
chmodSync(path, 0o600);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
async function secureMarker(path) {
|
|
599
|
+
await mkdir(dirname(path), { recursive: true });
|
|
600
|
+
await writeFile(path, '', { mode: 0o600 });
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function spawnNativeDatabase(spec) {
|
|
604
|
+
if (existsSync(spec.pidPath)) {
|
|
605
|
+
const pid = Number(readFileSync(spec.pidPath, 'utf8').trim());
|
|
606
|
+
if (Number.isInteger(pid) && processAlive(pid)) return { pid };
|
|
607
|
+
unlinkSync(spec.pidPath);
|
|
608
|
+
}
|
|
609
|
+
mkdirSync(dirname(spec.logPath), { recursive: true });
|
|
610
|
+
const log = openSync(spec.logPath, 'a');
|
|
611
|
+
try {
|
|
612
|
+
const child = spawn(spec.command, spec.args, {
|
|
613
|
+
detached: true,
|
|
614
|
+
windowsHide: true,
|
|
615
|
+
stdio: ['ignore', log, log],
|
|
616
|
+
env: spec.environment
|
|
617
|
+
});
|
|
618
|
+
child.unref();
|
|
619
|
+
return child;
|
|
620
|
+
} finally {
|
|
621
|
+
closeSync(log);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function spawnNativeProvider(spec) {
|
|
626
|
+
mkdirSync(dirname(spec.logPath), { recursive: true });
|
|
627
|
+
const log = openSync(spec.logPath, 'a');
|
|
628
|
+
try {
|
|
629
|
+
const child = spawn(spec.python, [
|
|
630
|
+
'-m', 'uvicorn', 'fuli_graph.app:app',
|
|
631
|
+
'--host', '127.0.0.1',
|
|
632
|
+
'--port', String(spec.port)
|
|
633
|
+
], {
|
|
634
|
+
detached: true,
|
|
635
|
+
windowsHide: true,
|
|
636
|
+
stdio: ['ignore', log, log],
|
|
637
|
+
env: spec.environment
|
|
638
|
+
});
|
|
639
|
+
child.unref();
|
|
640
|
+
return child;
|
|
641
|
+
} finally {
|
|
642
|
+
closeSync(log);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async function waitForProvider(url, fetchImpl, wait) {
|
|
647
|
+
for (let attempt = 0; attempt < 120; attempt += 1) {
|
|
648
|
+
if (await providerReady(url, fetchImpl)) return;
|
|
649
|
+
if (attempt < 119) await wait(1000);
|
|
650
|
+
}
|
|
651
|
+
throw new Error('The native graph Provider did not become ready after starting.');
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async function providerReady(url, fetchImpl) {
|
|
655
|
+
try {
|
|
656
|
+
const response = await fetchImpl(`${String(url).replace(/\/$/, '')}/health`, {
|
|
657
|
+
signal: AbortSignal.timeout(2500)
|
|
658
|
+
});
|
|
659
|
+
return response.ok;
|
|
660
|
+
} catch {
|
|
661
|
+
return false;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async function waitForTcp(host, port) {
|
|
666
|
+
for (let attempt = 0; attempt < 120; attempt += 1) {
|
|
667
|
+
if (await tcpReady(host, port)) return;
|
|
668
|
+
if (attempt < 119) await delay(1000);
|
|
669
|
+
}
|
|
670
|
+
throw new Error(`Native Neo4j did not open its local Bolt port ${port}.`);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function tcpReady(host, port) {
|
|
674
|
+
return new Promise((resolve) => {
|
|
675
|
+
const socket = createConnection({ host, port });
|
|
676
|
+
const done = (ready) => {
|
|
677
|
+
socket.destroy();
|
|
678
|
+
resolve(ready);
|
|
679
|
+
};
|
|
680
|
+
socket.setTimeout(1000, () => done(false));
|
|
681
|
+
socket.once('connect', () => done(true));
|
|
682
|
+
socket.once('error', () => done(false));
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async function processMatchesCommand(pid, command) {
|
|
687
|
+
const result = await commandResult('ps', ['-p', String(pid), '-o', 'command=']);
|
|
688
|
+
return result.status === 0 && result.stdout.includes(command);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
async function stopOwnedProcess(pid) {
|
|
692
|
+
const target = process.platform === 'win32' ? pid : -pid;
|
|
693
|
+
try {
|
|
694
|
+
process.kill(target, 'SIGTERM');
|
|
695
|
+
} catch (error) {
|
|
696
|
+
if (error?.code === 'ESRCH') return;
|
|
697
|
+
throw error;
|
|
698
|
+
}
|
|
699
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
700
|
+
if (!processAlive(pid)) return;
|
|
701
|
+
await delay(100);
|
|
702
|
+
}
|
|
703
|
+
if (processAlive(pid)) process.kill(target, 'SIGKILL');
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function processAlive(pid) {
|
|
707
|
+
try {
|
|
708
|
+
process.kill(pid, 0);
|
|
709
|
+
return true;
|
|
710
|
+
} catch {
|
|
711
|
+
return false;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function delay(ms) {
|
|
716
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
717
|
+
}
|