harness-async 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.
- package/LICENSE +21 -0
- package/README.md +152 -0
- package/dist/dashboard/assets/index-TGNGdtwt.js +246 -0
- package/dist/dashboard/assets/index-f4TpA4iP.css +1 -0
- package/dist/dashboard/index.html +13 -0
- package/dist/src/adapters/claude-adapter.js +52 -0
- package/dist/src/adapters/codex-adapter.js +55 -0
- package/dist/src/adapters/index.js +14 -0
- package/dist/src/adapters/shared.js +74 -0
- package/dist/src/cli/commands/daemon.js +116 -0
- package/dist/src/cli/commands/doctor.js +50 -0
- package/dist/src/cli/commands/hook.js +188 -0
- package/dist/src/cli/commands/init.js +22 -0
- package/dist/src/cli/commands/run.js +129 -0
- package/dist/src/cli/commands/schedule.js +105 -0
- package/dist/src/cli/commands/task.js +188 -0
- package/dist/src/cli/index.js +23 -0
- package/dist/src/cli/utils/notify.js +32 -0
- package/dist/src/cli/utils/output.js +94 -0
- package/dist/src/core/daemon.js +375 -0
- package/dist/src/core/dag.js +80 -0
- package/dist/src/core/event-log.js +34 -0
- package/dist/src/core/lock.js +25 -0
- package/dist/src/core/run-manager.js +265 -0
- package/dist/src/core/run-orchestrator.js +193 -0
- package/dist/src/core/scheduler.js +106 -0
- package/dist/src/core/sessions.js +48 -0
- package/dist/src/core/store.js +225 -0
- package/dist/src/core/task-manager.js +375 -0
- package/dist/src/core/tmux.js +51 -0
- package/dist/src/daemon.js +35 -0
- package/dist/src/dashboard/routes.js +107 -0
- package/dist/src/dashboard/server.js +142 -0
- package/dist/src/dashboard/ws.js +75 -0
- package/dist/src/types/adapter.js +30 -0
- package/dist/src/types/index.js +87 -0
- package/package.json +65 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { getRun, listRuns } from '../../core/run-manager.js';
|
|
2
|
+
import { collectRunOutput, getRunAttachCommand, startTaskRun, stopTaskRun, syncRunState, } from '../../core/run-orchestrator.js';
|
|
3
|
+
import { renderRunTable } from '../utils/output.js';
|
|
4
|
+
export function registerRunCommand(program) {
|
|
5
|
+
const run = program.command('run').description('Manage task execution runs');
|
|
6
|
+
run
|
|
7
|
+
.command('start')
|
|
8
|
+
.description('Start a task run with a target tool')
|
|
9
|
+
.argument('[taskId]', 'Task ID')
|
|
10
|
+
.option('--tool <tool>', 'Tool name, for example claude or codex')
|
|
11
|
+
.option('--auto', 'Pick the next runnable task automatically')
|
|
12
|
+
.action(async (taskId, options) => {
|
|
13
|
+
try {
|
|
14
|
+
if (!options.tool) {
|
|
15
|
+
throw new Error('--tool is required');
|
|
16
|
+
}
|
|
17
|
+
const created = await startTaskRun({
|
|
18
|
+
cwd: process.cwd(),
|
|
19
|
+
homeDir: process.env.HA_HOME,
|
|
20
|
+
taskId: taskId ? Number(taskId) : undefined,
|
|
21
|
+
tool: options.tool,
|
|
22
|
+
auto: options.auto,
|
|
23
|
+
});
|
|
24
|
+
console.log(`Started run ${created.id} in tmux session ${created.tmuxSession}`);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
console.error(error.message);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
run
|
|
32
|
+
.command('list')
|
|
33
|
+
.description('Run list on task executions')
|
|
34
|
+
.option('--task-id <id>', 'Filter runs by task ID')
|
|
35
|
+
.action(async (options) => {
|
|
36
|
+
try {
|
|
37
|
+
const runs = await listRuns({
|
|
38
|
+
cwd: process.cwd(),
|
|
39
|
+
homeDir: process.env.HA_HOME,
|
|
40
|
+
scope: 'project',
|
|
41
|
+
taskId: options.taskId ? Number(options.taskId) : undefined,
|
|
42
|
+
});
|
|
43
|
+
const synced = await Promise.all(runs.map(async (entry) => entry.status === 'running'
|
|
44
|
+
? syncRunState({
|
|
45
|
+
cwd: process.cwd(),
|
|
46
|
+
homeDir: process.env.HA_HOME,
|
|
47
|
+
}, entry.id)
|
|
48
|
+
: entry));
|
|
49
|
+
console.log(renderRunTable(synced));
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
console.error(error.message);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
run
|
|
57
|
+
.command('show')
|
|
58
|
+
.description('Run show on task executions')
|
|
59
|
+
.argument('<runId>', 'Run ID')
|
|
60
|
+
.action(async (runId) => {
|
|
61
|
+
try {
|
|
62
|
+
const current = await syncRunState({
|
|
63
|
+
cwd: process.cwd(),
|
|
64
|
+
homeDir: process.env.HA_HOME,
|
|
65
|
+
}, runId);
|
|
66
|
+
console.log(JSON.stringify(current, null, 2));
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
console.error(error.message);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
run
|
|
74
|
+
.command('logs')
|
|
75
|
+
.description('Run logs on task executions')
|
|
76
|
+
.argument('<runId>', 'Run ID')
|
|
77
|
+
.action(async (runId) => {
|
|
78
|
+
try {
|
|
79
|
+
const logs = await collectRunOutput({
|
|
80
|
+
cwd: process.cwd(),
|
|
81
|
+
homeDir: process.env.HA_HOME,
|
|
82
|
+
}, runId);
|
|
83
|
+
console.log(logs || '(empty)');
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
process.exitCode = 1;
|
|
87
|
+
console.error(error.message);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
run
|
|
91
|
+
.command('attach')
|
|
92
|
+
.description('Run attach on task executions')
|
|
93
|
+
.argument('<runId>', 'Run ID')
|
|
94
|
+
.action(async (runId) => {
|
|
95
|
+
try {
|
|
96
|
+
console.log(await getRunAttachCommand({
|
|
97
|
+
cwd: process.cwd(),
|
|
98
|
+
homeDir: process.env.HA_HOME,
|
|
99
|
+
}, runId));
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
console.error(error.message);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
run
|
|
107
|
+
.command('stop')
|
|
108
|
+
.description('Run stop on task executions')
|
|
109
|
+
.argument('<runId>', 'Run ID')
|
|
110
|
+
.action(async (runId) => {
|
|
111
|
+
try {
|
|
112
|
+
await stopTaskRun({
|
|
113
|
+
cwd: process.cwd(),
|
|
114
|
+
homeDir: process.env.HA_HOME,
|
|
115
|
+
}, runId);
|
|
116
|
+
const stopped = await getRun({
|
|
117
|
+
cwd: process.cwd(),
|
|
118
|
+
homeDir: process.env.HA_HOME,
|
|
119
|
+
runId,
|
|
120
|
+
scope: 'all',
|
|
121
|
+
});
|
|
122
|
+
console.log(`Stopped run ${stopped.id}`);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
process.exitCode = 1;
|
|
126
|
+
console.error(error.message);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { addSchedule, listSchedules, removeSchedule, triggerSchedule } from '../../core/scheduler.js';
|
|
2
|
+
import { readConfig, resolveStoreDir } from '../../core/store.js';
|
|
3
|
+
import { renderTable } from '../utils/output.js';
|
|
4
|
+
export function registerScheduleCommand(program) {
|
|
5
|
+
const schedule = program
|
|
6
|
+
.command('schedule')
|
|
7
|
+
.description('Manage scheduled task creation and execution');
|
|
8
|
+
schedule
|
|
9
|
+
.command('add')
|
|
10
|
+
.description('Add a new schedule entry')
|
|
11
|
+
.requiredOption('--name <name>', 'Schedule name')
|
|
12
|
+
.requiredOption('--cron <expr>', 'Cron expression')
|
|
13
|
+
.option('--command <command>', 'Command to execute')
|
|
14
|
+
.option('--auto', 'Run the next unlocked L1 task automatically')
|
|
15
|
+
.option('--tool <tool>', 'Tool used with --auto, defaults to config.defaultAgent')
|
|
16
|
+
.action(async (options) => {
|
|
17
|
+
try {
|
|
18
|
+
const command = await resolveScheduleCommand(options);
|
|
19
|
+
const created = await addSchedule({
|
|
20
|
+
cwd: process.cwd(),
|
|
21
|
+
homeDir: process.env.HA_HOME,
|
|
22
|
+
name: options.name,
|
|
23
|
+
cron: options.cron,
|
|
24
|
+
command,
|
|
25
|
+
});
|
|
26
|
+
console.log(`Added schedule ${created.name}`);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
console.error(error.message);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
schedule
|
|
34
|
+
.command('list')
|
|
35
|
+
.description('List configured schedule entries')
|
|
36
|
+
.action(async () => {
|
|
37
|
+
try {
|
|
38
|
+
const schedules = await listSchedules({
|
|
39
|
+
cwd: process.cwd(),
|
|
40
|
+
homeDir: process.env.HA_HOME,
|
|
41
|
+
});
|
|
42
|
+
console.log(renderTable(schedules.map((entry) => ({
|
|
43
|
+
name: entry.name,
|
|
44
|
+
status: entry.enabled ? 'enabled' : 'disabled',
|
|
45
|
+
detail: `${entry.cron} | ${entry.command}`,
|
|
46
|
+
}))));
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
console.error(error.message);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
schedule
|
|
54
|
+
.command('remove')
|
|
55
|
+
.description('Remove a schedule entry')
|
|
56
|
+
.argument('<name>', 'Schedule name')
|
|
57
|
+
.action(async (name) => {
|
|
58
|
+
try {
|
|
59
|
+
await removeSchedule({
|
|
60
|
+
cwd: process.cwd(),
|
|
61
|
+
homeDir: process.env.HA_HOME,
|
|
62
|
+
name,
|
|
63
|
+
});
|
|
64
|
+
console.log(`Removed schedule ${name}`);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
process.exitCode = 1;
|
|
68
|
+
console.error(error.message);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
schedule
|
|
72
|
+
.command('trigger')
|
|
73
|
+
.description('Trigger a schedule entry immediately')
|
|
74
|
+
.argument('<name>', 'Schedule name')
|
|
75
|
+
.action(async (name) => {
|
|
76
|
+
try {
|
|
77
|
+
await triggerSchedule({
|
|
78
|
+
cwd: process.cwd(),
|
|
79
|
+
homeDir: process.env.HA_HOME,
|
|
80
|
+
name,
|
|
81
|
+
});
|
|
82
|
+
console.log(`Triggered schedule ${name}`);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
process.exitCode = 1;
|
|
86
|
+
console.error(error.message);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
async function resolveScheduleCommand(options) {
|
|
91
|
+
if (options.auto) {
|
|
92
|
+
const storeDir = resolveStoreDir({
|
|
93
|
+
cwd: process.cwd(),
|
|
94
|
+
homeDir: process.env.HA_HOME,
|
|
95
|
+
scope: 'global',
|
|
96
|
+
});
|
|
97
|
+
const config = await readConfig(storeDir);
|
|
98
|
+
const tool = options.tool ?? config.defaultAgent;
|
|
99
|
+
return `ha run start --auto --tool ${tool}`;
|
|
100
|
+
}
|
|
101
|
+
if (!options.command) {
|
|
102
|
+
throw new Error('--command is required unless --auto is set');
|
|
103
|
+
}
|
|
104
|
+
return options.command;
|
|
105
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { createTask, failTask, getTaskGraph, listTasks, showTask, updateTaskStatus, } from '../../core/task-manager.js';
|
|
2
|
+
import { renderTaskGraphAscii, renderTaskGraphMermaid, renderTaskTable, } from '../utils/output.js';
|
|
3
|
+
import { taskLevelSchema, taskStatusSchema } from '../../types/index.js';
|
|
4
|
+
export function registerTaskCommand(program) {
|
|
5
|
+
const task = program.command('task').description('Create and inspect tasks');
|
|
6
|
+
task
|
|
7
|
+
.command('create')
|
|
8
|
+
.description('Create a new task from inline text or a markdown file')
|
|
9
|
+
.argument('[title]', 'Task title')
|
|
10
|
+
.option('--level <level>', 'Task level: L1, L2, or L3')
|
|
11
|
+
.option('--deps <ids>', 'Comma-separated dependency task IDs')
|
|
12
|
+
.option('--assignee <assignee>', 'Task assignee: claude, codex, human, or auto')
|
|
13
|
+
.option('--global', 'Create the task in ~/.ha instead of the project store')
|
|
14
|
+
.option('--file <file>', 'Import task content from a markdown file')
|
|
15
|
+
.addHelpText('after', '\nExamples:\n $ ha task create "为认证模块编写单元测试" --level L1 --deps 2 --assignee claude\n $ ha task create --global "每日 lint 检查" --level L1\n $ ha task create --file ./my-task.md')
|
|
16
|
+
.action(async (title, options) => {
|
|
17
|
+
try {
|
|
18
|
+
const created = await createTask({
|
|
19
|
+
cwd: process.cwd(),
|
|
20
|
+
homeDir: process.env.HA_HOME,
|
|
21
|
+
title,
|
|
22
|
+
level: taskLevelSchema.parse(options.level ?? 'L1'),
|
|
23
|
+
assignee: options.assignee,
|
|
24
|
+
deps: parseDeps(options.deps),
|
|
25
|
+
scope: options.global ? 'global' : 'project',
|
|
26
|
+
file: options.file,
|
|
27
|
+
});
|
|
28
|
+
console.log(`Created task #${created.task.id}: ${created.task.title} [${created.task.level}]`);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
console.error(error.message);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
task
|
|
36
|
+
.command('list')
|
|
37
|
+
.description('List tasks from project, global, or aggregated stores')
|
|
38
|
+
.option('--global', 'Read from ~/.ha')
|
|
39
|
+
.option('--all', 'Read from all configured stores')
|
|
40
|
+
.option('--level <level>', 'Filter by level')
|
|
41
|
+
.option('--status <status>', 'Filter by status')
|
|
42
|
+
.action(async (options) => {
|
|
43
|
+
try {
|
|
44
|
+
const tasks = await listTasks({
|
|
45
|
+
cwd: process.cwd(),
|
|
46
|
+
homeDir: process.env.HA_HOME,
|
|
47
|
+
scope: options.all ? 'all' : options.global ? 'global' : 'project',
|
|
48
|
+
level: options.level ? taskLevelSchema.parse(options.level) : undefined,
|
|
49
|
+
status: options.status ? taskStatusSchema.parse(options.status) : undefined,
|
|
50
|
+
});
|
|
51
|
+
console.log(renderTaskTable(tasks.map((item) => ({
|
|
52
|
+
id: item.id,
|
|
53
|
+
level: item.level,
|
|
54
|
+
status: item.status,
|
|
55
|
+
title: item.title,
|
|
56
|
+
updated: item.updated,
|
|
57
|
+
}))));
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
process.exitCode = 1;
|
|
61
|
+
console.error(error.message);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
task
|
|
65
|
+
.command('show')
|
|
66
|
+
.description('Show the full markdown task document')
|
|
67
|
+
.argument('<id>', 'Task ID')
|
|
68
|
+
.action(async (id) => {
|
|
69
|
+
try {
|
|
70
|
+
const raw = await showTask({
|
|
71
|
+
cwd: process.cwd(),
|
|
72
|
+
homeDir: process.env.HA_HOME,
|
|
73
|
+
id: Number(id),
|
|
74
|
+
});
|
|
75
|
+
process.stdout.write(raw);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
console.error(error.message);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
task
|
|
83
|
+
.command('update')
|
|
84
|
+
.description('Update task metadata or status')
|
|
85
|
+
.argument('<id>', 'Task ID')
|
|
86
|
+
.option('--status <status>', 'New task status')
|
|
87
|
+
.action(async (id, options) => {
|
|
88
|
+
try {
|
|
89
|
+
if (!options.status) {
|
|
90
|
+
throw new Error('--status is required');
|
|
91
|
+
}
|
|
92
|
+
const updated = await updateTaskStatus({
|
|
93
|
+
cwd: process.cwd(),
|
|
94
|
+
homeDir: process.env.HA_HOME,
|
|
95
|
+
id: Number(id),
|
|
96
|
+
status: taskStatusSchema.parse(options.status),
|
|
97
|
+
actor: 'system',
|
|
98
|
+
});
|
|
99
|
+
console.log(`Updated task #${updated.id} -> ${updated.status}`);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
console.error(error.message);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
task
|
|
107
|
+
.command('complete')
|
|
108
|
+
.description('Mark a task as completed')
|
|
109
|
+
.argument('<id>', 'Task ID')
|
|
110
|
+
.action(async (id) => {
|
|
111
|
+
try {
|
|
112
|
+
const updated = await updateTaskStatus({
|
|
113
|
+
cwd: process.cwd(),
|
|
114
|
+
homeDir: process.env.HA_HOME,
|
|
115
|
+
id: Number(id),
|
|
116
|
+
status: 'completed',
|
|
117
|
+
actor: 'system',
|
|
118
|
+
});
|
|
119
|
+
console.log(`Completed task #${updated.id}`);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
process.exitCode = 1;
|
|
123
|
+
console.error(error.message);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
task
|
|
127
|
+
.command('fail')
|
|
128
|
+
.description('Mark a task as failed')
|
|
129
|
+
.argument('<id>', 'Task ID')
|
|
130
|
+
.option('--reason <reason>', 'Failure reason')
|
|
131
|
+
.action(async (id, options) => {
|
|
132
|
+
try {
|
|
133
|
+
const updated = await failTask({
|
|
134
|
+
cwd: process.cwd(),
|
|
135
|
+
homeDir: process.env.HA_HOME,
|
|
136
|
+
id: Number(id),
|
|
137
|
+
actor: 'system',
|
|
138
|
+
reason: options.reason,
|
|
139
|
+
});
|
|
140
|
+
console.log(`Failed task #${updated.id}`);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
process.exitCode = 1;
|
|
144
|
+
console.error(error.message);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
task
|
|
148
|
+
.command('graph')
|
|
149
|
+
.description('Render the task DAG')
|
|
150
|
+
.option('--format <format>', 'Output format, for example ascii or mermaid')
|
|
151
|
+
.option('--all', 'Aggregate all configured stores')
|
|
152
|
+
.action(async (options) => {
|
|
153
|
+
try {
|
|
154
|
+
const graph = await getTaskGraph({
|
|
155
|
+
cwd: process.cwd(),
|
|
156
|
+
homeDir: process.env.HA_HOME,
|
|
157
|
+
scope: options.all ? 'all' : 'project',
|
|
158
|
+
});
|
|
159
|
+
if (options.format === 'mermaid') {
|
|
160
|
+
console.log(renderTaskGraphMermaid(graph.tasks.map((task) => ({
|
|
161
|
+
id: task.id,
|
|
162
|
+
level: task.level,
|
|
163
|
+
title: task.title,
|
|
164
|
+
})), graph.edges));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
console.log(renderTaskGraphAscii(graph.tasks.map((task) => ({
|
|
168
|
+
id: task.id,
|
|
169
|
+
level: task.level,
|
|
170
|
+
status: task.status,
|
|
171
|
+
title: task.title,
|
|
172
|
+
})), graph.edges));
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
process.exitCode = 1;
|
|
176
|
+
console.error(error.message);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
function parseDeps(value) {
|
|
181
|
+
if (!value) {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
return value
|
|
185
|
+
.split(',')
|
|
186
|
+
.map((part) => Number.parseInt(part.trim(), 10))
|
|
187
|
+
.filter((part) => Number.isFinite(part));
|
|
188
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { registerDaemonCommand } from './commands/daemon.js';
|
|
4
|
+
import { registerDoctorCommand } from './commands/doctor.js';
|
|
5
|
+
import { registerHookCommand } from './commands/hook.js';
|
|
6
|
+
import { registerInitCommand } from './commands/init.js';
|
|
7
|
+
import { registerRunCommand } from './commands/run.js';
|
|
8
|
+
import { registerScheduleCommand } from './commands/schedule.js';
|
|
9
|
+
import { registerTaskCommand } from './commands/task.js';
|
|
10
|
+
const program = new Command();
|
|
11
|
+
program
|
|
12
|
+
.name('ha')
|
|
13
|
+
.description('Agent-first task orchestration CLI')
|
|
14
|
+
.showHelpAfterError()
|
|
15
|
+
.showSuggestionAfterError();
|
|
16
|
+
registerInitCommand(program);
|
|
17
|
+
registerDoctorCommand(program);
|
|
18
|
+
registerHookCommand(program);
|
|
19
|
+
registerTaskCommand(program);
|
|
20
|
+
registerScheduleCommand(program);
|
|
21
|
+
registerDaemonCommand(program);
|
|
22
|
+
registerRunCommand(program);
|
|
23
|
+
await program.parseAsync(process.argv);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function buildNotification(type, taskId, title) {
|
|
2
|
+
const taskLabel = `Task #${taskId} "${title}"`;
|
|
3
|
+
switch (type) {
|
|
4
|
+
case 'task.completed':
|
|
5
|
+
return { title: `${taskLabel} 已完成`, message: '任务已完成', open: 'http://localhost:3777' };
|
|
6
|
+
case 'task.failed':
|
|
7
|
+
return { title: `${taskLabel} 执行失败`, message: '任务执行失败', open: 'http://localhost:3777' };
|
|
8
|
+
case 'task.paused':
|
|
9
|
+
return { title: `${taskLabel} 会话已暂停`, message: '请检查 agent 会话状态', open: 'http://localhost:3777' };
|
|
10
|
+
case 'task.waiting-review':
|
|
11
|
+
return { title: `${taskLabel} 需要你的 review`, message: '任务等待 review', open: 'http://localhost:3777' };
|
|
12
|
+
case 'task.dependency_unlocked':
|
|
13
|
+
return { title: `Task #${taskId} 已解锁`, message: title, open: 'http://localhost:3777' };
|
|
14
|
+
default:
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export async function notifyTaskEvent(event, adapter = defaultNotifyAdapter) {
|
|
19
|
+
const payload = buildNotification(event.type, event.taskId, event.title);
|
|
20
|
+
if (!payload) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
await adapter(payload);
|
|
24
|
+
}
|
|
25
|
+
async function defaultNotifyAdapter(payload) {
|
|
26
|
+
const notifierModule = (await import('node-notifier'));
|
|
27
|
+
const notify = notifierModule.default?.notify ?? notifierModule.notify;
|
|
28
|
+
if (!notify) {
|
|
29
|
+
throw new Error('node-notifier notify function is unavailable');
|
|
30
|
+
}
|
|
31
|
+
notify(payload);
|
|
32
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
export function renderComingSoon(commandName) {
|
|
3
|
+
return `${commandName} is coming soon`;
|
|
4
|
+
}
|
|
5
|
+
export function renderTable(rows) {
|
|
6
|
+
const nameWidth = Math.max('Check'.length, ...rows.map((row) => row.name.length));
|
|
7
|
+
const statusWidth = Math.max('Status'.length, ...rows.map((row) => row.status.length));
|
|
8
|
+
const header = [
|
|
9
|
+
'Check'.padEnd(nameWidth),
|
|
10
|
+
'Status'.padEnd(statusWidth),
|
|
11
|
+
'Detail',
|
|
12
|
+
].join(' | ');
|
|
13
|
+
const divider = [
|
|
14
|
+
'-'.repeat(nameWidth),
|
|
15
|
+
'-'.repeat(statusWidth),
|
|
16
|
+
'------',
|
|
17
|
+
].join('-|-');
|
|
18
|
+
const lines = rows.map((row) => [row.name.padEnd(nameWidth), row.status.padEnd(statusWidth), row.detail].join(' | '));
|
|
19
|
+
return [header, divider, ...lines].join('\n');
|
|
20
|
+
}
|
|
21
|
+
export function renderTaskTable(tasks) {
|
|
22
|
+
const headers = ['ID', 'Level', 'Status', 'Title', 'Updated'];
|
|
23
|
+
const rows = tasks.map((task) => [
|
|
24
|
+
String(task.id),
|
|
25
|
+
colorizeLevel(task.level),
|
|
26
|
+
task.status,
|
|
27
|
+
task.title,
|
|
28
|
+
task.updated,
|
|
29
|
+
]);
|
|
30
|
+
const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => stripAnsi(row[index] ?? '').length)));
|
|
31
|
+
const header = headers
|
|
32
|
+
.map((value, index) => value.padEnd(widths[index] ?? value.length))
|
|
33
|
+
.join(' | ');
|
|
34
|
+
const divider = widths.map((width) => '-'.repeat(width)).join('-|-');
|
|
35
|
+
const lines = rows.map((row) => row
|
|
36
|
+
.map((value, index) => padAnsi(value, widths[index] ?? stripAnsi(value).length))
|
|
37
|
+
.join(' | '));
|
|
38
|
+
return [header, divider, ...lines].join('\n');
|
|
39
|
+
}
|
|
40
|
+
export function renderTaskGraphAscii(tasks, edges) {
|
|
41
|
+
const lines = [];
|
|
42
|
+
for (const task of tasks) {
|
|
43
|
+
lines.push(`[${task.id}] [${task.level}] [${task.status}] ${task.title}`);
|
|
44
|
+
for (const edge of edges.filter((entry) => entry.to === task.id)) {
|
|
45
|
+
lines.push(` └─ depends on [${edge.from}]`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return lines.join('\n');
|
|
49
|
+
}
|
|
50
|
+
export function renderTaskGraphMermaid(tasks, edges) {
|
|
51
|
+
const nodes = tasks.map((task) => `${task.id}["[${task.level}] ${task.title}"]`);
|
|
52
|
+
const connectors = edges.map((edge) => `${edge.from}["${nodeLabel(tasks, edge.from)}"] --> ${edge.to}["${nodeLabel(tasks, edge.to)}"]`);
|
|
53
|
+
return ['graph TD', ...nodes, ...connectors].join('\n');
|
|
54
|
+
}
|
|
55
|
+
export function renderRunTable(runs) {
|
|
56
|
+
const headers = ['Run', 'Task', 'Tool', 'Status', 'Started', 'Session'];
|
|
57
|
+
const rows = runs.map((run) => [
|
|
58
|
+
run.id,
|
|
59
|
+
String(run.taskId),
|
|
60
|
+
run.tool,
|
|
61
|
+
run.status,
|
|
62
|
+
run.startedAt,
|
|
63
|
+
run.tmuxSession,
|
|
64
|
+
]);
|
|
65
|
+
const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => row[index]?.length ?? 0)));
|
|
66
|
+
return [
|
|
67
|
+
headers.map((header, index) => header.padEnd(widths[index] ?? header.length)).join(' | '),
|
|
68
|
+
widths.map((width) => '-'.repeat(width)).join('-|-'),
|
|
69
|
+
...rows.map((row) => row
|
|
70
|
+
.map((value, index) => value.padEnd(widths[index] ?? value.length))
|
|
71
|
+
.join(' | ')),
|
|
72
|
+
].join('\n');
|
|
73
|
+
}
|
|
74
|
+
function colorizeLevel(level) {
|
|
75
|
+
if (level === 'L1') {
|
|
76
|
+
return chalk.green(level);
|
|
77
|
+
}
|
|
78
|
+
if (level === 'L2') {
|
|
79
|
+
return chalk.yellow(level);
|
|
80
|
+
}
|
|
81
|
+
return chalk.red(level);
|
|
82
|
+
}
|
|
83
|
+
function nodeLabel(tasks, id) {
|
|
84
|
+
const task = tasks.find((entry) => entry.id === id);
|
|
85
|
+
return task ? `[${task.level}] ${task.title}` : String(id);
|
|
86
|
+
}
|
|
87
|
+
function stripAnsi(value) {
|
|
88
|
+
const escape = String.fromCharCode(27);
|
|
89
|
+
return value.replace(new RegExp(`${escape}\\[[0-9;]*m`, 'g'), '');
|
|
90
|
+
}
|
|
91
|
+
function padAnsi(value, width) {
|
|
92
|
+
const visibleLength = stripAnsi(value).length;
|
|
93
|
+
return `${value}${' '.repeat(Math.max(0, width - visibleLength))}`;
|
|
94
|
+
}
|