loop-task 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 +21 -0
- package/README.md +113 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +60 -0
- package/dist/cli.js.map +1 -0
- package/dist/duration.d.ts +3 -0
- package/dist/duration.d.ts.map +1 -0
- package/dist/duration.js +19 -0
- package/dist/duration.js.map +1 -0
- package/dist/logger.d.ts +11 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +27 -0
- package/dist/logger.js.map +1 -0
- package/dist/loop.d.ts +5 -0
- package/dist/loop.d.ts.map +1 -0
- package/dist/loop.js +118 -0
- package/dist/loop.js.map +1 -0
- package/dist/types.d.ts +19 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Quique Fdez Guerra
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# loop-task
|
|
2
|
+
|
|
3
|
+
A cross-platform CLI that repeatedly executes a shell command at a human-readable interval.
|
|
4
|
+
|
|
5
|
+
Inspired by agent loops, but intentionally simple.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g loop-task
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or use directly with npx:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx loop-task 30m npm test
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
loop [options] <interval> <command>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Basic examples
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
loop 30m npm test
|
|
29
|
+
loop 1h opencode --prompt '/ob-init'
|
|
30
|
+
loop 1d node sync.js
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### With npx
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx loop-task 30m npm test
|
|
37
|
+
npx loop-task 1h opencode --prompt '/ob-init'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Options
|
|
41
|
+
|
|
42
|
+
Options must come before the interval:
|
|
43
|
+
|
|
44
|
+
| Option | Description |
|
|
45
|
+
| ----------------- | -------------------------------- |
|
|
46
|
+
| `--immediate` | Run immediately before waiting |
|
|
47
|
+
| `--max-runs <n>` | Stop after N executions |
|
|
48
|
+
| `--verbose` | Show execution details |
|
|
49
|
+
| `-h, --help` | Display help |
|
|
50
|
+
| `-V, --version` | Display version |
|
|
51
|
+
|
|
52
|
+
## Examples
|
|
53
|
+
|
|
54
|
+
### Agent workflows
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npx loop-task 30m opencode --prompt "search for missing translation text and translate them, 3 maximum" --model "opencode/big-pickle"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Run tests every 30 minutes
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
loop 30m npm test
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Run immediately, then every hour
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
loop --immediate 1h npm test
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Run up to 5 times then stop
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
loop --max-runs 5 5m npm test
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Verbose mode
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
loop --verbose 30m npm test
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Shows start/end timestamps, exit code, execution duration, and next scheduled run.
|
|
85
|
+
|
|
86
|
+
## Supported intervals
|
|
87
|
+
|
|
88
|
+
| Format | Description |
|
|
89
|
+
| ------ | ----------- |
|
|
90
|
+
| `10s` | 10 seconds |
|
|
91
|
+
| `5m` | 5 minutes |
|
|
92
|
+
| `1h` | 1 hour |
|
|
93
|
+
| `1d` | 1 day |
|
|
94
|
+
| `1w` | 1 week |
|
|
95
|
+
|
|
96
|
+
## Behavior
|
|
97
|
+
|
|
98
|
+
- **No overlapping**: waits for the command to finish before starting the next interval
|
|
99
|
+
- **Resilient**: continues looping even if a command exits with a non-zero code
|
|
100
|
+
- **Graceful shutdown**: finishes current execution on Ctrl+C, then exits cleanly
|
|
101
|
+
|
|
102
|
+
## Development
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
pnpm install
|
|
106
|
+
pnpm run build
|
|
107
|
+
pnpm run test
|
|
108
|
+
pnpm run lint
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
MIT
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { parseDuration } from "./duration.js";
|
|
4
|
+
import { Logger } from "./logger.js";
|
|
5
|
+
import { runLoop } from "./loop.js";
|
|
6
|
+
const program = new Command();
|
|
7
|
+
program
|
|
8
|
+
.name("loop")
|
|
9
|
+
.description("Repeatedly execute a shell command at a human-readable interval")
|
|
10
|
+
.version("1.0.0")
|
|
11
|
+
.argument("<interval>", "Interval between runs (e.g. 10s, 5m, 1h, 1d, 1w)")
|
|
12
|
+
.argument("<command...>", "Command to execute")
|
|
13
|
+
.option("--immediate", "Run immediately before waiting", false)
|
|
14
|
+
.option("--max-runs <n>", "Stop after N executions", (v) => {
|
|
15
|
+
const n = parseInt(v, 10);
|
|
16
|
+
if (isNaN(n) || n < 1) {
|
|
17
|
+
throw new Error("--max-runs must be a positive integer");
|
|
18
|
+
}
|
|
19
|
+
return n;
|
|
20
|
+
}, null)
|
|
21
|
+
.option("--verbose", "Show execution details", false)
|
|
22
|
+
.enablePositionalOptions()
|
|
23
|
+
.passThroughOptions()
|
|
24
|
+
.addHelpText("before", `
|
|
25
|
+
Usage:
|
|
26
|
+
loop [options] <interval> <command>
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
loop 30m npm test
|
|
30
|
+
loop 1h opencode --prompt '/ob-init'
|
|
31
|
+
loop --immediate 1h npm test
|
|
32
|
+
loop --max-runs 5 5m npm test
|
|
33
|
+
`)
|
|
34
|
+
.action(async (intervalStr, commandParts, opts) => {
|
|
35
|
+
const logger = new Logger(opts.verbose);
|
|
36
|
+
let interval;
|
|
37
|
+
try {
|
|
38
|
+
interval = parseDuration(intervalStr);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
42
|
+
logger.error(`Error: ${message}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
const command = commandParts.join(" ");
|
|
46
|
+
const options = {
|
|
47
|
+
interval,
|
|
48
|
+
command,
|
|
49
|
+
immediate: opts.immediate,
|
|
50
|
+
maxRuns: opts.maxRuns,
|
|
51
|
+
verbose: opts.verbose,
|
|
52
|
+
};
|
|
53
|
+
const controller = new AbortController();
|
|
54
|
+
process.on("SIGINT", () => controller.abort());
|
|
55
|
+
process.on("SIGTERM", () => controller.abort());
|
|
56
|
+
await runLoop(options, logger, controller.signal);
|
|
57
|
+
process.exit(0);
|
|
58
|
+
});
|
|
59
|
+
program.parse();
|
|
60
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,MAAM,CAAC;KACZ,WAAW,CACV,iEAAiE,CAClE;KACA,OAAO,CAAC,OAAO,CAAC;KAChB,QAAQ,CAAC,YAAY,EAAE,kDAAkD,CAAC;KAC1E,QAAQ,CAAC,cAAc,EAAE,oBAAoB,CAAC;KAC9C,MAAM,CAAC,aAAa,EAAE,gCAAgC,EAAE,KAAK,CAAC;KAC9D,MAAM,CAAC,gBAAgB,EAAE,yBAAyB,EAAE,CAAC,CAAC,EAAE,EAAE;IACzD,MAAM,CAAC,GAAG,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC1B,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC3D,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC,EAAE,IAAI,CAAC;KACP,MAAM,CAAC,WAAW,EAAE,wBAAwB,EAAE,KAAK,CAAC;KACpD,uBAAuB,EAAE;KACzB,kBAAkB,EAAE;KACpB,WAAW,CACV,QAAQ,EACR;;;;;;;;;CASH,CACE;KACA,MAAM,CAAC,KAAK,EAAE,WAAmB,EAAE,YAAsB,EAAE,IAA6B,EAAE,EAAE;IAC3F,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,OAAkB,CAAC,CAAC;IAEnD,IAAI,QAAgB,CAAC;IACrB,IAAI,CAAC;QACH,QAAQ,GAAG,aAAa,CAAC,WAAW,CAAC,CAAC;IACxC,CAAC;IAAC,OAAO,KAAc,EAAE,CAAC;QACxB,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACvE,MAAM,CAAC,KAAK,CAAC,UAAU,OAAO,EAAE,CAAC,CAAC;QAClC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAEvC,MAAM,OAAO,GAAgB;QAC3B,QAAQ;QACR,OAAO;QACP,SAAS,EAAE,IAAI,CAAC,SAAoB;QACpC,OAAO,EAAE,IAAI,CAAC,OAAwB;QACtC,OAAO,EAAE,IAAI,CAAC,OAAkB;KACjC,CAAC;IAEF,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IAEzC,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC,CAAC;IAC/C,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC,CAAC;IAEhD,MAAM,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;IAClD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAEL,OAAO,CAAC,KAAK,EAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duration.d.ts","sourceRoot":"","sources":["../src/duration.ts"],"names":[],"mappings":"AAEA,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAkBnD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpD"}
|
package/dist/duration.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import ms from "ms";
|
|
2
|
+
export function parseDuration(input) {
|
|
3
|
+
const trimmed = input.trim();
|
|
4
|
+
if (!trimmed) {
|
|
5
|
+
throw new Error("Duration cannot be empty");
|
|
6
|
+
}
|
|
7
|
+
const result = ms(trimmed);
|
|
8
|
+
if (typeof result !== "number" || isNaN(result)) {
|
|
9
|
+
throw new Error(`Invalid duration: "${input}". Use formats like 10s, 5m, 1h, 1d, 1w`);
|
|
10
|
+
}
|
|
11
|
+
if (result <= 0) {
|
|
12
|
+
throw new Error(`Duration must be positive, got: "${input}"`);
|
|
13
|
+
}
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
export function formatDuration(value) {
|
|
17
|
+
return ms(value, { long: true });
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=duration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duration.js","sourceRoot":"","sources":["../src/duration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAEpB,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAE7B,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;IAC9C,CAAC;IAED,MAAM,MAAM,GAAG,EAAE,CAAC,OAA8C,CAAC,CAAC;IAElE,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;QAChD,MAAM,IAAI,KAAK,CAAC,sBAAsB,KAAK,yCAAyC,CAAC,CAAC;IACxF,CAAC;IAED,IAAI,MAAM,IAAI,CAAC,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CAAC,oCAAoC,KAAK,GAAG,CAAC,CAAC;IAChE,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;AACnC,CAAC"}
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare class Logger {
|
|
2
|
+
private verbose;
|
|
3
|
+
constructor(verbose?: boolean);
|
|
4
|
+
info(message: string): void;
|
|
5
|
+
error(message: string): void;
|
|
6
|
+
success(message: string): void;
|
|
7
|
+
debug(message: string): void;
|
|
8
|
+
warn(message: string): void;
|
|
9
|
+
timestamp(): string;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,qBAAa,MAAM;IACjB,OAAO,CAAC,OAAO,CAAU;gBAEb,OAAO,UAAQ;IAI3B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI3B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI5B,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI9B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAM5B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI3B,SAAS,IAAI,MAAM;CAGpB"}
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export class Logger {
|
|
2
|
+
verbose;
|
|
3
|
+
constructor(verbose = false) {
|
|
4
|
+
this.verbose = verbose;
|
|
5
|
+
}
|
|
6
|
+
info(message) {
|
|
7
|
+
console.log(message);
|
|
8
|
+
}
|
|
9
|
+
error(message) {
|
|
10
|
+
console.error(message);
|
|
11
|
+
}
|
|
12
|
+
success(message) {
|
|
13
|
+
console.log(message);
|
|
14
|
+
}
|
|
15
|
+
debug(message) {
|
|
16
|
+
if (this.verbose) {
|
|
17
|
+
console.log(`[verbose] ${message}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
warn(message) {
|
|
21
|
+
console.warn(message);
|
|
22
|
+
}
|
|
23
|
+
timestamp() {
|
|
24
|
+
return new Date().toISOString();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,MAAM;IACT,OAAO,CAAU;IAEzB,YAAY,OAAO,GAAG,KAAK;QACzB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED,IAAI,CAAC,OAAe;QAClB,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACvB,CAAC;IAED,KAAK,CAAC,OAAe;QACnB,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACzB,CAAC;IAED,OAAO,CAAC,OAAe;QACrB,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACvB,CAAC;IAED,KAAK,CAAC,OAAe;QACnB,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,OAAO,CAAC,GAAG,CAAC,aAAa,OAAO,EAAE,CAAC,CAAC;QACtC,CAAC;IACH,CAAC;IAED,IAAI,CAAC,OAAe;QAClB,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACxB,CAAC;IAED,SAAS;QACP,OAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAClC,CAAC;CACF"}
|
package/dist/loop.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { LoopOptions, ExecutionResult } from "./types.js";
|
|
2
|
+
import { Logger } from "./logger.js";
|
|
3
|
+
export declare function executeCommand(command: string, logger: Logger): Promise<ExecutionResult>;
|
|
4
|
+
export declare function runLoop(options: LoopOptions, logger: Logger, signal: AbortSignal): Promise<void>;
|
|
5
|
+
//# sourceMappingURL=loop.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../src/loop.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAa,MAAM,YAAY,CAAC;AAC1E,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAwBrC,wBAAsB,cAAc,CAClC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,eAAe,CAAC,CAiC1B;AAED,wBAAsB,OAAO,CAC3B,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,IAAI,CAAC,CAyFf"}
|
package/dist/loop.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { execaCommand } from "execa";
|
|
2
|
+
import { formatDuration } from "./duration.js";
|
|
3
|
+
function sleep(ms, signal) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
if (signal.aborted) {
|
|
6
|
+
reject(signal.reason);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
const timer = setTimeout(() => {
|
|
10
|
+
signal.removeEventListener("abort", onAbort);
|
|
11
|
+
resolve();
|
|
12
|
+
}, ms);
|
|
13
|
+
const onAbort = () => {
|
|
14
|
+
clearTimeout(timer);
|
|
15
|
+
reject(signal.reason);
|
|
16
|
+
};
|
|
17
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export async function executeCommand(command, logger) {
|
|
21
|
+
const startedAt = new Date();
|
|
22
|
+
logger.debug(`Executing: ${command}`);
|
|
23
|
+
try {
|
|
24
|
+
const result = await execaCommand(command, {
|
|
25
|
+
stdout: "inherit",
|
|
26
|
+
stderr: "inherit",
|
|
27
|
+
stdin: "inherit",
|
|
28
|
+
});
|
|
29
|
+
const endedAt = new Date();
|
|
30
|
+
return {
|
|
31
|
+
exitCode: result.exitCode ?? 0,
|
|
32
|
+
duration: endedAt.getTime() - startedAt.getTime(),
|
|
33
|
+
startedAt,
|
|
34
|
+
endedAt,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
const endedAt = new Date();
|
|
39
|
+
const exitCode = error && typeof error === "object" && "exitCode" in error
|
|
40
|
+
? error.exitCode
|
|
41
|
+
: 1;
|
|
42
|
+
return {
|
|
43
|
+
exitCode,
|
|
44
|
+
duration: endedAt.getTime() - startedAt.getTime(),
|
|
45
|
+
startedAt,
|
|
46
|
+
endedAt,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function runLoop(options, logger, signal) {
|
|
51
|
+
const state = {
|
|
52
|
+
running: false,
|
|
53
|
+
runCount: 0,
|
|
54
|
+
shuttingDown: false,
|
|
55
|
+
};
|
|
56
|
+
const onSignal = () => {
|
|
57
|
+
state.shuttingDown = true;
|
|
58
|
+
logger.info("\nShutting down gracefully...");
|
|
59
|
+
};
|
|
60
|
+
process.on("SIGINT", onSignal);
|
|
61
|
+
process.on("SIGTERM", onSignal);
|
|
62
|
+
try {
|
|
63
|
+
let isFirstRun = true;
|
|
64
|
+
while (!state.shuttingDown) {
|
|
65
|
+
if (options.maxRuns !== null &&
|
|
66
|
+
state.runCount >= options.maxRuns) {
|
|
67
|
+
logger.info(`Completed ${state.runCount} run(s). Exiting.`);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
if (isFirstRun && !options.immediate) {
|
|
71
|
+
logger.info(`Waiting ${formatDuration(options.interval)} before first run...`);
|
|
72
|
+
try {
|
|
73
|
+
await sleep(options.interval, signal);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
if (state.shuttingDown)
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
isFirstRun = false;
|
|
82
|
+
state.running = true;
|
|
83
|
+
state.runCount++;
|
|
84
|
+
logger.info(`\n--- Run ${state.runCount}${options.maxRuns !== null ? `/${options.maxRuns}` : ""} ---`);
|
|
85
|
+
const result = await executeCommand(options.command, logger);
|
|
86
|
+
state.running = false;
|
|
87
|
+
if (result.exitCode !== 0) {
|
|
88
|
+
logger.error(`Command failed with exit code ${result.exitCode}`);
|
|
89
|
+
}
|
|
90
|
+
if (options.verbose) {
|
|
91
|
+
logger.debug(`Started at: ${result.startedAt.toISOString()}`);
|
|
92
|
+
logger.debug(`Ended at: ${result.endedAt.toISOString()}`);
|
|
93
|
+
logger.debug(`Exit code: ${result.exitCode}`);
|
|
94
|
+
logger.debug(`Duration: ${formatDuration(result.duration)}`);
|
|
95
|
+
}
|
|
96
|
+
if (state.shuttingDown)
|
|
97
|
+
break;
|
|
98
|
+
if (options.maxRuns !== null &&
|
|
99
|
+
state.runCount >= options.maxRuns) {
|
|
100
|
+
logger.info(`Completed ${state.runCount} run(s). Exiting.`);
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
const nextRun = new Date(Date.now() + options.interval);
|
|
104
|
+
logger.info(`Next run in ${formatDuration(options.interval)} (at ${nextRun.toLocaleTimeString()})`);
|
|
105
|
+
try {
|
|
106
|
+
await sleep(options.interval, signal);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
process.removeListener("SIGINT", onSignal);
|
|
115
|
+
process.removeListener("SIGTERM", onSignal);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=loop.js.map
|
package/dist/loop.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loop.js","sourceRoot":"","sources":["../src/loop.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface LoopOptions {
|
|
2
|
+
interval: number;
|
|
3
|
+
command: string;
|
|
4
|
+
immediate: boolean;
|
|
5
|
+
maxRuns: number | null;
|
|
6
|
+
verbose: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface ExecutionResult {
|
|
9
|
+
exitCode: number;
|
|
10
|
+
duration: number;
|
|
11
|
+
startedAt: Date;
|
|
12
|
+
endedAt: Date;
|
|
13
|
+
}
|
|
14
|
+
export interface LoopState {
|
|
15
|
+
running: boolean;
|
|
16
|
+
runCount: number;
|
|
17
|
+
shuttingDown: boolean;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,IAAI,CAAC;IAChB,OAAO,EAAE,IAAI,CAAC;CACf;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,OAAO,CAAC;CACvB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "loop-task",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A cross-platform CLI that repeatedly executes a shell command at a human-readable interval",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"loop": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/cli.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"cli",
|
|
15
|
+
"loop",
|
|
16
|
+
"repeat",
|
|
17
|
+
"interval",
|
|
18
|
+
"schedule",
|
|
19
|
+
"timer",
|
|
20
|
+
"cron",
|
|
21
|
+
"automation",
|
|
22
|
+
"devops",
|
|
23
|
+
"agent"
|
|
24
|
+
],
|
|
25
|
+
"author": "Quique Fdez Guerra",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=20.0.0"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"commander": "^13.1.0",
|
|
32
|
+
"execa": "^9.6.0",
|
|
33
|
+
"ms": "^2.1.3"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/ms": "^2.1.0",
|
|
37
|
+
"@types/node": "^22.15.0",
|
|
38
|
+
"@vitest/coverage-v8": "^3.1.0",
|
|
39
|
+
"eslint": "^9.25.0",
|
|
40
|
+
"typescript": "^5.8.0",
|
|
41
|
+
"typescript-eslint": "^8.30.0",
|
|
42
|
+
"vitest": "^3.1.0"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsc",
|
|
46
|
+
"dev": "tsc --watch",
|
|
47
|
+
"start": "node dist/cli.js",
|
|
48
|
+
"test": "vitest run",
|
|
49
|
+
"test:watch": "vitest",
|
|
50
|
+
"test:coverage": "vitest run --coverage",
|
|
51
|
+
"lint": "eslint src/ tests/",
|
|
52
|
+
"typecheck": "tsc --noEmit",
|
|
53
|
+
"release:dry": "pnpm run build && pnpm publish --dry-run",
|
|
54
|
+
"release": "pnpm run build && pnpm publish"
|
|
55
|
+
}
|
|
56
|
+
}
|