schrun 0.0.0-stage → 0.2.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/CLAUDE.md +106 -0
- package/CONTRIBUTING.md +40 -0
- package/LICENSE +21 -0
- package/README.md +60 -2
- package/dist/index.mjs +158 -0
- package/package.json +27 -4
- package/src/commands/default/index.ts +55 -0
- package/src/commands/run/index.ts +31 -0
- package/src/constant/app.ts +6 -0
- package/src/index.ts +5 -0
- package/src/readJSON.ts +18 -0
- package/src/run/background.ts +26 -0
- package/src/run/daemon.ts +39 -0
- package/src/run/startscheduler.ts +29 -0
- package/src/run/stopscheduler.ts +29 -0
- package/src/run/surface.ts +27 -0
- package/src/type.ts +11 -0
- package/src/version.ts +15 -0
- package/tsconfig.json +30 -0
- package/tsdown.config.ts +8 -0
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
|
|
2
|
+
Default to using Bun instead of Node.js.
|
|
3
|
+
|
|
4
|
+
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
|
5
|
+
- Use `bun test` instead of `jest` or `vitest`
|
|
6
|
+
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
|
7
|
+
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
|
8
|
+
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
|
9
|
+
- Use `bunx <package> <command>` instead of `npx <package> <command>`
|
|
10
|
+
- Bun automatically loads .env, so don't use dotenv.
|
|
11
|
+
|
|
12
|
+
## APIs
|
|
13
|
+
|
|
14
|
+
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
|
15
|
+
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
|
16
|
+
- `Bun.redis` for Redis. Don't use `ioredis`.
|
|
17
|
+
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
|
18
|
+
- `WebSocket` is built-in. Don't use `ws`.
|
|
19
|
+
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
|
20
|
+
- Bun.$`ls` instead of execa.
|
|
21
|
+
|
|
22
|
+
## Testing
|
|
23
|
+
|
|
24
|
+
Use `bun test` to run tests.
|
|
25
|
+
|
|
26
|
+
```ts#index.test.ts
|
|
27
|
+
import { test, expect } from "bun:test";
|
|
28
|
+
|
|
29
|
+
test("hello world", () => {
|
|
30
|
+
expect(1).toBe(1);
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Frontend
|
|
35
|
+
|
|
36
|
+
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
|
37
|
+
|
|
38
|
+
Server:
|
|
39
|
+
|
|
40
|
+
```ts#index.ts
|
|
41
|
+
import index from "./index.html"
|
|
42
|
+
|
|
43
|
+
Bun.serve({
|
|
44
|
+
routes: {
|
|
45
|
+
"/": index,
|
|
46
|
+
"/api/users/:id": {
|
|
47
|
+
GET: (req) => {
|
|
48
|
+
return new Response(JSON.stringify({ id: req.params.id }));
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
// optional websocket support
|
|
53
|
+
websocket: {
|
|
54
|
+
open: (ws) => {
|
|
55
|
+
ws.send("Hello, world!");
|
|
56
|
+
},
|
|
57
|
+
message: (ws, message) => {
|
|
58
|
+
ws.send(message);
|
|
59
|
+
},
|
|
60
|
+
close: (ws) => {
|
|
61
|
+
// handle close
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
development: {
|
|
65
|
+
hmr: true,
|
|
66
|
+
console: true,
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
|
72
|
+
|
|
73
|
+
```html#index.html
|
|
74
|
+
<html>
|
|
75
|
+
<body>
|
|
76
|
+
<h1>Hello, world!</h1>
|
|
77
|
+
<script type="module" src="./frontend.tsx"></script>
|
|
78
|
+
</body>
|
|
79
|
+
</html>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
With the following `frontend.tsx`:
|
|
83
|
+
|
|
84
|
+
```tsx#frontend.tsx
|
|
85
|
+
import React from "react";
|
|
86
|
+
import { createRoot } from "react-dom/client";
|
|
87
|
+
|
|
88
|
+
// import .css files directly and it works
|
|
89
|
+
import './index.css';
|
|
90
|
+
|
|
91
|
+
const root = createRoot(document.body);
|
|
92
|
+
|
|
93
|
+
export default function Frontend() {
|
|
94
|
+
return <h1>Hello, world!</h1>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
root.render(<Frontend />);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Then, run index.ts
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
bun --hot ./index.ts
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Contributing to schrun
|
|
2
|
+
|
|
3
|
+
Thanks for your interest in contributing to `schrun`! Contributions such as bug reports, documentation improvements, and focused code changes are welcome.
|
|
4
|
+
|
|
5
|
+
## Getting started
|
|
6
|
+
|
|
7
|
+
- Install [Bun](https://bun.sh/).
|
|
8
|
+
- Clone the repository and work from its root directory.
|
|
9
|
+
- Install workspace dependencies:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun install
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Making changes
|
|
16
|
+
|
|
17
|
+
1. Create a branch for your change.
|
|
18
|
+
2. Keep changes focused and follow the existing TypeScript style and project conventions.
|
|
19
|
+
3. Update the documentation when behavior or usage changes.
|
|
20
|
+
4. Run the relevant checks before opening a pull request.
|
|
21
|
+
|
|
22
|
+
The CLI entry point is `apps/schrun/src/index.ts`. You can run it from the repository root with:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
bun run apps/schrun/src/index.ts --help
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Testing
|
|
29
|
+
|
|
30
|
+
There is currently no test script configured for this app. For changes that affect CLI behavior, manually verify the relevant command using a temporary task file under `~/.config/schrun/`. Avoid committing personal configuration or credentials.
|
|
31
|
+
|
|
32
|
+
## Pull requests
|
|
33
|
+
|
|
34
|
+
When opening a pull request, please include:
|
|
35
|
+
|
|
36
|
+
- A concise description of the problem and the proposed change.
|
|
37
|
+
- Any relevant usage or behavior changes.
|
|
38
|
+
- The checks or manual verification you performed.
|
|
39
|
+
|
|
40
|
+
Keep pull requests focused and be open to feedback during review.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nazozokc
|
|
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
CHANGED
|
@@ -1,3 +1,61 @@
|
|
|
1
|
-
#
|
|
1
|
+
# schrun
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- [Bun](https://bun.sh/)
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
From the repository root, install the workspace dependencies:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bun install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Configure a task
|
|
18
|
+
|
|
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
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
For example, save the following as `~/.config/schrun/hello.json`:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"id": 1,
|
|
30
|
+
"description": "Print a greeting",
|
|
31
|
+
"run": "echo hello"
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
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
|
|
40
|
+
|
|
41
|
+
## Run a task
|
|
42
|
+
|
|
43
|
+
Run from the repository root:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
bun run apps/schrun/src/index.ts hello.json
|
|
47
|
+
```
|
|
48
|
+
|
|
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:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
bun run apps/schrun/src/index.ts --background hello.json
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Background tasks have their standard input and output ignored; `schrun` returns after starting the process.
|
|
58
|
+
|
|
59
|
+
## Limitations
|
|
60
|
+
|
|
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.
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { i as PID_FILE, n as CLI_COMMAND_NAME, t as readJSON } from "./readJSON-p8OldMa9.mjs";
|
|
3
|
+
import { cli, define } from "gunshi";
|
|
4
|
+
import { open, readFile, unlink, writeFile } from "node:fs/promises";
|
|
5
|
+
import { consola } from "consola";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
//#region src/version.ts
|
|
9
|
+
const version = async () => {
|
|
10
|
+
const packageJsonText = await readFile(new URL("../package.json", import.meta.url), "utf-8");
|
|
11
|
+
return JSON.parse(packageJsonText).version;
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/run/background.ts
|
|
15
|
+
const runbackground = async (FileName) => {
|
|
16
|
+
const [command, ...args] = (await readJSON(FileName)).run.split(/\s+/);
|
|
17
|
+
if (!command) {
|
|
18
|
+
consola.error("do not run commands");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const spawncli = spawn(command, args, {
|
|
22
|
+
stdio: "ignore",
|
|
23
|
+
detached: true
|
|
24
|
+
});
|
|
25
|
+
spawncli.on("error", (error) => {
|
|
26
|
+
consola.error("failed to start processing", error);
|
|
27
|
+
});
|
|
28
|
+
spawncli.unref();
|
|
29
|
+
};
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/run/surface.ts
|
|
32
|
+
const runsurface = async (FileName) => {
|
|
33
|
+
const [command, ...args] = (await readJSON(FileName)).run.split(/\s+/);
|
|
34
|
+
if (!command) {
|
|
35
|
+
consola.error("do not run commands");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const spawncli = spawn(command, args, { stdio: "inherit" });
|
|
39
|
+
spawncli.on("error", (error) => {
|
|
40
|
+
consola.error("failed run command", error);
|
|
41
|
+
});
|
|
42
|
+
spawncli.on("close", (code) => {
|
|
43
|
+
consola.success(`done code: ${code}`);
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/run/startscheduler.ts
|
|
48
|
+
const startschedule = async () => {
|
|
49
|
+
const daemonPath = fileURLToPath(new URL("./daemon.mjs", import.meta.url));
|
|
50
|
+
const logFile = await open("/tmp/schrun-daemon.log", "a");
|
|
51
|
+
const child = spawn(process.execPath, [daemonPath], {
|
|
52
|
+
detached: true,
|
|
53
|
+
stdio: [
|
|
54
|
+
"ignore",
|
|
55
|
+
logFile.fd,
|
|
56
|
+
logFile.fd
|
|
57
|
+
]
|
|
58
|
+
});
|
|
59
|
+
if (child.pid === void 0) {
|
|
60
|
+
await logFile.close();
|
|
61
|
+
throw new Error("Failed to start daemon");
|
|
62
|
+
}
|
|
63
|
+
child.on("error", (error) => {
|
|
64
|
+
console.error("Failed to start daemon:", error);
|
|
65
|
+
});
|
|
66
|
+
await writeFile(PID_FILE, String(child.pid));
|
|
67
|
+
child.unref();
|
|
68
|
+
await logFile.close();
|
|
69
|
+
};
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/run/stopscheduler.ts
|
|
72
|
+
const stopschedule = async () => {
|
|
73
|
+
let pid;
|
|
74
|
+
try {
|
|
75
|
+
pid = Number(await readFile(PID_FILE, "utf-8"));
|
|
76
|
+
} catch {
|
|
77
|
+
consola.warn("no look schedule pid file");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) {
|
|
81
|
+
consola.error("warning pid files");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
process.kill(pid, "SIGINT");
|
|
86
|
+
consola.success("send stop to scheduler");
|
|
87
|
+
} catch (error) {
|
|
88
|
+
consola.error("failed stop to scheduler", error);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
await unlink(PID_FILE).catch(() => {});
|
|
92
|
+
};
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/commands/run/index.ts
|
|
95
|
+
const runCommand = {
|
|
96
|
+
name: "run",
|
|
97
|
+
args: {
|
|
98
|
+
start: {
|
|
99
|
+
type: "boolean",
|
|
100
|
+
required: false,
|
|
101
|
+
description: "Start scheduler"
|
|
102
|
+
},
|
|
103
|
+
stop: {
|
|
104
|
+
type: "boolean",
|
|
105
|
+
required: false,
|
|
106
|
+
description: "Stop scheduler"
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
run: (ctx) => {
|
|
110
|
+
const { start, stop } = ctx.values;
|
|
111
|
+
if (start == true) startschedule();
|
|
112
|
+
if (stop == true) stopschedule();
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src/commands/default/index.ts
|
|
117
|
+
const mainCommand = async () => {
|
|
118
|
+
const RunmainCommand = define({
|
|
119
|
+
name: CLI_COMMAND_NAME,
|
|
120
|
+
description: "agent and other task runner",
|
|
121
|
+
version: await version(),
|
|
122
|
+
args: {
|
|
123
|
+
defaultInput: {
|
|
124
|
+
type: "positional",
|
|
125
|
+
required: false,
|
|
126
|
+
description: "Input value"
|
|
127
|
+
},
|
|
128
|
+
background: {
|
|
129
|
+
type: "string",
|
|
130
|
+
required: false,
|
|
131
|
+
description: "Run a task in the background"
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
run: async (ctx) => {
|
|
135
|
+
const { defaultInput, background } = ctx.values;
|
|
136
|
+
if (background !== void 0) {
|
|
137
|
+
console.log("Background mode:", background);
|
|
138
|
+
await runbackground(background);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (defaultInput !== void 0) {
|
|
142
|
+
console.log("Normal mode:", defaultInput);
|
|
143
|
+
await runsurface(defaultInput);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
consola.error("No Input String");
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
await cli(process.argv.slice(2), RunmainCommand, {
|
|
150
|
+
name: CLI_COMMAND_NAME,
|
|
151
|
+
subCommands: { run: runCommand }
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/index.ts
|
|
156
|
+
await mainCommand();
|
|
157
|
+
//#endregion
|
|
158
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "schrun",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"module": "src/index.ts",
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"schrun": "./dist/index.mjs"
|
|
9
|
+
},
|
|
10
|
+
"url": "https://github.com/nazozokc/schrun",
|
|
11
|
+
"author": {
|
|
12
|
+
"name": "nazozokc",
|
|
13
|
+
"url": "https://github.com/nazozokc"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@types/bun": "latest",
|
|
17
|
+
"tsdown": "latest"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"typescript": "^7"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"consola": "^3.4.2",
|
|
24
|
+
"execa": "^10.1.0",
|
|
25
|
+
"gunshi": "^0.37.3",
|
|
26
|
+
"node-cron": "^4.6.0",
|
|
27
|
+
"zod": "^4.6.7"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { cli, define } from "gunshi";
|
|
2
|
+
import { version } from "../../version.ts";
|
|
3
|
+
import { runbackground } from "../../run/background.ts";
|
|
4
|
+
import { runsurface } from "../../run/surface.ts";
|
|
5
|
+
import { consola } from "consola";
|
|
6
|
+
import { CLI_COMMAND_NAME } from "../../constant/app.ts";
|
|
7
|
+
import { runCommand } from "../run/index.ts";
|
|
8
|
+
|
|
9
|
+
export const mainCommand = async (): Promise<void> => {
|
|
10
|
+
const RunmainCommand = define({
|
|
11
|
+
name: CLI_COMMAND_NAME,
|
|
12
|
+
description: "agent and other task runner",
|
|
13
|
+
version: await version(),
|
|
14
|
+
|
|
15
|
+
args: {
|
|
16
|
+
defaultInput: {
|
|
17
|
+
type: "positional",
|
|
18
|
+
required: false,
|
|
19
|
+
description: "Input value",
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
background: {
|
|
23
|
+
type: "string",
|
|
24
|
+
required: false,
|
|
25
|
+
description: "Run a task in the background",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
run: async (ctx) => {
|
|
30
|
+
const { defaultInput, background } = ctx.values;
|
|
31
|
+
|
|
32
|
+
if (background !== undefined) {
|
|
33
|
+
console.log("Background mode:", background);
|
|
34
|
+
await runbackground(background);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (defaultInput !== undefined) {
|
|
39
|
+
console.log("Normal mode:", defaultInput);
|
|
40
|
+
await runsurface(defaultInput);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
consola.error("No Input String");
|
|
45
|
+
return;
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await cli(process.argv.slice(2), RunmainCommand, {
|
|
50
|
+
name: CLI_COMMAND_NAME,
|
|
51
|
+
subCommands: {
|
|
52
|
+
run: runCommand,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Command } from "gunshi";
|
|
2
|
+
import { startschedule } from "../../run/startscheduler.ts";
|
|
3
|
+
import { stopschedule } from "../../run/stopscheduler.ts";
|
|
4
|
+
|
|
5
|
+
export const runCommand: Command = {
|
|
6
|
+
name: "run",
|
|
7
|
+
args: {
|
|
8
|
+
start: {
|
|
9
|
+
type: "boolean",
|
|
10
|
+
required: false,
|
|
11
|
+
description: "Start scheduler",
|
|
12
|
+
},
|
|
13
|
+
stop: {
|
|
14
|
+
type: "boolean",
|
|
15
|
+
required: false,
|
|
16
|
+
description: "Stop scheduler",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
run: (ctx) => {
|
|
21
|
+
const { start, stop } = ctx.values;
|
|
22
|
+
|
|
23
|
+
if (start == true) {
|
|
24
|
+
startschedule();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (stop == true) {
|
|
28
|
+
stopschedule();
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
};
|
package/src/index.ts
ADDED
package/src/readJSON.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { CONFIG_DIR } from "./constant/app";
|
|
4
|
+
import { fileSchema, type FileType } from "./type";
|
|
5
|
+
import { consola } from "consola";
|
|
6
|
+
|
|
7
|
+
export const readJSON = async (FileName: string): Promise<FileType> => {
|
|
8
|
+
const file = join(CONFIG_DIR, `${FileName}.json`);
|
|
9
|
+
const reads = await readFile(file, "utf-8");
|
|
10
|
+
|
|
11
|
+
const safeParseSchema = fileSchema.safeParse(JSON.parse(reads));
|
|
12
|
+
|
|
13
|
+
if (!safeParseSchema.success) {
|
|
14
|
+
throw consola.error(safeParseSchema.error);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return safeParseSchema.data as FileType;
|
|
18
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { consola } from "consola";
|
|
2
|
+
import { readJSON } from "../readJSON.ts";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
export const runbackground = async (FileName: string): Promise<void> => {
|
|
6
|
+
const reads = await readJSON(FileName);
|
|
7
|
+
const commandsplit = reads.run.split(/\s+/);
|
|
8
|
+
|
|
9
|
+
const [command, ...args] = commandsplit;
|
|
10
|
+
|
|
11
|
+
if (!command) {
|
|
12
|
+
consola.error("do not run commands");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const spawncli = spawn(command, args, {
|
|
17
|
+
stdio: "ignore",
|
|
18
|
+
detached: true,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
spawncli.on("error", (error) => {
|
|
22
|
+
consola.error("failed to start processing", error);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
spawncli.unref();
|
|
26
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import cron from "node-cron";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { readJSON } from "../readJSON.ts";
|
|
4
|
+
import { readdir } from "node:fs/promises";
|
|
5
|
+
import { CONFIG_DIR } from "../constant/app.ts";
|
|
6
|
+
import { consola } from "consola";
|
|
7
|
+
|
|
8
|
+
export const startschedule = async (): Promise<void> => {
|
|
9
|
+
const readConfig = await readdir(CONFIG_DIR);
|
|
10
|
+
|
|
11
|
+
for (const filename of readConfig) {
|
|
12
|
+
if (!filename.endsWith(".json")) continue;
|
|
13
|
+
if (!filename.match("foo.json")) continue;
|
|
14
|
+
const [filenamesplit] = filename.split(".");
|
|
15
|
+
const read = await readJSON(filenamesplit!);
|
|
16
|
+
|
|
17
|
+
if (!read.enabled) continue;
|
|
18
|
+
|
|
19
|
+
const [command, ...args] = read.run.trim().split(/\s+/);
|
|
20
|
+
|
|
21
|
+
if (!command) continue;
|
|
22
|
+
|
|
23
|
+
cron.schedule(read.cron, () => {
|
|
24
|
+
const cronspawn = spawn(command!, args!, {
|
|
25
|
+
detached: true,
|
|
26
|
+
stdio: "inherit",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
cronspawn.on("error", (error) => {
|
|
30
|
+
consola.error(`コマンドの起動に失敗: ${command}`, error);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
startschedule().catch((error) => {
|
|
37
|
+
consola.error("failed to up scheduler", error);
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { open, writeFile } from "node:fs/promises";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { PID_FILE } from "../constant/app.ts";
|
|
5
|
+
|
|
6
|
+
export const startschedule = async (): Promise<void> => {
|
|
7
|
+
const daemonPath = fileURLToPath(new URL("./daemon.mjs", import.meta.url));
|
|
8
|
+
|
|
9
|
+
const logFile = await open("/tmp/schrun-daemon.log", "a");
|
|
10
|
+
|
|
11
|
+
const child = spawn(process.execPath, [daemonPath], {
|
|
12
|
+
detached: true,
|
|
13
|
+
stdio: ["ignore", logFile.fd, logFile.fd],
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
if (child.pid === undefined) {
|
|
17
|
+
await logFile.close();
|
|
18
|
+
throw new Error("Failed to start daemon");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
child.on("error", (error) => {
|
|
22
|
+
console.error("Failed to start daemon:", error);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
await writeFile(PID_FILE, String(child.pid));
|
|
26
|
+
child.unref();
|
|
27
|
+
|
|
28
|
+
await logFile.close();
|
|
29
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { readFile, unlink } from "node:fs/promises";
|
|
2
|
+
import { PID_FILE } from "../constant/app.ts";
|
|
3
|
+
import { consola } from "consola";
|
|
4
|
+
|
|
5
|
+
export const stopschedule = async (): Promise<void> => {
|
|
6
|
+
let pid: number;
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
pid = Number(await readFile(PID_FILE, "utf-8"));
|
|
10
|
+
} catch {
|
|
11
|
+
consola.warn("no look schedule pid file");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) {
|
|
16
|
+
consola.error("warning pid files");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
process.kill(pid, "SIGINT");
|
|
22
|
+
consola.success("send stop to scheduler");
|
|
23
|
+
} catch (error) {
|
|
24
|
+
consola.error("failed stop to scheduler", error);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
await unlink(PID_FILE).catch(() => {});
|
|
29
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { consola } from "consola";
|
|
2
|
+
import { readJSON } from "../readJSON.ts";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
export const runsurface = async (FileName: string): Promise<void> => {
|
|
6
|
+
const reads = await readJSON(FileName);
|
|
7
|
+
const commandsplit = reads.run.split(/\s+/);
|
|
8
|
+
|
|
9
|
+
const [command, ...args] = commandsplit;
|
|
10
|
+
|
|
11
|
+
if (!command) {
|
|
12
|
+
consola.error("do not run commands");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const spawncli = spawn(command, args, {
|
|
17
|
+
stdio: "inherit",
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
spawncli.on("error", (error) => {
|
|
21
|
+
consola.error("failed run command", error);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
spawncli.on("close", (code) => {
|
|
25
|
+
consola.success(`done code: ${code}`);
|
|
26
|
+
});
|
|
27
|
+
};
|
package/src/type.ts
ADDED
package/src/version.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
type packagejson = {
|
|
4
|
+
version: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const version = async (): Promise<string> => {
|
|
8
|
+
const packageJsonText = await readFile(
|
|
9
|
+
new URL("../package.json", import.meta.url),
|
|
10
|
+
"utf-8",
|
|
11
|
+
);
|
|
12
|
+
const packageJson: packagejson = JSON.parse(packageJsonText);
|
|
13
|
+
|
|
14
|
+
return packageJson.version;
|
|
15
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
"types": ["bun"],
|
|
11
|
+
|
|
12
|
+
// Bundler mode
|
|
13
|
+
"moduleResolution": "bundler",
|
|
14
|
+
"allowImportingTsExtensions": true,
|
|
15
|
+
"verbatimModuleSyntax": true,
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
|
|
18
|
+
// Best practices
|
|
19
|
+
"strict": true,
|
|
20
|
+
"skipLibCheck": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true,
|
|
22
|
+
"noUncheckedIndexedAccess": true,
|
|
23
|
+
"noImplicitOverride": true,
|
|
24
|
+
|
|
25
|
+
// Some stricter flags (disabled by default)
|
|
26
|
+
"noUnusedLocals": false,
|
|
27
|
+
"noUnusedParameters": false,
|
|
28
|
+
"noPropertyAccessFromIndexSignature": false
|
|
29
|
+
}
|
|
30
|
+
}
|