Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
513 changes: 513 additions & 0 deletions .claude/myplans/show-running-tasks.md

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions packages/nx/src/command-line/show/command-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ export type ShowTargetBaseOptions = NxShowArgs & {
verbose?: boolean;
};

export type ShowRunningTasksOptions = NxShowArgs & {
task?: string;
verbose?: boolean;
};

export type ShowTargetInputsOptions = NxShowArgs & {
target?: string;
check?: string[];
Expand All @@ -60,6 +65,7 @@ export const yargsShowCommand: CommandModule<
.command(showProjectsCommand)
.command(showProjectCommand)
.command(showTargetCommand)
.command(showRunningTasksCommand)
.demandCommand()
.option('json', {
type: 'boolean',
Expand Down Expand Up @@ -358,6 +364,37 @@ const showTargetOutputsCommand: CommandModule<
},
};

const showRunningTasksCommand: CommandModule<
NxShowArgs,
ShowRunningTasksOptions
> = {
command: 'running-tasks',
describe:
'Show currently running Nx tasks and their status. Queries the Nx daemon for live task state.',
builder: (yargs) =>
withVerbose(yargs)
.option('task', {
type: 'string',
description:
'Show the log output of a specific running task (e.g., myapp:serve).',
})
.example(
'$0 show running-tasks',
'List all currently running tasks as JSON'
)
.example(
'$0 show running-tasks --task myapp:serve',
'Show log output of the myapp:serve task'
) as any,
handler: async (args) => {
const exitCode = await handleErrors(args.verbose as boolean, async () => {
const { showRunningTasksHandler } = await import('./running-tasks');
await showRunningTasksHandler(args);
});
process.exit(exitCode);
},
};

const showTargetCommand: CommandModule<NxShowArgs, ShowTargetBaseOptions> = {
command: 'target',
describe:
Expand Down
114 changes: 114 additions & 0 deletions packages/nx/src/command-line/show/running-tasks.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { showRunningTasksHandler } from './running-tasks';

function createMockDaemonClient(
runningTasks: any[] = [],
taskOutput: string = ''
) {
return {
getRunningTasks: jest.fn().mockResolvedValue(runningTasks),
getRunningTaskOutput: jest.fn().mockResolvedValue(taskOutput),
} as any;
}

describe('showRunningTasksHandler', () => {
let stdoutSpy: jest.SpyInstance;

beforeEach(() => {
stdoutSpy = jest.spyOn(process.stdout, 'write').mockImplementation();
});

afterEach(() => {
stdoutSpy.mockRestore();
});

it('should output JSON of all running tasks', async () => {
const client = createMockDaemonClient([
{
pid: 1234,
command: 'nx serve app',
startTime: '2026-02-24T10:00:00.000Z',
tasks: {
'app:serve': { status: 'in-progress', continuous: true },
},
},
]);

await showRunningTasksHandler({}, client);

const output = stdoutSpy.mock.calls[0][0];
const parsed = JSON.parse(output);
expect(parsed).toHaveLength(1);
expect(parsed[0].tasks['app:serve'].status).toBe('in-progress');
});

it('should output task log when --task is provided', async () => {
const client = createMockDaemonClient(
[
{
pid: 1234,
command: 'nx serve',
tasks: { 'app:serve': { status: 'in-progress' } },
},
],
'Server running on http://localhost:4200\n'
);

await showRunningTasksHandler({ task: 'app:serve' }, client);

expect(client.getRunningTaskOutput).toHaveBeenCalledWith(1234, 'app:serve');
expect(stdoutSpy).toHaveBeenCalledWith(
expect.stringContaining('localhost:4200')
);
});

it('should return empty array when no tasks running', async () => {
const client = createMockDaemonClient([]);

await showRunningTasksHandler({}, client);

const output = stdoutSpy.mock.calls[0][0];
const parsed = JSON.parse(output);
expect(parsed).toEqual([]);
});

it('should error when --task specifies non-existent task', async () => {
const client = createMockDaemonClient([
{
pid: 1234,
command: 'nx serve',
tasks: { 'app:serve': { status: 'in-progress' } },
},
]);

const exitSpy = jest
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);

await showRunningTasksHandler({ task: 'nonexistent:task' }, client);

expect(exitSpy).toHaveBeenCalledWith(1);
exitSpy.mockRestore();
});

it('should handle multiple runs and find task in correct one', async () => {
const client = createMockDaemonClient(
[
{
pid: 1111,
command: 'nx build app',
tasks: { 'app:build': { status: 'success' } },
},
{
pid: 2222,
command: 'nx serve app',
tasks: { 'app:serve': { status: 'in-progress' } },
},
],
'Server started'
);

await showRunningTasksHandler({ task: 'app:serve' }, client);

expect(client.getRunningTaskOutput).toHaveBeenCalledWith(2222, 'app:serve');
});
});
49 changes: 49 additions & 0 deletions packages/nx/src/command-line/show/running-tasks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { daemonClient } from '../../daemon/client/client';
import { output } from '../../utils/output';

export interface ShowRunningTasksOptions {
json?: boolean;
task?: string;
}

export async function showRunningTasksHandler(
args: ShowRunningTasksOptions,
client = daemonClient
): Promise<void> {
const runningTasks = await client.getRunningTasks();

if (args.task) {
// Find which process has this task
let targetPid: number | null = null;
for (const run of runningTasks) {
if (run.tasks[args.task]) {
targetPid = run.pid;
break;
}
}

if (targetPid === null) {
output.error({
title: `Task '${args.task}' is not currently running`,
bodyLines: runningTasks.length
? [
'Currently running tasks:',
...runningTasks.flatMap((run) =>
Object.keys(run.tasks).map((t) => ` - ${t}`)
),
]
: ['No tasks are currently running.'],
});
process.exit(1);
}

const taskOutput = await client.getRunningTaskOutput(targetPid, args.task);
process.stdout.write(taskOutput);
if (taskOutput && !taskOutput.endsWith('\n')) {
process.stdout.write('\n');
}
} else {
process.stdout.write(JSON.stringify(runningTasks, null, 2));
process.stdout.write('\n');
}
}
66 changes: 66 additions & 0 deletions packages/nx/src/daemon/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,19 @@ import {
type HandleUpdateWorkspaceContextMessage,
UPDATE_WORKSPACE_CONTEXT,
} from '../message-types/update-workspace-context';
import {
GET_RUNNING_TASKS,
GET_RUNNING_TASK_OUTPUT,
REGISTER_RUNNING_TASKS,
UNREGISTER_RUNNING_TASKS,
UPDATE_RUNNING_TASKS,
type HandleGetRunningTaskOutputMessage,
type HandleGetRunningTasksMessage,
type HandleRegisterRunningTasksMessage,
type HandleUnregisterRunningTasksMessage,
type HandleUpdateRunningTasksMessage,
type RunningTaskStatusUpdate,
} from '../message-types/running-tasks';
import {
DAEMON_DIR_FOR_CURRENT_WORKSPACE,
DAEMON_OUTPUT_LOG_FILE,
Expand Down Expand Up @@ -1343,6 +1356,59 @@ export class DaemonClient {
}
}

registerRunningTasks(
pid: number,
command: string,
taskIds: string[]
): Promise<void> {
const message: HandleRegisterRunningTasksMessage = {
type: REGISTER_RUNNING_TASKS,
pid,
command,
taskIds,
};
return this.sendToDaemonViaQueue(message);
}

updateRunningTasks(
pid: number,
taskUpdates: RunningTaskStatusUpdate[],
outputChunks: Record<string, string>
): Promise<void> {
const message: HandleUpdateRunningTasksMessage = {
type: UPDATE_RUNNING_TASKS,
pid,
taskUpdates,
outputChunks,
};
return this.sendToDaemonViaQueue(message);
}

unregisterRunningTasks(pid: number): Promise<void> {
const message: HandleUnregisterRunningTasksMessage = {
type: UNREGISTER_RUNNING_TASKS,
pid,
};
return this.sendToDaemonViaQueue(message);
}

getRunningTasks(): Promise<any[]> {
const message: HandleGetRunningTasksMessage = {
type: GET_RUNNING_TASKS,
};
return this.sendToDaemonViaQueue(message);
}

async getRunningTaskOutput(pid: number, taskId: string): Promise<string> {
const message: HandleGetRunningTaskOutputMessage = {
type: GET_RUNNING_TASK_OUTPUT,
pid,
taskId,
};
const result = await this.sendToDaemonViaQueue(message);
return result?.output ?? '';
}

async stop(): Promise<void> {
try {
const pid = getDaemonProcessIdSync();
Expand Down
Loading
Loading