@nodeint/migent 0.0.0-stage → 0.1.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.
@@ -0,0 +1,169 @@
1
+ import { openSync, readSync, closeSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { CancellationRequested, ConfigurationError, SessionStore, log, raiseCancellation, readExecutionFile, watchSnapshot, } from "@nodeint/migent-kernel";
4
+ import { printNotice, printWatchEvent, printWatchSnapshot } from "../display.js";
5
+ const TERMINAL_EXECUTION_STATUSES = new Set(["completed", "failed", "cancelled"]);
6
+ export async function watchSession(workspace, sessionId, options = {}) {
7
+ const once = options.once ?? false;
8
+ const untilTerminal = options.untilTerminal ?? false;
9
+ const jsonLines = options.jsonLines ?? false;
10
+ const executionId = options.executionId ?? null;
11
+ const cursor = options.cursor === undefined ? null : options.cursor;
12
+ if (once && untilTerminal)
13
+ throw new ConfigurationError("--once cannot be combined with --until.");
14
+ if (once && cursor !== null)
15
+ throw new ConfigurationError("--once cannot be combined with --cursor.");
16
+ if (untilTerminal && !executionId)
17
+ throw new ConfigurationError("--until requires --execution.");
18
+ if (cursor !== null && cursor < 0)
19
+ throw new ConfigurationError("--cursor must be >= 0.");
20
+ const sessionDirectory = path.join(workspace, ".migent", "sessions", sessionId);
21
+ const eventsPath = path.join(sessionDirectory, "events.jsonl");
22
+ if (!isFile(eventsPath))
23
+ throw new ConfigurationError(`No events found for session: ${sessionId}`);
24
+ new SessionStore(workspace).reapOrphanedExecutions(sessionId, "ignore");
25
+ let executionPath = null;
26
+ if (executionId !== null) {
27
+ const resolved = path.join(sessionDirectory, "executions", executionId, "execution.json");
28
+ executionPath = resolved;
29
+ if (!isFile(resolved))
30
+ throw new ConfigurationError(`No execution found: ${executionId}`);
31
+ }
32
+ if (once) {
33
+ printSnapshot(sessionDirectory, executionId, jsonLines);
34
+ return 0;
35
+ }
36
+ let followCursor = cursor;
37
+ if (followCursor === null) {
38
+ const snapshot = printSnapshot(sessionDirectory, executionId, jsonLines);
39
+ followCursor = Number(snapshot.events_cursor ?? 0);
40
+ }
41
+ printNotice(`Watching session ${sessionId}.`);
42
+ const onSigint = () => {
43
+ try {
44
+ raiseCancellation(2);
45
+ }
46
+ catch {
47
+ // The command loop observes the pending cancellation.
48
+ }
49
+ };
50
+ const onSigterm = () => {
51
+ try {
52
+ raiseCancellation(15);
53
+ }
54
+ catch {
55
+ // The command loop observes the pending cancellation.
56
+ }
57
+ };
58
+ process.on("SIGINT", onSigint);
59
+ process.on("SIGTERM", onSigterm);
60
+ const fd = openSync(eventsPath, "r");
61
+ try {
62
+ let position = followCursor;
63
+ let pending = "";
64
+ for (;;) {
65
+ const read = readCompleteLines(fd, position, pending);
66
+ position = read.position;
67
+ pending = read.pending;
68
+ for (const line of read.lines)
69
+ emitWatchEvent(line, executionId, jsonLines);
70
+ if (read.lines.length > 0)
71
+ continue;
72
+ if (untilTerminal) {
73
+ if (executionPath === null)
74
+ throw new ConfigurationError("--until requires --execution.");
75
+ const record = readExecutionFile(executionPath);
76
+ if (record !== null && TERMINAL_EXECUTION_STATUSES.has(String(record.status))) {
77
+ const drained = readCompleteLines(fd, position, pending);
78
+ for (const line of drained.lines)
79
+ emitWatchEvent(line, executionId, jsonLines);
80
+ return untilTerminalExitCode(record);
81
+ }
82
+ }
83
+ await delay(250);
84
+ }
85
+ }
86
+ catch (error) {
87
+ if (error instanceof CancellationRequested) {
88
+ log(error.message);
89
+ return 130;
90
+ }
91
+ throw error;
92
+ }
93
+ finally {
94
+ closeSync(fd);
95
+ process.off("SIGINT", onSigint);
96
+ process.off("SIGTERM", onSigterm);
97
+ }
98
+ }
99
+ function printSnapshot(sessionDirectory, executionId, jsonLines) {
100
+ const snapshot = watchSnapshot(sessionDirectory, executionId);
101
+ if (jsonLines)
102
+ process.stdout.write(`${JSON.stringify(snapshot)}\n`);
103
+ else
104
+ printWatchSnapshot(snapshot);
105
+ return snapshot;
106
+ }
107
+ function emitWatchEvent(line, executionId, jsonLines) {
108
+ let event;
109
+ try {
110
+ event = JSON.parse(line);
111
+ }
112
+ catch {
113
+ return;
114
+ }
115
+ if (!isRecord(event))
116
+ return;
117
+ if (executionId !== null && event.execution_id !== executionId)
118
+ return;
119
+ if (jsonLines) {
120
+ process.stdout.write(`${JSON.stringify(event)}\n`);
121
+ return;
122
+ }
123
+ printWatchEvent(event);
124
+ }
125
+ function untilTerminalExitCode(record) {
126
+ if (record.status === "completed") {
127
+ const outcome = record.outcome;
128
+ if (isRecord(outcome) && outcome.status === "complete")
129
+ return 0;
130
+ return 1;
131
+ }
132
+ if (record.status === "failed" || record.status === "cancelled")
133
+ return 1;
134
+ return 1;
135
+ }
136
+ function readCompleteLines(fd, position, pending) {
137
+ const chunks = [];
138
+ const buffer = Buffer.alloc(64 * 1024);
139
+ let offset = position;
140
+ for (;;) {
141
+ const count = readSync(fd, buffer, 0, buffer.length, offset);
142
+ if (count <= 0)
143
+ break;
144
+ chunks.push(Buffer.from(buffer.subarray(0, count)));
145
+ offset += count;
146
+ }
147
+ if (chunks.length === 0)
148
+ return { lines: [], pending, position };
149
+ pending += Buffer.concat(chunks).toString("utf8");
150
+ const pieces = pending.split("\n");
151
+ const rest = pieces.pop() ?? "";
152
+ return { lines: pieces, pending: rest, position: offset };
153
+ }
154
+ function delay(ms) {
155
+ return new Promise((resolve) => {
156
+ setTimeout(resolve, ms);
157
+ });
158
+ }
159
+ function isFile(filePath) {
160
+ try {
161
+ return statSync(filePath).isFile();
162
+ }
163
+ catch {
164
+ return false;
165
+ }
166
+ }
167
+ function isRecord(value) {
168
+ return typeof value === "object" && value !== null && !Array.isArray(value);
169
+ }
@@ -0,0 +1,8 @@
1
+ import { callService, startDetached } from "@nodeint/migent-registry";
2
+ export declare const workspaceApi: {
3
+ startDetached: typeof startDetached;
4
+ callService: typeof callService;
5
+ };
6
+ export declare function runRegister(workspacePath: string | null, name: string | null): Promise<number>;
7
+ export declare function runWorkspaceList(asJson: boolean): Promise<number>;
8
+ export declare function runRemove(workspaceId: string): Promise<number>;
@@ -0,0 +1,106 @@
1
+ import { realpathSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { ServiceError, callService, endpointFromHome, ensureRegistryHome, startDetached } from "@nodeint/migent-registry";
4
+ import { printError } from "../display.js";
5
+ import { findWorkspace, invocationCwdPath } from "../workspace.js";
6
+ export const workspaceApi = {
7
+ startDetached,
8
+ callService,
9
+ };
10
+ export async function runRegister(workspacePath, name) {
11
+ const root = workspaceRoot(workspacePath);
12
+ if (!path.isAbsolute(root)) {
13
+ printError("Workspace path is not absolute.");
14
+ return 2;
15
+ }
16
+ process.stderr.write(`Registering ${root}\n`);
17
+ let endpoint;
18
+ try {
19
+ endpoint = await workspaceApi.startDetached(ensureRegistryHome());
20
+ }
21
+ catch (error) {
22
+ if (error instanceof ServiceError) {
23
+ printError(error.message);
24
+ return 1;
25
+ }
26
+ throw error;
27
+ }
28
+ const body = { path: root, tool: { name: "migent", version: "0.1.0" } };
29
+ if (name !== null)
30
+ body.name = name;
31
+ if (!path.isAbsolute(String(body.path))) {
32
+ printError("Workspace path is not absolute.");
33
+ return 2;
34
+ }
35
+ return finish("POST", "/v1/workspaces", endpoint, body);
36
+ }
37
+ export async function runWorkspaceList(asJson) {
38
+ const endpoint = endpointFromHome(ensureRegistryHome());
39
+ if (endpoint === null) {
40
+ printError("Registry service is not running.");
41
+ return 1;
42
+ }
43
+ let status;
44
+ let payload;
45
+ try {
46
+ [status, payload] = await workspaceApi.callService(endpoint.host, endpoint.port, endpoint.token, "GET", "/v1/workspaces");
47
+ }
48
+ catch (error) {
49
+ printError(`Registry request failed: ${errorText(error)}`);
50
+ return 1;
51
+ }
52
+ if (status !== 200)
53
+ return statusExit(status, payload);
54
+ const rows = payload.workspaces;
55
+ if (!Array.isArray(rows)) {
56
+ printError("Registry response was invalid.");
57
+ return 1;
58
+ }
59
+ if (asJson) {
60
+ process.stdout.write(`${JSON.stringify({ workspaces: rows })}\n`);
61
+ }
62
+ else {
63
+ for (const row of rows) {
64
+ const record = row && typeof row === "object" ? row : {};
65
+ const available = record.available ? "true" : "false";
66
+ process.stdout.write(`${String(record.id)} ${String(record.name)} ${String(record.path)} ${available}\n`);
67
+ }
68
+ }
69
+ return 0;
70
+ }
71
+ export async function runRemove(workspaceId) {
72
+ const endpoint = endpointFromHome(ensureRegistryHome());
73
+ if (endpoint === null) {
74
+ printError("Registry service is not running.");
75
+ return 1;
76
+ }
77
+ return finish("DELETE", `/v1/workspaces/${workspaceId}`, endpoint, null);
78
+ }
79
+ async function finish(method, requestPath, endpoint, body) {
80
+ let status;
81
+ let payload;
82
+ try {
83
+ [status, payload] = await workspaceApi.callService(endpoint.host, endpoint.port, endpoint.token, method, requestPath, body);
84
+ }
85
+ catch (error) {
86
+ printError(`Registry request failed: ${errorText(error)}`);
87
+ return 1;
88
+ }
89
+ if (status === 200 || status === 201) {
90
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
91
+ return 0;
92
+ }
93
+ return statusExit(status, payload);
94
+ }
95
+ function statusExit(status, payload) {
96
+ const message = payload.message;
97
+ printError(typeof message === "string" && message ? message : "Registry request failed.");
98
+ return status === 404 ? 2 : 1;
99
+ }
100
+ function workspaceRoot(workspacePath) {
101
+ const start = workspacePath === null ? invocationCwdPath() : path.resolve(invocationCwdPath(), workspacePath);
102
+ return realpathSync(findWorkspace(start));
103
+ }
104
+ function errorText(error) {
105
+ return error instanceof Error ? error.message : String(error);
106
+ }
@@ -0,0 +1,12 @@
1
+ import type { Check } from "@nodeint/migent-kernel";
2
+ export declare function printError(message: string): void;
3
+ export declare function printNotice(message: string): void;
4
+ export declare function printCreated(paths: readonly string[], nextCommand: string): void;
5
+ export declare function printAlreadyInitialized(): void;
6
+ export declare function printInitConflicts(paths: readonly string[]): void;
7
+ export declare function printDoctor(checks: readonly Check[]): void;
8
+ export declare function printSessions(rows: readonly string[][]): void;
9
+ export declare function printAgent(result: Record<string, unknown>, role: string, provider: string, model: string): void;
10
+ export declare function printWatchSnapshot(snapshot: Record<string, unknown>): void;
11
+ export declare function printWatchEvent(event: Record<string, unknown>): void;
12
+ export declare function formatWatchTimestamp(timestamp: unknown): string;
@@ -0,0 +1,123 @@
1
+ import { createRequire } from "node:module";
2
+ const require = createRequire(import.meta.url);
3
+ const pc = require("picocolors");
4
+ export function printError(message) {
5
+ const tty = Boolean(process.stderr.isTTY);
6
+ const prefix = tty ? pc.red(pc.bold("migent: ")) : "migent: ";
7
+ process.stderr.write(`${prefix}${message}\n`);
8
+ }
9
+ export function printNotice(message) {
10
+ const tty = Boolean(process.stderr.isTTY);
11
+ const prefix = tty ? pc.cyan(pc.bold("[migent] ")) : "[migent] ";
12
+ process.stderr.write(`${prefix}${message}\n`);
13
+ }
14
+ export function printCreated(paths, nextCommand) {
15
+ process.stdout.write("Created:\n");
16
+ for (const filePath of paths)
17
+ process.stdout.write(` ${filePath}\n`);
18
+ process.stdout.write("Next:\n");
19
+ process.stdout.write(` ${nextCommand}\n`);
20
+ }
21
+ export function printAlreadyInitialized() {
22
+ process.stdout.write("Already initialized.\n");
23
+ }
24
+ export function printInitConflicts(paths) {
25
+ printError("existing files block init.");
26
+ for (const filePath of paths)
27
+ process.stderr.write(` ${filePath}\n`);
28
+ }
29
+ export function printDoctor(checks) {
30
+ const tty = Boolean(process.stdout.isTTY);
31
+ for (const check of checks) {
32
+ const status = check.ok ? (tty ? pc.green("ok") : "ok") : tty ? pc.red("failed") : "failed";
33
+ process.stdout.write(`${check.name.padEnd(12)}${status}\n`);
34
+ for (const item of check.items) {
35
+ const line = ` ${item}`;
36
+ process.stdout.write(`${tty ? pc.dim(line) : line}\n`);
37
+ }
38
+ for (const problem of check.problems)
39
+ process.stdout.write(` ${problem}\n`);
40
+ }
41
+ }
42
+ export function printSessions(rows) {
43
+ if (rows.length === 0) {
44
+ process.stdout.write("No sessions.\n");
45
+ return;
46
+ }
47
+ for (const row of rows) {
48
+ const [, sessionId, workflowId, status, executionId, decision, outputs, task] = row;
49
+ process.stdout.write(`${sessionId ?? ""} ${workflowId ?? ""} ${status ?? ""} ${executionId ?? ""} ${decision ?? ""} ${outputs ?? ""} ${task ?? ""}\n`);
50
+ }
51
+ }
52
+ export function printAgent(result, role, provider, model) {
53
+ const decision = String(result.outcome_decision ?? "");
54
+ let outcome = String(result.outcome_status ?? "failed");
55
+ if (decision)
56
+ outcome = `${outcome} ${decision}`;
57
+ const rows = [
58
+ ["role", role],
59
+ ["model", `${provider}/${model}`],
60
+ ["outcome", outcome],
61
+ ["session", String(result.session_id ?? "")],
62
+ ["execution", String(result.execution_id ?? "")],
63
+ ];
64
+ for (const [label, value] of rows)
65
+ process.stdout.write(`${label} ${value}\n`);
66
+ }
67
+ export function printWatchSnapshot(snapshot) {
68
+ const lines = [
69
+ ["session_id", String(snapshot.session_id ?? "--")],
70
+ ["status", String(snapshot.status ?? "unknown")],
71
+ ];
72
+ if (typeof snapshot.workflow_id === "string" && snapshot.workflow_id) {
73
+ lines.push(["workflow", snapshot.workflow_id]);
74
+ }
75
+ if (typeof snapshot.task === "string" && snapshot.task)
76
+ lines.push(["task", snapshot.task]);
77
+ const executionId = snapshot.execution_id;
78
+ const executionStatus = snapshot.execution_status ?? snapshot.latest_execution_status;
79
+ if (typeof executionId === "string" && executionId) {
80
+ const value = typeof executionStatus === "string" && executionStatus
81
+ ? `${executionId} · ${executionStatus}`
82
+ : executionId;
83
+ lines.push(["execution", value]);
84
+ }
85
+ const outcome = snapshot.outcome;
86
+ if (isRecord(outcome) && typeof outcome.status === "string") {
87
+ lines.push(["outcome", outcome.status]);
88
+ if (typeof outcome.decision === "string" && outcome.decision)
89
+ lines.push(["decision", outcome.decision]);
90
+ }
91
+ lines.push(["updated_at", formatWatchTimestamp(snapshot.updated_at)]);
92
+ const outputs = snapshot.outputs;
93
+ if (isRecord(outputs) && Object.keys(outputs).length > 0)
94
+ lines.push(["outputs", Object.keys(outputs).join(", ")]);
95
+ else
96
+ lines.push(["outputs", "none"]);
97
+ lines.push(["events_cursor", String(snapshot.events_cursor ?? 0)]);
98
+ for (const [label, value] of lines)
99
+ process.stdout.write(`${label} ${value}\n`);
100
+ }
101
+ export function printWatchEvent(event) {
102
+ const eventName = event.event;
103
+ const stageId = event.stage_id;
104
+ if (typeof eventName !== "string" || typeof stageId !== "string")
105
+ return;
106
+ const summary = typeof event.summary === "string" && event.summary ? ` · ${event.summary}` : "";
107
+ const label = eventName.replace(/^provider\./, "").replaceAll("_", " ");
108
+ process.stdout.write(`${formatWatchTimestamp(event.timestamp)} ${stageId} ${label}${summary}\n`);
109
+ }
110
+ export function formatWatchTimestamp(timestamp) {
111
+ if (typeof timestamp !== "string" || timestamp.trim() === "")
112
+ return "--";
113
+ let text = timestamp.trim();
114
+ if (text.endsWith("Z"))
115
+ text = `${text.slice(0, -1)}+00:00`;
116
+ const parsed = new Date(text);
117
+ if (Number.isNaN(parsed.getTime()))
118
+ return timestamp.trim();
119
+ return parsed.toISOString().replace("T", " ").replace(/\.\d+Z$/, "+00:00").replace("Z", "+00:00");
120
+ }
121
+ function isRecord(value) {
122
+ return typeof value === "object" && value !== null && !Array.isArray(value);
123
+ }
@@ -0,0 +1,6 @@
1
+ import type { AgentToolRunner } from "@nodeint/migent-kernel";
2
+ /** Test seams. Production leaves both null. */
3
+ export declare const cliHooks: {
4
+ which: ((command: string) => string | null) | null;
5
+ runner: AgentToolRunner | null;
6
+ };
package/dist/hooks.js ADDED
@@ -0,0 +1,5 @@
1
+ /** Test seams. Production leaves both null. */
2
+ export const cliHooks = {
3
+ which: null,
4
+ runner: null,
5
+ };
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export declare function main(argv?: string[], cwd?: string): Promise<number>;
package/dist/index.js ADDED
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ import { realpathSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import { CommanderError } from "commander";
5
+ import { ConfigurationError } from "@nodeint/migent-kernel";
6
+ import { printError } from "./display.js";
7
+ import { cliHooks } from "./hooks.js";
8
+ import { createProgram } from "./program.js";
9
+ import { setInvocationCwd } from "./workspace.js";
10
+ export async function main(argv = process.argv.slice(2), cwd) {
11
+ setInvocationCwd(cwd);
12
+ const status = { code: 0, handled: false };
13
+ try {
14
+ const program = createProgram(status);
15
+ await program.parseAsync(argv, { from: "user" });
16
+ if (!status.handled) {
17
+ program.outputHelp();
18
+ return 2;
19
+ }
20
+ return status.code;
21
+ }
22
+ catch (error) {
23
+ if (error instanceof ConfigurationError) {
24
+ printError(error.message);
25
+ return 2;
26
+ }
27
+ if (error instanceof CommanderError)
28
+ return error.exitCode === 0 ? 0 : 2;
29
+ throw error;
30
+ }
31
+ finally {
32
+ setInvocationCwd(undefined);
33
+ cliHooks.which = null;
34
+ cliHooks.runner = null;
35
+ }
36
+ }
37
+ function isDirectRun() {
38
+ const entry = process.argv[1];
39
+ if (!entry)
40
+ return false;
41
+ try {
42
+ return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }
48
+ if (isDirectRun()) {
49
+ main()
50
+ .then((code) => {
51
+ process.exit(code);
52
+ })
53
+ .catch((error) => {
54
+ console.error(error);
55
+ process.exit(1);
56
+ });
57
+ }
@@ -0,0 +1,8 @@
1
+ import { type MigrationAdapter } from "@nodeint/migent-kernel";
2
+ export declare function requireCurrentSchema(workspace: string): void;
3
+ export declare function runMigrate(workspace: string, options?: {
4
+ assumeYes?: boolean;
5
+ confirm?: ((message: string) => boolean | Promise<boolean>) | null;
6
+ adapters?: Record<string, MigrationAdapter> | null;
7
+ }): Promise<number>;
8
+ export declare function migrationAdapters(adapters?: Record<string, unknown>): Record<string, MigrationAdapter>;