tasktui 1.0.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.
- package/README.md +21 -0
- package/dist/app.d.ts +3 -0
- package/dist/app.js +71 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +20 -0
- package/dist/components/SubprocessOutput.d.ts +6 -0
- package/dist/components/SubprocessOutput.js +21 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/lib/constants.d.ts +2 -0
- package/dist/lib/constants.js +2 -0
- package/dist/lib/types.d.ts +15 -0
- package/dist/lib/types.js +9 -0
- package/dist/lib/utils.d.ts +5 -0
- package/dist/lib/utils.js +44 -0
- package/dist/renderer.d.ts +4 -0
- package/dist/renderer.js +58 -0
- package/dist/state.d.ts +31 -0
- package/dist/state.js +24 -0
- package/dist/tasks.d.ts +5 -0
- package/dist/tasks.js +92 -0
- package/dist/types.d.ts +15 -0
- package/dist/types.js +9 -0
- package/dist/ui.d.ts +11 -0
- package/dist/ui.js +121 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +44 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# tasktui
|
|
2
|
+
|
|
3
|
+
Run tasks in a multiplexed terminal with dependency management.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
$ npm install --save-dev task-tui
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
$ task-tui --help
|
|
15
|
+
|
|
16
|
+
Usage
|
|
17
|
+
$ tasktui [--config <PATH> | --help]
|
|
18
|
+
|
|
19
|
+
Options
|
|
20
|
+
--config Path to config file (default: ./tasktui.config.json)
|
|
21
|
+
```
|
package/dist/app.d.ts
ADDED
package/dist/app.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { render, showError } from './renderer.js';
|
|
2
|
+
import { createState, getAllTasksInOrder } from './state.js';
|
|
3
|
+
import { cleanup, ensureDependencies, spawnTask } from './tasks.js';
|
|
4
|
+
import { createUI } from './ui.js';
|
|
5
|
+
import { ensureError, loadConfig } from './utils.js';
|
|
6
|
+
const ui = createUI();
|
|
7
|
+
const state = createState();
|
|
8
|
+
function handleMove(steps) {
|
|
9
|
+
const allTasks = getAllTasksInOrder(state);
|
|
10
|
+
if (allTasks.length === 0)
|
|
11
|
+
return;
|
|
12
|
+
const selectedIndex = allTasks.indexOf(state.selectedTask);
|
|
13
|
+
const newIndex = (selectedIndex + steps + allTasks.length) % allTasks.length;
|
|
14
|
+
const newTask = allTasks[newIndex];
|
|
15
|
+
if (newTask) {
|
|
16
|
+
state.selectedTask = newTask;
|
|
17
|
+
render(ui, state);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function loadAndProcessConfig(configPath) {
|
|
21
|
+
try {
|
|
22
|
+
const config = loadConfig(configPath);
|
|
23
|
+
state.config = config;
|
|
24
|
+
const tasks = config?.tasks ?? {};
|
|
25
|
+
if (!Object.keys(tasks).length && state.init) {
|
|
26
|
+
cleanup(state);
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
state.init = true;
|
|
30
|
+
state.tasks = tasks;
|
|
31
|
+
// Process tasks
|
|
32
|
+
for (const [name, task] of Object.entries(tasks)) {
|
|
33
|
+
if (state.spawnedTasks.has(name))
|
|
34
|
+
continue;
|
|
35
|
+
state.spawnedTasks.add(name);
|
|
36
|
+
if (task.dependsOn.length) {
|
|
37
|
+
const allDepsExist = ensureDependencies(name, task.dependsOn, state);
|
|
38
|
+
if (!allDepsExist)
|
|
39
|
+
continue;
|
|
40
|
+
state.queue.push({
|
|
41
|
+
name,
|
|
42
|
+
task,
|
|
43
|
+
remainingDeps: task.dependsOn,
|
|
44
|
+
});
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
spawnTask(name, task, state, () => render(ui, state));
|
|
48
|
+
}
|
|
49
|
+
render(ui, state);
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
const error = ensureError(e);
|
|
53
|
+
showError(error.message, ui, state);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Key bindings
|
|
57
|
+
ui.screen.key(['up', 'k'], () => {
|
|
58
|
+
handleMove(-1);
|
|
59
|
+
});
|
|
60
|
+
ui.screen.key(['down', 'j'], () => {
|
|
61
|
+
handleMove(1);
|
|
62
|
+
});
|
|
63
|
+
ui.screen.key(['C-c', 'q'], () => {
|
|
64
|
+
cleanup(state);
|
|
65
|
+
process.exit(0);
|
|
66
|
+
});
|
|
67
|
+
// Handle resize
|
|
68
|
+
ui.screen.on('resize', () => {
|
|
69
|
+
render(ui, state);
|
|
70
|
+
});
|
|
71
|
+
export default ui;
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import meow from 'meow';
|
|
3
|
+
import ui, { loadAndProcessConfig } from './app.js';
|
|
4
|
+
const cli = meow(`
|
|
5
|
+
Usage
|
|
6
|
+
$ tasktui [--config <PATH> | --help]
|
|
7
|
+
|
|
8
|
+
Options
|
|
9
|
+
--config Path to config file (default: ./tasktui.config.json)
|
|
10
|
+
`, {
|
|
11
|
+
importMeta: import.meta,
|
|
12
|
+
flags: {
|
|
13
|
+
config: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
},
|
|
16
|
+
help: {},
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
loadAndProcessConfig(cli.flags.config);
|
|
20
|
+
ui.screen.render();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Box, Text } from 'ink';
|
|
2
|
+
import childProcess from 'node:child_process';
|
|
3
|
+
import React, { useEffect, useState } from 'react';
|
|
4
|
+
import stripAnsi from 'strip-ansi';
|
|
5
|
+
export default function TaskWindow({ tasks, selected, }) {
|
|
6
|
+
const [buffers, setBuffers] = useState({});
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
for (const [name, task] of Object.entries(tasks)) {
|
|
9
|
+
const subProcess = childProcess.spawn('sh', ['-c', task.command]);
|
|
10
|
+
subProcess.stdout.on('data', (newOutput) => {
|
|
11
|
+
const text = stripAnsi(newOutput.toString('utf8')).trim();
|
|
12
|
+
setBuffers(prev => {
|
|
13
|
+
return { ...prev, [name]: text };
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
}, [tasks]);
|
|
18
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
19
|
+
React.createElement(Text, { dimColor: true }, selected),
|
|
20
|
+
React.createElement(Text, null, buffers[selected] ?? '')));
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
export declare const TaskSchema: z.ZodObject<{
|
|
3
|
+
command: z.ZodString;
|
|
4
|
+
dependsOn: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
5
|
+
cwd: z.ZodDefault<z.ZodString>;
|
|
6
|
+
}, z.z.core.$strip>;
|
|
7
|
+
export type Task = z.infer<typeof TaskSchema>;
|
|
8
|
+
export declare const TasksConfigSchema: z.ZodObject<{
|
|
9
|
+
tasks: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
10
|
+
command: z.ZodString;
|
|
11
|
+
dependsOn: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
12
|
+
cwd: z.ZodDefault<z.ZodString>;
|
|
13
|
+
}, z.z.core.$strip>>;
|
|
14
|
+
}, z.z.core.$strip>;
|
|
15
|
+
export type TasksConfig = z.infer<typeof TasksConfigSchema>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
export const TaskSchema = z.object({
|
|
3
|
+
command: z.string(),
|
|
4
|
+
dependsOn: z.string().array().default([]),
|
|
5
|
+
cwd: z.string().default(process.cwd()),
|
|
6
|
+
});
|
|
7
|
+
export const TasksConfigSchema = z.object({
|
|
8
|
+
tasks: z.record(z.string(), TaskSchema),
|
|
9
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import z from 'zod';
|
|
4
|
+
import { CONFIG_PATH } from './constants.js';
|
|
5
|
+
import { TasksConfigSchema } from './types.js';
|
|
6
|
+
function getConfigPath(cliOption) {
|
|
7
|
+
if (cliOption)
|
|
8
|
+
return cliOption;
|
|
9
|
+
return CONFIG_PATH;
|
|
10
|
+
}
|
|
11
|
+
export default function formatZodError(error) {
|
|
12
|
+
if (error.issues.length === 0) {
|
|
13
|
+
return 'Unknown Zod error';
|
|
14
|
+
}
|
|
15
|
+
const issue = error.issues[0];
|
|
16
|
+
if (!issue)
|
|
17
|
+
return error.message;
|
|
18
|
+
const path = issue.path.join('.');
|
|
19
|
+
return `${issue.message} at '${path}' [code: ${issue.code}]`;
|
|
20
|
+
}
|
|
21
|
+
export function loadConfig(configPath) {
|
|
22
|
+
try {
|
|
23
|
+
const relativePath = path.join(process.cwd(), getConfigPath(configPath));
|
|
24
|
+
const raw = fs.readFileSync(relativePath, 'utf-8');
|
|
25
|
+
const parsed = JSON.parse(raw);
|
|
26
|
+
return z.parse(TasksConfigSchema, parsed);
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
const error = ensureError(e);
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function ensureError(error) {
|
|
34
|
+
if (error instanceof z.ZodError)
|
|
35
|
+
return new Error(formatZodError(error));
|
|
36
|
+
if (error instanceof Error)
|
|
37
|
+
return error;
|
|
38
|
+
let stringified = '[Unable to stringify the thrown value]';
|
|
39
|
+
try {
|
|
40
|
+
stringified = JSON.stringify(error);
|
|
41
|
+
}
|
|
42
|
+
catch { }
|
|
43
|
+
return new Error(stringified);
|
|
44
|
+
}
|
package/dist/renderer.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { getOrderedTasks } from './state.js';
|
|
2
|
+
export function showError(message, ui, state) {
|
|
3
|
+
state.error = message;
|
|
4
|
+
ui.errorBox.setContent(`{red-fg}Error: ${message}{/}`);
|
|
5
|
+
ui.errorBox.show();
|
|
6
|
+
ui.screen.render();
|
|
7
|
+
}
|
|
8
|
+
export function render(ui, state) {
|
|
9
|
+
const { running, queued, completed } = getOrderedTasks(state);
|
|
10
|
+
const lines = [];
|
|
11
|
+
// Running section
|
|
12
|
+
if (running.length > 0) {
|
|
13
|
+
lines.push(`{gray-fg} Running (${running.length}){/}`);
|
|
14
|
+
for (const name of running) {
|
|
15
|
+
const isSelected = name === state.selectedTask;
|
|
16
|
+
const color = isSelected ? 'yellow-fg' : 'white-fg';
|
|
17
|
+
lines.push(`{${color}}${name}{/}`);
|
|
18
|
+
}
|
|
19
|
+
lines.push('');
|
|
20
|
+
}
|
|
21
|
+
// Queued section
|
|
22
|
+
if (queued.length > 0) {
|
|
23
|
+
lines.push(`{gray-fg} Queued (${queued.length}){/}`);
|
|
24
|
+
for (const queueItem of queued) {
|
|
25
|
+
const isSelected = queueItem.name === state.selectedTask;
|
|
26
|
+
const color = isSelected ? 'yellow-fg' : 'gray-fg';
|
|
27
|
+
lines.push(`{${color}}${queueItem.name}{/}`);
|
|
28
|
+
}
|
|
29
|
+
lines.push('');
|
|
30
|
+
}
|
|
31
|
+
// Completed section
|
|
32
|
+
if (completed.length > 0) {
|
|
33
|
+
lines.push(`{gray-fg} Completed (${completed.length}){/}`);
|
|
34
|
+
for (const name of completed) {
|
|
35
|
+
const buffer = state.buffers[name];
|
|
36
|
+
const isSelected = name === state.selectedTask;
|
|
37
|
+
let statusSymbol = '✓';
|
|
38
|
+
let color = 'gray-fg';
|
|
39
|
+
if (buffer?.errored) {
|
|
40
|
+
statusSymbol = '✗';
|
|
41
|
+
color = 'red-fg';
|
|
42
|
+
}
|
|
43
|
+
if (isSelected) {
|
|
44
|
+
color = 'yellow-fg';
|
|
45
|
+
}
|
|
46
|
+
lines.push(`{${color}}${name}{/} ${statusSymbol}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
ui.taskList.setContent(lines.join('\n'));
|
|
50
|
+
// Update output pane
|
|
51
|
+
if (state.selectedTask) {
|
|
52
|
+
ui.taskNameBox.setContent(`{gray-fg}${state.selectedTask}{/}`);
|
|
53
|
+
const output = state.buffers[state.selectedTask]?.text ?? '';
|
|
54
|
+
ui.taskOutputBox.setContent(output);
|
|
55
|
+
ui.taskOutputBox.setScrollPerc(100); // Auto-scroll to bottom
|
|
56
|
+
}
|
|
57
|
+
ui.screen.render();
|
|
58
|
+
}
|
package/dist/state.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import childProcess from 'node:child_process';
|
|
2
|
+
import { Task, TasksConfig } from './types.js';
|
|
3
|
+
export interface TaskBuffer {
|
|
4
|
+
running: boolean;
|
|
5
|
+
errored: boolean;
|
|
6
|
+
text: string;
|
|
7
|
+
}
|
|
8
|
+
export interface QueueItem {
|
|
9
|
+
name: string;
|
|
10
|
+
task: Task;
|
|
11
|
+
remainingDeps: string[];
|
|
12
|
+
}
|
|
13
|
+
export interface AppState {
|
|
14
|
+
init: boolean;
|
|
15
|
+
spawnedTasks: Set<string>;
|
|
16
|
+
childProcesses: Map<string, childProcess.ChildProcess>;
|
|
17
|
+
taskOrder: string[];
|
|
18
|
+
config?: TasksConfig;
|
|
19
|
+
tasks: Record<string, Task>;
|
|
20
|
+
error: string | null;
|
|
21
|
+
selectedTask: string;
|
|
22
|
+
buffers: Record<string, TaskBuffer>;
|
|
23
|
+
queue: QueueItem[];
|
|
24
|
+
}
|
|
25
|
+
export declare function createState(): AppState;
|
|
26
|
+
export declare function getOrderedTasks(state: AppState): {
|
|
27
|
+
running: string[];
|
|
28
|
+
queued: QueueItem[];
|
|
29
|
+
completed: string[];
|
|
30
|
+
};
|
|
31
|
+
export declare function getAllTasksInOrder(state: AppState): string[];
|
package/dist/state.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function createState() {
|
|
2
|
+
return {
|
|
3
|
+
init: false,
|
|
4
|
+
spawnedTasks: new Set(),
|
|
5
|
+
childProcesses: new Map(),
|
|
6
|
+
taskOrder: [],
|
|
7
|
+
config: undefined,
|
|
8
|
+
tasks: {},
|
|
9
|
+
error: null,
|
|
10
|
+
selectedTask: '',
|
|
11
|
+
buffers: {},
|
|
12
|
+
queue: [],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function getOrderedTasks(state) {
|
|
16
|
+
const running = state.taskOrder.filter((name) => state.buffers[name]?.running);
|
|
17
|
+
const queued = state.queue;
|
|
18
|
+
const completed = state.taskOrder.filter((name) => state.buffers[name] && !state.buffers[name].running);
|
|
19
|
+
return { running, queued, completed };
|
|
20
|
+
}
|
|
21
|
+
export function getAllTasksInOrder(state) {
|
|
22
|
+
const { running, queued, completed } = getOrderedTasks(state);
|
|
23
|
+
return [...running, ...queued.map((q) => q.name), ...completed];
|
|
24
|
+
}
|
package/dist/tasks.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { AppState } from './state.js';
|
|
2
|
+
import { Task } from './types.js';
|
|
3
|
+
export declare function ensureDependencies(task: string, deps: string[], state: AppState): boolean;
|
|
4
|
+
export declare function spawnTask(name: string, task: Task, state: AppState, onUpdate: () => void): void;
|
|
5
|
+
export declare function cleanup(state: AppState): void;
|
package/dist/tasks.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import childProcess from 'node:child_process';
|
|
2
|
+
import ui from './app.js';
|
|
3
|
+
import { showError } from './renderer.js';
|
|
4
|
+
import { ensureError } from './utils.js';
|
|
5
|
+
export function ensureDependencies(task, deps, state) {
|
|
6
|
+
let allDepsExist = true;
|
|
7
|
+
for (const dep of deps) {
|
|
8
|
+
if (!Object.keys(state.tasks).includes(dep)) {
|
|
9
|
+
state.buffers[task] = {
|
|
10
|
+
running: false,
|
|
11
|
+
text: `Cannot depend on ${dep} as it does not exist`,
|
|
12
|
+
errored: true,
|
|
13
|
+
};
|
|
14
|
+
state.taskOrder.push(task);
|
|
15
|
+
allDepsExist = false;
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return allDepsExist;
|
|
20
|
+
}
|
|
21
|
+
export function spawnTask(name, task, state, onUpdate) {
|
|
22
|
+
console.log(task.command);
|
|
23
|
+
const subProcess = childProcess.spawn('sh', ['-c', task.command], {
|
|
24
|
+
cwd: task.cwd,
|
|
25
|
+
env: {
|
|
26
|
+
...process.env,
|
|
27
|
+
FORCE_COLOR: '1',
|
|
28
|
+
CLICOLOR_FORCE: '1',
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
state.childProcesses.set(name, subProcess);
|
|
32
|
+
subProcess.on('spawn', () => {
|
|
33
|
+
if (Object.keys(state.buffers).length === 0) {
|
|
34
|
+
state.selectedTask = name;
|
|
35
|
+
}
|
|
36
|
+
state.buffers[name] = { running: true, text: '', errored: false };
|
|
37
|
+
state.taskOrder.push(name);
|
|
38
|
+
onUpdate();
|
|
39
|
+
});
|
|
40
|
+
const handleOutput = (newOutput) => {
|
|
41
|
+
const text = newOutput.toString('utf8');
|
|
42
|
+
const currentText = state.buffers[name]?.text ?? '';
|
|
43
|
+
state.buffers[name] = {
|
|
44
|
+
running: true,
|
|
45
|
+
text: currentText + text,
|
|
46
|
+
errored: false,
|
|
47
|
+
};
|
|
48
|
+
if (state.selectedTask === name) {
|
|
49
|
+
onUpdate();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
subProcess.stdout.on('data', handleOutput);
|
|
53
|
+
subProcess.stderr.on('data', handleOutput);
|
|
54
|
+
subProcess.on('close', (code) => {
|
|
55
|
+
const currentText = state.buffers[name]?.text ?? '';
|
|
56
|
+
state.buffers[name] = {
|
|
57
|
+
running: false,
|
|
58
|
+
text: currentText,
|
|
59
|
+
errored: code !== null && code !== 0,
|
|
60
|
+
};
|
|
61
|
+
state.childProcesses.delete(name);
|
|
62
|
+
// Check queue for dependent tasks
|
|
63
|
+
checkQueue(state, onUpdate);
|
|
64
|
+
onUpdate();
|
|
65
|
+
});
|
|
66
|
+
subProcess.on('error', (e) => {
|
|
67
|
+
const error = ensureError(e);
|
|
68
|
+
showError(error.message, ui, state);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function checkQueue(state, onUpdate) {
|
|
72
|
+
const next = [];
|
|
73
|
+
for (const queueItem of state.queue) {
|
|
74
|
+
const remaining = queueItem.remainingDeps.filter((dep) => {
|
|
75
|
+
const buffer = state.buffers[dep];
|
|
76
|
+
return buffer?.running || buffer?.errored || !buffer;
|
|
77
|
+
});
|
|
78
|
+
if (remaining.length === 0) {
|
|
79
|
+
spawnTask(queueItem.name, queueItem.task, state, onUpdate);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
next.push({ ...queueItem, remainingDeps: remaining });
|
|
83
|
+
}
|
|
84
|
+
state.queue = next;
|
|
85
|
+
}
|
|
86
|
+
export function cleanup(state) {
|
|
87
|
+
for (const [_, proc] of state.childProcesses.entries()) {
|
|
88
|
+
if (!proc.killed) {
|
|
89
|
+
proc.kill('SIGTERM');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
export declare const TaskSchema: z.ZodObject<{
|
|
3
|
+
command: z.ZodString;
|
|
4
|
+
dependsOn: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
5
|
+
cwd: z.ZodDefault<z.ZodString>;
|
|
6
|
+
}, z.z.core.$strip>;
|
|
7
|
+
export type Task = z.infer<typeof TaskSchema>;
|
|
8
|
+
export declare const TasksConfigSchema: z.ZodObject<{
|
|
9
|
+
tasks: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
10
|
+
command: z.ZodString;
|
|
11
|
+
dependsOn: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
12
|
+
cwd: z.ZodDefault<z.ZodString>;
|
|
13
|
+
}, z.z.core.$strip>>;
|
|
14
|
+
}, z.z.core.$strip>;
|
|
15
|
+
export type TasksConfig = z.infer<typeof TasksConfigSchema>;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
export const TaskSchema = z.object({
|
|
3
|
+
command: z.string(),
|
|
4
|
+
dependsOn: z.string().array().default([]),
|
|
5
|
+
cwd: z.string().default(process.cwd()),
|
|
6
|
+
});
|
|
7
|
+
export const TasksConfigSchema = z.object({
|
|
8
|
+
tasks: z.record(z.string(), TaskSchema),
|
|
9
|
+
});
|
package/dist/ui.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import blessed from 'blessed';
|
|
2
|
+
export interface UIComponents {
|
|
3
|
+
screen: blessed.Widgets.Screen;
|
|
4
|
+
sidebar: blessed.Widgets.BoxElement;
|
|
5
|
+
taskList: blessed.Widgets.BoxElement;
|
|
6
|
+
queueContainer: blessed.Widgets.BoxElement;
|
|
7
|
+
taskNameBox: blessed.Widgets.BoxElement;
|
|
8
|
+
taskOutputBox: blessed.Widgets.Log;
|
|
9
|
+
errorBox: blessed.Widgets.BoxElement;
|
|
10
|
+
}
|
|
11
|
+
export declare function createUI(): UIComponents;
|
package/dist/ui.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import blessed from 'blessed';
|
|
2
|
+
export function createUI() {
|
|
3
|
+
// Create screen
|
|
4
|
+
const screen = blessed.screen({
|
|
5
|
+
smartCSR: true,
|
|
6
|
+
title: 'Task Runner',
|
|
7
|
+
fullUnicode: true,
|
|
8
|
+
});
|
|
9
|
+
// Create sidebar container
|
|
10
|
+
const sidebar = blessed.box({
|
|
11
|
+
parent: screen,
|
|
12
|
+
left: 0,
|
|
13
|
+
top: 0,
|
|
14
|
+
width: 25,
|
|
15
|
+
height: '100%',
|
|
16
|
+
});
|
|
17
|
+
// Add a vertical line separator
|
|
18
|
+
blessed.line({
|
|
19
|
+
parent: screen,
|
|
20
|
+
orientation: 'vertical',
|
|
21
|
+
left: 24,
|
|
22
|
+
top: 0,
|
|
23
|
+
height: '100%',
|
|
24
|
+
style: {
|
|
25
|
+
fg: 'white',
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
// Tasks list
|
|
29
|
+
const taskList = blessed.box({
|
|
30
|
+
parent: sidebar,
|
|
31
|
+
width: '100%',
|
|
32
|
+
height: 'shrink',
|
|
33
|
+
tags: true,
|
|
34
|
+
});
|
|
35
|
+
// Queue section
|
|
36
|
+
const queueContainer = blessed.box({
|
|
37
|
+
parent: sidebar,
|
|
38
|
+
top: 'center',
|
|
39
|
+
width: '100%',
|
|
40
|
+
height: 'shrink',
|
|
41
|
+
tags: true,
|
|
42
|
+
});
|
|
43
|
+
// Help text at bottom
|
|
44
|
+
blessed.box({
|
|
45
|
+
parent: sidebar,
|
|
46
|
+
bottom: 0,
|
|
47
|
+
width: '100%',
|
|
48
|
+
height: 1,
|
|
49
|
+
content: '{gray-fg}↑↓ - Navigate{/}',
|
|
50
|
+
tags: true,
|
|
51
|
+
});
|
|
52
|
+
// Output pane container
|
|
53
|
+
const outputPane = blessed.box({
|
|
54
|
+
parent: screen,
|
|
55
|
+
left: 25,
|
|
56
|
+
top: 0,
|
|
57
|
+
width: '100%-25',
|
|
58
|
+
height: '100%',
|
|
59
|
+
});
|
|
60
|
+
// Task name header
|
|
61
|
+
const taskNameBox = blessed.box({
|
|
62
|
+
parent: outputPane,
|
|
63
|
+
top: 0,
|
|
64
|
+
width: '100%',
|
|
65
|
+
height: 1,
|
|
66
|
+
content: '',
|
|
67
|
+
tags: true,
|
|
68
|
+
style: {
|
|
69
|
+
fg: 'gray',
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
// Task output (scrollable log)
|
|
73
|
+
const taskOutputBox = blessed.log({
|
|
74
|
+
parent: outputPane,
|
|
75
|
+
top: 1,
|
|
76
|
+
width: '100%',
|
|
77
|
+
height: '100%-1',
|
|
78
|
+
scrollable: true,
|
|
79
|
+
alwaysScroll: true,
|
|
80
|
+
scrollbar: {
|
|
81
|
+
ch: '█',
|
|
82
|
+
style: {
|
|
83
|
+
fg: 'white',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
keys: true,
|
|
87
|
+
vi: true,
|
|
88
|
+
mouse: true,
|
|
89
|
+
tags: true,
|
|
90
|
+
});
|
|
91
|
+
// Error display
|
|
92
|
+
const errorBox = blessed.box({
|
|
93
|
+
parent: screen,
|
|
94
|
+
top: 'center',
|
|
95
|
+
left: 'center',
|
|
96
|
+
width: '80%',
|
|
97
|
+
height: 'shrink',
|
|
98
|
+
border: {
|
|
99
|
+
type: 'line',
|
|
100
|
+
},
|
|
101
|
+
style: {
|
|
102
|
+
fg: 'red',
|
|
103
|
+
border: {
|
|
104
|
+
fg: 'red',
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
tags: true,
|
|
108
|
+
hidden: true,
|
|
109
|
+
});
|
|
110
|
+
// Focus on output box for scrolling
|
|
111
|
+
taskOutputBox.focus();
|
|
112
|
+
return {
|
|
113
|
+
screen,
|
|
114
|
+
sidebar,
|
|
115
|
+
taskList,
|
|
116
|
+
queueContainer,
|
|
117
|
+
taskNameBox,
|
|
118
|
+
taskOutputBox,
|
|
119
|
+
errorBox,
|
|
120
|
+
};
|
|
121
|
+
}
|
package/dist/utils.d.ts
ADDED
package/dist/utils.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import z from 'zod';
|
|
4
|
+
import { CONFIG_PATH } from './constants.js';
|
|
5
|
+
import { TasksConfigSchema } from './types.js';
|
|
6
|
+
function getConfigPath(cliOption) {
|
|
7
|
+
if (cliOption)
|
|
8
|
+
return cliOption;
|
|
9
|
+
return CONFIG_PATH;
|
|
10
|
+
}
|
|
11
|
+
export default function formatZodError(error) {
|
|
12
|
+
if (error.issues.length === 0) {
|
|
13
|
+
return 'Unknown Zod error';
|
|
14
|
+
}
|
|
15
|
+
const issue = error.issues[0];
|
|
16
|
+
if (!issue)
|
|
17
|
+
return error.message;
|
|
18
|
+
const path = issue.path.join('.');
|
|
19
|
+
return `${issue.message} at '${path}' [code: ${issue.code}]`;
|
|
20
|
+
}
|
|
21
|
+
export function loadConfig(configPath) {
|
|
22
|
+
try {
|
|
23
|
+
const relativePath = path.join(process.cwd(), getConfigPath(configPath));
|
|
24
|
+
const raw = fs.readFileSync(relativePath, 'utf-8');
|
|
25
|
+
const parsed = JSON.parse(raw);
|
|
26
|
+
return z.parse(TasksConfigSchema, parsed);
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
const error = ensureError(e);
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function ensureError(error) {
|
|
34
|
+
if (error instanceof z.ZodError)
|
|
35
|
+
return new Error(formatZodError(error));
|
|
36
|
+
if (error instanceof Error)
|
|
37
|
+
return error;
|
|
38
|
+
let stringified = '[Unable to stringify the thrown value]';
|
|
39
|
+
try {
|
|
40
|
+
stringified = JSON.stringify(error);
|
|
41
|
+
}
|
|
42
|
+
catch { }
|
|
43
|
+
return new Error(stringified);
|
|
44
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tasktui",
|
|
3
|
+
"description": "Run tasks in a multiplexed terminal with dependency management.",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"bin": "dist/cli.js",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=16"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"dev": "tsc --watch"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"blessed": "^0.1.81",
|
|
20
|
+
"meow": "^11.0.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@sindresorhus/tsconfig": "^3.0.1",
|
|
24
|
+
"@types/blessed": "^0.1.25",
|
|
25
|
+
"@vdemedes/prettier-config": "^2.0.1",
|
|
26
|
+
"prettier": "^2.8.7",
|
|
27
|
+
"ts-node": "^10.9.1",
|
|
28
|
+
"typescript": "^5.0.3",
|
|
29
|
+
"zod": "^4.1.12"
|
|
30
|
+
}
|
|
31
|
+
}
|