arkgate 3.7.0 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +76 -1145
  2. package/README.md +59 -19
  3. package/bin/ark-check-runtime.mjs +1598 -0
  4. package/bin/ark-check.mjs +32 -1565
  5. package/bin/ark-mcp-runtime.mjs +1976 -0
  6. package/bin/ark-mcp.mjs +84 -1495
  7. package/bin/ark-shared.mjs +34 -38
  8. package/bin/ark.mjs +33 -66
  9. package/bin/lib/adapter-contract.mjs +161 -9
  10. package/bin/lib/agent-gates.mjs +1 -0
  11. package/bin/lib/analysis-completeness.mjs +28 -0
  12. package/bin/lib/analysis-engine.mjs +8 -8
  13. package/bin/lib/analysis-policy.mjs +27 -0
  14. package/bin/lib/architecture-scan.mjs +70 -357
  15. package/bin/lib/auto-patch.mjs +76 -8
  16. package/bin/lib/ci-and-commands.mjs +1 -1
  17. package/bin/lib/codex-home.mjs +43 -16
  18. package/bin/lib/design-delta.mjs +4 -0
  19. package/bin/lib/doctor-advisories.mjs +4 -3
  20. package/bin/lib/doctor-plan.mjs +40 -41
  21. package/bin/lib/enforcement-state.mjs +2 -0
  22. package/bin/lib/github-enforcement.mjs +443 -0
  23. package/bin/lib/hook-templates.mjs +12 -148
  24. package/bin/lib/html-report-advisories.mjs +1 -1
  25. package/bin/lib/html-report-depth.mjs +9 -0
  26. package/bin/lib/html-report.mjs +5 -5
  27. package/bin/lib/install-migrate.mjs +83 -79
  28. package/bin/lib/managed-upgrade.mjs +622 -0
  29. package/bin/lib/mcp-adoption.mjs +3 -1
  30. package/bin/lib/parse-health.mjs +6 -5
  31. package/bin/lib/port-proof.mjs +2 -2
  32. package/bin/lib/prepare-change.mjs +68 -38
  33. package/bin/lib/prepare-write.mjs +7 -1
  34. package/bin/lib/resident-doctor-client.mjs +55 -0
  35. package/bin/lib/resident-hook.mjs +247 -0
  36. package/bin/lib/resolved-candidate-facts.mjs +1160 -0
  37. package/bin/lib/scan-files.mjs +19 -6
  38. package/bin/lib/snippet-analysis.mjs +119 -0
  39. package/bin/lib/source-policy.mjs +24 -0
  40. package/bin/lib/typescript-host.mjs +15 -18
  41. package/bin/lib/unavailable-analysis.mjs +76 -0
  42. package/bin/lib/upgrade-command.mjs +115 -0
  43. package/bin/lib/weakest-link.mjs +21 -179
  44. package/bin/lib/write-path-capabilities.mjs +167 -16
  45. package/bin/lib/write-path-detect.mjs +3 -2
  46. package/dist/eslint/index.cjs +3 -3
  47. package/dist/eslint/index.d.ts +3 -0
  48. package/dist/eslint/index.js +3 -3
  49. package/dist/index.cjs +7 -7
  50. package/dist/index.d.ts +1073 -141
  51. package/dist/index.js +7 -7
  52. package/docs/agent-guide.md +103 -59
  53. package/docs/ai-gates.md +97 -16
  54. package/docs/demos/01-write-gate-self-correction.md +2 -2
  55. package/docs/enthusiast/README.md +10 -10
  56. package/docs/enthusiast/how-to-gallery-starter.md +2 -2
  57. package/docs/enthusiast/reference-commands.md +18 -1
  58. package/docs/enthusiast/tutorial-first-project.md +2 -2
  59. package/docs/package-surface.md +98 -12
  60. package/docs/typescript-support.md +109 -37
  61. package/package.json +32 -4
  62. package/schemas/ark.analysis-result.schema.json +159 -2
  63. package/schemas/ark.design-delta.schema.json +1 -0
  64. package/schemas/ark.enforcement-state.schema.json +84 -0
  65. package/schemas/ark.resolved-candidate-facts.schema.json +1 -0
  66. package/server.json +2 -2
  67. package/templates/skills/ark-explore.md +5 -5
  68. package/templates/skills/ark-fix.md +1 -1
  69. package/templates/skills/ark-runtime.md +15 -8
  70. package/templates/skills/ark-upgrade.md +122 -182
  71. package/bin/lib/ai-velocity.mjs +0 -293
  72. package/bin/lib/graph-cycles.mjs +0 -6
  73. package/bin/lib/safety-diagnostics.mjs +0 -284
  74. package/bin/lib/ts-resolve.mjs +0 -228
  75. package/dist/configTypes-DAPvBqK6.d.cts +0 -61
  76. package/dist/eslint/index.d.cts +0 -146
  77. package/dist/index.d.cts +0 -986
@@ -1,9 +1,18 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { loadArchitectureChangeMap, loadContract, preflightChange } from './analysis-engine.mjs';
3
+ import {
4
+ loadArchitectureChangeMap,
5
+ loadContract,
6
+ preflightResolvedChange,
7
+ } from './analysis-engine.mjs';
4
8
  import { createAdapterResult } from './adapter-contract.mjs';
5
- import { collectGovernedFiles, isGovernableSourceFile, normalize } from './scan-files.mjs';
6
- import { isScanExcludedRelative, layerForRelativePath } from '../ark-shared.mjs';
9
+ import { isGovernableSourceFile } from './scan-files.mjs';
10
+ import {
11
+ canonicalizeCandidateChanges,
12
+ resolveCandidateFacts,
13
+ } from './resolved-candidate-facts.mjs';
14
+ import { effectiveAnalysisConfig } from './analysis-policy.mjs';
15
+ import { isScanExcludedRelative } from '../ark-shared.mjs';
7
16
 
8
17
  function candidatePath(value) {
9
18
  if (typeof value !== 'string' || value.trim() === '') {
@@ -27,16 +36,21 @@ function isIncluded(relativePath, include) {
27
36
  });
28
37
  }
29
38
 
39
+ export function isCandidateSourceInScope(config, relativePath) {
40
+ return (
41
+ isGovernableSourceFile(path.basename(relativePath)) &&
42
+ isIncluded(relativePath, config.include) &&
43
+ !isScanExcludedRelative(relativePath, config)
44
+ );
45
+ }
46
+
30
47
  function assertGovernedSource(config, relativePath) {
31
48
  if (!isGovernableSourceFile(path.basename(relativePath))) {
32
49
  throw new Error(`Atomic preflight only accepts governed production source files: ${relativePath}`);
33
50
  }
34
- if (!isIncluded(relativePath, config.include) || isScanExcludedRelative(relativePath, config)) {
51
+ if (!isCandidateSourceInScope(config, relativePath)) {
35
52
  throw new Error(`Change path is outside the configured source scope: ${relativePath}`);
36
53
  }
37
- if (!layerForRelativePath(relativePath, config.layers)) {
38
- throw new Error(`Change path is not assigned to an architecture layer: ${relativePath}`);
39
- }
40
54
  }
41
55
 
42
56
  function assertInsideProject(root, relativePath) {
@@ -73,22 +87,6 @@ export function normalizeChangeSet(input) {
73
87
  });
74
88
  }
75
89
 
76
- function baseFilesForChange(root, config, changes) {
77
- const byPath = new Map(
78
- collectGovernedFiles(root, config).map((absolute) => [
79
- normalize(path.relative(root, absolute)),
80
- { path: normalize(path.relative(root, absolute)), content: fs.readFileSync(absolute, 'utf8') },
81
- ])
82
- );
83
- for (const change of changes) {
84
- if (byPath.has(change.path) || !isGovernableSourceFile(path.basename(change.path))) continue;
85
- const absolute = path.join(root, change.path);
86
- if (!fs.existsSync(absolute) || !fs.statSync(absolute).isFile()) continue;
87
- byPath.set(change.path, { path: change.path, content: fs.readFileSync(absolute, 'utf8') });
88
- }
89
- return [...byPath.values()].sort((left, right) => left.path.localeCompare(right.path));
90
- }
91
-
92
90
  export function prepareChangeFromRoot({
93
91
  root,
94
92
  config,
@@ -96,33 +94,65 @@ export function prepareChangeFromRoot({
96
94
  changes,
97
95
  changeMap,
98
96
  changeMapSource,
99
- compilerOptions,
97
+ ts,
98
+ tsconfig,
99
+ manifest,
100
+ overlayChanges,
100
101
  }) {
101
102
  const normalizedChanges = normalizeChangeSet(changes);
103
+ const normalizedOverlayChanges =
104
+ overlayChanges === undefined ? normalizedChanges : normalizeChangeSet(overlayChanges);
105
+ const effectiveConfig = effectiveAnalysisConfig(config, manifest);
102
106
  for (const change of normalizedChanges) {
103
107
  assertInsideProject(root, change.path);
104
- assertGovernedSource(config, change.path);
105
108
  }
106
- const contract = loadContract(config, configSource ?? path.join(root, 'ark.config.json'));
109
+ const contract = loadContract(
110
+ effectiveConfig,
111
+ configSource ?? path.join(root, 'ark.config.json')
112
+ );
107
113
  const loadedChangeMap =
108
114
  changeMap === undefined
109
115
  ? undefined
110
116
  : loadArchitectureChangeMap(changeMap, contract.config, changeMapSource);
111
- const result = preflightChange({
112
- contract,
113
- files: baseFilesForChange(root, config, normalizedChanges),
117
+ const canonicalChanges = canonicalizeCandidateChanges({
118
+ root,
119
+ config: contract.config,
114
120
  changes: normalizedChanges,
121
+ });
122
+ for (const change of canonicalChanges) assertGovernedSource(effectiveConfig, change.path);
123
+ const baseFacts = resolveCandidateFacts({
124
+ root,
125
+ config: contract.config,
126
+ ts,
127
+ ...(tsconfig ? { tsconfig } : {}),
128
+ });
129
+ const candidateFacts = resolveCandidateFacts({
130
+ root,
131
+ config: contract.config,
132
+ ts,
133
+ changes: normalizedOverlayChanges,
134
+ ...(tsconfig ? { tsconfig } : {}),
135
+ });
136
+ const result = preflightResolvedChange({
137
+ contract,
138
+ baseFacts,
139
+ candidateFacts,
140
+ changes: canonicalChanges,
115
141
  ...(loadedChangeMap ? { changeMap: loadedChangeMap } : {}),
116
- ...(compilerOptions ? { compilerOptions } : {}),
117
142
  });
118
- return {
119
- ...createAdapterResult({
120
- valid: result.valid,
121
- violations: result.violations,
122
- warnings: result.warnings,
123
- }),
124
- ...result,
125
- };
143
+ const completeness =
144
+ result.baseCompleteness === 'unavailable' || result.candidateCompleteness === 'unavailable'
145
+ ? 'unavailable'
146
+ : result.baseCompleteness === 'partial' || result.candidateCompleteness === 'partial'
147
+ ? 'partial'
148
+ : 'complete';
149
+ const { diagnostics } = createAdapterResult({
150
+ valid: result.valid,
151
+ completeness,
152
+ violations: result.violations,
153
+ warnings: result.warnings,
154
+ });
155
+ return { ...result, diagnostics };
126
156
  }
127
157
 
128
158
  export function renderChangePreflight(result) {
@@ -73,7 +73,7 @@ export function buildJudgmentBrief(violations) {
73
73
  * placement: object,
74
74
  * root: string,
75
75
  * ts: object,
76
- * validate: (source: string) => { valid: boolean, violations?: any[] },
76
+ * validate: (source: string) => { valid: boolean, completeness?: string, violations?: any[] },
77
77
  * resolveTargetAbs?: Function,
78
78
  * }} opts
79
79
  */
@@ -117,7 +117,13 @@ export function composePrepareWrite(opts) {
117
117
  ...(placement?.description ? { description: placement.description } : {}),
118
118
  // Q03: pass through golden pattern from ark_place (advisory; absent is normal).
119
119
  ...(placement?.goldenPattern ? { goldenPattern: placement.goldenPattern } : {}),
120
+ mode: gate.mode,
120
121
  valid: gate.valid,
122
+ lexicalValid: gate.lexicalValid,
123
+ ...(gate.completeness ? { completeness: gate.completeness } : {}),
124
+ ...(Array.isArray(gate.completenessReasons)
125
+ ? { completenessReasons: gate.completenessReasons }
126
+ : {}),
121
127
  violations: gate.violations,
122
128
  ...(gate.autoPatch ? { autoPatch: gate.autoPatch } : {}),
123
129
  ...(judgment ? { judgmentBrief: judgment } : {}),
@@ -0,0 +1,55 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+
4
+ import {
5
+ RESIDENT_HOOK_PROTOCOL_VERSION,
6
+ requestResidentHook,
7
+ residentDoctorEnvironment,
8
+ residentHookEndpoint,
9
+ } from './resident-hook.mjs';
10
+
11
+ const arkMcpLauncher = fileURLToPath(new URL('../ark-mcp.mjs', import.meta.url));
12
+
13
+ /** Try the opt-in resident doctor and let the caller continue through the cold fallback. */
14
+ export async function tryResidentDoctor(args) {
15
+ if (!args.resident) return false;
16
+ if (!args.doctor || !args.json) throw new Error('--resident requires --doctor --json.');
17
+ const endpoint = residentHookEndpoint({
18
+ root: args.root,
19
+ config: args.config,
20
+ manifest: args.manifest,
21
+ tsconfig: args.tsconfig,
22
+ launcher: arkMcpLauncher,
23
+ });
24
+ const configuredTimeout = Number(process.env.ARK_RESIDENT_DOCTOR_TIMEOUT_MS);
25
+ const response = await requestResidentHook({
26
+ socket: endpoint.socket,
27
+ timeoutMs:
28
+ Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : 500,
29
+ request: {
30
+ protocolVersion: RESIDENT_HOOK_PROTOCOL_VERSION,
31
+ kind: 'doctor',
32
+ root: path.resolve(args.root),
33
+ config: args.config,
34
+ manifest: args.manifest ?? null,
35
+ tsconfig: args.tsconfig ?? null,
36
+ environment: residentDoctorEnvironment(),
37
+ },
38
+ });
39
+ if (
40
+ !response ||
41
+ response.fallback === true ||
42
+ !Number.isInteger(response.status) ||
43
+ typeof response.stdout !== 'string' ||
44
+ typeof response.stderr !== 'string'
45
+ ) {
46
+ if (process.env.ARK_RESIDENT_DOCTOR_REQUIRED === '1') {
47
+ throw new Error('Resident doctor was required but unavailable.');
48
+ }
49
+ return false;
50
+ }
51
+ if (response.stdout) process.stdout.write(response.stdout);
52
+ if (response.stderr) process.stderr.write(response.stderr);
53
+ process.exitCode = response.status;
54
+ return true;
55
+ }
@@ -0,0 +1,247 @@
1
+ /** Z07 transport only; ark-mcp-runtime owns every hook decision. */
2
+ import { createHash } from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import net from 'node:net';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+
8
+ export const RESIDENT_HOOK_PROTOCOL_VERSION = 1;
9
+ const RESIDENT_DOCTOR_ENV = [
10
+ 'ARK_ACTIVE_HOST',
11
+ 'ARK_DOCTOR_GITHUB',
12
+ 'CODEX_HOME',
13
+ 'CODEX_CI',
14
+ 'CODEX_SANDBOX',
15
+ 'CODEX_SESSION_ID',
16
+ 'CODEX_THREAD_ID',
17
+ 'CLAUDE_CODE',
18
+ 'CLAUDE_CODE_ENTRYPOINT',
19
+ 'CLAUDE_PROJECT_DIR',
20
+ 'CLAUDECODE',
21
+ 'CURSOR_AGENT',
22
+ 'CURSOR_AGENT_CLI',
23
+ 'CURSOR_TRACE_ID',
24
+ 'GH_HOST',
25
+ 'GH_TOKEN',
26
+ 'GITHUB_TOKEN',
27
+ 'GROK_BUILD',
28
+ 'GROK_AGENT',
29
+ 'GROK_SESSION_ID',
30
+ 'GROK_WORKSPACE_ROOT',
31
+ 'HOME',
32
+ 'HOMEDRIVE',
33
+ 'HOMEPATH',
34
+ 'PATH',
35
+ 'USERPROFILE',
36
+ 'XAI_GROK',
37
+ ];
38
+
39
+ export function residentDoctorEnvironment(env = process.env) {
40
+ const hash = createHash('sha256');
41
+ for (const name of RESIDENT_DOCTOR_ENV) {
42
+ hash.update(name).update('\0').update(env[name] ?? '').update('\0');
43
+ }
44
+ return { digest: `sha256:${hash.digest('hex')}` };
45
+ }
46
+
47
+ function residentRuntimeIdentity(launcher) {
48
+ const hash = createHash('sha256');
49
+ const packageRoot = path.dirname(path.dirname(launcher));
50
+ const files = [path.join(packageRoot, 'package.json')];
51
+ const visit = (directory) => {
52
+ try {
53
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
54
+ const absolute = path.join(directory, entry.name);
55
+ if (entry.isDirectory()) visit(absolute);
56
+ else if (/\.(?:[cm]?js|json)$/.test(entry.name)) files.push(absolute);
57
+ }
58
+ } catch { files.push(directory); }
59
+ };
60
+ visit(path.join(packageRoot, 'bin'));
61
+ visit(path.join(packageRoot, 'dist'));
62
+ for (const file of [...new Set(files)].sort()) {
63
+ hash.update(path.relative(packageRoot, file)).update('\0');
64
+ try { hash.update(fs.readFileSync(file)); } catch { hash.update('missing'); }
65
+ hash.update('\0');
66
+ }
67
+ return hash.digest('hex');
68
+ }
69
+
70
+ export function residentHookEndpoint({ root, config, manifest, tsconfig, launcher }) {
71
+ let realRoot;
72
+ try { realRoot = fs.realpathSync(root); } catch { realRoot = path.resolve(root); }
73
+ const uid = typeof process.getuid === 'function' ? process.getuid() : 'user';
74
+ const directory = path.join(os.tmpdir(), `arkgate-${uid}`);
75
+ const digest = createHash('sha256').update(JSON.stringify({
76
+ root: realRoot,
77
+ config: path.resolve(root, config),
78
+ manifest: manifest ? path.resolve(root, manifest) : null,
79
+ tsconfig: tsconfig ? path.resolve(root, tsconfig) : null,
80
+ launcher: path.resolve(launcher),
81
+ executable: process.execPath,
82
+ protocolVersion: RESIDENT_HOOK_PROTOCOL_VERSION,
83
+ runtimeIdentity: residentRuntimeIdentity(path.resolve(launcher)),
84
+ })).digest('hex').slice(0, 24);
85
+ return {
86
+ directory,
87
+ socket: process.platform === 'win32'
88
+ ? `\\\\.\\pipe\\arkgate-z07-${uid}-${digest}`
89
+ : path.join(directory, `${digest}.sock`),
90
+ };
91
+ }
92
+
93
+ export function residentEnvironmentIdentity(paths, tokens = []) {
94
+ const hash = createHash('sha256');
95
+ for (const token of [...tokens].map(String).sort()) hash.update('token\0').update(token).update('\0');
96
+ for (const file of [...new Set(paths.map((entry) => path.resolve(entry)))].sort()) {
97
+ hash.update('path\0').update(file).update('\0');
98
+ try {
99
+ const stat = fs.lstatSync(file);
100
+ if (stat.isSymbolicLink()) {
101
+ hash.update('link\0').update(fs.readlinkSync(file)).update('\0');
102
+ hash.update(fs.realpathSync(file)).update('\0');
103
+ const target = fs.statSync(file);
104
+ if (target.isFile()) hash.update('target-file\0').update(fs.readFileSync(file));
105
+ else hash.update('target-other\0');
106
+ }
107
+ else if (stat.isFile()) hash.update('file\0').update(fs.readFileSync(file));
108
+ else hash.update('other\0');
109
+ } catch { hash.update('missing\0'); }
110
+ hash.update('\0');
111
+ }
112
+ return `sha256:${hash.digest('hex')}`;
113
+ }
114
+
115
+ function inputState(file) { // Stryker disable StringLiteral: opaque state tags have no public meaning
116
+ const stat = fs.lstatSync(file, { bigint: true, throwIfNoEntry: false });
117
+ if (!stat) return 'missing';
118
+ const metadata = [stat.dev, stat.ino, stat.mode, stat.size, stat.mtimeNs, stat.ctimeNs]
119
+ .map(String)
120
+ .join(':');
121
+ if (stat.isSymbolicLink()) {
122
+ const target = fs.realpathSync(file);
123
+ const targetStat = fs.statSync(file, { bigint: true });
124
+ return `link:${metadata}:${fs.readlinkSync(file)}:${target}:${[
125
+ targetStat.dev,
126
+ targetStat.ino,
127
+ targetStat.mode,
128
+ targetStat.size,
129
+ targetStat.mtimeNs,
130
+ targetStat.ctimeNs,
131
+ ].map(String).join(':')}`;
132
+ }
133
+ if (stat.isDirectory()) {
134
+ const entries = fs.readdirSync(file, { withFileTypes: true })
135
+ .map((entry) => `${entry.name}:${entry.isDirectory() ? 'd' : entry.isFile() ? 'f' : 'o'}`)
136
+ .sort();
137
+ return `dir:${metadata}:${entries.join('\0')}`;
138
+ }
139
+ return `${stat.isFile() ? 'file' : 'other'}:${metadata}`;
140
+ } // Stryker restore StringLiteral
141
+
142
+ /** Process-local, metadata-based invalidation ledger; never a persisted result cache. */
143
+ export function createResidentInputLedger(paths) {
144
+ const normalized = [...new Set(paths.map((entry) => path.resolve(entry)))].sort();
145
+ const states = new Map(normalized.map((file) => [file, inputState(file)]));
146
+ return {
147
+ paths: normalized,
148
+ matches(candidatePaths = normalized) {
149
+ try {
150
+ const candidate = [...new Set(candidatePaths.map((entry) => path.resolve(entry)))].sort();
151
+ return (
152
+ candidate.length === normalized.length &&
153
+ candidate.every((file, index) =>
154
+ file === normalized[index] && inputState(file) === states.get(file)
155
+ )
156
+ );
157
+ } catch {
158
+ return false;
159
+ }
160
+ },
161
+ };
162
+ }
163
+
164
+ export function requestResidentHook({ socket, request, timeoutMs = 75 }) {
165
+ return new Promise((resolve) => {
166
+ let done = false;
167
+ let buffered = '';
168
+ const client = net.createConnection(socket);
169
+ const finish = (value) => {
170
+ if (done) return;
171
+ done = true;
172
+ clearTimeout(timer);
173
+ client.destroy();
174
+ resolve(value);
175
+ };
176
+ const timer = setTimeout(() => finish(null), timeoutMs);
177
+ client.setEncoding('utf8');
178
+ client.once('connect', () => client.write(`${JSON.stringify(request)}\n`));
179
+ client.on('data', (chunk) => {
180
+ buffered += chunk;
181
+ if (buffered.length > 24 * 1024 * 1024) return finish(null);
182
+ const newline = buffered.indexOf('\n');
183
+ if (newline < 0) return;
184
+ try {
185
+ const parsed = JSON.parse(buffered.slice(0, newline));
186
+ finish(parsed?.protocolVersion === RESIDENT_HOOK_PROTOCOL_VERSION ? parsed : null);
187
+ } catch { finish(null); }
188
+ });
189
+ client.once('error', () => finish(null));
190
+ client.once('end', () => finish(null));
191
+ });
192
+ }
193
+
194
+ function secureDirectory(directory) {
195
+ if (process.platform === 'win32') return true;
196
+ try {
197
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
198
+ const stat = fs.lstatSync(directory);
199
+ if (!stat.isDirectory() || stat.isSymbolicLink()) return false;
200
+ if (typeof process.getuid === 'function' && stat.uid !== process.getuid()) return false;
201
+ fs.chmodSync(directory, 0o700);
202
+ return true;
203
+ } catch { return false; }
204
+ }
205
+
206
+ export async function startResidentHookServer({ endpoint, handle }) {
207
+ if (!secureDirectory(endpoint.directory)) return null;
208
+ const server = net.createServer((client) => {
209
+ client.setEncoding('utf8');
210
+ let buffered = '';
211
+ let handled = false;
212
+ client.on('data', async (chunk) => {
213
+ if (handled) return;
214
+ buffered += chunk;
215
+ if (buffered.length > 12 * 1024 * 1024) return client.destroy();
216
+ const newline = buffered.indexOf('\n');
217
+ if (newline < 0) return;
218
+ handled = true;
219
+ let response;
220
+ try { response = await handle(JSON.parse(buffered.slice(0, newline))); }
221
+ catch { response = { protocolVersion: RESIDENT_HOOK_PROTOCOL_VERSION, fallback: true }; }
222
+ if (!client.destroyed) client.end(`${JSON.stringify(response)}\n`);
223
+ });
224
+ });
225
+ const listening = await new Promise((resolve) => {
226
+ const failed = () => resolve(false);
227
+ server.once('error', failed);
228
+ server.listen(endpoint.socket, () => {
229
+ server.off('error', failed);
230
+ resolve(true);
231
+ });
232
+ });
233
+ if (!listening) {
234
+ try { server.close(); } catch { /* The endpoint is already owned. */ }
235
+ return null;
236
+ }
237
+ let cleaned = false;
238
+ const cleanup = () => {
239
+ if (cleaned) return;
240
+ cleaned = true;
241
+ try { server.close(); } catch { /* Already closed. */ }
242
+ if (process.platform !== 'win32') {
243
+ try { fs.unlinkSync(endpoint.socket); } catch { /* Exact endpoint already absent. */ }
244
+ }
245
+ };
246
+ return { cleanup };
247
+ }