@noego/captain 1.0.0 → 1.0.2

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.
@@ -2,19 +2,22 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CLI = void 0;
4
4
  const commander_1 = require("commander");
5
+ const trace_1 = require("@noego/trace");
6
+ const packageJson = require('../../package.json');
5
7
  class CLI {
6
8
  constructor(logger, parser, scheduler, executor) {
7
9
  this.logger = logger;
8
10
  this.parser = parser;
9
11
  this.scheduler = scheduler;
10
12
  this.executor = executor;
13
+ this.trace = (0, trace_1.getTrace)('CLI');
11
14
  this.program = new commander_1.Command();
12
15
  }
13
16
  setup() {
14
17
  this.program
15
18
  .name('captain')
16
19
  .description('YAML-driven task runner for TypeScript')
17
- .version('1.0.0')
20
+ .version(packageJson.version)
18
21
  .argument('[files...]', 'YAML task files to process')
19
22
  .option('-r, --run <task>', 'Run a specific task immediately')
20
23
  .option('-l, --list', 'List all tasks without running them')
@@ -24,10 +27,17 @@ class CLI {
24
27
  });
25
28
  }
26
29
  async processFiles(files, options) {
30
+ this.trace.info('cli.process.started', {
31
+ fileCount: files.length,
32
+ selectedMode: options.list ? 'list' : options.run ? 'run' : 'schedule',
33
+ requestedTask: options.run
34
+ });
27
35
  if (files.length === 0) {
36
+ this.trace.warn('cli.process.skipped', {
37
+ reason: 'missing_files'
38
+ });
28
39
  this.logger.error('No task files specified');
29
40
  this.program.help();
30
- return;
31
41
  }
32
42
  // Parse all files
33
43
  const allTasks = {};
@@ -45,25 +55,45 @@ class CLI {
45
55
  }
46
56
  }
47
57
  catch (error) {
58
+ this.trace.error('cli.file.failed', {
59
+ file,
60
+ error: error instanceof Error ? error.message : String(error)
61
+ });
48
62
  this.logger.error(`Error processing file ${file}: ${error}`);
49
63
  }
50
64
  }
51
65
  // Handle list option
52
66
  if (options.list) {
53
67
  this.listTasks(allTasks);
68
+ this.trace.info('cli.process.completed', {
69
+ selectedMode: 'list',
70
+ taskCount: Object.keys(allTasks).length
71
+ });
54
72
  return;
55
73
  }
56
74
  // Handle run option
57
75
  if (options.run) {
58
76
  await this.runSingleTask(options.run, allTasks);
77
+ this.trace.info('cli.process.completed', {
78
+ selectedMode: 'run',
79
+ taskCount: Object.keys(allTasks).length,
80
+ requestedTask: options.run
81
+ });
59
82
  return;
60
83
  }
61
84
  // Schedule all tasks
62
85
  this.scheduleTasks(allTasks);
86
+ this.trace.info('cli.process.completed', {
87
+ selectedMode: 'schedule',
88
+ taskCount: Object.keys(allTasks).length
89
+ });
63
90
  // Keep the process running
64
91
  this.logger.info('Captain is running. Press Ctrl+C to exit.');
65
92
  }
66
93
  listTasks(tasks) {
94
+ this.trace.info('cli.tasks.list.started', {
95
+ taskCount: Object.keys(tasks).length
96
+ });
67
97
  this.logger.info('Available tasks:');
68
98
  Object.entries(tasks).forEach(([name, task]) => {
69
99
  this.logger.info(`- ${name}: ${task.description || 'No description'}`);
@@ -71,25 +101,45 @@ class CLI {
71
101
  this.logger.info(` File: ${task.file}`);
72
102
  this.logger.info('');
73
103
  });
104
+ this.trace.info('cli.tasks.list.completed', {
105
+ taskCount: Object.keys(tasks).length
106
+ });
74
107
  }
75
108
  async runSingleTask(taskName, tasks) {
76
109
  const task = tasks[taskName];
77
110
  if (!task) {
111
+ this.trace.warn('cli.task.run.skipped', {
112
+ taskName,
113
+ reason: 'not_found'
114
+ });
78
115
  this.logger.error(`Task "${taskName}" not found`);
79
116
  return;
80
117
  }
118
+ this.trace.info('cli.task.run.started', { taskName });
81
119
  this.logger.info(`Running task ${taskName} immediately`);
82
- await this.executor.executeTask(taskName, task);
120
+ const success = await this.executor.executeTask(taskName, task);
121
+ this.trace.info('cli.task.run.completed', { taskName, success });
83
122
  this.logger.info('Task execution complete');
84
123
  }
85
124
  scheduleTasks(tasks) {
125
+ let validCount = 0;
126
+ let scheduledCount = 0;
86
127
  Object.entries(tasks).forEach(([name, task]) => {
87
128
  if (this.parser.validateTask(name, task)) {
88
- this.scheduler.scheduleTask(name, task, async () => {
129
+ validCount += 1;
130
+ const scheduled = this.scheduler.scheduleTask(name, task, async () => {
89
131
  await this.executor.executeTask(name, task);
90
132
  });
133
+ if (scheduled) {
134
+ scheduledCount += 1;
135
+ }
91
136
  }
92
137
  });
138
+ this.trace.info('cli.tasks.schedule.completed', {
139
+ taskCount: Object.keys(tasks).length,
140
+ validCount,
141
+ scheduledCount
142
+ });
93
143
  this.logger.info(`Scheduled ${Object.keys(tasks).length} tasks`);
94
144
  }
95
145
  parse(argv) {
@@ -36,15 +36,31 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.Executor = void 0;
37
37
  const child_process_1 = require("child_process");
38
38
  const path = __importStar(require("path"));
39
+ const trace_1 = require("@noego/trace");
39
40
  class Executor {
40
41
  constructor(logger) {
41
42
  this.logger = logger;
42
43
  this.runningTasks = new Map();
44
+ this.trace = (0, trace_1.getTrace)('Executor');
43
45
  }
44
46
  async executeTask(taskName, taskDef) {
45
47
  try {
48
+ this.trace.info('executor.task.started', {
49
+ taskName,
50
+ targetType: taskDef.file ? 'file' : taskDef.command ? 'command' : 'none',
51
+ timeoutSeconds: taskDef.timeout,
52
+ maxConcurrency: taskDef.concurrency?.max || 1,
53
+ allowOverlap: taskDef.concurrency?.allow_overlap
54
+ });
46
55
  // Check concurrency limits
47
- if (!this.checkConcurrency(taskName, taskDef)) {
56
+ const concurrency = this.checkConcurrency(taskName, taskDef);
57
+ if (!concurrency.allowed) {
58
+ this.trace.warn('executor.task.skipped', {
59
+ taskName,
60
+ reason: concurrency.reason,
61
+ runningCount: concurrency.runningCount,
62
+ maxConcurrency: concurrency.maxConcurrency
63
+ });
48
64
  return false;
49
65
  }
50
66
  const env = { ...process.env, ...taskDef.env };
@@ -52,6 +68,11 @@ class Executor {
52
68
  if (taskDef.file) {
53
69
  // Execute TypeScript/JavaScript file using tsx
54
70
  const filePath = path.resolve(process.cwd(), taskDef.file);
71
+ this.trace.info('executor.task.spawned', {
72
+ taskName,
73
+ targetType: 'file',
74
+ filePath
75
+ });
55
76
  this.logger.info(`Executing task ${taskName}: ${filePath}`);
56
77
  childProcess = (0, child_process_1.spawn)('npx', ['tsx', filePath], {
57
78
  env,
@@ -61,6 +82,12 @@ class Executor {
61
82
  else if (taskDef.command) {
62
83
  // Execute shell command using user's shell
63
84
  const shell = process.env.SHELL || '/bin/sh';
85
+ this.trace.info('executor.task.spawned', {
86
+ taskName,
87
+ targetType: 'command',
88
+ shell,
89
+ commandLength: taskDef.command.length
90
+ });
64
91
  this.logger.info(`Executing task ${taskName}: ${taskDef.command}`);
65
92
  childProcess = (0, child_process_1.spawn)(shell, ['-c', taskDef.command], {
66
93
  env,
@@ -68,6 +95,10 @@ class Executor {
68
95
  });
69
96
  }
70
97
  else {
98
+ this.trace.warn('executor.task.skipped', {
99
+ taskName,
100
+ reason: 'missing_target'
101
+ });
71
102
  this.logger.error(`Task ${taskName} has no file or command`);
72
103
  return false;
73
104
  }
@@ -80,6 +111,10 @@ class Executor {
80
111
  // Set timeout if specified
81
112
  if (taskDef.timeout) {
82
113
  execution.timeout = setTimeout(() => {
114
+ this.trace.warn('executor.task.timed_out', {
115
+ taskName,
116
+ timeoutSeconds: taskDef.timeout
117
+ });
83
118
  this.logger.warn(`Task ${taskName} timed out after ${taskDef.timeout}s`);
84
119
  childProcess.kill();
85
120
  }, taskDef.timeout * 1000);
@@ -90,10 +125,18 @@ class Executor {
90
125
  this.runningTasks.set(taskName, runningTasks);
91
126
  // Handle stdout
92
127
  childProcess.stdout.on('data', (data) => {
128
+ this.trace.debug('executor.task.stdout', {
129
+ taskName,
130
+ outputLength: data.toString().trim().length
131
+ });
93
132
  this.logger.info(`[${taskName}] ${data.toString().trim()}`);
94
133
  });
95
134
  // Handle stderr
96
135
  childProcess.stderr.on('data', (data) => {
136
+ this.trace.debug('executor.task.stderr', {
137
+ taskName,
138
+ outputLength: data.toString().trim().length
139
+ });
97
140
  this.logger.error(`[${taskName}] ${data.toString().trim()}`);
98
141
  });
99
142
  // Handle process completion
@@ -108,9 +151,19 @@ class Executor {
108
151
  const success = code === 0;
109
152
  const duration = (new Date().getTime() - execution.startTime.getTime()) / 1000;
110
153
  if (success) {
154
+ this.trace.info('executor.task.completed', {
155
+ taskName,
156
+ exitCode: code,
157
+ durationSeconds: duration
158
+ });
111
159
  this.logger.info(`Task ${taskName} completed successfully in ${duration}s`);
112
160
  }
113
161
  else {
162
+ this.trace.error('executor.task.failed', {
163
+ taskName,
164
+ exitCode: code,
165
+ durationSeconds: duration
166
+ });
114
167
  this.logger.error(`Task ${taskName} failed with exit code ${code} after ${duration}s`);
115
168
  // Implement retry logic here if needed
116
169
  }
@@ -119,6 +172,10 @@ class Executor {
119
172
  });
120
173
  }
121
174
  catch (error) {
175
+ this.trace.error('executor.task.failed', {
176
+ taskName,
177
+ error: error instanceof Error ? error.message : String(error)
178
+ });
122
179
  this.logger.error(`Failed to execute task ${taskName}: ${error}`);
123
180
  return false;
124
181
  }
@@ -129,14 +186,28 @@ class Executor {
129
186
  // Check if max concurrency would be exceeded
130
187
  if (runningTasks.length >= maxConcurrency) {
131
188
  this.logger.warn(`Task ${taskName} reached max concurrency (${maxConcurrency})`);
132
- return false;
189
+ return {
190
+ allowed: false,
191
+ maxConcurrency,
192
+ reason: 'max_concurrency',
193
+ runningCount: runningTasks.length
194
+ };
133
195
  }
134
196
  // Check if overlap is allowed
135
197
  if (runningTasks.length > 0 && taskDef.concurrency?.allow_overlap === false) {
136
198
  this.logger.warn(`Task ${taskName} is already running and overlap is not allowed`);
137
- return false;
199
+ return {
200
+ allowed: false,
201
+ maxConcurrency,
202
+ reason: 'overlap_disallowed',
203
+ runningCount: runningTasks.length
204
+ };
138
205
  }
139
- return true;
206
+ return {
207
+ allowed: true,
208
+ maxConcurrency,
209
+ runningCount: runningTasks.length
210
+ };
140
211
  }
141
212
  removeRunningTask(taskName, execution) {
142
213
  const runningTasks = this.runningTasks.get(taskName) || [];
@@ -6,12 +6,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.Parser = void 0;
7
7
  const promises_1 = __importDefault(require("fs/promises"));
8
8
  const js_yaml_1 = __importDefault(require("js-yaml"));
9
+ const trace_1 = require("@noego/trace");
9
10
  class Parser {
10
11
  constructor(logger) {
11
12
  this.logger = logger;
13
+ this.trace = (0, trace_1.getTrace)('Parser');
12
14
  }
13
15
  async parseFile(filePath) {
14
16
  try {
17
+ this.trace.info('parser.parse.started', { filePath });
15
18
  this.logger.debug(`Parsing file: ${filePath}`);
16
19
  // Read file
17
20
  const fileContents = await promises_1.default.readFile(filePath, 'utf8');
@@ -21,10 +24,16 @@ class Parser {
21
24
  if (!parsedData.tasks) {
22
25
  throw new Error(`Invalid task file: ${filePath}. No tasks defined.`);
23
26
  }
24
- this.logger.info(`Parsed ${Object.keys(parsedData.tasks).length} tasks from ${filePath}`);
27
+ const taskCount = Object.keys(parsedData.tasks).length;
28
+ this.trace.info('parser.parse.completed', { filePath, taskCount });
29
+ this.logger.info(`Parsed ${taskCount} tasks from ${filePath}`);
25
30
  return parsedData;
26
31
  }
27
32
  catch (error) {
33
+ this.trace.error('parser.parse.failed', {
34
+ filePath,
35
+ error: error instanceof Error ? error.message : String(error)
36
+ });
28
37
  this.logger.error(`Failed to parse file ${filePath}: ${error}`);
29
38
  throw error;
30
39
  }
@@ -32,17 +41,33 @@ class Parser {
32
41
  validateTask(taskName, task) {
33
42
  // Check required fields
34
43
  if (!task.schedule) {
44
+ this.trace.warn('parser.task.validation.failed', {
45
+ taskName,
46
+ reason: 'missing_schedule'
47
+ });
35
48
  this.logger.error(`Task ${taskName} is missing required field: schedule`);
36
49
  return false;
37
50
  }
38
51
  if (!task.file && !task.command) {
52
+ this.trace.warn('parser.task.validation.failed', {
53
+ taskName,
54
+ reason: 'missing_target'
55
+ });
39
56
  this.logger.error(`Task ${taskName} must have either 'file' or 'command'`);
40
57
  return false;
41
58
  }
42
59
  if (task.file && task.command) {
60
+ this.trace.warn('parser.task.validation.failed', {
61
+ taskName,
62
+ reason: 'multiple_targets'
63
+ });
43
64
  this.logger.error(`Task ${taskName} cannot have both 'file' and 'command'`);
44
65
  return false;
45
66
  }
67
+ this.trace.info('parser.task.validation.completed', {
68
+ taskName,
69
+ targetType: task.file ? 'file' : 'command'
70
+ });
46
71
  return true;
47
72
  }
48
73
  }
@@ -35,34 +35,60 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.Scheduler = void 0;
37
37
  const cron = __importStar(require("node-cron"));
38
+ const trace_1 = require("@noego/trace");
38
39
  class Scheduler {
39
40
  constructor(logger) {
40
41
  this.logger = logger;
41
42
  this.schedules = new Map();
43
+ this.trace = (0, trace_1.getTrace)('Scheduler');
42
44
  }
43
45
  scheduleTask(taskName, taskDef, callback) {
44
46
  try {
47
+ this.trace.info('scheduler.task.schedule.started', {
48
+ taskName,
49
+ schedule: taskDef.schedule
50
+ });
45
51
  // Validate cron expression
46
52
  if (!cron.validate(taskDef.schedule)) {
53
+ this.trace.warn('scheduler.task.schedule.skipped', {
54
+ taskName,
55
+ schedule: taskDef.schedule,
56
+ reason: 'invalid_cron'
57
+ });
47
58
  this.logger.error(`Invalid cron schedule for task ${taskName}: ${taskDef.schedule}`);
48
59
  return false;
49
60
  }
50
61
  // Schedule the task
51
62
  const scheduledTask = cron.schedule(taskDef.schedule, async () => {
63
+ this.trace.info('scheduler.task.triggered', { taskName });
52
64
  this.logger.info(`Running scheduled task: ${taskName}`);
53
65
  try {
54
66
  await callback();
67
+ this.trace.info('scheduler.task.completed', { taskName });
55
68
  }
56
69
  catch (error) {
70
+ this.trace.error('scheduler.task.failed', {
71
+ taskName,
72
+ error: error instanceof Error ? error.message : String(error)
73
+ });
57
74
  this.logger.error(`Error in scheduled task ${taskName}: ${error}`);
58
75
  }
59
76
  });
60
77
  // Store the scheduled task
61
78
  this.schedules.set(taskName, scheduledTask);
79
+ this.trace.info('scheduler.task.schedule.completed', {
80
+ taskName,
81
+ schedule: taskDef.schedule
82
+ });
62
83
  this.logger.info(`Scheduled task ${taskName} with cron: ${taskDef.schedule}`);
63
84
  return true;
64
85
  }
65
86
  catch (error) {
87
+ this.trace.error('scheduler.task.schedule.failed', {
88
+ taskName,
89
+ schedule: taskDef.schedule,
90
+ error: error instanceof Error ? error.message : String(error)
91
+ });
66
92
  this.logger.error(`Failed to schedule task ${taskName}: ${error}`);
67
93
  return false;
68
94
  }
@@ -72,17 +98,27 @@ class Scheduler {
72
98
  if (scheduledTask) {
73
99
  scheduledTask.stop();
74
100
  this.schedules.delete(taskName);
101
+ this.trace.info('scheduler.task.stopped', { taskName });
75
102
  this.logger.info(`Stopped scheduled task: ${taskName}`);
76
103
  return true;
77
104
  }
105
+ this.trace.warn('scheduler.task.stop.skipped', {
106
+ taskName,
107
+ reason: 'not_found'
108
+ });
78
109
  return false;
79
110
  }
80
111
  stopAllTasks() {
112
+ this.trace.info('scheduler.tasks.stop_all.started', {
113
+ taskCount: this.schedules.size
114
+ });
81
115
  for (const [taskName, scheduledTask] of this.schedules.entries()) {
82
116
  scheduledTask.stop();
117
+ this.trace.info('scheduler.task.stopped', { taskName });
83
118
  this.logger.info(`Stopped scheduled task: ${taskName}`);
84
119
  }
85
120
  this.schedules.clear();
121
+ this.trace.info('scheduler.tasks.stop_all.completed');
86
122
  }
87
123
  }
88
124
  exports.Scheduler = Scheduler;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noego/captain",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "YAML-driven task runner for TypeScript files",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -31,12 +31,17 @@
31
31
  "author": "",
32
32
  "license": "ISC",
33
33
  "type": "commonjs",
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
34
37
  "dependencies": {
35
- "@noego/ioc": "^0.0.10",
38
+ "@noego/ioc": "^0.3.1",
39
+ "@noego/trace": "^0.0.4",
36
40
  "commander": "^13.1.0",
37
41
  "dotenv": "^16.5.0",
38
42
  "js-yaml": "^4.1.0",
39
43
  "node-cron": "^3.0.3",
44
+ "rxjs": "^7.8.2",
40
45
  "ts-node": "^10.9.2",
41
46
  "winston": "^3.17.0"
42
47
  },