Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 10f942b577 | |||
| 76b547d37a | |||
| 1b2ff37097 | |||
| 4add0d88c6 | |||
| a8404dc096 | |||
| 891db36152 | |||
| 569c034b49 | |||
| 85fa282d2e | |||
| b75a112c95 | |||
| 606eff6d70 | |||
| 97305bd9af | |||
| 3f2c9df75d | |||
| 1511cfd595 | |||
| 362dc94582 | |||
| 9e7de3b4e0 |
@@ -9,33 +9,7 @@ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
// Inline the template builder (same logic as in init.ts) for isolated testing
|
||||
function buildWorkflowTemplate(name: string): string {
|
||||
return `import type { WorkflowDefinition } from "@uncaged/nerve-daemon";
|
||||
|
||||
const workflow: WorkflowDefinition = {
|
||||
roles: {
|
||||
main: {
|
||||
async execute(prompt, ctx) {
|
||||
ctx.log("${name} started");
|
||||
// TODO: implement your role logic here
|
||||
return { type: "done" };
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
moderate(thread, event) {
|
||||
if (event.type === "thread_start") {
|
||||
return { role: "main", prompt: {} };
|
||||
}
|
||||
return null; // workflow complete
|
||||
},
|
||||
};
|
||||
|
||||
export default workflow;
|
||||
`;
|
||||
}
|
||||
import { buildWorkflowTemplate } from "../commands/init.js";
|
||||
|
||||
let tmpDir: string;
|
||||
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
/**
|
||||
* Tests for nerve logs command — pure helper functions only.
|
||||
*
|
||||
* We test sliceLogs and buildLogFooter without touching the filesystem or
|
||||
* spawning a real process.
|
||||
*/
|
||||
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { DEFAULT_LOG_LINES, buildLogFooter, readAllLines, sliceLogs } from "../commands/logs.js";
|
||||
import { logsCommand } from "../commands/logs.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// sliceLogs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("sliceLogs", () => {
|
||||
const make = (n: number) => Array.from({ length: n }, (_, i) => `line ${i + 1}`);
|
||||
|
||||
it("returns empty result for empty array", () => {
|
||||
const r = sliceLogs([], 0, 50);
|
||||
expect(r.lines).toHaveLength(0);
|
||||
expect(r.total).toBe(0);
|
||||
expect(r.nextOffset).toBeNull();
|
||||
});
|
||||
|
||||
it("tail mode (offset=0): returns last N lines", () => {
|
||||
const lines = make(100);
|
||||
const r = sliceLogs(lines, 0, 10);
|
||||
expect(r.lines).toHaveLength(10);
|
||||
expect(r.lines[0]).toBe("line 91");
|
||||
expect(r.lines[9]).toBe("line 100");
|
||||
expect(r.startLine).toBe(91);
|
||||
expect(r.endLine).toBe(100);
|
||||
});
|
||||
|
||||
it("tail mode: when file shorter than limit, returns all", () => {
|
||||
const lines = make(20);
|
||||
const r = sliceLogs(lines, 0, 50);
|
||||
expect(r.lines).toHaveLength(20);
|
||||
expect(r.startLine).toBe(1);
|
||||
expect(r.endLine).toBe(20);
|
||||
expect(r.nextOffset).toBeNull();
|
||||
});
|
||||
|
||||
it("tail mode: provides nextOffset when earlier lines exist", () => {
|
||||
const lines = make(200);
|
||||
const r = sliceLogs(lines, 0, 50);
|
||||
expect(r.nextOffset).not.toBeNull();
|
||||
expect(r.nextOffset).toBe(151 - 50); // startLine=151, prev page starts at 101
|
||||
});
|
||||
|
||||
it("tail mode: nextOffset is null when showing from line 1", () => {
|
||||
const lines = make(40);
|
||||
const r = sliceLogs(lines, 0, 50);
|
||||
expect(r.nextOffset).toBeNull();
|
||||
});
|
||||
|
||||
it("offset mode: starts at given 1-based line number", () => {
|
||||
const lines = make(100);
|
||||
const r = sliceLogs(lines, 10, 5);
|
||||
expect(r.lines[0]).toBe("line 10");
|
||||
expect(r.startLine).toBe(10);
|
||||
expect(r.endLine).toBe(14);
|
||||
});
|
||||
|
||||
it("offset mode: clamps start to 0 for offset=1", () => {
|
||||
const lines = make(50);
|
||||
const r = sliceLogs(lines, 1, 10);
|
||||
expect(r.startLine).toBe(1);
|
||||
});
|
||||
|
||||
it("offset mode: nextOffset is null when slice starts at line 1", () => {
|
||||
const lines = make(50);
|
||||
const r = sliceLogs(lines, 1, 20);
|
||||
expect(r.nextOffset).toBeNull();
|
||||
});
|
||||
|
||||
it("offset mode: nextOffset points to previous page", () => {
|
||||
const lines = make(100);
|
||||
const r = sliceLogs(lines, 51, 50); // lines 51-100
|
||||
expect(r.nextOffset).toBe(1); // previous page starts at line 1
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// buildLogFooter
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("buildLogFooter", () => {
|
||||
it("returns empty-file message when total=0", () => {
|
||||
const slice = { lines: [], total: 0, startLine: 0, endLine: 0, nextOffset: null };
|
||||
expect(buildLogFooter(slice, 50, "/path/to/nerve.log")).toContain("empty");
|
||||
});
|
||||
|
||||
it("includes range and path in footer", () => {
|
||||
const slice = { lines: ["x"], total: 200, startLine: 151, endLine: 200, nextOffset: 101 };
|
||||
const footer = buildLogFooter(slice, 50, "/var/log/nerve.log");
|
||||
expect(footer).toContain("lines 151-200 of 200");
|
||||
expect(footer).toContain("/var/log/nerve.log");
|
||||
});
|
||||
|
||||
it("includes pagination hint when nextOffset is set", () => {
|
||||
const slice = { lines: ["x"], total: 200, startLine: 151, endLine: 200, nextOffset: 101 };
|
||||
const footer = buildLogFooter(slice, 50, "/path/nerve.log");
|
||||
expect(footer).toContain("nerve logs --offset 101 -n 50");
|
||||
});
|
||||
|
||||
it("no pagination hint when nextOffset is null", () => {
|
||||
const slice = { lines: ["x"], total: 20, startLine: 1, endLine: 20, nextOffset: null };
|
||||
const footer = buildLogFooter(slice, 50, "/path/nerve.log");
|
||||
expect(footer).not.toContain("nerve logs --offset");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DEFAULT_LOG_LINES constant
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("DEFAULT_LOG_LINES", () => {
|
||||
it("is 50", () => {
|
||||
expect(DEFAULT_LOG_LINES).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// readAllLines
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("readAllLines", () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = mkdtempSync(join(tmpdir(), "nerve-logs-test-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("returns empty array for nonexistent file", async () => {
|
||||
const result = await readAllLines(join(tmpDir, "missing.log"));
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("reads all lines from a file", async () => {
|
||||
const logFile = join(tmpDir, "test.log");
|
||||
writeFileSync(logFile, "line1\nline2\nline3\n");
|
||||
const result = await readAllLines(logFile);
|
||||
expect(result).toEqual(["line1", "line2", "line3"]);
|
||||
});
|
||||
|
||||
it("handles file with no trailing newline", async () => {
|
||||
const logFile = join(tmpDir, "test.log");
|
||||
writeFileSync(logFile, "a\nb\nc");
|
||||
const result = await readAllLines(logFile);
|
||||
expect(result).toEqual(["a", "b", "c"]);
|
||||
});
|
||||
|
||||
it("returns empty array for empty file", async () => {
|
||||
const logFile = join(tmpDir, "empty.log");
|
||||
writeFileSync(logFile, "");
|
||||
const result = await readAllLines(logFile);
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Integration: readAllLines + sliceLogs end-to-end
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("readAllLines + sliceLogs integration", () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = mkdtempSync(join(tmpdir(), "nerve-logs-int-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("tail-paginates a large log file correctly", async () => {
|
||||
const logFile = join(tmpDir, "big.log");
|
||||
const content = Array.from({ length: 120 }, (_, i) => `entry ${i + 1}`).join("\n");
|
||||
writeFileSync(logFile, content);
|
||||
|
||||
const all = await readAllLines(logFile);
|
||||
const page1 = sliceLogs(all, 0, 50); // last 50: lines 71-120
|
||||
expect(page1.startLine).toBe(71);
|
||||
expect(page1.endLine).toBe(120);
|
||||
expect(page1.nextOffset).toBe(21); // max(1, 71-50)
|
||||
|
||||
const page2 = sliceLogs(all, page1.nextOffset!, 50); // lines 21-70
|
||||
expect(page2.startLine).toBe(21);
|
||||
expect(page2.endLine).toBe(70);
|
||||
expect(page2.nextOffset).toBe(1); // max(1, 21-50) = 1
|
||||
|
||||
const page3 = sliceLogs(all, page2.nextOffset!, 50); // lines 1-50
|
||||
expect(page3.startLine).toBe(1);
|
||||
expect(page3.endLine).toBe(50);
|
||||
expect(page3.nextOffset).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// logsCommand: negative offset validation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("logsCommand negative offset", () => {
|
||||
let stderrOutput: string;
|
||||
let exitCode: number | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
stderrOutput = "";
|
||||
exitCode = undefined;
|
||||
vi.spyOn(process.stderr, "write").mockImplementation((chunk) => {
|
||||
stderrOutput += typeof chunk === "string" ? chunk : chunk.toString();
|
||||
return true;
|
||||
});
|
||||
vi.spyOn(process, "exit").mockImplementation((code?: number | string | null) => {
|
||||
exitCode = typeof code === "number" ? code : 1;
|
||||
throw new Error(`process.exit(${exitCode})`);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("exits with code 1 and writes to stderr when offset is negative", async () => {
|
||||
await expect(
|
||||
logsCommand.run!({ args: { n: "50", offset: "-5", follow: false }, rawArgs: [], cmd: logsCommand as never }),
|
||||
).rejects.toThrow("process.exit(1)");
|
||||
expect(exitCode).toBe(1);
|
||||
expect(stderrOutput).toContain("--offset must be a non-negative integer");
|
||||
expect(stderrOutput).toContain("-5");
|
||||
});
|
||||
|
||||
it("exits with code 1 for offset=-1", async () => {
|
||||
await expect(
|
||||
logsCommand.run!({ args: { n: "10", offset: "-1", follow: false }, rawArgs: [], cmd: logsCommand as never }),
|
||||
).rejects.toThrow("process.exit(1)");
|
||||
expect(exitCode).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@
|
||||
import { defineCommand, runMain } from "citty";
|
||||
|
||||
import { initCommand } from "./commands/init.js";
|
||||
import { logsCommand } from "./commands/logs.js";
|
||||
import { startCommand } from "./commands/start.js";
|
||||
import { statusCommand } from "./commands/status.js";
|
||||
import { stopCommand } from "./commands/stop.js";
|
||||
@@ -19,6 +20,7 @@ const main = defineCommand({
|
||||
start: startCommand,
|
||||
stop: stopCommand,
|
||||
status: statusCommand,
|
||||
logs: logsCommand,
|
||||
validate: validateCommand,
|
||||
workflow: workflowCommand,
|
||||
},
|
||||
|
||||
@@ -124,7 +124,7 @@ export function validateWorkflowName(name: string): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildWorkflowTemplate(name: string): string {
|
||||
export function buildWorkflowTemplate(name: string): string {
|
||||
return `import type { WorkflowDefinition } from "@uncaged/nerve-daemon";
|
||||
|
||||
const workflow: WorkflowDefinition = {
|
||||
@@ -248,6 +248,8 @@ async function runInitWorkspace(force: boolean): Promise<void> {
|
||||
if (!existsSync(join(nerveRoot, ".git"))) {
|
||||
try {
|
||||
await runCommand("git", ["init"], nerveRoot);
|
||||
await runCommand("git", ["add", "."], nerveRoot);
|
||||
await runCommand("git", ["commit", "-m", "Initial nerve workspace"], nerveRoot);
|
||||
} catch {
|
||||
process.stdout.write("⚠️ git init failed — skipping.\n");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
import { createReadStream, existsSync, statSync } from "node:fs";
|
||||
import { createInterface } from "node:readline";
|
||||
|
||||
import { defineCommand } from "citty";
|
||||
|
||||
import { getLogPath } from "../workspace.js";
|
||||
|
||||
export const DEFAULT_LOG_LINES = 50;
|
||||
|
||||
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
/**
|
||||
* Read all lines from a file. Returns empty array if file does not exist.
|
||||
*
|
||||
* TODO: For tail mode (offset=0), avoid reading the whole file into memory by
|
||||
* seeking to the last N bytes via createReadStream({ start: max(0, size - CHUNK) }).
|
||||
*/
|
||||
export async function readAllLines(filePath: string): Promise<string[]> {
|
||||
if (!existsSync(filePath)) return [];
|
||||
const lines: string[] = [];
|
||||
const rl = createInterface({
|
||||
input: createReadStream(filePath, { encoding: "utf8" }),
|
||||
crlfDelay: Number.POSITIVE_INFINITY,
|
||||
});
|
||||
for await (const line of rl) {
|
||||
lines.push(line);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Slice a log line array respecting offset + limit semantics.
|
||||
*
|
||||
* When offset is 0 the function returns the *last* `limit` lines (tail mode).
|
||||
* When offset > 0 it is treated as a 1-based line number and the slice starts
|
||||
* there (for pagination of earlier pages from the tail).
|
||||
*
|
||||
* Returns the selected lines plus metadata used to build the footer.
|
||||
*/
|
||||
export type LogSlice = {
|
||||
lines: string[];
|
||||
total: number;
|
||||
startLine: number; // 1-based, inclusive
|
||||
endLine: number; // 1-based, inclusive
|
||||
nextOffset: number | null; // null when no previous page exists
|
||||
};
|
||||
|
||||
export function sliceLogs(allLines: string[], offset: number, limit: number): LogSlice {
|
||||
const total = allLines.length;
|
||||
|
||||
if (total === 0) {
|
||||
return { lines: [], total: 0, startLine: 0, endLine: 0, nextOffset: null };
|
||||
}
|
||||
|
||||
let start: number;
|
||||
if (offset === 0) {
|
||||
// Tail mode: last `limit` lines
|
||||
start = Math.max(0, total - limit);
|
||||
} else {
|
||||
// offset is 1-based line number
|
||||
start = Math.max(0, offset - 1);
|
||||
}
|
||||
|
||||
const end = Math.min(start + limit, total);
|
||||
const lines = allLines.slice(start, end);
|
||||
|
||||
const startLine = start + 1;
|
||||
const endLine = end;
|
||||
|
||||
// nextOffset points to lines *before* current slice (earlier in file)
|
||||
const nextOffset = start > 0 ? Math.max(1, startLine - limit) : null;
|
||||
|
||||
return { lines, total, startLine, endLine, nextOffset };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the footer string shown after the log lines.
|
||||
*/
|
||||
export function buildLogFooter(slice: LogSlice, nArg: number, logPath: string): string {
|
||||
if (slice.total === 0) {
|
||||
return "📭 Log file is empty.\n";
|
||||
}
|
||||
|
||||
const rangeStr = `lines ${slice.startLine}-${slice.endLine} of ${slice.total}`;
|
||||
let footer = `\n📄 ${rangeStr} | ${logPath}\n`;
|
||||
|
||||
if (slice.nextOffset !== null) {
|
||||
footer += `⏩ Earlier lines available. Fetch previous page:\n`;
|
||||
footer += ` nerve logs --offset ${slice.nextOffset} -n ${nArg}\n`;
|
||||
}
|
||||
|
||||
return footer;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// nerve logs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const logsCommand = defineCommand({
|
||||
meta: {
|
||||
name: "logs",
|
||||
description: "Show daemon log output",
|
||||
},
|
||||
args: {
|
||||
n: {
|
||||
type: "string",
|
||||
description: `Number of lines to show (default: ${DEFAULT_LOG_LINES})`,
|
||||
default: String(DEFAULT_LOG_LINES),
|
||||
},
|
||||
offset: {
|
||||
type: "string",
|
||||
description: "Start from line N (1-based, for pagination)",
|
||||
default: "0",
|
||||
},
|
||||
follow: {
|
||||
type: "boolean",
|
||||
alias: "f",
|
||||
description: "Stream new log lines in real time",
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
async run({ args }) {
|
||||
const logPath = getLogPath();
|
||||
const nLines = Math.max(1, Number.parseInt(args.n, 10) || DEFAULT_LOG_LINES);
|
||||
const rawOffset = Number.parseInt(args.offset, 10) || 0;
|
||||
|
||||
if (rawOffset < 0) {
|
||||
process.stderr.write(`❌ --offset must be a non-negative integer, got: ${args.offset}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const offset = rawOffset;
|
||||
|
||||
if (!existsSync(logPath)) {
|
||||
process.stderr.write(`❌ Log file not found: ${logPath}\n`);
|
||||
process.stderr.write(" Has the daemon been started? Try: nerve start\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (args.follow) {
|
||||
await followLog(logPath, nLines);
|
||||
return;
|
||||
}
|
||||
|
||||
const allLines = await readAllLines(logPath);
|
||||
const slice = sliceLogs(allLines, offset, nLines);
|
||||
|
||||
for (const line of slice.lines) {
|
||||
process.stdout.write(`${line}\n`);
|
||||
}
|
||||
|
||||
process.stdout.write(buildLogFooter(slice, nLines, logPath));
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Stream new lines from a log file as they are appended.
|
||||
* Shows the last `tailLines` lines first, then watches for new content.
|
||||
*/
|
||||
async function followLog(logPath: string, tailLines: number): Promise<void> {
|
||||
const allLines = await readAllLines(logPath);
|
||||
const initial = allLines.slice(Math.max(0, allLines.length - tailLines));
|
||||
for (const line of initial) {
|
||||
process.stdout.write(`${line}\n`);
|
||||
}
|
||||
|
||||
let size = statSync(logPath).size;
|
||||
|
||||
process.stdout.write(`\n👁 Following ${logPath} — press Ctrl+C to stop\n`);
|
||||
|
||||
let stopped = false;
|
||||
process.once("SIGINT", () => {
|
||||
stopped = true;
|
||||
});
|
||||
|
||||
while (!stopped) {
|
||||
await sleep(300);
|
||||
if (stopped) break;
|
||||
try {
|
||||
const newSize = statSync(logPath).size;
|
||||
if (newSize < size) {
|
||||
// Log rotation: file was truncated or replaced, read from the beginning
|
||||
size = 0;
|
||||
}
|
||||
if (newSize <= size) continue;
|
||||
|
||||
const stream = createReadStream(logPath, { start: size, encoding: "utf8" });
|
||||
const rl = createInterface({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
|
||||
for await (const line of rl) {
|
||||
process.stdout.write(`${line}\n`);
|
||||
}
|
||||
size = newSize;
|
||||
} catch {
|
||||
stopped = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { createWriteStream, existsSync, readFileSync } from "node:fs";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { parseNerveConfig } from "@uncaged/nerve-core";
|
||||
@@ -85,6 +84,23 @@ async function runForeground(nerveRoot: string): Promise<void> {
|
||||
await kernel.ready;
|
||||
}
|
||||
|
||||
/** Path to the CLI entry script (for spawning `start` without `-d`). */
|
||||
function cliEntryScript(): string {
|
||||
const here = fileURLToPath(import.meta.url);
|
||||
const ext = here.endsWith(".ts") ? ".ts" : ".js";
|
||||
// When bundled, `here` is already the CLI entry (e.g. dist/cli.js).
|
||||
// When running from source, `here` is src/commands/start.ts → go up to src/cli.ts.
|
||||
const candidates = [
|
||||
join(dirname(here), `cli${ext}`), // bundled: dist/cli.js
|
||||
join(dirname(here), "..", `cli${ext}`), // source: src/commands/start.ts → src/cli.ts
|
||||
];
|
||||
const cliPath = candidates.find((p) => existsSync(p));
|
||||
if (!cliPath) {
|
||||
throw new Error(`CLI entry not found (searched: ${candidates.join(", ")})`);
|
||||
}
|
||||
return cliPath;
|
||||
}
|
||||
|
||||
async function runDaemon(nerveRoot: string): Promise<void> {
|
||||
if (isRunning()) {
|
||||
const pid = readPidFile();
|
||||
@@ -108,9 +124,9 @@ async function runDaemon(nerveRoot: string): Promise<void> {
|
||||
else resolve();
|
||||
});
|
||||
|
||||
const selfPath = fileURLToPath(import.meta.url);
|
||||
const cliPath = cliEntryScript();
|
||||
|
||||
const child = spawn(process.execPath, [selfPath, "start"], {
|
||||
const child = spawn(process.execPath, [cliPath, "start"], {
|
||||
detached: true,
|
||||
stdio: ["ignore", logStream.fd, logStream.fd],
|
||||
env: { ...process.env, NERVE_DAEMON_MODE: "1" },
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"name": "@uncaged/nerve-core",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -64,9 +64,15 @@ export function createDaemonIpcServer(
|
||||
return;
|
||||
}
|
||||
|
||||
workflowManager.startWorkflow(req.workflow, req.payload);
|
||||
const resp: DaemonResponse = { ok: true };
|
||||
socket.write(`${JSON.stringify(resp)}\n`);
|
||||
try {
|
||||
workflowManager.startWorkflow(req.workflow, req.payload);
|
||||
const resp: DaemonResponse = { ok: true };
|
||||
socket.write(`${JSON.stringify(resp)}\n`);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
const resp: DaemonResponse = { ok: false, error: msg };
|
||||
socket.write(`${JSON.stringify(resp)}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
const server: Server = createServer((socket) => {
|
||||
|
||||
Reference in New Issue
Block a user