@noego/captain 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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # Captain
2
+
3
+ A lightweight, easy-to-use task scheduler for running TypeScript files and shell commands with cron expressions.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # Install from npm
9
+ npm install -g captain
10
+ ```
11
+
12
+ ### TypeScript/JavaScript Tasks
13
+
14
+ Create a TypeScript task:
15
+
16
+ ```typescript
17
+ // task.ts
18
+ console.log("Task running at:", new Date());
19
+ ```
20
+
21
+ Create a task configuration:
22
+
23
+ ```yaml
24
+ # tasks.yaml
25
+ tasks:
26
+ simple:
27
+ description: "A simple job that runs every minute"
28
+ schedule: "* * * * *"
29
+ file: "./task.ts"
30
+ ```
31
+
32
+ Run Captain:
33
+
34
+ ```bash
35
+ captain ./tasks.yaml
36
+ ```
37
+
38
+ ### Shell Commands
39
+
40
+ Create a task configuration with shell commands:
41
+
42
+ ```yaml
43
+ # tasks.yaml
44
+ tasks:
45
+ health_check:
46
+ description: "Check API health every minute"
47
+ schedule: "* * * * *"
48
+ command: "curl -s https://httpbin.org/get"
49
+ ```
50
+
51
+ Run Captain:
52
+
53
+ ```bash
54
+ captain ./tasks.yaml
55
+ ```
56
+
57
+ ## Installation
58
+
59
+ ```bash
60
+ # Global installation (recommended for CLI usage)
61
+ npm install -g captain
62
+
63
+ # Local installation (for project usage)
64
+ npm install --save captain
65
+ ```
66
+
67
+ ## Usage Guide
68
+
69
+ ### 1. Create TypeScript Tasks
70
+
71
+ Create TypeScript files for your scheduled tasks:
72
+
73
+ ```typescript
74
+ // tasks/log-metrics.ts
75
+ import fs from 'fs';
76
+
77
+ // Access environment variables from YAML config
78
+ const logPath = process.env.LOG_PATH || './metrics.log';
79
+
80
+ // Log system metrics
81
+ const metrics = {
82
+ timestamp: new Date().toISOString(),
83
+ memoryUsage: process.memoryUsage(),
84
+ uptime: process.uptime()
85
+ };
86
+
87
+ // Append to log file
88
+ fs.appendFileSync(logPath, JSON.stringify(metrics) + '\n');
89
+ console.log(`Metrics logged to ${logPath}`);
90
+ ```
91
+
92
+ ### 2. Configure Tasks with YAML
93
+
94
+ Create a YAML configuration file defining your tasks:
95
+
96
+ ```yaml
97
+ # scheduler.yaml
98
+ tasks:
99
+ metrics_logger:
100
+ description: "Log system metrics every 15 minutes"
101
+ schedule: "*/15 * * * *"
102
+ file: "./tasks/log-metrics.ts"
103
+ timeout: 30 # seconds
104
+ env:
105
+ LOG_PATH: "./logs/system-metrics.log"
106
+
107
+ cleanup_task:
108
+ description: "Clean up temp files daily at midnight"
109
+ schedule: "0 0 * * *"
110
+ file: "./tasks/cleanup.ts"
111
+ concurrency:
112
+ max: 1
113
+ allow_overlap: false
114
+
115
+ # Shell command example
116
+ health_check:
117
+ description: "Check API health every 5 minutes"
118
+ schedule: "*/5 * * * *"
119
+ command: "curl -s https://api.example.com/health"
120
+ timeout: 10
121
+
122
+ backup:
123
+ description: "Run backup script daily"
124
+ schedule: "0 2 * * *"
125
+ command: "./scripts/backup.sh"
126
+
127
+ config:
128
+ default_timeout: 60
129
+ log_level: "info"
130
+ ```
131
+
132
+ ### 3. Run Captain
133
+
134
+ ```bash
135
+ # Start the scheduler
136
+ captain ./scheduler.yaml
137
+
138
+ # List all configured tasks
139
+ captain ./scheduler.yaml --list
140
+
141
+ # Run a specific task immediately
142
+ captain ./scheduler.yaml --run metrics_logger
143
+
144
+ # Combine multiple configuration files
145
+ captain ./system-tasks.yaml ./app-tasks.yaml
146
+ ```
147
+
148
+ ## Task Configuration Reference
149
+
150
+ Each task supports the following options:
151
+
152
+ | Option | Type | Required | Description |
153
+ |--------|------|----------|-------------|
154
+ | `description` | string | No | Human-readable description of the task |
155
+ | `schedule` | string | Yes | Cron expression (e.g. "*/5 * * * *") |
156
+ | `file` | string | No* | Path to TypeScript file to execute |
157
+ | `command` | string | No* | Shell command to execute (uses $SHELL or /bin/sh) |
158
+ | `timeout` | number | No | Maximum runtime in seconds before termination |
159
+ | `concurrency` | object | No | Concurrency settings |
160
+ | `concurrency.max` | number | No | Maximum instances (default: 1) |
161
+ | `concurrency.allow_overlap` | boolean | No | Allow task to overlap itself (default: false) |
162
+ | `env` | object | No | Environment variables to pass to task |
163
+ | `retry` | object | No | Retry settings for failed tasks |
164
+ | `retry.attempts` | number | No | Number of retry attempts |
165
+ | `retry.backoff` | string | No | Backoff strategy ("fixed" or "exponential") |
166
+ | `retry.delay` | number | No | Delay in seconds between retries |
167
+
168
+ \* Either `file` or `command` is required, but not both.
169
+
170
+ ## Common Use Cases
171
+
172
+ - **Recurring database maintenance**: Schedule database cleanup, backups, or migrations
173
+ - **Application monitoring**: Log metrics, check service health, or generate reports
174
+ - **Data processing**: Schedule batch processing of data at regular intervals
175
+ - **Service coordination**: Trigger API calls or service interactions on a schedule
176
+
177
+ ## Developing with Captain
178
+
179
+ For developers who want to modify or extend Captain:
180
+
181
+ ```bash
182
+ # Clone the repository
183
+ git clone <repository-url>
184
+ cd captain
185
+
186
+ # Install dependencies
187
+ npm install
188
+
189
+ # Run in development mode
190
+ npm run dev
191
+
192
+ # Run tests
193
+ npm test
194
+ ```
195
+
196
+ ## License
197
+
198
+ ISC
package/bin/captain.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ require('../dist/index');
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CLI = void 0;
4
+ const commander_1 = require("commander");
5
+ class CLI {
6
+ constructor(logger, parser, scheduler, executor) {
7
+ this.logger = logger;
8
+ this.parser = parser;
9
+ this.scheduler = scheduler;
10
+ this.executor = executor;
11
+ this.program = new commander_1.Command();
12
+ }
13
+ setup() {
14
+ this.program
15
+ .name('captain')
16
+ .description('YAML-driven task runner for TypeScript')
17
+ .version('1.0.0')
18
+ .argument('[files...]', 'YAML task files to process')
19
+ .option('-r, --run <task>', 'Run a specific task immediately')
20
+ .option('-l, --list', 'List all tasks without running them')
21
+ .option('-e, --env-file <file>', 'Path to environment file', '.env')
22
+ .action(async (files, options) => {
23
+ await this.processFiles(files, options);
24
+ });
25
+ }
26
+ async processFiles(files, options) {
27
+ if (files.length === 0) {
28
+ this.logger.error('No task files specified');
29
+ this.program.help();
30
+ return;
31
+ }
32
+ // Parse all files
33
+ const allTasks = {};
34
+ let globalConfig = {};
35
+ for (const file of files) {
36
+ try {
37
+ const tasksFile = await this.parser.parseFile(file);
38
+ // Merge tasks
39
+ Object.entries(tasksFile.tasks).forEach(([name, task]) => {
40
+ allTasks[name] = { ...task, source: file };
41
+ });
42
+ // Update global config (last file wins for conflicts)
43
+ if (tasksFile.config) {
44
+ globalConfig = { ...globalConfig, ...tasksFile.config };
45
+ }
46
+ }
47
+ catch (error) {
48
+ this.logger.error(`Error processing file ${file}: ${error}`);
49
+ }
50
+ }
51
+ // Handle list option
52
+ if (options.list) {
53
+ this.listTasks(allTasks);
54
+ return;
55
+ }
56
+ // Handle run option
57
+ if (options.run) {
58
+ await this.runSingleTask(options.run, allTasks);
59
+ return;
60
+ }
61
+ // Schedule all tasks
62
+ this.scheduleTasks(allTasks);
63
+ // Keep the process running
64
+ this.logger.info('Captain is running. Press Ctrl+C to exit.');
65
+ }
66
+ listTasks(tasks) {
67
+ this.logger.info('Available tasks:');
68
+ Object.entries(tasks).forEach(([name, task]) => {
69
+ this.logger.info(`- ${name}: ${task.description || 'No description'}`);
70
+ this.logger.info(` Schedule: ${task.schedule}`);
71
+ this.logger.info(` File: ${task.file}`);
72
+ this.logger.info('');
73
+ });
74
+ }
75
+ async runSingleTask(taskName, tasks) {
76
+ const task = tasks[taskName];
77
+ if (!task) {
78
+ this.logger.error(`Task "${taskName}" not found`);
79
+ return;
80
+ }
81
+ this.logger.info(`Running task ${taskName} immediately`);
82
+ await this.executor.executeTask(taskName, task);
83
+ this.logger.info('Task execution complete');
84
+ }
85
+ scheduleTasks(tasks) {
86
+ Object.entries(tasks).forEach(([name, task]) => {
87
+ if (this.parser.validateTask(name, task)) {
88
+ this.scheduler.scheduleTask(name, task, async () => {
89
+ await this.executor.executeTask(name, task);
90
+ });
91
+ }
92
+ });
93
+ this.logger.info(`Scheduled ${Object.keys(tasks).length} tasks`);
94
+ }
95
+ parse(argv) {
96
+ this.program.parse(argv);
97
+ }
98
+ }
99
+ exports.CLI = CLI;
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.Executor = void 0;
37
+ const child_process_1 = require("child_process");
38
+ const path = __importStar(require("path"));
39
+ class Executor {
40
+ constructor(logger) {
41
+ this.logger = logger;
42
+ this.runningTasks = new Map();
43
+ }
44
+ async executeTask(taskName, taskDef) {
45
+ try {
46
+ // Check concurrency limits
47
+ if (!this.checkConcurrency(taskName, taskDef)) {
48
+ return false;
49
+ }
50
+ const env = { ...process.env, ...taskDef.env };
51
+ let childProcess;
52
+ if (taskDef.file) {
53
+ // Execute TypeScript/JavaScript file using tsx
54
+ const filePath = path.resolve(process.cwd(), taskDef.file);
55
+ this.logger.info(`Executing task ${taskName}: ${filePath}`);
56
+ childProcess = (0, child_process_1.spawn)('npx', ['tsx', filePath], {
57
+ env,
58
+ stdio: ['ignore', 'pipe', 'pipe']
59
+ });
60
+ }
61
+ else if (taskDef.command) {
62
+ // Execute shell command using user's shell
63
+ const shell = process.env.SHELL || '/bin/sh';
64
+ this.logger.info(`Executing task ${taskName}: ${taskDef.command}`);
65
+ childProcess = (0, child_process_1.spawn)(shell, ['-c', taskDef.command], {
66
+ env,
67
+ stdio: ['ignore', 'pipe', 'pipe']
68
+ });
69
+ }
70
+ else {
71
+ this.logger.error(`Task ${taskName} has no file or command`);
72
+ return false;
73
+ }
74
+ // Create execution record
75
+ const execution = {
76
+ taskName,
77
+ process: childProcess,
78
+ startTime: new Date()
79
+ };
80
+ // Set timeout if specified
81
+ if (taskDef.timeout) {
82
+ execution.timeout = setTimeout(() => {
83
+ this.logger.warn(`Task ${taskName} timed out after ${taskDef.timeout}s`);
84
+ childProcess.kill();
85
+ }, taskDef.timeout * 1000);
86
+ }
87
+ // Store in running tasks
88
+ const runningTasks = this.runningTasks.get(taskName) || [];
89
+ runningTasks.push(execution);
90
+ this.runningTasks.set(taskName, runningTasks);
91
+ // Handle stdout
92
+ childProcess.stdout.on('data', (data) => {
93
+ this.logger.info(`[${taskName}] ${data.toString().trim()}`);
94
+ });
95
+ // Handle stderr
96
+ childProcess.stderr.on('data', (data) => {
97
+ this.logger.error(`[${taskName}] ${data.toString().trim()}`);
98
+ });
99
+ // Handle process completion
100
+ return new Promise((resolve) => {
101
+ childProcess.on('close', (code) => {
102
+ // Clear timeout if it exists
103
+ if (execution.timeout) {
104
+ clearTimeout(execution.timeout);
105
+ }
106
+ // Remove from running tasks
107
+ this.removeRunningTask(taskName, execution);
108
+ const success = code === 0;
109
+ const duration = (new Date().getTime() - execution.startTime.getTime()) / 1000;
110
+ if (success) {
111
+ this.logger.info(`Task ${taskName} completed successfully in ${duration}s`);
112
+ }
113
+ else {
114
+ this.logger.error(`Task ${taskName} failed with exit code ${code} after ${duration}s`);
115
+ // Implement retry logic here if needed
116
+ }
117
+ resolve(success);
118
+ });
119
+ });
120
+ }
121
+ catch (error) {
122
+ this.logger.error(`Failed to execute task ${taskName}: ${error}`);
123
+ return false;
124
+ }
125
+ }
126
+ checkConcurrency(taskName, taskDef) {
127
+ const runningTasks = this.runningTasks.get(taskName) || [];
128
+ const maxConcurrency = taskDef.concurrency?.max || 1;
129
+ // Check if max concurrency would be exceeded
130
+ if (runningTasks.length >= maxConcurrency) {
131
+ this.logger.warn(`Task ${taskName} reached max concurrency (${maxConcurrency})`);
132
+ return false;
133
+ }
134
+ // Check if overlap is allowed
135
+ if (runningTasks.length > 0 && taskDef.concurrency?.allow_overlap === false) {
136
+ this.logger.warn(`Task ${taskName} is already running and overlap is not allowed`);
137
+ return false;
138
+ }
139
+ return true;
140
+ }
141
+ removeRunningTask(taskName, execution) {
142
+ const runningTasks = this.runningTasks.get(taskName) || [];
143
+ const index = runningTasks.indexOf(execution);
144
+ if (index !== -1) {
145
+ runningTasks.splice(index, 1);
146
+ if (runningTasks.length === 0) {
147
+ this.runningTasks.delete(taskName);
148
+ }
149
+ else {
150
+ this.runningTasks.set(taskName, runningTasks);
151
+ }
152
+ }
153
+ }
154
+ }
155
+ exports.Executor = Executor;
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Parser = void 0;
7
+ const promises_1 = __importDefault(require("fs/promises"));
8
+ const js_yaml_1 = __importDefault(require("js-yaml"));
9
+ class Parser {
10
+ constructor(logger) {
11
+ this.logger = logger;
12
+ }
13
+ async parseFile(filePath) {
14
+ try {
15
+ this.logger.debug(`Parsing file: ${filePath}`);
16
+ // Read file
17
+ const fileContents = await promises_1.default.readFile(filePath, 'utf8');
18
+ // Parse YAML
19
+ const parsedData = js_yaml_1.default.load(fileContents);
20
+ // Validate structure
21
+ if (!parsedData.tasks) {
22
+ throw new Error(`Invalid task file: ${filePath}. No tasks defined.`);
23
+ }
24
+ this.logger.info(`Parsed ${Object.keys(parsedData.tasks).length} tasks from ${filePath}`);
25
+ return parsedData;
26
+ }
27
+ catch (error) {
28
+ this.logger.error(`Failed to parse file ${filePath}: ${error}`);
29
+ throw error;
30
+ }
31
+ }
32
+ validateTask(taskName, task) {
33
+ // Check required fields
34
+ if (!task.schedule) {
35
+ this.logger.error(`Task ${taskName} is missing required field: schedule`);
36
+ return false;
37
+ }
38
+ if (!task.file && !task.command) {
39
+ this.logger.error(`Task ${taskName} must have either 'file' or 'command'`);
40
+ return false;
41
+ }
42
+ if (task.file && task.command) {
43
+ this.logger.error(`Task ${taskName} cannot have both 'file' and 'command'`);
44
+ return false;
45
+ }
46
+ return true;
47
+ }
48
+ }
49
+ exports.Parser = Parser;
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.Scheduler = void 0;
37
+ const cron = __importStar(require("node-cron"));
38
+ class Scheduler {
39
+ constructor(logger) {
40
+ this.logger = logger;
41
+ this.schedules = new Map();
42
+ }
43
+ scheduleTask(taskName, taskDef, callback) {
44
+ try {
45
+ // Validate cron expression
46
+ if (!cron.validate(taskDef.schedule)) {
47
+ this.logger.error(`Invalid cron schedule for task ${taskName}: ${taskDef.schedule}`);
48
+ return false;
49
+ }
50
+ // Schedule the task
51
+ const scheduledTask = cron.schedule(taskDef.schedule, async () => {
52
+ this.logger.info(`Running scheduled task: ${taskName}`);
53
+ try {
54
+ await callback();
55
+ }
56
+ catch (error) {
57
+ this.logger.error(`Error in scheduled task ${taskName}: ${error}`);
58
+ }
59
+ });
60
+ // Store the scheduled task
61
+ this.schedules.set(taskName, scheduledTask);
62
+ this.logger.info(`Scheduled task ${taskName} with cron: ${taskDef.schedule}`);
63
+ return true;
64
+ }
65
+ catch (error) {
66
+ this.logger.error(`Failed to schedule task ${taskName}: ${error}`);
67
+ return false;
68
+ }
69
+ }
70
+ stopTask(taskName) {
71
+ const scheduledTask = this.schedules.get(taskName);
72
+ if (scheduledTask) {
73
+ scheduledTask.stop();
74
+ this.schedules.delete(taskName);
75
+ this.logger.info(`Stopped scheduled task: ${taskName}`);
76
+ return true;
77
+ }
78
+ return false;
79
+ }
80
+ stopAllTasks() {
81
+ for (const [taskName, scheduledTask] of this.schedules.entries()) {
82
+ scheduledTask.stop();
83
+ this.logger.info(`Stopped scheduled task: ${taskName}`);
84
+ }
85
+ this.schedules.clear();
86
+ }
87
+ }
88
+ exports.Scheduler = Scheduler;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.initializeContainer = initializeContainer;
40
+ const ioc_1 = __importStar(require("@noego/ioc"));
41
+ const winston_1 = __importDefault(require("winston"));
42
+ // Import components (we'll create these soon)
43
+ const parser_1 = require("./components/parser");
44
+ const scheduler_1 = require("./components/scheduler");
45
+ const executor_1 = require("./components/executor");
46
+ const cli_1 = require("./components/cli");
47
+ // Create and configure logger
48
+ function createLogger() {
49
+ return winston_1.default.createLogger({
50
+ level: 'info',
51
+ format: winston_1.default.format.combine(winston_1.default.format.timestamp(), winston_1.default.format.printf(({ level, message, timestamp }) => {
52
+ return `${timestamp} ${level}: ${message}`;
53
+ })),
54
+ transports: [
55
+ new winston_1.default.transports.Console()
56
+ ]
57
+ });
58
+ }
59
+ // Initialize container
60
+ function initializeContainer() {
61
+ const container = (0, ioc_1.default)();
62
+ // Register logger as singleton
63
+ container.registerFunction('logger', createLogger, {
64
+ loadAs: ioc_1.LoadAs.Singleton
65
+ });
66
+ // Register components
67
+ container.registerClass(parser_1.Parser, {
68
+ param: ['logger'],
69
+ loadAs: ioc_1.LoadAs.Singleton
70
+ });
71
+ container.registerClass(scheduler_1.Scheduler, {
72
+ param: ['logger'],
73
+ loadAs: ioc_1.LoadAs.Singleton
74
+ });
75
+ container.registerClass(executor_1.Executor, {
76
+ param: ['logger'],
77
+ loadAs: ioc_1.LoadAs.Singleton
78
+ });
79
+ container.registerClass(cli_1.CLI, {
80
+ param: ['logger', parser_1.Parser, scheduler_1.Scheduler, executor_1.Executor],
81
+ loadAs: ioc_1.LoadAs.Singleton
82
+ });
83
+ return container;
84
+ }
package/dist/index.js ADDED
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const dotenv_1 = require("dotenv");
4
+ const fs_1 = require("fs");
5
+ const container_1 = require("./container");
6
+ const cli_1 = require("./components/cli");
7
+ // Function to load environment file
8
+ function loadEnvFile(envPath = '.env') {
9
+ if ((0, fs_1.existsSync)(envPath)) {
10
+ (0, dotenv_1.config)({ path: envPath });
11
+ console.log(`Loaded environment variables from ${envPath}`);
12
+ }
13
+ else if (envPath !== '.env') {
14
+ // Only error if a specific file was requested
15
+ console.error(`Environment file ${envPath} not found`);
16
+ process.exit(1);
17
+ }
18
+ }
19
+ // Main entry point
20
+ async function main() {
21
+ process.title = 'Captain';
22
+ try {
23
+ // Parse arguments early to get env-file option
24
+ const envFileIndex = process.argv.indexOf('--env-file') !== -1
25
+ ? process.argv.indexOf('--env-file')
26
+ : process.argv.indexOf('-e');
27
+ const envFile = envFileIndex !== -1 && envFileIndex + 1 < process.argv.length
28
+ ? process.argv[envFileIndex + 1]
29
+ : '.env';
30
+ loadEnvFile(envFile);
31
+ // Initialize IOC container
32
+ const container = (0, container_1.initializeContainer)();
33
+ // Get CLI component
34
+ const cli = await container.instance(cli_1.CLI);
35
+ // Setup CLI
36
+ cli.setup();
37
+ // Parse command line arguments
38
+ cli.parse(process.argv);
39
+ }
40
+ catch (error) {
41
+ console.error('Failed to start Captain:', error);
42
+ process.exit(1);
43
+ }
44
+ }
45
+ // Run the application
46
+ main();
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@noego/captain",
3
+ "version": "1.0.0",
4
+ "description": "YAML-driven task runner for TypeScript files",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "captain": "./bin/captain.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "bin",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "directories": {
16
+ "example": "examples",
17
+ "test": "tests"
18
+ },
19
+ "scripts": {
20
+ "build": "npx tsc",
21
+ "test": "jest",
22
+ "start": "node dist/index.js",
23
+ "dev": "ts-node src/index.ts"
24
+ },
25
+ "keywords": [
26
+ "cron",
27
+ "task",
28
+ "scheduler",
29
+ "yaml"
30
+ ],
31
+ "author": "",
32
+ "license": "ISC",
33
+ "type": "commonjs",
34
+ "dependencies": {
35
+ "@noego/ioc": "^0.0.10",
36
+ "commander": "^13.1.0",
37
+ "dotenv": "^16.5.0",
38
+ "js-yaml": "^4.1.0",
39
+ "node-cron": "^3.0.3",
40
+ "ts-node": "^10.9.2",
41
+ "winston": "^3.17.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/jest": "^29.5.14",
45
+ "@types/js-yaml": "^4.0.9",
46
+ "@types/node": "^22.13.9",
47
+ "@types/node-cron": "^3.0.11",
48
+ "jest": "^29.7.0",
49
+ "ts-jest": "^29.2.6",
50
+ "typescript": "^5.8.3"
51
+ }
52
+ }