@evref-bl/dev-nexus 0.1.0-alpha.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.
- package/README.md +677 -0
- package/dist/browserOpener.d.ts +9 -0
- package/dist/browserOpener.js +47 -0
- package/dist/cli.d.ts +18 -0
- package/dist/cli.js +2374 -0
- package/dist/gitWorktreeService.d.ts +57 -0
- package/dist/gitWorktreeService.js +157 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +47 -0
- package/dist/nexusAgentMcpConfig.d.ts +30 -0
- package/dist/nexusAgentMcpConfig.js +228 -0
- package/dist/nexusAutomation.d.ts +103 -0
- package/dist/nexusAutomation.js +390 -0
- package/dist/nexusAutomationAgentLaunch.d.ts +148 -0
- package/dist/nexusAutomationAgentLaunch.js +855 -0
- package/dist/nexusAutomationAgentProfile.d.ts +39 -0
- package/dist/nexusAutomationAgentProfile.js +103 -0
- package/dist/nexusAutomationAgentSurface.d.ts +62 -0
- package/dist/nexusAutomationAgentSurface.js +90 -0
- package/dist/nexusAutomationCommandExecutor.d.ts +29 -0
- package/dist/nexusAutomationCommandExecutor.js +251 -0
- package/dist/nexusAutomationConfig.d.ts +114 -0
- package/dist/nexusAutomationConfig.js +547 -0
- package/dist/nexusAutomationEnqueue.d.ts +37 -0
- package/dist/nexusAutomationEnqueue.js +128 -0
- package/dist/nexusAutomationRunOnce.d.ts +91 -0
- package/dist/nexusAutomationRunOnce.js +586 -0
- package/dist/nexusAutomationScheduler.d.ts +50 -0
- package/dist/nexusAutomationScheduler.js +196 -0
- package/dist/nexusAutomationStatus.d.ts +55 -0
- package/dist/nexusAutomationStatus.js +462 -0
- package/dist/nexusAutomationTarget.d.ts +19 -0
- package/dist/nexusAutomationTarget.js +33 -0
- package/dist/nexusAutomationTargetCycle.d.ts +90 -0
- package/dist/nexusAutomationTargetCycle.js +282 -0
- package/dist/nexusAutomationTargetReport.d.ts +136 -0
- package/dist/nexusAutomationTargetReport.js +504 -0
- package/dist/nexusAutomationWorktreeSetup.d.ts +89 -0
- package/dist/nexusAutomationWorktreeSetup.js +661 -0
- package/dist/nexusCoordination.d.ts +198 -0
- package/dist/nexusCoordination.js +1018 -0
- package/dist/nexusExtension.d.ts +31 -0
- package/dist/nexusExtension.js +1 -0
- package/dist/nexusHomeConfig.d.ts +38 -0
- package/dist/nexusHomeConfig.js +133 -0
- package/dist/nexusMcpServer.d.ts +31 -0
- package/dist/nexusMcpServer.js +1036 -0
- package/dist/nexusPluginCapabilities.d.ts +197 -0
- package/dist/nexusPluginCapabilities.js +201 -0
- package/dist/nexusProjectConfig.d.ts +95 -0
- package/dist/nexusProjectConfig.js +880 -0
- package/dist/nexusProjectHomeService.d.ts +121 -0
- package/dist/nexusProjectHomeService.js +171 -0
- package/dist/nexusProjectLifecycle.d.ts +62 -0
- package/dist/nexusProjectLifecycle.js +205 -0
- package/dist/nexusProjectOperations.d.ts +101 -0
- package/dist/nexusProjectOperations.js +296 -0
- package/dist/nexusProjectRegistry.d.ts +42 -0
- package/dist/nexusProjectRegistry.js +91 -0
- package/dist/nexusProjectScaffold.d.ts +25 -0
- package/dist/nexusProjectScaffold.js +61 -0
- package/dist/nexusProjectTemplate.d.ts +34 -0
- package/dist/nexusProjectTemplate.js +354 -0
- package/dist/nexusSkills.d.ts +134 -0
- package/dist/nexusSkills.js +647 -0
- package/dist/nexusWorkerContextBundle.d.ts +142 -0
- package/dist/nexusWorkerContextBundle.js +375 -0
- package/dist/processSupervisor.d.ts +89 -0
- package/dist/processSupervisor.js +440 -0
- package/dist/vibeKanbanApi.d.ts +11 -0
- package/dist/vibeKanbanApi.js +14 -0
- package/dist/vibeKanbanAuth.d.ts +25 -0
- package/dist/vibeKanbanAuth.js +101 -0
- package/dist/vibeKanbanBoardAdapter.d.ts +36 -0
- package/dist/vibeKanbanBoardAdapter.js +196 -0
- package/dist/vibeKanbanMcpConfig.d.ts +36 -0
- package/dist/vibeKanbanMcpConfig.js +191 -0
- package/dist/vibeKanbanProjectAdapter.d.ts +39 -0
- package/dist/vibeKanbanProjectAdapter.js +113 -0
- package/dist/vibeKanbanWorkspaceSetup.d.ts +1 -0
- package/dist/vibeKanbanWorkspaceSetup.js +96 -0
- package/dist/workItemService.d.ts +60 -0
- package/dist/workItemService.js +163 -0
- package/dist/workTrackingGitHubProvider.d.ts +71 -0
- package/dist/workTrackingGitHubProvider.js +663 -0
- package/dist/workTrackingGitLabProvider.d.ts +62 -0
- package/dist/workTrackingGitLabProvider.js +523 -0
- package/dist/workTrackingJiraProvider.d.ts +67 -0
- package/dist/workTrackingJiraProvider.js +652 -0
- package/dist/workTrackingLocalProvider.d.ts +49 -0
- package/dist/workTrackingLocalProvider.js +463 -0
- package/dist/workTrackingProviderService.d.ts +21 -0
- package/dist/workTrackingProviderService.js +117 -0
- package/dist/workTrackingTypes.d.ts +202 -0
- package/dist/workTrackingTypes.js +1 -0
- package/dist/workTrackingVibeProvider.d.ts +35 -0
- package/dist/workTrackingVibeProvider.js +119 -0
- package/dist/worktreeExecutionMetadata.d.ts +76 -0
- package/dist/worktreeExecutionMetadata.js +239 -0
- package/package.json +37 -0
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import http from "node:http";
|
|
4
|
+
import net from "node:net";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
export class ProcessSupervisorError extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "ProcessSupervisorError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function isWindowsPlatform(platform = process.platform) {
|
|
13
|
+
return platform === "win32";
|
|
14
|
+
}
|
|
15
|
+
export function defaultDetachedForReleasedProcess(platform = process.platform) {
|
|
16
|
+
return !isWindowsPlatform(platform);
|
|
17
|
+
}
|
|
18
|
+
export function defaultDetachedForPersistentService(platform = process.platform) {
|
|
19
|
+
return isWindowsPlatform(platform);
|
|
20
|
+
}
|
|
21
|
+
export function defaultKillTreeForPlatform(platform = process.platform) {
|
|
22
|
+
return isWindowsPlatform(platform);
|
|
23
|
+
}
|
|
24
|
+
export function stopMethodForPlatform(killTree, platform = process.platform) {
|
|
25
|
+
return killTree && isWindowsPlatform(platform) ? "taskkill" : "process.kill";
|
|
26
|
+
}
|
|
27
|
+
function assertNonEmptyString(value, name) {
|
|
28
|
+
if (value.trim().length === 0) {
|
|
29
|
+
throw new ProcessSupervisorError(`${name} must be a non-empty string`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function assertPid(pid) {
|
|
33
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
34
|
+
throw new ProcessSupervisorError("pid must be a positive integer");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function sanitizeLogName(value) {
|
|
38
|
+
const sanitized = value.trim().replace(/[^A-Za-z0-9._-]+/g, "-");
|
|
39
|
+
return sanitized.replace(/^-+|-+$/g, "") || "process";
|
|
40
|
+
}
|
|
41
|
+
function processLogPaths(logDirectory, name) {
|
|
42
|
+
const baseName = sanitizeLogName(name);
|
|
43
|
+
return {
|
|
44
|
+
stdout: path.join(logDirectory, `${baseName}.stdout.log`),
|
|
45
|
+
stderr: path.join(logDirectory, `${baseName}.stderr.log`),
|
|
46
|
+
lifecycle: path.join(logDirectory, `${baseName}.lifecycle.log`),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function writeLifecycleEvent(stream, event) {
|
|
50
|
+
stream.write(`${JSON.stringify({ timestamp: new Date().toISOString(), ...event })}\n`);
|
|
51
|
+
}
|
|
52
|
+
function openLogFile(filePath, appendLogs) {
|
|
53
|
+
return fs.openSync(filePath, appendLogs ? "a" : "w");
|
|
54
|
+
}
|
|
55
|
+
export function envValue(env, key) {
|
|
56
|
+
const entry = Object.entries(env).find(([envKey]) => envKey.toLowerCase() === key.toLowerCase());
|
|
57
|
+
return entry?.[1];
|
|
58
|
+
}
|
|
59
|
+
export function commandHasPathSegment(command) {
|
|
60
|
+
return command.includes("/") || command.includes("\\");
|
|
61
|
+
}
|
|
62
|
+
export function executableExtensions(env, platform = process.platform) {
|
|
63
|
+
if (!isWindowsPlatform(platform)) {
|
|
64
|
+
return [""];
|
|
65
|
+
}
|
|
66
|
+
const pathExt = envValue(env, "PATHEXT") ?? ".COM;.EXE;.BAT;.CMD";
|
|
67
|
+
const extensions = pathExt
|
|
68
|
+
.split(";")
|
|
69
|
+
.map((extension) => extension.trim())
|
|
70
|
+
.filter(Boolean);
|
|
71
|
+
return [...extensions, ""];
|
|
72
|
+
}
|
|
73
|
+
function isFile(filePath) {
|
|
74
|
+
try {
|
|
75
|
+
return fs.statSync(filePath).isFile();
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export function candidateExecutablePaths(command, env, platform = process.platform) {
|
|
82
|
+
const extensions = executableExtensions(env, platform);
|
|
83
|
+
const commandExtension = path.extname(command);
|
|
84
|
+
const candidates = isWindowsPlatform(platform) && commandExtension
|
|
85
|
+
? [command]
|
|
86
|
+
: extensions.map((extension) => `${command}${extension}`);
|
|
87
|
+
if (commandHasPathSegment(command)) {
|
|
88
|
+
return candidates.map((candidate) => path.resolve(candidate));
|
|
89
|
+
}
|
|
90
|
+
const pathValue = envValue(env, "PATH") ?? "";
|
|
91
|
+
return pathValue
|
|
92
|
+
.split(path.delimiter)
|
|
93
|
+
.filter(Boolean)
|
|
94
|
+
.flatMap((directory) => candidates.map((candidate) => path.join(directory, candidate)));
|
|
95
|
+
}
|
|
96
|
+
export function findExecutablePath(command, env, platform = process.platform) {
|
|
97
|
+
return candidateExecutablePaths(command, env, platform).find(isFile);
|
|
98
|
+
}
|
|
99
|
+
function resolveCommandForSpawn(command, env, processName) {
|
|
100
|
+
const resolved = findExecutablePath(command, env);
|
|
101
|
+
if (resolved) {
|
|
102
|
+
const extension = path.extname(resolved).toLowerCase();
|
|
103
|
+
if (isWindowsPlatform() && [".bat", ".cmd"].includes(extension)) {
|
|
104
|
+
return {
|
|
105
|
+
command: envValue(env, "COMSPEC") ?? "cmd.exe",
|
|
106
|
+
argsPrefix: ["/d", "/s", "/c", resolved],
|
|
107
|
+
displayCommand: resolved,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
command: resolved,
|
|
112
|
+
argsPrefix: [],
|
|
113
|
+
displayCommand: resolved,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
throw new ProcessSupervisorError([
|
|
117
|
+
`Cannot start ${processName}: executable not found: ${command}.`,
|
|
118
|
+
"Make sure it is installed and available on PATH,",
|
|
119
|
+
"or update the configured command in dev-nexus.home.json.",
|
|
120
|
+
].join(" "));
|
|
121
|
+
}
|
|
122
|
+
function recordChildError(child, lifecycleLog) {
|
|
123
|
+
child.once("error", (error) => {
|
|
124
|
+
if (!lifecycleLog.writableEnded) {
|
|
125
|
+
writeLifecycleEvent(lifecycleLog, {
|
|
126
|
+
event: "error",
|
|
127
|
+
message: error.message,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
export function startManagedProcess(options) {
|
|
133
|
+
assertNonEmptyString(options.name, "name");
|
|
134
|
+
assertNonEmptyString(options.command, "command");
|
|
135
|
+
const args = [...(options.args ?? [])];
|
|
136
|
+
const childEnv = options.env ? { ...process.env, ...options.env } : process.env;
|
|
137
|
+
const resolvedCommand = resolveCommandForSpawn(options.command, childEnv, options.name);
|
|
138
|
+
const spawnArgs = [...resolvedCommand.argsPrefix, ...args];
|
|
139
|
+
const logDirectory = path.resolve(options.logDirectory);
|
|
140
|
+
fs.mkdirSync(logDirectory, { recursive: true });
|
|
141
|
+
const logPaths = processLogPaths(logDirectory, options.name);
|
|
142
|
+
const logFlags = options.appendLogs ? "a" : "w";
|
|
143
|
+
const release = options.release ?? false;
|
|
144
|
+
const detached = options.detached ?? (release && defaultDetachedForReleasedProcess());
|
|
145
|
+
const lifecycleLog = fs.createWriteStream(logPaths.lifecycle, {
|
|
146
|
+
flags: logFlags,
|
|
147
|
+
});
|
|
148
|
+
if (release) {
|
|
149
|
+
const stdoutFd = openLogFile(logPaths.stdout, options.appendLogs ?? false);
|
|
150
|
+
const stderrFd = openLogFile(logPaths.stderr, options.appendLogs ?? false);
|
|
151
|
+
const child = spawn(resolvedCommand.command, spawnArgs, {
|
|
152
|
+
cwd: options.cwd,
|
|
153
|
+
env: childEnv,
|
|
154
|
+
detached,
|
|
155
|
+
shell: false,
|
|
156
|
+
stdio: ["ignore", stdoutFd, stderrFd],
|
|
157
|
+
windowsHide: true,
|
|
158
|
+
});
|
|
159
|
+
recordChildError(child, lifecycleLog);
|
|
160
|
+
fs.closeSync(stdoutFd);
|
|
161
|
+
fs.closeSync(stderrFd);
|
|
162
|
+
if (!child.pid) {
|
|
163
|
+
lifecycleLog.end();
|
|
164
|
+
throw new ProcessSupervisorError(`Failed to capture pid for process: ${options.name}`);
|
|
165
|
+
}
|
|
166
|
+
const startedAt = new Date().toISOString();
|
|
167
|
+
writeLifecycleEvent(lifecycleLog, {
|
|
168
|
+
event: "started",
|
|
169
|
+
name: options.name,
|
|
170
|
+
command: resolvedCommand.displayCommand,
|
|
171
|
+
args,
|
|
172
|
+
spawnCommand: resolvedCommand.command,
|
|
173
|
+
spawnArgs,
|
|
174
|
+
pid: child.pid,
|
|
175
|
+
cwd: options.cwd,
|
|
176
|
+
released: true,
|
|
177
|
+
detached,
|
|
178
|
+
});
|
|
179
|
+
lifecycleLog.end();
|
|
180
|
+
child.unref();
|
|
181
|
+
return {
|
|
182
|
+
name: options.name,
|
|
183
|
+
command: resolvedCommand.displayCommand,
|
|
184
|
+
args,
|
|
185
|
+
pid: child.pid,
|
|
186
|
+
startedAt,
|
|
187
|
+
logPaths,
|
|
188
|
+
child,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const stdoutLog = fs.createWriteStream(logPaths.stdout, { flags: logFlags });
|
|
192
|
+
const stderrLog = fs.createWriteStream(logPaths.stderr, { flags: logFlags });
|
|
193
|
+
const child = spawn(resolvedCommand.command, spawnArgs, {
|
|
194
|
+
cwd: options.cwd,
|
|
195
|
+
env: childEnv,
|
|
196
|
+
detached,
|
|
197
|
+
shell: false,
|
|
198
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
199
|
+
windowsHide: true,
|
|
200
|
+
});
|
|
201
|
+
recordChildError(child, lifecycleLog);
|
|
202
|
+
child.stdout?.pipe(stdoutLog);
|
|
203
|
+
child.stderr?.pipe(stderrLog);
|
|
204
|
+
if (!child.pid) {
|
|
205
|
+
stdoutLog.end();
|
|
206
|
+
stderrLog.end();
|
|
207
|
+
lifecycleLog.end();
|
|
208
|
+
throw new ProcessSupervisorError(`Failed to capture pid for process: ${options.name}`);
|
|
209
|
+
}
|
|
210
|
+
const startedAt = new Date().toISOString();
|
|
211
|
+
writeLifecycleEvent(lifecycleLog, {
|
|
212
|
+
event: "started",
|
|
213
|
+
name: options.name,
|
|
214
|
+
command: resolvedCommand.displayCommand,
|
|
215
|
+
args,
|
|
216
|
+
spawnCommand: resolvedCommand.command,
|
|
217
|
+
spawnArgs,
|
|
218
|
+
pid: child.pid,
|
|
219
|
+
cwd: options.cwd,
|
|
220
|
+
detached,
|
|
221
|
+
});
|
|
222
|
+
child.once("close", (exitCode, signal) => {
|
|
223
|
+
writeLifecycleEvent(lifecycleLog, {
|
|
224
|
+
event: "closed",
|
|
225
|
+
exitCode,
|
|
226
|
+
signal,
|
|
227
|
+
});
|
|
228
|
+
lifecycleLog.end();
|
|
229
|
+
});
|
|
230
|
+
return {
|
|
231
|
+
name: options.name,
|
|
232
|
+
command: resolvedCommand.displayCommand,
|
|
233
|
+
args,
|
|
234
|
+
pid: child.pid,
|
|
235
|
+
startedAt,
|
|
236
|
+
logPaths,
|
|
237
|
+
child,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
export function isProcessRunning(pid) {
|
|
241
|
+
assertPid(pid);
|
|
242
|
+
try {
|
|
243
|
+
process.kill(pid, 0);
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
const code = error.code;
|
|
248
|
+
if (code === "ESRCH") {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
if (code === "EPERM") {
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function sleep(durationMs) {
|
|
258
|
+
return new Promise((resolve) => setTimeout(resolve, durationMs));
|
|
259
|
+
}
|
|
260
|
+
async function waitForProcessExit(pid, timeoutMs, pollIntervalMs) {
|
|
261
|
+
const startedAt = Date.now();
|
|
262
|
+
while (Date.now() - startedAt <= timeoutMs) {
|
|
263
|
+
if (!isProcessRunning(pid)) {
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
await sleep(pollIntervalMs);
|
|
267
|
+
}
|
|
268
|
+
return !isProcessRunning(pid);
|
|
269
|
+
}
|
|
270
|
+
function runTaskkill(pid, force) {
|
|
271
|
+
return new Promise((resolve, reject) => {
|
|
272
|
+
const args = ["/pid", String(pid), "/t", ...(force ? ["/f"] : [])];
|
|
273
|
+
const child = spawn("taskkill.exe", args, {
|
|
274
|
+
shell: false,
|
|
275
|
+
windowsHide: true,
|
|
276
|
+
});
|
|
277
|
+
let stderr = "";
|
|
278
|
+
child.stderr?.on("data", (chunk) => {
|
|
279
|
+
stderr += chunk.toString("utf8");
|
|
280
|
+
});
|
|
281
|
+
child.once("error", reject);
|
|
282
|
+
child.once("close", (exitCode) => {
|
|
283
|
+
if (exitCode === 0 || !isProcessRunning(pid)) {
|
|
284
|
+
resolve();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
reject(new ProcessSupervisorError(`taskkill failed for pid ${pid}: ${stderr.trim() || exitCode}`));
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
export async function stopProcessByPid(pid, options = {}) {
|
|
292
|
+
assertPid(pid);
|
|
293
|
+
const timeoutMs = options.timeoutMs ?? 5_000;
|
|
294
|
+
const pollIntervalMs = options.pollIntervalMs ?? 100;
|
|
295
|
+
const killTree = options.killTree ?? defaultKillTreeForPlatform();
|
|
296
|
+
const method = stopMethodForPlatform(killTree);
|
|
297
|
+
if (!isProcessRunning(pid)) {
|
|
298
|
+
return { pid, stopped: true, alreadyExited: true, method };
|
|
299
|
+
}
|
|
300
|
+
if (method === "taskkill") {
|
|
301
|
+
await runTaskkill(pid, options.force ?? true);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
process.kill(pid, options.signal ?? "SIGTERM");
|
|
305
|
+
}
|
|
306
|
+
let stopped = await waitForProcessExit(pid, timeoutMs, pollIntervalMs);
|
|
307
|
+
if (!stopped && options.force && method === "process.kill") {
|
|
308
|
+
process.kill(pid, "SIGKILL");
|
|
309
|
+
stopped = await waitForProcessExit(pid, timeoutMs, pollIntervalMs);
|
|
310
|
+
}
|
|
311
|
+
return { pid, stopped, alreadyExited: false, method };
|
|
312
|
+
}
|
|
313
|
+
export function isTcpPortListening(port, timeoutMs = 200) {
|
|
314
|
+
return new Promise((resolve) => {
|
|
315
|
+
const socket = net.createConnection({ host: "127.0.0.1", port });
|
|
316
|
+
let settled = false;
|
|
317
|
+
const finish = (listening) => {
|
|
318
|
+
if (settled) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
settled = true;
|
|
322
|
+
socket.destroy();
|
|
323
|
+
resolve(listening);
|
|
324
|
+
};
|
|
325
|
+
socket.setTimeout(timeoutMs, () => finish(false));
|
|
326
|
+
socket.once("connect", () => finish(true));
|
|
327
|
+
socket.once("error", () => finish(false));
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
function powershellSingleQuotedString(value) {
|
|
331
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
332
|
+
}
|
|
333
|
+
export function findProcessListeningOnPort(port, options = {}) {
|
|
334
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
|
|
335
|
+
throw new ProcessSupervisorError("port owner lookup port must be an integer between 1 and 65535");
|
|
336
|
+
}
|
|
337
|
+
const platform = options.platform ?? process.platform;
|
|
338
|
+
if (!isWindowsPlatform(platform)) {
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
const allowedProcessNames = options.allowedProcessNames ?? [];
|
|
342
|
+
const allowedLiteral = allowedProcessNames
|
|
343
|
+
.map(powershellSingleQuotedString)
|
|
344
|
+
.join(", ");
|
|
345
|
+
const script = [
|
|
346
|
+
"$ErrorActionPreference = 'SilentlyContinue'",
|
|
347
|
+
`$allowed = @(${allowedLiteral})`,
|
|
348
|
+
"function Test-AllowedProcessName($name) {",
|
|
349
|
+
" if ($allowed.Count -eq 0) { return $true }",
|
|
350
|
+
" foreach ($allowedName in $allowed) {",
|
|
351
|
+
" if ($name -eq $allowedName -or $name -like $allowedName) { return $true }",
|
|
352
|
+
" }",
|
|
353
|
+
" return $false",
|
|
354
|
+
"}",
|
|
355
|
+
`$connection = Get-NetTCPConnection -LocalPort ${port} -State Listen | Select-Object -First 1`,
|
|
356
|
+
"if ($connection) {",
|
|
357
|
+
" $owner = Get-Process -Id $connection.OwningProcess",
|
|
358
|
+
" if ($owner -and (Test-AllowedProcessName $owner.ProcessName)) {",
|
|
359
|
+
" [Console]::Out.Write($connection.OwningProcess)",
|
|
360
|
+
" }",
|
|
361
|
+
"}",
|
|
362
|
+
].join("; ");
|
|
363
|
+
const result = spawnSync("powershell.exe", ["-NoProfile", "-Command", script], {
|
|
364
|
+
encoding: "utf8",
|
|
365
|
+
shell: false,
|
|
366
|
+
timeout: options.timeoutMs ?? 2_000,
|
|
367
|
+
windowsHide: true,
|
|
368
|
+
});
|
|
369
|
+
const pid = Number(result.stdout.trim());
|
|
370
|
+
return Number.isInteger(pid) && pid > 0
|
|
371
|
+
? { pid, platform, method: "powershell" }
|
|
372
|
+
: undefined;
|
|
373
|
+
}
|
|
374
|
+
function healthUrl(options) {
|
|
375
|
+
const host = options.host ?? "127.0.0.1";
|
|
376
|
+
const pathName = options.path ?? "/";
|
|
377
|
+
return `http://${host}:${options.port}${pathName.startsWith("/") ? pathName : `/${pathName}`}`;
|
|
378
|
+
}
|
|
379
|
+
export function checkHttpPort(options) {
|
|
380
|
+
if (!Number.isInteger(options.port) ||
|
|
381
|
+
options.port < 1 ||
|
|
382
|
+
options.port > 65_535) {
|
|
383
|
+
throw new ProcessSupervisorError("HTTP health check port must be an integer between 1 and 65535");
|
|
384
|
+
}
|
|
385
|
+
const url = healthUrl(options);
|
|
386
|
+
const startedAt = Date.now();
|
|
387
|
+
const healthyStatusMin = options.healthyStatusMin ?? 200;
|
|
388
|
+
const healthyStatusMax = options.healthyStatusMax ?? 399;
|
|
389
|
+
return new Promise((resolve) => {
|
|
390
|
+
let settled = false;
|
|
391
|
+
const finish = (result) => {
|
|
392
|
+
if (settled) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
settled = true;
|
|
396
|
+
resolve({
|
|
397
|
+
url,
|
|
398
|
+
durationMs: Date.now() - startedAt,
|
|
399
|
+
...result,
|
|
400
|
+
});
|
|
401
|
+
};
|
|
402
|
+
const request = http.get(url, (response) => {
|
|
403
|
+
response.resume();
|
|
404
|
+
const statusCode = response.statusCode;
|
|
405
|
+
finish({
|
|
406
|
+
ok: statusCode !== undefined &&
|
|
407
|
+
statusCode >= healthyStatusMin &&
|
|
408
|
+
statusCode <= healthyStatusMax,
|
|
409
|
+
...(statusCode !== undefined ? { statusCode } : {}),
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
request.setTimeout(options.timeoutMs ?? 1_000, () => {
|
|
413
|
+
request.destroy();
|
|
414
|
+
finish({ ok: false, error: "HTTP health check timed out" });
|
|
415
|
+
});
|
|
416
|
+
request.once("error", (error) => {
|
|
417
|
+
finish({ ok: false, error: error.message });
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
export async function waitForHttpPort(options) {
|
|
422
|
+
const totalTimeoutMs = options.totalTimeoutMs ?? 30_000;
|
|
423
|
+
const intervalMs = options.intervalMs ?? 250;
|
|
424
|
+
const startedAt = Date.now();
|
|
425
|
+
let lastResult;
|
|
426
|
+
while (Date.now() - startedAt <= totalTimeoutMs) {
|
|
427
|
+
lastResult = await checkHttpPort(options);
|
|
428
|
+
if (lastResult.ok) {
|
|
429
|
+
return lastResult;
|
|
430
|
+
}
|
|
431
|
+
await sleep(intervalMs);
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
ok: false,
|
|
435
|
+
url: healthUrl(options),
|
|
436
|
+
durationMs: Date.now() - startedAt,
|
|
437
|
+
statusCode: lastResult?.statusCode,
|
|
438
|
+
error: lastResult?.error ?? "Timed out waiting for HTTP port",
|
|
439
|
+
};
|
|
440
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const defaultVibeKanbanHost = "127.0.0.1";
|
|
2
|
+
export declare const vibeKanbanPinnedVersion = "0.1.43";
|
|
3
|
+
export interface VibeKanbanApiOptions {
|
|
4
|
+
host?: string;
|
|
5
|
+
port: number;
|
|
6
|
+
fetch?: typeof fetch;
|
|
7
|
+
}
|
|
8
|
+
export declare class VibeKanbanApiError extends Error {
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
11
|
+
export declare function vibeKanbanApiBaseUrl(options: VibeKanbanApiOptions): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const defaultVibeKanbanHost = "127.0.0.1";
|
|
2
|
+
export const vibeKanbanPinnedVersion = "0.1.43";
|
|
3
|
+
export class VibeKanbanApiError extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "VibeKanbanApiError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function vibeKanbanApiBaseUrl(options) {
|
|
10
|
+
if (!Number.isInteger(options.port) || options.port < 1 || options.port > 65_535) {
|
|
11
|
+
throw new VibeKanbanApiError("Vibe Kanban port must be an integer between 1 and 65535");
|
|
12
|
+
}
|
|
13
|
+
return `http://${options.host ?? defaultVibeKanbanHost}:${options.port}`;
|
|
14
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type VibeKanbanApiOptions } from "./vibeKanbanApi.js";
|
|
2
|
+
export interface VibeKanbanLocalAuthCredentials {
|
|
3
|
+
email: string;
|
|
4
|
+
password: string;
|
|
5
|
+
envFile?: string;
|
|
6
|
+
}
|
|
7
|
+
export type VibeKanbanAutoLoginStatus = "already-logged-in" | "logged-in" | "skipped";
|
|
8
|
+
export interface VibeKanbanAutoLoginResult {
|
|
9
|
+
status: VibeKanbanAutoLoginStatus;
|
|
10
|
+
attempted: boolean;
|
|
11
|
+
loggedIn: boolean;
|
|
12
|
+
email?: string;
|
|
13
|
+
reason?: string;
|
|
14
|
+
raw?: {
|
|
15
|
+
status?: unknown;
|
|
16
|
+
login?: unknown;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export interface EnsureVibeKanbanLocalLoginOptions extends VibeKanbanApiOptions {
|
|
20
|
+
credentials?: VibeKanbanLocalAuthCredentials | null;
|
|
21
|
+
}
|
|
22
|
+
export declare class VibeKanbanAuthError extends Error {
|
|
23
|
+
constructor(message: string);
|
|
24
|
+
}
|
|
25
|
+
export declare function ensureVibeKanbanLocalLogin(options: EnsureVibeKanbanLocalLoginOptions): Promise<VibeKanbanAutoLoginResult>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { vibeKanbanApiBaseUrl, } from "./vibeKanbanApi.js";
|
|
2
|
+
export class VibeKanbanAuthError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "VibeKanbanAuthError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
function assertRecord(value, pathName) {
|
|
9
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
10
|
+
throw new VibeKanbanAuthError(`${pathName} must be an object`);
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
function parseApiSuccess(value) {
|
|
15
|
+
const response = assertRecord(value, "response");
|
|
16
|
+
if (response.success !== true) {
|
|
17
|
+
const message = typeof response.message === "string"
|
|
18
|
+
? response.message
|
|
19
|
+
: typeof response.error === "string"
|
|
20
|
+
? response.error
|
|
21
|
+
: "Vibe Kanban auth request failed";
|
|
22
|
+
throw new VibeKanbanAuthError(message);
|
|
23
|
+
}
|
|
24
|
+
return response.data;
|
|
25
|
+
}
|
|
26
|
+
async function readJsonResponse(response) {
|
|
27
|
+
const text = await response.text();
|
|
28
|
+
if (text.trim().length === 0) {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
return JSON.parse(text.replace(/^\uFEFF/u, ""));
|
|
32
|
+
}
|
|
33
|
+
async function requestJson(fetchImpl, url, init) {
|
|
34
|
+
const response = await fetchImpl(url, init);
|
|
35
|
+
const body = await readJsonResponse(response);
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
const detail = body && typeof body === "object" ? `: ${JSON.stringify(body)}` : "";
|
|
38
|
+
throw new VibeKanbanAuthError(`Vibe Kanban auth request failed with HTTP ${response.status}${detail}`);
|
|
39
|
+
}
|
|
40
|
+
return body;
|
|
41
|
+
}
|
|
42
|
+
async function getAuthStatus(options, fetchImpl) {
|
|
43
|
+
const url = new URL("/api/auth/status", vibeKanbanApiBaseUrl(options));
|
|
44
|
+
const raw = await requestJson(fetchImpl, url.toString());
|
|
45
|
+
const data = assertRecord(parseApiSuccess(raw), "auth status");
|
|
46
|
+
if (typeof data.logged_in !== "boolean") {
|
|
47
|
+
throw new VibeKanbanAuthError("auth status.logged_in must be a boolean");
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
loggedIn: data.logged_in,
|
|
51
|
+
raw,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async function localLogin(options, credentials, fetchImpl) {
|
|
55
|
+
const url = new URL("/api/auth/local/login", vibeKanbanApiBaseUrl(options));
|
|
56
|
+
return requestJson(fetchImpl, url.toString(), {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: {
|
|
59
|
+
"content-type": "application/json",
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify({
|
|
62
|
+
email: credentials.email,
|
|
63
|
+
password: credentials.password,
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export async function ensureVibeKanbanLocalLogin(options) {
|
|
68
|
+
const credentials = options.credentials ?? undefined;
|
|
69
|
+
if (!credentials) {
|
|
70
|
+
return {
|
|
71
|
+
status: "skipped",
|
|
72
|
+
attempted: false,
|
|
73
|
+
loggedIn: false,
|
|
74
|
+
reason: "No Vibe Kanban local-auth credentials were provided.",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
78
|
+
const status = await getAuthStatus(options, fetchImpl);
|
|
79
|
+
if (status.loggedIn) {
|
|
80
|
+
return {
|
|
81
|
+
status: "already-logged-in",
|
|
82
|
+
attempted: false,
|
|
83
|
+
loggedIn: true,
|
|
84
|
+
email: credentials.email,
|
|
85
|
+
raw: {
|
|
86
|
+
status: status.raw,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const login = await localLogin(options, credentials, fetchImpl);
|
|
91
|
+
return {
|
|
92
|
+
status: "logged-in",
|
|
93
|
+
attempted: true,
|
|
94
|
+
loggedIn: true,
|
|
95
|
+
email: credentials.email,
|
|
96
|
+
raw: {
|
|
97
|
+
status: status.raw,
|
|
98
|
+
login,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type VibeKanbanApiOptions } from "./vibeKanbanApi.js";
|
|
2
|
+
export interface VibeKanbanOrganization {
|
|
3
|
+
id: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
is_personal?: boolean;
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
}
|
|
8
|
+
export interface VibeKanbanBoard {
|
|
9
|
+
id: string;
|
|
10
|
+
organization_id?: string;
|
|
11
|
+
name: string;
|
|
12
|
+
color?: string;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
export interface EnsureVibeKanbanBoardOptions extends VibeKanbanApiOptions {
|
|
16
|
+
name: string;
|
|
17
|
+
organizationId?: string;
|
|
18
|
+
color?: string;
|
|
19
|
+
uuid?: () => string;
|
|
20
|
+
}
|
|
21
|
+
export interface EnsureVibeKanbanBoardResult {
|
|
22
|
+
boardId: string;
|
|
23
|
+
board: VibeKanbanBoard;
|
|
24
|
+
organization: VibeKanbanOrganization;
|
|
25
|
+
created: boolean;
|
|
26
|
+
raw: {
|
|
27
|
+
info?: unknown;
|
|
28
|
+
organizations?: unknown;
|
|
29
|
+
projects?: unknown;
|
|
30
|
+
create?: unknown;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export declare class VibeKanbanBoardAdapterError extends Error {
|
|
34
|
+
constructor(message: string);
|
|
35
|
+
}
|
|
36
|
+
export declare function ensureVibeKanbanBoard(options: EnsureVibeKanbanBoardOptions): Promise<EnsureVibeKanbanBoardResult>;
|