schrun 0.2.3 → 0.3.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 CHANGED
@@ -1,61 +1,70 @@
1
- # schrun
1
+ <p align="center">
2
+ <img src="./assets/schrun-logo-small.jpg" alt="schrun logo">
3
+ </p>
2
4
 
3
- `schrun` is a small task runner for launching commands defined in JSON files. It supports running a task in the foreground or starting it as a detached background process.
5
+ <h1 align="center">schrun</h1>
6
+
7
+ `schrun` is a small task runner. It reads task definitions from JSON files and can run them in the foreground, start them in the background, or schedule enabled tasks with cron expressions.
8
+
9
+ > [!TIP]
10
+ > `schrun` is a completely independent package and is not related to any other tools.
4
11
 
5
12
  ## Requirements
6
13
 
7
14
  - [Bun](https://bun.sh/)
8
15
 
9
- ## Installation
10
-
11
- From the repository root, install the workspace dependencies:
16
+ ## Install and run from this repository
12
17
 
13
- ```bash
18
+ ```sh
14
19
  bun install
15
20
  ```
16
21
 
17
- ## Configure a task
22
+ Until a published package is available, run the CLI from the repository root with Bun:
18
23
 
19
- Create the configuration directory and a JSON file inside it. The file name is the task identifier passed to `schrun` (it does not need a `.json` extension).
20
-
21
- ```bash
22
- mkdir -p ~/.config/schrun
24
+ ```sh
25
+ bunx schrun --surface hello
26
+ bunx schrun --background hello
23
27
  ```
24
28
 
25
- For example, save the following as `~/.config/schrun/hello.json`:
29
+ The task name is passed without the `.json` extension. schrun reads `~/.config/schrun/<name>.json`.
30
+
31
+ ## Define a task
32
+
33
+ Create `~/.config/schrun/hello.json`:
26
34
 
27
35
  ```json
28
36
  {
29
37
  "id": 1,
30
38
  "description": "Print a greeting",
31
- "run": "echo hello"
39
+ "cron": "* * * * *",
40
+ "run": "echo hello",
41
+ "enabled": true
32
42
  }
33
43
  ```
34
44
 
35
- Each task file must contain:
36
-
37
- - `id`: a number
38
- - `description`: a string
39
- - `run`: a command and its arguments, separated by whitespace
45
+ Every field is required: `id` (number), `description` (string), `cron` (cron expression string), `run` (command string), and `enabled` (boolean). The scheduler considers only enabled tasks.
40
46
 
41
- ## Run a task
47
+ ## Run tasks
42
48
 
43
- Run from the repository root:
49
+ Foreground mode inherits standard input/output and waits for the command to finish:
44
50
 
45
- ```bash
46
- bun run apps/schrun/src/index.ts hello.json
51
+ ```sh
52
+ bunx schrun --surface hello
47
53
  ```
48
54
 
49
- The command inherits the terminal's standard input and output, and `schrun` waits for it to finish.
50
-
51
- To start it detached in the background:
55
+ Detached background mode ignores standard input/output and returns after starting the process:
52
56
 
53
- ```bash
54
- bun run apps/schrun/src/index.ts --background hello.json
57
+ ```sh
58
+ bunx schrun --background hello
55
59
  ```
56
60
 
57
- Background tasks have their standard input and output ignored; `schrun` returns after starting the process.
61
+ Start or stop the scheduler:
62
+
63
+ ```sh
64
+ bunx schrun run --start
65
+ bunx schrun run --stop
66
+ ```
58
67
 
59
68
  ## Limitations
60
69
 
61
- The `run` value is split on whitespace and launched directly, without a shell. Shell features such as pipes, redirects, variable expansion, and quoted arguments are not interpreted.
70
+ Commands are split on whitespace and launched directly, without a shell. Pipes, redirects, variable expansion, and quoted arguments are not interpreted. Use a script file when a task needs shell features.
package/dist/daemon.mjs CHANGED
@@ -1,14 +1,14 @@
1
- import { r as CONFIG_DIR, t as readJSON } from "./readJSON-p8OldMa9.mjs";
1
+ import { r as CONFIG_DIR, t as readJSON } from "./readJSON-BIHboQ76.mjs";
2
2
  import { readdir } from "node:fs/promises";
3
3
  import { consola } from "consola";
4
4
  import { spawn } from "node:child_process";
5
5
  import cron from "node-cron";
6
- //#region src/run/daemon.ts
6
+ // src/run/daemon.ts
7
7
  const startschedule = async () => {
8
8
  const readConfig = await readdir(CONFIG_DIR);
9
9
  for (const filename of readConfig) {
10
10
  if (!filename.endsWith(".json")) continue;
11
- if (!filename.match("foo.json")) continue;
11
+ if (filename.match("pid.json")) continue;
12
12
  const [filenamesplit] = filename.split(".");
13
13
  const read = await readJSON(filenamesplit);
14
14
  if (!read.enabled) continue;
@@ -28,5 +28,4 @@ startschedule().catch((error) => {
28
28
  consola.error("failed to up scheduler", error);
29
29
  process.exitCode = 1;
30
30
  });
31
- //#endregion
32
31
  export { startschedule };
package/dist/index.mjs CHANGED
@@ -1,17 +1,17 @@
1
1
  #!/usr/bin/env node
2
- import { i as PID_FILE, n as CLI_COMMAND_NAME, t as readJSON } from "./readJSON-p8OldMa9.mjs";
2
+ import { i as PID_FILE, n as CLI_COMMAND_NAME, r as CONFIG_DIR, t as readJSON } from "./readJSON-BIHboQ76.mjs";
3
3
  import { cli, define } from "gunshi";
4
- import { open, readFile, unlink, writeFile } from "node:fs/promises";
4
+ import { readFile, readdir, unlink, writeFile } from "node:fs/promises";
5
5
  import { consola } from "consola";
6
6
  import { spawn } from "node:child_process";
7
7
  import { fileURLToPath } from "node:url";
8
- //#region src/version.ts
8
+ import Table from "cli-table3";
9
+ // src/version.ts
9
10
  const version = async () => {
10
11
  const packageJsonText = await readFile(new URL("../package.json", import.meta.url), "utf-8");
11
12
  return JSON.parse(packageJsonText).version;
12
13
  };
13
- //#endregion
14
- //#region src/run/background.ts
14
+ // src/run/background.ts
15
15
  const runbackground = async (FileName) => {
16
16
  const [command, ...args] = (await readJSON(FileName)).run.split(/\s+/);
17
17
  if (!command) {
@@ -27,8 +27,7 @@ const runbackground = async (FileName) => {
27
27
  });
28
28
  spawncli.unref();
29
29
  };
30
- //#endregion
31
- //#region src/run/surface.ts
30
+ // src/run/surface.ts
32
31
  const runsurface = async (FileName) => {
33
32
  const [command, ...args] = (await readJSON(FileName)).run.split(/\s+/);
34
33
  if (!command) {
@@ -43,55 +42,43 @@ const runsurface = async (FileName) => {
43
42
  consola.success(`done code: ${code}`);
44
43
  });
45
44
  };
46
- //#endregion
47
- //#region src/run/startscheduler.ts
48
- const startschedule = async () => {
45
+ // src/run/startScheduler.ts
46
+ const startSchedule = async () => {
49
47
  const daemonPath = fileURLToPath(new URL("./daemon.mjs", import.meta.url));
50
- const logFile = await open("/tmp/schrun-daemon.log", "a");
51
48
  const child = spawn(process.execPath, [daemonPath], {
52
49
  detached: true,
53
- stdio: [
54
- "ignore",
55
- logFile.fd,
56
- logFile.fd
57
- ]
50
+ stdio: "ignore"
58
51
  });
59
- if (child.pid === void 0) {
60
- await logFile.close();
61
- throw new Error("Failed to start daemon");
62
- }
52
+ if (child.pid === void 0) throw new Error("Failed to start daemon");
63
53
  child.on("error", (error) => {
64
- console.error("Failed to start daemon:", error);
54
+ throw new Error("Failed to start daemon:", error);
65
55
  });
66
- await writeFile(PID_FILE, String(child.pid));
56
+ await writeFile(PID_FILE, String(child.pid), "utf-8");
67
57
  child.unref();
68
- await logFile.close();
69
58
  };
70
- //#endregion
71
- //#region src/run/stopscheduler.ts
72
- const stopschedule = async () => {
59
+ // src/run/stopScheduler.ts
60
+ const stopSchedule = async () => {
73
61
  let pid;
74
62
  try {
75
63
  pid = Number(await readFile(PID_FILE, "utf-8"));
76
- } catch {
77
- consola.warn("no look schedule pid file");
64
+ } catch (error) {
65
+ consola.warn("No look schedule pid file");
78
66
  return;
79
67
  }
80
68
  if (!Number.isSafeInteger(pid) || pid <= 0) {
81
- consola.error("warning pid files");
69
+ consola.error("Unsafe pid files");
82
70
  return;
83
71
  }
84
72
  try {
85
73
  process.kill(pid, "SIGINT");
86
- consola.success("send stop to scheduler");
74
+ consola.success("Success Send stop scheduler");
87
75
  } catch (error) {
88
- consola.error("failed stop to scheduler", error);
76
+ consola.error("Failed Send stop scheduler", error);
89
77
  return;
90
78
  }
91
79
  await unlink(PID_FILE).catch(() => {});
92
80
  };
93
- //#endregion
94
- //#region src/commands/run/index.ts
81
+ // src/commands/run/index.ts
95
82
  const runCommand = {
96
83
  name: "run",
97
84
  args: {
@@ -108,20 +95,47 @@ const runCommand = {
108
95
  },
109
96
  run: (ctx) => {
110
97
  const { start, stop } = ctx.values;
111
- if (start == true) startschedule();
112
- if (stop == true) stopschedule();
98
+ if (start == true) startSchedule();
99
+ if (stop == true) stopSchedule();
100
+ }
101
+ };
102
+ // src/table.ts
103
+ const list = async () => {
104
+ const read = await readdir(CONFIG_DIR);
105
+ if (read.length === 0) consola.error("No schedule found");
106
+ const table = new Table({ head: [
107
+ "description",
108
+ "cron",
109
+ "run",
110
+ "enabled"
111
+ ] });
112
+ for (const readdir of read) {
113
+ if (readdir.match("pid.json")) continue;
114
+ const [splits] = readdir.split(".");
115
+ const readfunc = await readJSON(splits);
116
+ table.push([
117
+ readfunc.description,
118
+ readfunc.cron,
119
+ readfunc.run,
120
+ readfunc.enabled
121
+ ]);
113
122
  }
123
+ console.log(table.toString());
114
124
  };
115
- //#endregion
116
- //#region src/commands/default/index.ts
125
+ // src/commands/list/index.ts
126
+ const listCommand = {
127
+ name: "list",
128
+ run: list
129
+ };
130
+ // src/commands/default/index.ts
117
131
  const mainCommand = async () => {
118
132
  const RunmainCommand = define({
119
133
  name: CLI_COMMAND_NAME,
120
134
  description: "agent and other task runner",
121
135
  version: await version(),
122
136
  args: {
123
- defaultInput: {
124
- type: "positional",
137
+ surface: {
138
+ type: "string",
125
139
  required: false,
126
140
  description: "Input value"
127
141
  },
@@ -132,15 +146,15 @@ const mainCommand = async () => {
132
146
  }
133
147
  },
134
148
  run: async (ctx) => {
135
- const { defaultInput, background } = ctx.values;
149
+ const { surface, background } = ctx.values;
136
150
  if (background !== void 0) {
137
151
  console.log("Background mode:", background);
138
152
  await runbackground(background);
139
153
  return;
140
154
  }
141
- if (defaultInput !== void 0) {
142
- console.log("Normal mode:", defaultInput);
143
- await runsurface(defaultInput);
155
+ if (surface !== void 0) {
156
+ console.log("Normal mode:", surface);
157
+ await runsurface(surface);
144
158
  return;
145
159
  }
146
160
  consola.error("No Input String");
@@ -148,11 +162,12 @@ const mainCommand = async () => {
148
162
  });
149
163
  await cli(process.argv.slice(2), RunmainCommand, {
150
164
  name: CLI_COMMAND_NAME,
151
- subCommands: { run: runCommand }
165
+ subCommands: {
166
+ run: runCommand,
167
+ list: listCommand
168
+ }
152
169
  });
153
170
  };
154
- //#endregion
155
- //#region src/index.ts
171
+ // src/index.ts
156
172
  await mainCommand();
157
- //#endregion
158
173
  export {};
@@ -1,29 +1,18 @@
1
1
  import { readFile } from "node:fs/promises";
2
- import { consola } from "consola";
3
2
  import { join } from "node:path";
4
3
  import { homedir } from "node:os";
5
- import { z } from "zod";
6
- //#region src/constant/app.ts
4
+ import "zod";
5
+ import typia from "typia";
6
+ // src/constant/app.ts
7
7
  const CLI_COMMAND_NAME = "schrun";
8
8
  const CONFIG_DIR = join(`${homedir()}`, ".config", "schrun");
9
9
  const PID_FILE = join(`${homedir()}`, ".config", "schrun", "pid.json");
10
- //#endregion
11
- //#region src/type.ts
12
- const fileSchema = z.object({
13
- id: z.number(),
14
- description: z.string(),
15
- cron: z.string(),
16
- run: z.string(),
17
- enabled: z.boolean()
18
- });
19
- //#endregion
20
- //#region src/readJSON.ts
10
+ // src/readJSON.ts
21
11
  const readJSON = async (FileName) => {
22
12
  const file = join(CONFIG_DIR, `${FileName}.json`);
23
13
  const reads = await readFile(file, "utf-8");
24
- const safeParseSchema = fileSchema.safeParse(JSON.parse(reads));
25
- if (!safeParseSchema.success) throw consola.error(safeParseSchema.error);
26
- return safeParseSchema.data;
14
+ const readparse = JSON.parse(reads);
15
+ if (typia.is(readparse)) return readparse;
16
+ else throw "The configuration format is invalid";
27
17
  };
28
- //#endregion
29
18
  export { PID_FILE as i, CLI_COMMAND_NAME as n, CONFIG_DIR as r, readJSON as t };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "schrun",
3
3
  "module": "src/index.ts",
4
- "version": "0.2.3",
4
+ "version": "0.3.0",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -15,6 +15,7 @@
15
15
  "files": [
16
16
  "dist",
17
17
  "package.json",
18
+ "bun.lock",
18
19
  "README.md"
19
20
  ],
20
21
  "devDependencies": {
@@ -25,10 +26,12 @@
25
26
  "typescript": "^7"
26
27
  },
27
28
  "dependencies": {
29
+ "cli-table3": "^0.6.5",
28
30
  "consola": "^3.4.2",
29
31
  "execa": "^10.1.0",
30
32
  "gunshi": "^0.37.3",
31
33
  "node-cron": "^4.6.0",
34
+ "typia": "^15.2.1",
32
35
  "zod": "^4.6.7"
33
36
  }
34
37
  }