nadle
Version:
A type-safe, Gradle-inspired task runner for Node.js. Sharp tasks. Fast builds.
1,407 lines (1,368 loc) • 100 kB
JavaScript
#!/usr/bin/env node
import {
CHECK,
CLEAR_SCREEN,
COLON,
CROSS,
CURSOR_TO_START,
CURVE_ARROW,
ConfigurationError,
CyclicDependencyError,
DASH,
DEFAULT_CACHE_DIR_NAME,
ERASE_DOWN,
ERASE_SCROLLBACK,
HIDE_CURSOR,
LINES,
LogLevels,
MaybeArray,
Messages,
NadleError,
RIGHT_ARROW,
TaskExecutionError,
TaskNotFoundError,
UNDERSCORE,
VERTICAL_BAR,
bindObject,
capitalize,
clamp,
createNadleConsola,
createPlainReporters,
formatTime,
highlight,
isPathExists,
noop,
runWithInstance
} from "./chunk-U2TDD2B5.js";
// packages/nadle/src/core/engine/worker.ts
import Path5 from "path";
import WorkerThreads2 from "worker_threads";
import c7 from "tinyrainbow";
import { getWorkspaceById as getWorkspaceById3 } from "@nadle/project-resolver";
// packages/nadle/src/core/nadle.ts
import Process2 from "process";
import { getWorkspaceById as getWorkspaceById2, ROOT_WORKSPACE_ID as ROOT_WORKSPACE_ID3, isRootWorkspaceId as isRootWorkspaceId4 } from "@nadle/project-resolver";
// packages/nadle/src/core/handlers/list-handler.ts
import c from "tinyrainbow";
import { groupBy } from "lodash-es";
import { isRootWorkspaceId } from "@nadle/project-resolver";
// packages/nadle/src/core/handlers/base-handler.ts
var BaseHandler = class {
constructor(context) {
this.context = context;
}
};
// packages/nadle/src/core/utilities/comparator.ts
function combineComparators(comparators) {
return (a, b) => {
for (const compare of comparators) {
const result = compare(a, b);
if (result !== 0) {
return result;
}
}
return 0;
};
}
// packages/nadle/src/core/handlers/list-handler.ts
var ListHandler = class _ListHandler extends BaseHandler {
constructor() {
super(...arguments);
this.name = "list";
this.description = "Lists all registered tasks with their groups and descriptions.";
}
static {
this.UncategorizedGroup = "Uncategorized";
}
canHandle() {
return this.context.options.list;
}
handle() {
if (this.context.taskRegistry.tasks.length === 0) {
this.context.logger.log("No tasks found");
return;
}
const groups = this.computeGroups();
for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
const [groupName, tasks] = groups[groupIndex];
const label = `${capitalize(groupName)} tasks`;
this.context.logger.log(c.bold(label));
this.context.logger.log(c.bold(DASH.repeat(label.length)));
for (const task of tasks) {
const { label: label2, description } = task;
if (description) {
this.context.logger.log(c.bold(c.green(label2)) + c.yellow(` - ${description}`));
} else {
this.context.logger.log(c.green(label2));
}
}
if (groupIndex < groups.length - 1) {
this.context.logger.log("");
}
}
}
computeGroups() {
const tasks = this.context.taskRegistry.tasks.map((task) => {
const { description, group = _ListHandler.UncategorizedGroup } = task.configResolver();
return { ...task, group, description };
});
const groupComparator = combineComparators([
([a], [b]) => Number(a === _ListHandler.UncategorizedGroup) - Number(b === _ListHandler.UncategorizedGroup),
([a], [b]) => a.localeCompare(b)
]);
const taskComparator = combineComparators([
(a, b) => Number(isRootWorkspaceId(b.workspaceId)) - Number(isRootWorkspaceId(a.workspaceId)),
(a, b) => a.label.localeCompare(b.label)
]);
return Object.entries(groupBy(tasks, "group")).sort(groupComparator).map(([groupName, tasks2]) => [groupName, tasks2.sort(taskComparator)]);
}
};
// packages/nadle/src/core/handlers/dry-run-handler.ts
import c2 from "tinyrainbow";
// packages/nadle/src/core/interfaces/resolved-task.ts
var ResolvedTask;
((ResolvedTask2) => {
ResolvedTask2.getId = (task) => task.taskId;
})(ResolvedTask || (ResolvedTask = {}));
// packages/nadle/src/core/handlers/dry-run-handler.ts
var DryRunHandler = class extends BaseHandler {
constructor() {
super(...arguments);
this.name = "dry-run";
this.description = "Prints the execution plan without running tasks.";
}
canHandle() {
return this.context.options.dryRun;
}
handle() {
if (this.context.options.tasks.length === 0) {
this.context.logger.log(Messages.NoTasksFound());
return;
}
const scheduler = this.context.taskScheduler.init();
const taskIds = scheduler.getExecutionPlan();
this.context.logger.log(c2.bold("Execution plan:"));
const { passthroughArgs } = this.context.options;
const requestedTaskIds = new Set(this.context.options.tasks.map(ResolvedTask.getId));
for (const taskId of taskIds) {
const label = this.context.taskRegistry.getTaskById(taskId).label;
const implicitDeps = scheduler.getImplicitDeps(taskId);
const argsSuffix = passthroughArgs.length > 0 && requestedTaskIds.has(taskId) ? c2.dim(` (args: ${passthroughArgs.join(" ")})`) : "";
const suffix = implicitDeps.length > 0 ? c2.dim(` (after ${implicitDeps.map((d) => this.context.taskRegistry.getTaskById(d).label).join(", ")} \u2014 implicit)`) : "";
this.context.logger.log(`${c2.yellow(">")} Task ${c2.bold(label)}${argsSuffix}${suffix}`);
}
}
};
// packages/nadle/src/core/engine/executor.ts
import WorkerThreads from "worker_threads";
import TinyPool from "tinypool";
var PoolExecutor = class {
constructor(params) {
this.pool = new TinyPool({
concurrentTasksPerWorker: 1,
minThreads: params.minThreads,
maxThreads: params.maxThreads,
filename: new URL("./worker.js", import.meta.url).href
});
}
async run(params, notify) {
const { port2: poolPort, port1: workerPort } = new WorkerThreads.MessageChannel();
let resolveMessageReceived;
const messageReceived = new Promise((resolve) => {
resolveMessageReceived = resolve;
});
poolPort.on("message", async (message) => {
try {
await notify(message);
} finally {
resolveMessageReceived();
}
});
try {
const outputsFingerprint = await this.pool.run({ ...params, port: workerPort }, { transferList: [workerPort] });
await messageReceived;
return outputsFingerprint;
} finally {
poolPort.close();
}
}
async destroy() {
await this.pool.destroy();
}
};
var InlineExecutor = class {
constructor(nadle) {
this.nadle = nadle;
// Serializes task execution: the scheduler dispatches ready tasks concurrently,
// but maxWorkers === 1 must run them strictly one at a time, matching the
// single-thread pool. Each run() chains onto the previous one's completion.
this.tail = Promise.resolve();
}
run(params, notify) {
const result = this.tail.then(() => runTask(this.nadle, params, notify));
this.tail = result.catch(() => void 0);
return result;
}
async destroy() {
}
};
// packages/nadle/src/core/engine/task-pool.ts
var TERMINATING_WORKER_ERROR = "Terminating worker thread";
function toTaskExecutionError(error, label) {
if (error instanceof NadleError) {
return error;
}
const message = error instanceof Error ? error.message : String(error);
return new TaskExecutionError(`Task ${label} failed: ${message}`, { cause: error });
}
var TaskPool = class {
constructor(context, getNextReadyTasks) {
this.context = context;
this.getNextReadyTasks = getNextReadyTasks;
this.outputFingerprints = /* @__PURE__ */ new Map();
const { minWorkers, maxWorkers } = this.context.options;
this.executor = maxWorkers === 1 ? new InlineExecutor(this.context) : new PoolExecutor({ minThreads: minWorkers, maxThreads: maxWorkers });
}
async run() {
try {
await Promise.all(Array.from(this.getNextReadyTasks()).map((taskId) => this.pushTask(taskId)));
} finally {
await this.executor.destroy();
}
}
async pushTask(taskId) {
const task = this.context.taskRegistry.getTaskById(taskId);
try {
const { executeType, outputsFingerprint } = await this.executeWorker(taskId);
if (outputsFingerprint) {
this.outputFingerprints.set(taskId, outputsFingerprint);
}
if (executeType === "execute") {
await this.context.eventEmitter.onTaskFinish(task);
} else if (executeType === "up-to-date") {
await this.context.eventEmitter.onTaskUpToDate(task);
} else if (executeType === "from-cache") {
await this.context.eventEmitter.onTaskRestoreFromCache(task);
} else {
throw new Error(`Unknown execute type: ${executeType}`);
}
} catch (error) {
if (error instanceof Error && error.message === TERMINATING_WORKER_ERROR && this.context.executionTracker.getTaskStatus(task.id) === "running" /* Running */) {
await this.context.eventEmitter.onTaskCanceled(task);
return;
}
await this.context.eventEmitter.onTaskFailed(task);
throw toTaskExecutionError(error, task.label);
}
await Promise.all(Array.from(this.getNextReadyTasks(taskId)).map((readyTaskId) => this.pushTask(readyTaskId)));
}
async executeWorker(taskId) {
const task = this.context.taskRegistry.getTaskById(taskId);
let executeType = "execute";
const notify = async (message) => {
if (message.type === "start") {
await this.context.eventEmitter.onTaskStart(task, message.threadId);
} else if (message.type === "up-to-date") {
executeType = "up-to-date";
} else if (message.type === "from-cache") {
executeType = "from-cache";
}
};
const workerParams = {
taskId: task.id,
env: process.env,
options: { ...this.context.options, footer: false },
dependencyFingerprints: this.collectDependencyFingerprints(taskId)
};
const outputsFingerprint = await this.executor.run(workerParams, notify);
return { executeType, outputsFingerprint };
}
collectDependencyFingerprints(taskId) {
const deps = this.context.taskScheduler.getDirectDependencies(taskId);
const fingerprints = {};
for (const depId of deps) {
const fp = this.outputFingerprints.get(depId);
if (fp) {
fingerprints[depId] = fp;
}
}
return fingerprints;
}
};
// packages/nadle/src/core/views/tasks-selection.tsx
import React2, { useState } from "react";
import { Box, Text as Text3, render, useApp, useInput } from "ink";
// packages/nadle/src/core/views/visible-task.tsx
import { Text } from "ink";
import { jsx, jsxs } from "react/jsx-runtime";
var VisibleTask = ({ task }) => {
const { label, selected, pointing, TaskLabel, description } = task;
return /* @__PURE__ */ jsxs(Text, { color: pointing ? PRIMARY_COLOR : void 0, children: [
pointing ? ">" : " ",
" ",
`[${selected ? "x" : " "}]`,
" ",
/* @__PURE__ */ jsx(TaskLabel, {}),
" ",
description ? /* @__PURE__ */ jsx(Text, { color: SECONDARY_COLOR, children: description }) : ""
] }, label);
};
// packages/nadle/src/core/views/use-searching.tsx
import React from "react";
import { Text as Text2 } from "ink";
import fuzzySort from "fuzzysort";
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
var VISIBLE_TASKS_LIMIT = 5;
function useSearching(options) {
const { tasks, cursor, searchText, selectedTasks } = options;
return React.useMemo(() => {
if (!searchText) {
const visibleTasks = tasks.slice(0, VISIBLE_TASKS_LIMIT);
const maxLength2 = Math.max(...visibleTasks.map((task) => task.label.length));
return visibleTasks.map((task) => {
return {
...task,
pointing: cursor === tasks.indexOf(task),
selected: selectedTasks.includes(task.id),
TaskLabel: () => task.label.padEnd(maxLength2, " ")
};
});
}
const filteredTasks = fuzzySort.go(searchText, tasks, { key: "label", limit: VISIBLE_TASKS_LIMIT });
const maxLength = Math.max(...filteredTasks.map((task) => task.obj.label.length));
return filteredTasks.map((task) => {
return {
...task.obj,
selected: selectedTasks.includes(task.obj.id),
pointing: cursor === filteredTasks.indexOf(task),
TaskLabel: () => /* @__PURE__ */ jsx2(HighlightedTaskLabel, { task, marginRight: maxLength - task.obj.label.length })
};
});
}, [cursor, searchText, selectedTasks, tasks]);
}
var HighlightedTaskLabel = (props) => {
const { task, marginRight } = props;
return /* @__PURE__ */ jsxs2(Fragment, { children: [
task.highlight((char, charIndex) => /* @__PURE__ */ jsx2(Text2, { color: HIGHLIGHT_COLOR, children: char }, charIndex)).map((part, i) => typeof part === "string" ? /* @__PURE__ */ jsx2(Text2, { children: part }, i) : /* @__PURE__ */ jsx2(React.Fragment, { children: part }, i)),
" ".repeat(marginRight)
] });
};
// packages/nadle/src/core/views/input-handlers.ts
var commandHandler = {
canHandle: ({ context }) => context.isCommandMode,
handle: ({ key, input, context }) => {
const { exit, onSubmit, commandBuffer, setSearchText, setCommandBuffer, setIsCommandMode } = context;
const exitCommandMode = () => {
setIsCommandMode(false);
setCommandBuffer("");
};
if (key.escape) {
return exitCommandMode();
}
if (key.return) {
if (commandBuffer === "q") {
exit();
onSubmit([]);
}
if (commandBuffer === "l") {
setSearchText("");
}
return exitCommandMode();
}
setCommandBuffer((prev) => prev + input);
}
};
var semiColonHandler = {
canHandle: ({ input }) => input === ":",
handle: ({ context: { setIsCommandMode, setCommandBuffer } }) => {
setIsCommandMode(true);
setCommandBuffer("");
}
};
var arrowsHandler = {
canHandle: ({ key }) => key.upArrow || key.downArrow,
handle: ({ key, context: { setCursor, searchingTasks } }) => {
if (key.upArrow) {
setCursor((cursor) => (cursor - 1 + searchingTasks.length) % searchingTasks.length);
}
if (key.downArrow) {
setCursor((cursor) => (cursor + 1 + searchingTasks.length) % searchingTasks.length);
}
}
};
var enterHandler = {
canHandle: ({ key }) => key.return,
handle: ({ context: { exit, onSubmit, selectedTasks } }) => {
exit();
onSubmit(Array.from(selectedTasks));
}
};
var escapeHandler = {
canHandle: ({ key }) => key.escape,
handle: ({ context: { exit } }) => {
exit();
}
};
var deleteHandler = {
canHandle: ({ key }) => key.backspace || key.delete,
handle: ({ context: { setSearchText } }) => {
setSearchText((searchText) => searchText.slice(0, -1));
}
};
var spaceHandler = {
canHandle: ({ input }) => input === " ",
handle: ({ context: { cursor, searchingTasks, setSelectedTasks } }) => {
const { id } = searchingTasks[cursor];
setSelectedTasks((tasks) => tasks.includes(id) ? tasks.filter((task) => task !== id) : [...tasks, id]);
}
};
var fallbackHandler = {
canHandle: () => true,
handle: ({ input, context: { setSearchText } }) => {
setSearchText((currentSearchText) => currentSearchText + input);
}
};
var inputHandlers = [
commandHandler,
semiColonHandler,
arrowsHandler,
enterHandler,
escapeHandler,
deleteHandler,
spaceHandler,
fallbackHandler
];
// packages/nadle/src/core/views/tasks-selection.tsx
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
function renderTaskSelection(tasks) {
return new Promise((resolve) => {
render(/* @__PURE__ */ jsx3(TasksSelection, { onSubmit: resolve, tasks }));
});
}
var PRIMARY_COLOR = "cyan";
var SECONDARY_COLOR = "yellow";
var HIGHLIGHT_COLOR = "cyanBright";
var TasksSelection = ({ tasks, onSubmit }) => {
const [cursor, setCursor] = useState(0);
const [selectedTasks, setSelectedTasks] = useState([]);
const [searchText, setSearchText] = useState("");
const [isCommandMode, setIsCommandMode] = useState(false);
const [commandBuffer, setCommandBuffer] = useState("");
const searchingTasks = useSearching({ tasks, cursor, searchText, selectedTasks });
const { exit } = useApp();
const context = React2.useMemo(() => {
return {
exit,
cursor,
onSubmit,
setCursor,
isCommandMode,
commandBuffer,
setSearchText,
selectedTasks,
searchingTasks,
setSelectedTasks,
setCommandBuffer,
setIsCommandMode
};
}, [commandBuffer, cursor, exit, isCommandMode, onSubmit, searchingTasks, selectedTasks]);
useInput((input, key) => {
const handleParams = { key, input, context };
for (const { handle, canHandle } of inputHandlers) {
if (canHandle(handleParams)) {
handle(handleParams);
return;
}
}
});
return /* @__PURE__ */ jsx3(
TasksSelectionView,
{
commandBuffer,
isCommandMode,
searchText,
searchingTasks,
selectedTasks,
tasks
}
);
};
var TasksSelectionView = (props) => {
const { tasks, searchText, selectedTasks, isCommandMode, commandBuffer, searchingTasks } = props;
return /* @__PURE__ */ jsxs3(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: PRIMARY_COLOR, children: [
/* @__PURE__ */ jsxs3(Text3, { color: PRIMARY_COLOR, children: [
"Select tasks to run (type to search): ",
searchText
] }),
/* @__PURE__ */ jsx3(Box, { flexDirection: "column", marginTop: 1, children: searchText && searchingTasks.length === 0 ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No matched tasks" }) : searchingTasks.map((task) => /* @__PURE__ */ jsx3(VisibleTask, { task }, task.label)) }),
/* @__PURE__ */ jsx3(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs3(Text3, { color: PRIMARY_COLOR, children: [
"Selected tasks: ",
selectedTasks.map((selectedTask) => tasks.find((task) => task.id === selectedTask)?.label ?? "").join(", ")
] }) }),
/* @__PURE__ */ jsx3(Box, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "[\u2191\u2193]: Navigate [Space]: Toggle [Enter]: Run [:q]: Quit [:l]: Clear" }) }),
isCommandMode && /* @__PURE__ */ jsx3(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs3(Text3, { color: PRIMARY_COLOR, children: [
"Command: :",
commandBuffer
] }) })
] });
};
// packages/nadle/src/core/handlers/execute-handler.ts
var ExecuteHandler = class extends BaseHandler {
constructor() {
super(...arguments);
this.name = "execute";
this.description = "Executes the specified tasks.";
}
canHandle() {
return true;
}
async handle() {
let chosenTasks = this.context.options.tasks.map(ResolvedTask.getId);
if (chosenTasks.length === 0) {
this.context.updateState((state) => ({ ...state, selectingTasks: true }));
chosenTasks = await renderTaskSelection(
this.context.taskRegistry.tasks.map(({ id, label, configResolver }) => {
return { id, label, description: configResolver().description };
})
);
this.context.updateState((state) => ({ ...state, selectingTasks: false }));
if (chosenTasks.length === 0) {
this.context.logger.log(Messages.NoTasksFound());
return;
}
}
const { passthroughArgs } = this.context.options;
if (passthroughArgs.length > 0 && chosenTasks.length > 1) {
this.context.logger.log(Messages.PassthroughArgsNotice([...passthroughArgs], chosenTasks));
}
const scheduler = this.context.taskScheduler.init(chosenTasks);
await this.context.eventEmitter.onTasksScheduled(scheduler.scheduledTask.map((taskId) => this.context.taskRegistry.getTaskById(taskId)));
await new TaskPool(this.context, (taskId) => scheduler.getReadyTasks(taskId)).run();
}
};
// packages/nadle/src/core/handlers/clean-cache-handler.ts
import Fs from "fs/promises";
var CleanCacheHandler = class extends BaseHandler {
constructor() {
super(...arguments);
this.name = "clean-cache";
this.description = "Cleans the cache directory.";
}
canHandle() {
return this.context.options.cleanCache;
}
async handle() {
try {
this.context.logger.log(`Cleaning cache at ${this.context.options.cacheDir}...`);
await Fs.rm(this.context.options.cacheDir, { force: true, recursive: true });
} catch (error) {
this.context.logger.error(`Failed to clean cache at ${this.context.options.cacheDir}:`, error);
throw error;
}
}
};
// packages/nadle/src/core/handlers/show-config-handler.ts
import { get } from "lodash-es";
// packages/nadle/src/core/utilities/stringify.ts
function stringify(value) {
return JSON.stringify(value, null, 2);
}
// packages/nadle/src/core/handlers/show-config-handler.ts
var ShowConfigHandler = class extends BaseHandler {
constructor() {
super(...arguments);
this.name = "show-config";
this.description = "Shows the current Nadle configuration.";
}
canHandle() {
return this.context.options.showConfig;
}
handle() {
const { configKey } = this.context.options;
if (!configKey) {
this.context.logger.log(stringify(this.context.options));
return;
}
this.context.logger.log(stringify(get(this.context.options, configKey)));
}
};
// packages/nadle/src/core/handlers/list-workspace-handler.ts
import c3 from "tinyrainbow";
import { getAllWorkspaces, isRootWorkspaceId as isRootWorkspaceId2 } from "@nadle/project-resolver";
// packages/nadle/src/core/utilities/create-tree.ts
var { VERTICAL, UP_RIGHT, HORIZONTAL, VERTICAL_RIGHT } = LINES;
function createTree(node, getChildren, getLabel) {
const lines = [getLabel(node)];
const children = getChildren(node);
for (let childIndex = 0; childIndex < children.length; childIndex++) {
for (const childLine of createTree(children[childIndex], getChildren, getLabel)) {
if (childLine.startsWith(VERTICAL) || childLine.startsWith(UP_RIGHT) || childLine.startsWith(VERTICAL_RIGHT)) {
lines.push(`${VERTICAL} ${childLine}`);
} else {
lines.push(`${childIndex === children.length - 1 ? UP_RIGHT : VERTICAL_RIGHT}${HORIZONTAL}${HORIZONTAL} ${childLine}`);
}
}
}
return lines;
}
// packages/nadle/src/core/utilities/string-builder.ts
var StringBuilder = class {
constructor(separator = " ") {
this.separator = separator;
this.items = [];
}
add(item) {
if (item === false) {
return this;
}
this.items.push(item);
return this;
}
addIf(condition, item) {
if (condition) {
return this.add(item);
}
return this;
}
build() {
if (this.items.length === 0) {
return "";
}
return this.items.join(this.separator);
}
};
// packages/nadle/src/core/handlers/list-workspace-handler.ts
var ListWorkspacesHandler = class extends BaseHandler {
constructor() {
super(...arguments);
this.name = "listWorkspaces";
this.description = "Lists all available workspaces.";
}
canHandle() {
return this.context.options.listWorkspaces;
}
handle() {
const workspaces = getAllWorkspaces(this.context.options.project);
const parentWorkspaceMap = this.computeParentWorkspaceMap(workspaces);
const childrenWorkspaceMap = Object.fromEntries(
workspaces.map(({ id }) => [id, workspaces.filter((workspace) => parentWorkspaceMap[workspace.id]?.id === id)])
);
this.context.logger.log(c3.bold("Available workspaces:\n"));
const tree = createTree(
this.context.options.project.rootWorkspace,
(workspace) => childrenWorkspaceMap[workspace.id],
(workspace) => new StringBuilder().add(isRootWorkspaceId2(workspace.id) ? "Root workspace" : "Workspace").add(highlight(workspace.id)).addIf(workspace.label !== workspace.id && workspace.label !== "", c3.dim(`(alias: ${workspace.label})`)).build()
);
this.context.logger.log(tree.join("\n"));
}
computeParentWorkspaceMap(workspaces) {
const parentWorkspaceMap = {};
for (const childWorkspace of workspaces) {
if (isRootWorkspaceId2(childWorkspace.id)) {
parentWorkspaceMap[childWorkspace.id] = null;
continue;
}
let currentParent = null;
for (const parent of workspaces) {
if (parent.id === childWorkspace.id || !this.isParentWorkspace(parent, childWorkspace)) {
continue;
}
if (!currentParent || parent.relativePath.length > currentParent.relativePath.length) {
currentParent = parent;
}
}
parentWorkspaceMap[childWorkspace.id] = currentParent;
}
return parentWorkspaceMap;
}
isParentWorkspace(parentWorkspace, childWorkspace) {
if (isRootWorkspaceId2(parentWorkspace.id)) {
return true;
}
if (parentWorkspace.id === childWorkspace.id) {
return false;
}
return childWorkspace.relativePath.startsWith(`${parentWorkspace.relativePath}/`);
}
};
// packages/nadle/src/core/handlers/index.ts
var Handlers = [
ListHandler,
ListWorkspacesHandler,
CleanCacheHandler,
DryRunHandler,
ShowConfigHandler,
ExecuteHandler
];
// packages/nadle/src/core/models/event-emitter.ts
var EventEmitter = class {
constructor(listeners) {
this.listeners = listeners;
}
addListener(listener) {
this.listeners.push(listener);
}
async emit(event, ...args) {
for (const listener of this.listeners) {
await listener[event]?.(...args);
}
}
async onInitialize() {
await this.emit("onInitialize");
return this;
}
async onExecutionFailed(error) {
await this.emit("onExecutionFailed", error);
}
async onExecutionFinish() {
await this.emit("onExecutionFinish");
}
async onExecutionStart() {
await this.emit("onExecutionStart");
}
async onTaskCanceled(task) {
await this.emit("onTaskCanceled", task);
}
async onTaskFailed(task) {
await this.emit("onTaskFailed", task);
}
async onTaskFinish(task) {
await this.emit("onTaskFinish", task);
}
async onTaskRestoreFromCache(task) {
await this.emit("onTaskRestoreFromCache", task);
}
async onTaskStart(task, threadId2) {
await this.emit("onTaskStart", task, threadId2);
}
async onTaskUpToDate(task) {
await this.emit("onTaskUpToDate", task);
}
async onTasksScheduled(tasks) {
await this.emit("onTasksScheduled", tasks);
}
};
// packages/nadle/src/core/reporting/reporter.ts
import Url from "url";
import c5 from "tinyrainbow";
import { isCI, isTest } from "std-env";
// packages/nadle/src/core/reporting/renderers/footer-renderer.ts
import Util from "util";
// packages/nadle/src/core/utilities/file-logger.ts
var logItems = [];
var FileLogger = class {
constructor(namespace, options) {
this.namespace = namespace;
this.options = options;
}
log(subspace, ...args) {
if (!this.options?.silent) {
logItems.push({ args, subspace, namespace: this.namespace });
}
}
};
// packages/nadle/src/core/reporting/renderers/footer-renderer.ts
var DEFAULT_RENDER_INTERVAL_MS = 100;
var ESC = "\x1B[";
var CLEAR_LINE = `${ESC}K`;
var MOVE_CURSOR_ONE_ROW_UP = `${ESC}1A`;
var SYNC_START = `${ESC}?2026h`;
var SYNC_END = `${ESC}?2026l`;
var l = new FileLogger("WindowRenderer");
var FooterRenderer = class {
constructor(options) {
this.buffer = [];
this.renderInterval = void 0;
this.renderScheduled = false;
this.windowHeight = 0;
this.finished = false;
this.cleanups = [];
this.options = {
interval: DEFAULT_RENDER_INTERVAL_MS,
...options
};
this.streams = {
error: options.logger.errorStream.write.bind(options.logger.errorStream),
output: options.logger.outputStream.write.bind(options.logger.outputStream)
};
this.cleanups.push(this.interceptStream(process.stdout, "output"), this.interceptStream(process.stderr, "error"));
this.start();
}
start() {
l.log("start");
this.finished = false;
this.renderInterval = setInterval(() => this.schedule(), this.options.interval).unref();
}
stop() {
l.log("stop");
this.cleanups.splice(0).forEach((fn) => fn());
clearInterval(this.renderInterval);
}
/**
* Write all buffered output and stop buffering.
* All intercepted writes are forwarded to actual write after this.
*/
finish() {
l.log("finish");
this.finished = true;
this.flushBuffer();
clearInterval(this.renderInterval);
}
/**
* Queue new render update
*/
schedule() {
if (!this.renderScheduled) {
this.renderScheduled = true;
this.flushBuffer();
setTimeout(() => {
this.renderScheduled = false;
}, 100).unref();
}
}
flushBuffer() {
if (this.buffer.length === 0) {
return this.render();
}
let current;
for (const next of this.buffer.splice(0)) {
if (!current) {
current = next;
continue;
}
if (current.type !== next.type) {
this.render(current.message, current.type);
current = next;
continue;
}
current.message += next.message;
}
if (current) {
this.render(current.message, current.type);
}
}
render(message, type = "output") {
l.log("render", { message });
if (this.finished) {
this.clearWindow();
return this.write(message ?? "", type);
}
const win = this.windowHeight;
const windowContent = this.options.getWindow();
const rowCount = getRenderedRowCount(windowContent, this.options.logger.getColumns());
let padding = this.windowHeight - rowCount;
if (padding > 0 && message) {
padding -= getRenderedRowCount([message], this.options.logger.getColumns());
}
l.log("render", { padding, rowCount, windowHeight: win, col: this.options.logger.getColumns() });
this.write(SYNC_START);
this.clearWindow();
if (message) {
this.write(message, type);
}
if (padding > 0) {
this.write("\n".repeat(padding));
}
this.write(windowContent.join("\n"));
this.write(SYNC_END);
this.windowHeight = rowCount + Math.max(0, padding);
}
clearWindow() {
l.log("clearWindow", `this.windowHeight = ${this.windowHeight}`);
if (this.windowHeight === 0) {
return;
}
this.write(CLEAR_LINE);
for (let i = 1; i < this.windowHeight; i++) {
this.write(`${MOVE_CURSOR_ONE_ROW_UP}${CLEAR_LINE}`);
}
this.windowHeight = 0;
}
interceptStream(stream, type) {
const original = stream.write;
stream.write = (chunk, _, callback) => {
if (chunk) {
if (this.finished) {
this.write(chunk.toString(), type);
} else {
this.buffer.push({ type, message: chunk.toString() });
}
}
callback?.();
};
return function restore() {
stream.write = original;
};
}
write(message, type = "output") {
l.log("write", { message });
this.streams[type](message);
}
};
function getRenderedRowCount(rows, columns) {
let count = 0;
for (const row of rows) {
const text = Util.stripVTControlCharacters(row);
count += Math.max(1, Math.ceil(text.length / columns));
}
return count;
}
// packages/nadle/src/core/reporting/profiling-summary.tsx
import c4 from "tinyrainbow";
var TASK_LIMIT = 5;
function renderProfilingSummary({ tasks, totalDuration }) {
if (tasks.length === 0) {
return "";
}
const headerRow = { label: "Task", duration: "Duration", percentage: "Percentage" };
const bodyRows = [];
for (const task of tasks.sort((task1, task2) => task2.duration - task1.duration).slice(0, TASK_LIMIT)) {
bodyRows.push({ label: task.label, duration: formatTime(task.duration), percentage: (task.duration / totalDuration * 100).toFixed(1) + "%" });
}
const columnWidths = { label: 0, duration: 0, percentage: 0 };
for (const row of [headerRow, ...bodyRows]) {
columnWidths.label = Math.max(columnWidths.label, row.label.length);
columnWidths.duration = Math.max(columnWidths.duration, row.duration.length);
columnWidths.percentage = Math.max(columnWidths.percentage, row.percentage.length);
}
const widths = Object.values(columnWidths).map((width) => width + 2);
return [
"",
c4.bold(c4.green("Profiling Summary")),
renderBorder("top", widths),
renderRow([headerRow.label, headerRow.duration, headerRow.percentage], widths, ["left", "right", "right"]),
renderBorder("join", widths),
...bodyRows.map((row) => renderRow([row.label, row.duration, row.percentage], widths, ["left", "right", "right"])),
renderBorder("bottom", widths)
].join("\n");
}
var toPad = (alignment) => alignment === "left" ? "padEnd" : "padStart";
var renderRow = (cells, lengths, alignments) => CHARS.bodyLeft + cells.map((cell, index) => ` ${cell[toPad(alignments[index])](lengths[index] - 2, " ")} `).join(CHARS.bodyJoin) + CHARS.bodyRight;
var renderBorder = (type, lengths) => CHARS[`${type}Left`] + lengths.map((length) => CHARS[`${type}Body`].repeat(length)).join(CHARS[`${type}Join`]) + CHARS[`${type}Right`];
var CHARS = {
topBody: "\u2500",
topJoin: "\u252C",
topLeft: "\u250C",
topRight: "\u2510",
bottomBody: "\u2500",
bottomJoin: "\u2534",
bottomLeft: "\u2514",
bottomRight: "\u2518",
bodyLeft: "\u2502",
bodyJoin: "\u2502",
bodyRight: "\u2502",
headerJoin: "\u252C",
joinBody: "\u2500",
joinLeft: "\u251C",
joinJoin: "\u253C",
joinRight: "\u2524"
};
// packages/nadle/src/core/reporting/renderers/default-renderer.ts
var DefaultRenderer = class {
start() {
noop();
}
finish() {
noop();
}
schedule() {
noop();
}
};
// packages/nadle/src/core/reporting/reporter.ts
var DefaultReporter = class {
constructor(context) {
this.context = context;
this.renderer = new DefaultRenderer();
this.tracker = this.context.executionTracker;
}
onInitialize() {
this.renderer = this.context.options.footer ? new FooterRenderer({ logger: this.context.logger, getWindow: () => this.createFooter() }) : new DefaultRenderer();
return this;
}
printLabel(label) {
return c5.dim(label.padEnd(11, " "));
}
createFooter() {
const footer = [""];
if (this.context.options.tasks.length === 0 && this.context.state.selectingTasks) {
return footer;
}
const doneTask = this.stats["finished" /* Finished */] + this.stats["from-cache" /* FromCache */] + this.stats["up-to-date" /* UpToDate */];
const stats = [
c5.cyanBright(`${this.stats["scheduled" /* Scheduled */] - this.stats["running" /* Running */] - this.stats["failed" /* Failed */] - doneTask} pending`),
c5.yellow(`${this.stats["running" /* Running */]} running`),
c5.green(`${this.stats["finished" /* Finished */]} done`)
].join(` ${c5.gray(VERTICAL_BAR)} `);
footer.push([this.printLabel("Tasks"), stats, c5.dim(`(${this.stats["scheduled" /* Scheduled */]} scheduled)`)].join(" "));
footer.push([this.printLabel("Duration"), formatTime(this.duration)].join(" "));
footer.push(...this.printRunningTasks());
footer.push("");
return footer;
}
printRunningTasks() {
const lines = Array.from({ length: this.context.options.maxWorkers }, () => ` ${c5.yellow(">")} ${c5.dim("IDLE")}`);
let maxThreadId = 0;
for (const { label, threadId: threadId2 } of this.tracker.getTaskStateByStatus("running" /* Running */)) {
if (threadId2 === null) {
throw new Error(`Task ${label} is running but has no worker ID assigned.`);
}
lines[threadId2 - 1] = ` ${c5.yellow(">")} ${c5.bold(label)}`;
maxThreadId = Math.max(maxThreadId, threadId2);
}
return lines.slice(0, maxThreadId);
}
async onTaskStart(task) {
if (!task.empty) {
this.context.logger.log(`${c5.yellow(">")} Task ${c5.bold(task.label)} ${c5.yellow("STARTED")}
`);
}
this.renderer.schedule();
}
async onTaskFinish(task) {
const prefix = task.empty ? "" : "\n";
this.context.logger.log(
`${prefix}${c5.green(CHECK)} Task ${c5.bold(task.label)} ${c5.green("DONE")} ${c5.dim(formatTime(this.tracker.getTaskState(task.id).duration ?? 0))}`
);
this.renderer.schedule();
}
async onTaskUpToDate(task) {
this.context.logger.log(`
${c5.green(DASH)} Task ${c5.bold(task.label)} ${c5.green("UP-TO-DATE")}`);
this.renderer.schedule();
}
async onTaskRestoreFromCache(task) {
this.context.logger.log(`
${c5.green(CURVE_ARROW)} Task ${c5.bold(task.label)} ${c5.green("FROM-CACHE")}`);
this.renderer.schedule();
}
async onTaskFailed(task) {
this.context.logger.log(
`
${c5.red(CROSS)} Task ${c5.bold(task.label)} ${c5.red("FAILED")} ${formatTime(this.tracker.getTaskState(task.id).duration ?? 0)}`
);
this.renderer.schedule();
}
async onTaskCanceled(task) {
this.context.logger.log(`
${c5.yellow(CROSS)} Task ${c5.bold(task.label)} ${c5.yellow("CANCELED")}`);
this.renderer.schedule();
}
async onTasksScheduled(tasks) {
this.context.logger.info(`Scheduled tasks: ${tasks.map((task) => task.id).join(", ")}`);
this.renderer.schedule();
}
onExecutionStart() {
this.renderer.start();
const { project, minWorkers, maxWorkers } = this.context.options;
const workspaceConfigFileCount = project.workspaces.flatMap((workspace) => workspace.configFilePath ?? []).length;
if (!this.context.options.showConfig) {
this.context.logger.log(c5.bold(c5.cyan(`\u25B6 Welcome to Nadle v${Nadle.version}!`)));
this.context.logger.log(`Using Nadle from ${Url.fileURLToPath(import.meta.resolve("nadle"))}`);
this.context.logger.log(
`Loaded configuration from ${project.rootWorkspace.configFilePath}${workspaceConfigFileCount > 0 ? ` and ${workspaceConfigFileCount} other(s) files` : ""}
`
);
}
this.context.logger.info(
`Using ${minWorkers === maxWorkers ? minWorkers : `${minWorkers}\u2013${maxWorkers}`} worker${maxWorkers > 1 ? "s" : ""} for task execution`
);
this.context.logger.info(`Project directory: ${project.rootWorkspace.absolutePath}`);
this.context.logger.info("Resolved options:", stringify(this.context.options));
this.context.logger.info("Detected environments:", { CI: isCI, TEST: isTest });
this.printResolvedTasks();
this.context.logger.info("Execution started");
}
printResolvedTasks() {
const resolvedTasks = [...this.context.options.tasks, ...this.context.options.excludedTasks].filter(({ corrected }) => corrected);
if (resolvedTasks.length === 0) {
return;
}
const maxOriginTaskLength = Math.max(...resolvedTasks.map(({ rawInput }) => rawInput.length));
const message = [
`Resolved tasks:`,
...resolvedTasks.flatMap(({ taskId, rawInput, corrected }) => {
if (!corrected) {
return [];
}
return `${" ".repeat(4)}${highlight(rawInput.padEnd(maxOriginTaskLength, " "))} ${RIGHT_ARROW} ${c5.green(c5.bold(taskId))}`;
})
].join("\n");
this.context.logger.log(message + "\n");
}
async onExecutionFinish() {
this.renderer.finish();
this.context.logger.info("Execution finished");
if (this.context.options.showConfig) {
return;
}
if (this.context.options.summary) {
this.context.logger.log(
renderProfilingSummary({
totalDuration: this.duration,
tasks: this.context.taskRegistry.tasks.flatMap((task) => {
const { status, duration } = this.tracker.getTaskState(task.id);
if (status !== "finished" /* Finished */) {
return [];
}
return { label: task.label, duration: duration ?? 0 };
})
})
);
}
const print = (count) => `${c5.bold(count)} task${count > 1 ? "s" : ""}`;
this.context.logger.log(`
${c5.bold(c5.green("RUN SUCCESSFUL"))} in ${c5.bold(formatTime(this.duration))}`);
this.context.logger.log(
new StringBuilder(", ").add(`${print(this.stats["finished" /* Finished */])} executed`).add(this.stats["up-to-date" /* UpToDate */] > 0 && `${print(this.stats["up-to-date" /* UpToDate */])} up-to-date`).add(this.stats["from-cache" /* FromCache */] > 0 && `${print(this.stats["from-cache" /* FromCache */])} restored from cache`).build()
);
}
async onExecutionFailed(error) {
this.renderer.finish();
this.context.logger.info("Execution failed");
const finishedTasks = `${c5.bold(this.stats["finished" /* Finished */])} task${this.stats["finished" /* Finished */] > 1 ? "s" : ""}`;
const failedTasks = `${c5.bold(this.stats["failed" /* Failed */])} task${this.stats["failed" /* Failed */] > 1 ? "s" : ""}`;
this.context.logger.log(
`
${c5.bold(c5.red("RUN FAILED"))} in ${c5.bold(formatTime(this.duration))} ${c5.dim(`(${finishedTasks} executed, ${failedTasks} failed)`)}`
);
if (!this.context.options.stacktrace) {
this.context.logger.log(
`
For more details, re-run the command with the ${c5.yellow("--stacktrace")} option to display the full error and help identify the root cause.`
);
} else {
this.context.logger.error(error instanceof Error ? error.stack ?? error.message : String(error));
}
}
get stats() {
return this.tracker.taskStats;
}
get duration() {
return this.tracker.duration;
}
};
// packages/nadle/src/core/utilities/ensure-map.ts
var EnsureMap = class extends Map {
constructor(initializer) {
super();
this.initializer = initializer;
}
get(key) {
if (this.has(key)) {
return super.get(key);
}
const value = this.initializer();
this.set(key, value);
return value;
}
update(key, updater) {
const value = this.get(key);
const updatedValue = updater(value);
this.set(key, updatedValue);
return updatedValue;
}
};
// packages/nadle/src/core/engine/implicit-dependency-resolver.ts
function resolveImplicitDependencies(taskName, workspaceId, deps) {
const implicitDeps = /* @__PURE__ */ new Set();
const upstreamWorkspaceIds = deps.getWorkspaceDependencies(workspaceId);
for (const upstreamWorkspaceId of upstreamWorkspaceIds) {
const sameNameTasks = deps.getTasksByName(taskName);
const upstreamTask = sameNameTasks.find((task) => task.workspaceId === upstreamWorkspaceId);
if (!upstreamTask || deps.excludedTaskIds.has(upstreamTask.id)) {
continue;
}
deps.logger.debug({ tag: "Scheduler" }, `Implicit dependency: ${workspaceId}:${taskName} \u2192 ${upstreamTask.id}`);
implicitDeps.add(upstreamTask.id);
}
return implicitDeps;
}
// packages/nadle/src/core/engine/task-scheduler.ts
var TaskScheduler = class {
constructor(deps) {
this.deps = deps;
this.dependentsGraph = new EnsureMap(() => /* @__PURE__ */ new Set());
this.dependencyGraph = new EnsureMap(() => /* @__PURE__ */ new Set());
this.transitiveDependencyGraph = new EnsureMap(() => /* @__PURE__ */ new Set());
this.indegree = new EnsureMap(() => 0);
this.readyTasks = /* @__PURE__ */ new Set();
this.implicitEdges = /* @__PURE__ */ new Set();
this.rootAggregationDeps = /* @__PURE__ */ new Map();
this.taskIds = [];
this.excludedTaskIds = /* @__PURE__ */ new Set();
this.mainTaskId = void 0;
}
init(taskIds = this.deps.options.tasks.map(({ taskId }) => taskId)) {
this.taskIds = this.expandWorkspaceTasks(taskIds);
this.excludedTaskIds = new Set(this.deps.options.excludedTasks.map(ResolvedTask.getId));
this.taskIds.forEach((taskId) => this.analyze(taskId));
this.taskIds.forEach((taskId) => this.detectCycle(taskId, [taskId]));
this.deps.logger.debug({ tag: "Scheduler" }, `transitiveDependencyGraph`, this.transitiveDependencyGraph);
this.deps.logger.debug({ tag: "Scheduler" }, `dependencyGraph`, this.dependencyGraph);
if (!this.deps.options.parallel) {
this.mainTaskId = this.taskIds[0];
}
return this;
}
expandWorkspaceTasks(taskIds) {
const expandedTaskIds = [];
for (const taskId of taskIds) {
expandedTaskIds.push(taskId);
const { name, workspaceId } = this.deps.getTaskById(taskId);
if (!this.deps.isRootWorkspace(workspaceId)) {
continue;
}
const childTaskIds = /* @__PURE__ */ new Set();
for (const sameNameTask of this.deps.getTasksByName(name)) {
if (!taskIds.includes(sameNameTask.id)) {
expandedTaskIds.push(sameNameTask.id);
childTaskIds.add(sameNameTask.id);
}
}
if (childTaskIds.size > 0 && this.deps.options.implicitDependencies) {
this.rootAggregationDeps.set(taskId, childTaskIds);
}
}
return expandedTaskIds;
}
detectCycle(taskId, paths) {
for (const dependency of this.dependencyGraph.get(taskId)) {
const startIndex = paths.indexOf(dependency);
if (startIndex !== -1) {
const message = Messages.CycleDetected([...paths.slice(startIndex), dependency].map(highlight).join(` ${RIGHT_ARROW} `));
this.deps.logger.error(message);
throw new CyclicDependencyError(message);
}
this.detectCycle(dependency, [...paths, dependency]);
}
}
analyze(taskId) {
if (this.dependencyGraph.has(taskId)) {
return;
}
const { workspaceId, configResolver } = this.deps.getTaskById(taskId);
const dependencies = new Set(
MaybeArray.toArray(configResolver().dependsOn ?? []).map((input) => this.deps.parseTaskRef(input, workspaceId)).filter((id) => !this.excludedTaskIds.has(id))
);
if (this.deps.options.implicitDependencies) {
this.addImplicitDeps(taskId, dependencies);
}
this.dependencyGraph.set(taskId, dependencies);
this.indegree.set(taskId, dependencies.size);
this.transitiveDependencyGraph.set(taskId, new Set(dependencies));
for (const dependency of dependencies) {
this.dependentsGraph.update(dependency, (s) => s.add(taskId));
this.analyze(dependency);
this.transitiveDependencyGraph.update(taskId, (current) => {
this.transitiveDependencyGraph.get(dependency).forEach((d) => current.add(d));
return current;
});
}
}
addImplicitDeps(taskId, dependencies) {
const { name, workspaceId } = this.deps.getTaskById(taskId);
const resolverDeps = {
logger: this.deps.logger,
excludedTaskIds: this.excludedTaskIds,
getTasksByName: (n) => this.deps.getTasksByName(n),
getWorkspaceDependencies: (id) => this.deps.getWorkspaceDependencies(id)
};
for (const dep of resolveImplicitDependencies(name, workspaceId, resolverDeps)) {
if (!dependencies.has(dep)) {
this.implicitEdges.add(`${dep}->${taskId}`);
}
dependencies.add(dep);
}
for (const childId of this.rootAggregationDeps.get(taskId) ?? []) {
if (!this.excludedTaskIds.has(childId)) {
if (!dependencies.has(childId)) {
this.implicitEdges.add(`${childId}->${taskId}`);
}
dependencies.add(childId);
}
}
}
get scheduledTask() {
return Array.from(this.dependencyGraph.keys());
}
getDirectDependencies(taskId) {
return this.dependencyGraph.get(taskId);
}
getImplicitDeps(taskId) {
return [...this.dependencyGraph.get(taskId)].filter((dep) => this.implicitEdges.has(`${dep}->${taskId}`));
}
getIndegreeEntries() {
if (!this.mainTaskId) {
return this.indegree.entries();
}
const rootId = this.mainTaskId;
return new Map(
Array.from(this.indegree.entries()).filter(([taskId]) => taskId === rootId || this.transitiveDependencyGraph.get(rootId)?.has(taskId))
);
}
isBelongToRootTaskTree(taskId) {
if (!this.mainTaskId || taskId === this.mainTaskId) {
return true;
}
return this.transitiveDependencyGraph.get(this.mainTaskId).has(taskId);
}
getReadyTasks(doneTaskId) {
this.deps.logger.debug({ tag: "Scheduler" }, `runningRoot = ${this.mainTaskId}, doneTaskId = ${doneTaskId}`);
if (doneTaskId === void 0) {
return this.getInitialReadyTasks();
}
const nextReadyTasks = /* @__PURE__ */ new Set();
for (const dependentTask of this.dependentsGraph.get(doneTaskId)) {
if (this.readyTasks.has(dependentTask)) {
continue;
}
let indegree = this.indegree.get(dependentTask);
if (indegree === 0) {
throw new Error(`Incorrect state. Expect ${dependentTask} to have indegree > 0`);
}
this.indegree.set(dependentTask, --indegree);
if (indegree === 0 && this.isBelongToRootTaskTree(dependentTask)) {
nextReadyTasks.add(dependentTask);
this.readyTasks.add(dependentTask);
}
}
if (doneTaskId === this.mainTaskId) {
if (!this.moveToNextMainTask()) {
return /* @__PURE__ */ new Set();
}
return this.getReadyTasks();
}
this.deps.logger.debug({ tag: "Scheduler" }, `Next tasks = ${Array.from(nextReadyTasks).join(",")}`);
return nextReadyTasks;
}
getInitialReadyTasks() {
const nextReadyTasks = /* @__PURE__ */ new Set();
for (const [taskId, indegree] of this.getIndegreeEntries()) {
this.deps.logger.debug({ tag: "Scheduler" }, `taskId = ${taskId}, indegree = ${indegree}`);
if (indegree === 0 && !this.readyTasks.has(taskId)) {
nextReadyTasks.add(taskId);
this.readyTasks.add(taskId);
}
}
this.deps.logger.debug({ tag: "Scheduler" }, `Next tasks = ${Array.from(nextReadyTasks).join(",")}`);
return nextReadyTasks;
}
moveToNextMainTask() {
const nextIndex = this.mainTaskId !== void 0 ? this.taskIds.indexOf(this.mainTaskId) + 1 : -1;
this.mainTaskId = nextIndex >= 0 ? this.taskIds[nextIndex] : void 0;
return this.mainTaskId;
}
getExecutionPlan(taskId) {
const readyTaskIds = Array.from(this.getReadyTasks(taskId));
for (const readyTaskId of readyTaskIds) {
readyTaskIds.push(...this.getExecutionPlan(readyTaskId));
}
return readyTaskIds;
}
};
// packages/nadle/src/core/reporting/agent-reporter.ts
var AgentReporter = class {
constructor(context) {
this.context = context;
this.tracker = this.context.executionTracker;