@spencer-kit/coder-studio 0.1.3

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/lib/http.mjs ADDED
@@ -0,0 +1,89 @@
1
+ // @ts-nocheck
2
+ import { setTimeout as delay } from 'node:timers/promises';
3
+ import { buildEndpoint } from './config.mjs';
4
+ function adminEndpoint(endpoint) {
5
+ const url = new URL(endpoint);
6
+ const host = url.hostname;
7
+ if (host === '0.0.0.0' || host === '::' || host === '[::]') {
8
+ url.hostname = '127.0.0.1';
9
+ }
10
+ return url.toString().replace(/\/$/, '');
11
+ }
12
+ async function requestJson(url, init = {}) {
13
+ const response = await fetch(url, {
14
+ ...init,
15
+ headers: {
16
+ 'content-type': 'application/json',
17
+ ...(init.headers ?? {}),
18
+ },
19
+ });
20
+ let body = null;
21
+ try {
22
+ body = await response.json();
23
+ }
24
+ catch {
25
+ body = null;
26
+ }
27
+ if (!response.ok || body?.ok === false) {
28
+ const error = body?.error || `${response.status}`;
29
+ throw new Error(error);
30
+ }
31
+ return body?.data ?? body ?? null;
32
+ }
33
+ export async function fetchHealth(endpoint) {
34
+ const response = await fetch(`${endpoint.replace(/\/$/, '')}/health`);
35
+ if (!response.ok) {
36
+ throw new Error(`health_http_${response.status}`);
37
+ }
38
+ return response.json();
39
+ }
40
+ export async function waitForHealth(endpoint, { timeoutMs = 15000, intervalMs = 250 } = {}) {
41
+ const startedAt = Date.now();
42
+ let lastError = null;
43
+ while (Date.now() - startedAt < timeoutMs) {
44
+ try {
45
+ return await fetchHealth(endpoint);
46
+ }
47
+ catch (error) {
48
+ lastError = error;
49
+ await delay(intervalMs);
50
+ }
51
+ }
52
+ throw lastError ?? new Error('health_timeout');
53
+ }
54
+ export async function requestShutdown(endpoint) {
55
+ const response = await fetch(`${endpoint.replace(/\/$/, '')}/api/system/shutdown`, {
56
+ method: 'POST',
57
+ headers: {
58
+ 'content-type': 'application/json'
59
+ }
60
+ });
61
+ if (!response.ok) {
62
+ throw new Error(`shutdown_http_${response.status}`);
63
+ }
64
+ return response.json().catch(() => ({ ok: true }));
65
+ }
66
+ export async function fetchAdminConfig(endpoint) {
67
+ return requestJson(`${adminEndpoint(endpoint)}/api/system/config`, { method: 'GET' });
68
+ }
69
+ export async function patchAdminConfig(endpoint, updates) {
70
+ return requestJson(`${adminEndpoint(endpoint)}/api/system/config`, {
71
+ method: 'PATCH',
72
+ body: JSON.stringify({ updates }),
73
+ });
74
+ }
75
+ export async function fetchAdminAuthStatus(endpoint) {
76
+ return requestJson(`${adminEndpoint(endpoint)}/api/system/auth/status`, { method: 'GET' });
77
+ }
78
+ export async function fetchAdminIpBlocks(endpoint) {
79
+ return requestJson(`${adminEndpoint(endpoint)}/api/system/auth/ip-blocks`, { method: 'GET' });
80
+ }
81
+ export async function unblockAdminIp(endpoint, payload) {
82
+ return requestJson(`${adminEndpoint(endpoint)}/api/system/auth/ip-blocks/unblock`, {
83
+ method: 'POST',
84
+ body: JSON.stringify(payload),
85
+ });
86
+ }
87
+ export function buildAdminEndpoint(host, port) {
88
+ return adminEndpoint(buildEndpoint(host, port));
89
+ }
@@ -0,0 +1,50 @@
1
+ // @ts-nocheck
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { createRequire } from 'node:module';
5
+ const require = createRequire(import.meta.url);
6
+ const PLATFORM_PACKAGES = {
7
+ 'linux:x64': '@spencer-kit/coder-studio-linux-x64',
8
+ 'darwin:arm64': '@spencer-kit/coder-studio-darwin-arm64',
9
+ 'darwin:x64': '@spencer-kit/coder-studio-darwin-x64',
10
+ 'win32:x64': '@spencer-kit/coder-studio-win32-x64'
11
+ };
12
+ export function resolvePlatformPackage(options = {}) {
13
+ const { env = process.env, platform = process.platform, arch = process.arch } = options;
14
+ const binaryName = platform === 'win32' ? 'coder-studio.exe' : 'coder-studio';
15
+ const binaryPath = env.CODER_STUDIO_BINARY_PATH ? path.resolve(env.CODER_STUDIO_BINARY_PATH) : '';
16
+ const distDir = env.CODER_STUDIO_DIST_DIR ? path.resolve(env.CODER_STUDIO_DIST_DIR) : '';
17
+ if (binaryPath) {
18
+ return {
19
+ packageName: 'override',
20
+ packageDir: path.dirname(binaryPath),
21
+ binaryPath,
22
+ distDir,
23
+ binaryName
24
+ };
25
+ }
26
+ const packageName = PLATFORM_PACKAGES[`${platform}:${arch}`];
27
+ if (!packageName) {
28
+ throw new Error(`Unsupported platform: ${platform}/${arch}`);
29
+ }
30
+ const packageJsonPath = require.resolve(`${packageName}/package.json`);
31
+ const packageDir = path.dirname(packageJsonPath);
32
+ return {
33
+ packageName,
34
+ packageDir,
35
+ binaryPath: path.join(packageDir, 'bin', binaryName),
36
+ distDir: path.join(packageDir, 'dist'),
37
+ binaryName
38
+ };
39
+ }
40
+ export function assertRuntimeBundle(bundle) {
41
+ if (!bundle.binaryPath || !fs.existsSync(bundle.binaryPath)) {
42
+ throw new Error(`Runtime binary not found: ${bundle.binaryPath || 'unknown'}`);
43
+ }
44
+ if (process.platform !== 'win32') {
45
+ fs.chmodSync(bundle.binaryPath, 0o755);
46
+ }
47
+ if (!bundle.distDir || !fs.existsSync(bundle.distDir)) {
48
+ throw new Error(`Frontend dist not found: ${bundle.distDir || 'unknown'}`);
49
+ }
50
+ }
@@ -0,0 +1,76 @@
1
+ // @ts-nocheck
2
+ import fs from 'node:fs/promises';
3
+ import { execFile, spawn } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+ const execFileAsync = promisify(execFile);
6
+ export function sleep(ms) {
7
+ return new Promise((resolve) => setTimeout(resolve, ms));
8
+ }
9
+ export function isPidRunning(pid) {
10
+ if (!pid || !Number.isInteger(pid))
11
+ return false;
12
+ try {
13
+ process.kill(pid, 0);
14
+ return true;
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ export async function terminateProcess(pid, { force = false } = {}) {
21
+ if (!pid)
22
+ return;
23
+ if (process.platform === 'win32') {
24
+ const args = ['/PID', String(pid), '/T'];
25
+ if (force)
26
+ args.push('/F');
27
+ await execFileAsync('taskkill', args);
28
+ return;
29
+ }
30
+ process.kill(pid, force ? 'SIGKILL' : 'SIGTERM');
31
+ }
32
+ export async function waitForProcessExit(pid, timeoutMs = 8000) {
33
+ const startedAt = Date.now();
34
+ while (Date.now() - startedAt < timeoutMs) {
35
+ if (!isPidRunning(pid)) {
36
+ return true;
37
+ }
38
+ await sleep(200);
39
+ }
40
+ return !isPidRunning(pid);
41
+ }
42
+ export function spawnBackground(command, args, options = {}) {
43
+ return spawn(command, args, {
44
+ ...options,
45
+ detached: true,
46
+ stdio: options.stdio ?? 'ignore',
47
+ windowsHide: true
48
+ });
49
+ }
50
+ export function spawnForeground(command, args, options = {}) {
51
+ return spawn(command, args, {
52
+ ...options,
53
+ stdio: options.stdio ?? 'inherit',
54
+ windowsHide: true
55
+ });
56
+ }
57
+ export async function openExternal(targetUrl, env = process.env) {
58
+ if (env.CODER_STUDIO_OPEN_COMMAND) {
59
+ const [command, ...extraArgs] = env.CODER_STUDIO_OPEN_COMMAND.split(' ');
60
+ await execFileAsync(command, [...extraArgs, targetUrl]);
61
+ return;
62
+ }
63
+ if (process.platform === 'darwin') {
64
+ await execFileAsync('open', [targetUrl]);
65
+ return;
66
+ }
67
+ if (process.platform === 'win32') {
68
+ await execFileAsync('cmd', ['/c', 'start', '', targetUrl]);
69
+ return;
70
+ }
71
+ await execFileAsync('xdg-open', [targetUrl]);
72
+ }
73
+ export async function ensureFile(pathname) {
74
+ const handle = await fs.open(pathname, 'a');
75
+ return handle;
76
+ }
@@ -0,0 +1,350 @@
1
+ // @ts-nocheck
2
+ import { once } from 'node:events';
3
+ import fs from 'node:fs/promises';
4
+ import { buildEndpoint, DEFAULT_HOST, DEFAULT_LOG_TAIL_LINES, DEFAULT_PORT, resolveDataDir, resolveLogPath, resolveStateDir } from './config.mjs';
5
+ import { fetchHealth, requestShutdown, waitForHealth } from './http.mjs';
6
+ import { assertRuntimeBundle, resolvePlatformPackage } from './platform.mjs';
7
+ import { ensureFile, isPidRunning, openExternal, spawnBackground, spawnForeground, terminateProcess, waitForProcessExit } from './process-utils.mjs';
8
+ import { buildRuntimeState, clearRuntimeState, ensureStateDirs, readPackageVersion, readRuntimeState, readLogTail, writeRuntimeState } from './state.mjs';
9
+ const DEFAULT_START_TIMEOUT_MS = 15000;
10
+ function resolveStartTimeout(input, env) {
11
+ const candidate = input ?? env?.CODER_STUDIO_START_TIMEOUT_MS;
12
+ const timeout = Number(candidate);
13
+ return Number.isFinite(timeout) && timeout > 0 ? timeout : DEFAULT_START_TIMEOUT_MS;
14
+ }
15
+ function resolveOptions(input = {}) {
16
+ const stateDir = input.stateDir || resolveStateDir(input.env);
17
+ const env = input.env || process.env;
18
+ const host = input.host || DEFAULT_HOST;
19
+ const port = Number(input.port ?? DEFAULT_PORT);
20
+ const endpoint = input.endpoint || buildEndpoint(host, port);
21
+ const dataDir = input.dataDir || resolveDataDir(stateDir, env);
22
+ const logPath = input.logPath || resolveLogPath(stateDir);
23
+ const tailLines = Number(input.tailLines ?? DEFAULT_LOG_TAIL_LINES);
24
+ const timeoutMs = resolveStartTimeout(input.timeoutMs, env);
25
+ return {
26
+ ...input,
27
+ stateDir,
28
+ host,
29
+ port,
30
+ endpoint,
31
+ dataDir,
32
+ logPath,
33
+ timeoutMs,
34
+ tailLines: Number.isFinite(tailLines) && tailLines > 0 ? tailLines : DEFAULT_LOG_TAIL_LINES,
35
+ openCommand: input.openCommand || null,
36
+ env
37
+ };
38
+ }
39
+ async function probeManagedStatus(options) {
40
+ const runtime = await readRuntimeState(options.stateDir);
41
+ if (!runtime)
42
+ return null;
43
+ const endpoint = runtime.endpoint || options.endpoint;
44
+ const pid = Number(runtime.pid || 0);
45
+ const running = pid > 0 && isPidRunning(pid);
46
+ if (!running) {
47
+ await clearRuntimeState(options.stateDir);
48
+ return {
49
+ status: 'stopped',
50
+ managed: true,
51
+ stale: true,
52
+ endpoint,
53
+ pid,
54
+ runtime
55
+ };
56
+ }
57
+ try {
58
+ const health = await fetchHealth(endpoint);
59
+ return {
60
+ status: 'running',
61
+ managed: true,
62
+ stale: false,
63
+ endpoint,
64
+ pid,
65
+ runtime,
66
+ health
67
+ };
68
+ }
69
+ catch (error) {
70
+ return {
71
+ status: 'degraded',
72
+ managed: true,
73
+ stale: false,
74
+ endpoint,
75
+ pid,
76
+ runtime,
77
+ error: error instanceof Error ? error.message : String(error)
78
+ };
79
+ }
80
+ }
81
+ export async function getStatus(input = {}) {
82
+ const options = resolveOptions(input);
83
+ const managed = await probeManagedStatus(options);
84
+ if (managed) {
85
+ return {
86
+ ...managed,
87
+ stateDir: options.stateDir,
88
+ logPath: managed.runtime?.logPath || options.logPath,
89
+ dataDir: options.dataDir
90
+ };
91
+ }
92
+ try {
93
+ const health = await fetchHealth(options.endpoint);
94
+ return {
95
+ status: 'running',
96
+ managed: false,
97
+ stale: false,
98
+ endpoint: options.endpoint,
99
+ pid: null,
100
+ runtime: null,
101
+ health,
102
+ stateDir: options.stateDir,
103
+ logPath: options.logPath,
104
+ dataDir: options.dataDir
105
+ };
106
+ }
107
+ catch (error) {
108
+ return {
109
+ status: 'stopped',
110
+ managed: false,
111
+ stale: false,
112
+ endpoint: options.endpoint,
113
+ pid: null,
114
+ runtime: null,
115
+ error: error instanceof Error ? error.message : String(error),
116
+ stateDir: options.stateDir,
117
+ logPath: options.logPath,
118
+ dataDir: options.dataDir
119
+ };
120
+ }
121
+ }
122
+ async function waitForReady(endpoint, pid, timeoutMs) {
123
+ const startedAt = Date.now();
124
+ let lastError = null;
125
+ while (Date.now() - startedAt < timeoutMs) {
126
+ if (pid && !isPidRunning(pid)) {
127
+ throw new Error('runtime_exited_early');
128
+ }
129
+ try {
130
+ return await waitForHealth(endpoint, { timeoutMs: 500, intervalMs: 200 });
131
+ }
132
+ catch (error) {
133
+ lastError = error;
134
+ }
135
+ }
136
+ throw lastError ?? new Error('health_timeout');
137
+ }
138
+ function buildChildEnv(options, bundle) {
139
+ return {
140
+ ...process.env,
141
+ ...options.env,
142
+ CODER_STUDIO_HOST: options.host,
143
+ CODER_STUDIO_PORT: String(options.port),
144
+ CODER_STUDIO_DATA_DIR: options.dataDir,
145
+ CODER_STUDIO_DIST_DIR: bundle.distDir
146
+ };
147
+ }
148
+ async function writeStateForPid(options, bundle, pid) {
149
+ const version = await readPackageVersion();
150
+ const state = buildRuntimeState({
151
+ version,
152
+ pid,
153
+ endpoint: options.endpoint,
154
+ binaryPath: bundle.binaryPath,
155
+ logPath: options.logPath
156
+ });
157
+ await writeRuntimeState(options.stateDir, state);
158
+ return state;
159
+ }
160
+ async function cleanupIfManagedPid(options, pid) {
161
+ const runtime = await readRuntimeState(options.stateDir);
162
+ if (runtime && Number(runtime.pid) === Number(pid)) {
163
+ await clearRuntimeState(options.stateDir);
164
+ }
165
+ }
166
+ export async function startRuntime(input = {}) {
167
+ const options = resolveOptions(input);
168
+ await ensureStateDirs(options.stateDir, options.dataDir);
169
+ const current = await getStatus(options);
170
+ if (current.status === 'running' || current.status === 'degraded') {
171
+ return {
172
+ changed: false,
173
+ ...current
174
+ };
175
+ }
176
+ const bundle = resolvePlatformPackage({ env: options.env });
177
+ assertRuntimeBundle(bundle);
178
+ const env = buildChildEnv(options, bundle);
179
+ if (options.foreground) {
180
+ const child = spawnForeground(bundle.binaryPath, [], {
181
+ cwd: options.stateDir,
182
+ env
183
+ });
184
+ if (!child.pid) {
185
+ throw new Error('runtime_pid_missing');
186
+ }
187
+ const exitPromise = once(child, 'exit').then(([code, signal]) => ({ code, signal }));
188
+ const errorPromise = once(child, 'error').then(([error]) => { throw error; });
189
+ await Promise.race([
190
+ waitForReady(options.endpoint, child.pid, options.timeoutMs),
191
+ exitPromise.then(() => {
192
+ throw new Error('runtime_exited_early');
193
+ }),
194
+ errorPromise
195
+ ]);
196
+ await writeStateForPid(options, bundle, child.pid);
197
+ if (typeof options.onReady === 'function') {
198
+ await options.onReady({ endpoint: options.endpoint, pid: child.pid, logPath: options.logPath });
199
+ }
200
+ const forward = (signal) => {
201
+ try {
202
+ child.kill(signal);
203
+ }
204
+ catch {
205
+ // Ignore when already stopped.
206
+ }
207
+ };
208
+ process.on('SIGINT', forward);
209
+ process.on('SIGTERM', forward);
210
+ try {
211
+ const { code, signal } = await Promise.race([exitPromise, errorPromise]);
212
+ await cleanupIfManagedPid(options, child.pid);
213
+ return {
214
+ changed: true,
215
+ status: code === 0 ? 'stopped' : 'failed',
216
+ endpoint: options.endpoint,
217
+ pid: child.pid,
218
+ exitCode: code,
219
+ signal
220
+ };
221
+ }
222
+ finally {
223
+ process.off('SIGINT', forward);
224
+ process.off('SIGTERM', forward);
225
+ }
226
+ }
227
+ const logHandle = await ensureFile(options.logPath);
228
+ const child = spawnBackground(bundle.binaryPath, [], {
229
+ cwd: options.stateDir,
230
+ env,
231
+ stdio: ['ignore', logHandle.fd, logHandle.fd]
232
+ });
233
+ if (!child.pid) {
234
+ await logHandle.close();
235
+ throw new Error('runtime_pid_missing');
236
+ }
237
+ child.unref();
238
+ await logHandle.close();
239
+ const exitPromise = once(child, 'exit').then(([code, signal]) => ({ code, signal }));
240
+ const errorPromise = once(child, 'error').then(([error]) => {
241
+ throw error;
242
+ });
243
+ try {
244
+ await Promise.race([
245
+ waitForReady(options.endpoint, child.pid, options.timeoutMs),
246
+ exitPromise.then(() => {
247
+ throw new Error('runtime_exited_early');
248
+ }),
249
+ errorPromise
250
+ ]);
251
+ await writeStateForPid(options, bundle, child.pid);
252
+ return {
253
+ changed: true,
254
+ status: 'running',
255
+ endpoint: options.endpoint,
256
+ pid: child.pid,
257
+ logPath: options.logPath,
258
+ stateDir: options.stateDir,
259
+ dataDir: options.dataDir
260
+ };
261
+ }
262
+ catch (error) {
263
+ await Promise.allSettled([
264
+ terminateProcess(child.pid, { force: false }),
265
+ clearRuntimeState(options.stateDir)
266
+ ]);
267
+ throw error;
268
+ }
269
+ }
270
+ export async function stopRuntime(input = {}) {
271
+ const options = resolveOptions(input);
272
+ const status = await getStatus(options);
273
+ if (status.status === 'stopped') {
274
+ return {
275
+ changed: false,
276
+ ...status
277
+ };
278
+ }
279
+ let shutdownError = null;
280
+ try {
281
+ await requestShutdown(status.endpoint);
282
+ }
283
+ catch (error) {
284
+ shutdownError = error instanceof Error ? error.message : String(error);
285
+ }
286
+ if (status.pid) {
287
+ const gracefulExit = await waitForProcessExit(status.pid, 8000);
288
+ if (!gracefulExit) {
289
+ try {
290
+ await terminateProcess(status.pid, { force: false });
291
+ }
292
+ catch {
293
+ // Ignore fallback kill failure here and try force next.
294
+ }
295
+ const terminated = await waitForProcessExit(status.pid, 4000);
296
+ if (!terminated) {
297
+ await terminateProcess(status.pid, { force: true });
298
+ await waitForProcessExit(status.pid, 2000);
299
+ }
300
+ }
301
+ }
302
+ await clearRuntimeState(options.stateDir);
303
+ return {
304
+ changed: true,
305
+ status: 'stopped',
306
+ endpoint: status.endpoint,
307
+ pid: status.pid,
308
+ shutdownError
309
+ };
310
+ }
311
+ export async function restartRuntime(input = {}) {
312
+ await stopRuntime(input);
313
+ return startRuntime(input);
314
+ }
315
+ export async function openRuntime(input = {}) {
316
+ const options = resolveOptions(input);
317
+ const status = await getStatus(options);
318
+ const running = status.status === 'running' || status.status === 'degraded';
319
+ const active = running ? status : await startRuntime(options);
320
+ const openEnv = options.openCommand ? { ...options.env, CODER_STUDIO_OPEN_COMMAND: options.openCommand } : options.env;
321
+ await openExternal(active.endpoint, openEnv);
322
+ return active;
323
+ }
324
+ export async function readRuntimeLogs(input = {}) {
325
+ const options = resolveOptions(input);
326
+ return readLogTail(options.logPath, input.lines ?? options.tailLines ?? DEFAULT_LOG_TAIL_LINES);
327
+ }
328
+ export async function doctorRuntime(input = {}) {
329
+ const options = resolveOptions(input);
330
+ const bundle = (() => {
331
+ try {
332
+ return resolvePlatformPackage({ env: options.env });
333
+ }
334
+ catch (error) {
335
+ return { error: error instanceof Error ? error.message : String(error) };
336
+ }
337
+ })();
338
+ const status = await getStatus(options);
339
+ const logExists = await fs.stat(options.logPath).then(() => true).catch(() => false);
340
+ const runtime = await readRuntimeState(options.stateDir);
341
+ return {
342
+ status,
343
+ bundle,
344
+ stateDir: options.stateDir,
345
+ dataDir: options.dataDir,
346
+ logPath: options.logPath,
347
+ logExists,
348
+ runtime
349
+ };
350
+ }
package/lib/state.mjs ADDED
@@ -0,0 +1,75 @@
1
+ // @ts-nocheck
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { resolveLogPath, resolvePidPath, resolveRuntimePath } from './config.mjs';
6
+ const PACKAGE_JSON_PATH = fileURLToPath(new URL('../package.json', import.meta.url));
7
+ export async function readPackageVersion() {
8
+ const raw = await fs.readFile(PACKAGE_JSON_PATH, 'utf8');
9
+ return JSON.parse(raw).version;
10
+ }
11
+ export async function ensureStateDirs(stateDir, dataDir) {
12
+ await fs.mkdir(stateDir, { recursive: true });
13
+ await fs.mkdir(dataDir, { recursive: true });
14
+ }
15
+ export async function readRuntimeState(stateDir) {
16
+ try {
17
+ const raw = await fs.readFile(resolveRuntimePath(stateDir), 'utf8');
18
+ return JSON.parse(raw);
19
+ }
20
+ catch (error) {
21
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
22
+ return null;
23
+ }
24
+ throw error;
25
+ }
26
+ }
27
+ export async function writeRuntimeState(stateDir, state) {
28
+ const runtimePath = resolveRuntimePath(stateDir);
29
+ const pidPath = resolvePidPath(stateDir);
30
+ await fs.writeFile(runtimePath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
31
+ await fs.writeFile(pidPath, `${state.pid}\n`, 'utf8');
32
+ }
33
+ export async function clearRuntimeState(stateDir) {
34
+ await Promise.allSettled([
35
+ fs.rm(resolveRuntimePath(stateDir), { force: true }),
36
+ fs.rm(resolvePidPath(stateDir), { force: true })
37
+ ]);
38
+ }
39
+ export function buildRuntimeState({ version, pid, endpoint, binaryPath, logPath }) {
40
+ return {
41
+ version,
42
+ pid,
43
+ endpoint,
44
+ binaryPath,
45
+ logPath,
46
+ startedAt: new Date().toISOString()
47
+ };
48
+ }
49
+ export async function readLogTail(logPath, lineCount = 80) {
50
+ try {
51
+ const raw = await fs.readFile(logPath, 'utf8');
52
+ return raw.trimEnd().split(/\r?\n/).slice(-lineCount).join('\n');
53
+ }
54
+ catch (error) {
55
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
56
+ return '';
57
+ }
58
+ throw error;
59
+ }
60
+ }
61
+ export async function statIfExists(filePath) {
62
+ try {
63
+ return await fs.stat(filePath);
64
+ }
65
+ catch (error) {
66
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
67
+ return null;
68
+ }
69
+ throw error;
70
+ }
71
+ }
72
+ export function resolvePackageRoot() {
73
+ return path.dirname(PACKAGE_JSON_PATH);
74
+ }
75
+ export { resolveLogPath, resolvePidPath, resolveRuntimePath };