Skip to content

Commit e124e11

Browse files
authored
Merge pull request #56 from fluentci-io/feat/support-for-services
feat: add commands for managing services (up, down, status, ps)
2 parents 2b92602 + 6c47292 commit e124e11

13 files changed

+439
-7
lines changed

.github/workflows/release.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ on:
66
jobs:
77
build:
88
name: release
9-
runs-on: ubuntu-latest
9+
runs-on: macos-latest
1010
strategy:
1111
matrix:
1212
target:

README.md

+13-5
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ Requirements:
8181

8282
**Latest (Desktop):**
8383

84-
- `Mac`: arm64: [fluentci-studio_v0.1.6_arm64.dmg](https://github.com/fluentci-io/fluentci-studio/releases/download/v0.1.6/fluentci-studio_v0.1.6_arm64.dmg) intel: [fluentci-studio_v0.1.6_x64.dmg](https://github.com/fluentci-io/fluentci-studio/releases/download/v0.1.6/fluentci-studio_v0.1.6_x64.dmg)
85-
- `Linux`: [fluentci-studio_v0.1.6.AppImage](https://github.com/fluentci-io/fluentci-studio/releases/download/v0.1.6/fluentci-studio_v0.1.6.AppImage)
84+
- `Mac`: arm64: [fluentci-studio_v0.1.7_arm64.dmg](https://github.com/fluentci-io/fluentci-studio/releases/download/v0.1.7/fluentci-studio_v0.1.7_arm64.dmg) intel: [fluentci-studio_v0.1.7_x64.dmg](https://github.com/fluentci-io/fluentci-studio/releases/download/v0.1.7/fluentci-studio_v0.1.7_x64.dmg)
85+
- `Linux`: [fluentci-studio_v0.1.7.AppImage](https://github.com/fluentci-io/fluentci-studio/releases/download/v0.1.7/fluentci-studio_v0.1.7.AppImage)
8686

8787
**Latest (CLI):**
8888

@@ -110,7 +110,7 @@ fluentci studio
110110
fluentci --help
111111

112112
Usage: fluentci [pipeline] [jobs...]
113-
Version: 0.15.2
113+
Version: 0.15.3
114114

115115
Description:
116116

@@ -148,8 +148,16 @@ Commands:
148148
whoami - Show current logged in user
149149
repl [pipelines...] - Start FluentCI REPL
150150
studio - Start FluentCI Studio, a web-based user interface
151-
project - Manage projects
152-
server - Start FluentCI GraphQL Server
151+
project - Manage projects
152+
server - Start FluentCI GraphQL Server
153+
up - Start services
154+
down - Stop services
155+
ps - List services
156+
status <service> - Show status of a service
157+
start <service> - Start a service
158+
restart <service> - Restart a service
159+
stop <service> - Stop a service
160+
echo <service> - Stream logs of a service
153161
```
154162
155163
## 📚 Documentation

deps.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import * as semver from "jsr:@std/semver@0.224.0";
22
export { semver };
3+
import procfile from "npm:procfile";
4+
export { procfile };
35
export {
46
bold,
57
brightGreen,

main.ts

+44
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ import repl from "./src/cmd/repl.ts";
1818
import studio from "./src/cmd/studio.ts";
1919
import * as projects from "./src/cmd/project.ts";
2020
import server from "./src/cmd/server.ts";
21+
import down from "./src/cmd/down.ts";
22+
import up from "./src/cmd/up.ts";
23+
import listServices from "./src/cmd/ps.ts";
24+
import status from "./src/cmd/status.ts";
25+
import restart from "./src/cmd/restart.ts";
26+
import stop from "./src/cmd/stop.ts";
27+
import echo from "./src/cmd/echo.ts";
2128

2229
export async function main() {
2330
Deno.env.set(
@@ -205,6 +212,43 @@ export async function main() {
205212
.action(function (options) {
206213
server(options);
207214
})
215+
.command("up", "Start services")
216+
.action(async function () {
217+
await up();
218+
})
219+
.command("down", "Stop services")
220+
.action(async function () {
221+
await down();
222+
})
223+
.command("ps", "List services")
224+
.action(async function () {
225+
await listServices();
226+
})
227+
.command("status", "Show status of a service")
228+
.arguments("<service:string>")
229+
.action(async function (_, service) {
230+
await status(service);
231+
})
232+
.command("start", "Start a service")
233+
.arguments("<service:string>")
234+
.action(async function (_, service) {
235+
await restart(service);
236+
})
237+
.command("restart", "Restart a service")
238+
.arguments("<service:string>")
239+
.action(async function (_, service) {
240+
await restart(service);
241+
})
242+
.command("stop", "Stop a service")
243+
.arguments("<service:string>")
244+
.action(async function (_, service) {
245+
await stop(service);
246+
})
247+
.command("echo", "Stream logs of a service")
248+
.arguments("<service:string>")
249+
.action(async function (_, service) {
250+
await echo(service);
251+
})
208252
.globalOption("--check-update <checkUpdate:boolean>", "check for update", {
209253
default: true,
210254
})

src/cmd/down.ts

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { green, procfile } from "../../deps.ts";
2+
import { getProcfiles } from "../utils.ts";
3+
4+
export default async function down() {
5+
const files = await getProcfiles();
6+
const services = [];
7+
// deno-lint-ignore no-explicit-any
8+
let infos: Record<string, any> = {};
9+
10+
for (const file of files) {
11+
const manifest = procfile.parse(Deno.readTextFileSync(file));
12+
services.push(...Object.keys(manifest));
13+
infos = {
14+
...infos,
15+
...manifest,
16+
};
17+
18+
for (const service of Object.keys(manifest)) {
19+
const socket = file.replace("Procfile", ".overmind.sock");
20+
infos[service].socket = socket;
21+
const command = new Deno.Command("sh", {
22+
args: ["-c", `echo stop | nc -U -w 1 ${socket}`],
23+
stdout: "piped",
24+
});
25+
const process = await command.spawn();
26+
const { success } = await process.output();
27+
if (!success) {
28+
console.log(`Failed to stop ${green(service)}`);
29+
continue;
30+
}
31+
console.log(`Successfully stopped ${green(service)}`);
32+
}
33+
}
34+
}

src/cmd/echo.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { green, procfile } from "../../deps.ts";
2+
import { getProcfiles } from "../utils.ts";
3+
4+
export default async function echo(name: string) {
5+
const files = await getProcfiles();
6+
const services = [];
7+
// deno-lint-ignore no-explicit-any
8+
let infos: Record<string, any> = {};
9+
10+
for (const file of files) {
11+
const manifest = procfile.parse(Deno.readTextFileSync(file));
12+
services.push(...Object.keys(manifest));
13+
infos = {
14+
...infos,
15+
...manifest,
16+
};
17+
18+
for (const service of Object.keys(manifest)) {
19+
const socket = file.replace("Procfile", ".overmind.sock");
20+
infos[service].socket = socket;
21+
}
22+
}
23+
24+
if (!infos[name]) {
25+
console.log("Service not found in Procfile");
26+
Deno.exit(1);
27+
}
28+
29+
const socket = infos[name].socket;
30+
const command = new Deno.Command("sh", {
31+
args: ["-c", `echo echo | nc -U ${socket}`],
32+
stdout: "inherit",
33+
stderr: "inherit",
34+
});
35+
const process = await command.spawn();
36+
const { success } = await process.output();
37+
if (!success) {
38+
console.log(`Failed to stream logs for ${green(name)}`);
39+
Deno.exit(1);
40+
}
41+
}

src/cmd/ps.ts

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { procfile, Table } from "../../deps.ts";
2+
import { getProcfiles, getServicePid } from "../utils.ts";
3+
4+
export default async function listServices() {
5+
const files = await getProcfiles();
6+
const services = [];
7+
// deno-lint-ignore no-explicit-any
8+
let infos: Record<string, any> = {};
9+
10+
for (const file of files) {
11+
const manifest = procfile.parse(Deno.readTextFileSync(file));
12+
services.push(...Object.keys(manifest));
13+
infos = {
14+
...infos,
15+
...manifest,
16+
};
17+
18+
for (const service of Object.keys(manifest)) {
19+
const socket = file.replace("Procfile", ".overmind.sock");
20+
infos[service].socket = socket;
21+
const command = new Deno.Command("sh", {
22+
args: ["-c", `echo status | nc -U -w 1 ${socket}`],
23+
stdout: "piped",
24+
});
25+
const process = await command.spawn();
26+
const { stdout, success } = await process.output();
27+
if (!success) {
28+
infos[service].status = "Stopped";
29+
continue;
30+
}
31+
const decoder = new TextDecoder();
32+
infos[service].status = decoder.decode(stdout).includes("running")
33+
? "Up"
34+
: "Stopped";
35+
}
36+
}
37+
38+
services.sort();
39+
40+
const table = new Table();
41+
table.header(["PROCESS", "PID", "STATUS", "COMMAND"]);
42+
for (const service of services) {
43+
const pid = await getServicePid(service, infos[service].socket);
44+
table.push([
45+
service,
46+
pid,
47+
infos[service].status,
48+
infos[service].command + " " + infos[service].options.join(" "),
49+
]);
50+
}
51+
table.render();
52+
}

src/cmd/restart.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { green, procfile } from "../../deps.ts";
2+
import { getProcfiles } from "../utils.ts";
3+
4+
export default async function restart(name: string) {
5+
const files = await getProcfiles();
6+
const services = [];
7+
// deno-lint-ignore no-explicit-any
8+
let infos: Record<string, any> = {};
9+
10+
for (const file of files) {
11+
const manifest = procfile.parse(Deno.readTextFileSync(file));
12+
services.push(...Object.keys(manifest));
13+
infos = {
14+
...infos,
15+
...manifest,
16+
};
17+
18+
for (const service of Object.keys(manifest)) {
19+
const socket = file.replace("Procfile", ".overmind.sock");
20+
infos[service].socket = socket;
21+
}
22+
}
23+
24+
if (!infos[name]) {
25+
console.log("Service not found in Procfile");
26+
Deno.exit(1);
27+
}
28+
29+
const socket = infos[name].socket;
30+
const command = new Deno.Command("sh", {
31+
args: ["-c", `echo restart | nc -U -w 1 ${socket}`],
32+
stdout: "piped",
33+
});
34+
const process = await command.spawn();
35+
const { success } = await process.output();
36+
if (!success) {
37+
console.log(`Failed to restart ${green(name)}`);
38+
return;
39+
}
40+
console.log(`Successfully restarted ${green(name)}`);
41+
}

src/cmd/status.ts

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { brightGreen, gray, bold, procfile, Table, Cell } from "../../deps.ts";
2+
import { getServicePid } from "../utils.ts";
3+
4+
export default async function status(name: string) {
5+
const command = new Deno.Command("bash", {
6+
args: ["-c", "ls .fluentci/*/Procfile"],
7+
stdout: "piped",
8+
});
9+
const process = await command.spawn();
10+
const { stdout, success } = await process.output();
11+
if (!success) {
12+
console.log("No services running");
13+
Deno.exit(0);
14+
}
15+
const decoder = new TextDecoder();
16+
const files = decoder.decode(stdout).trim().split("\n");
17+
const services = [];
18+
// deno-lint-ignore no-explicit-any
19+
let infos: Record<string, any> = {};
20+
21+
for (const file of files) {
22+
const manifest = procfile.parse(Deno.readTextFileSync(file));
23+
services.push(...Object.keys(manifest));
24+
infos = {
25+
...infos,
26+
...manifest,
27+
};
28+
29+
for (const service of Object.keys(manifest)) {
30+
infos[service].procfile = file;
31+
const socket = file.replace("Procfile", ".overmind.sock");
32+
infos[service].socket = socket;
33+
const command = new Deno.Command("sh", {
34+
args: ["-c", `echo status | nc -U -w 1 ${socket}`],
35+
stdout: "piped",
36+
});
37+
const process = await command.spawn();
38+
const { stdout, success } = await process.output();
39+
if (!success) {
40+
infos[service].status = "Stopped";
41+
continue;
42+
}
43+
const decoder = new TextDecoder();
44+
infos[service].status = decoder.decode(stdout).includes("running")
45+
? "Up"
46+
: "Stopped";
47+
}
48+
}
49+
50+
if (!infos[name]) {
51+
console.log("Service not found in Procfile");
52+
Deno.exit(1);
53+
}
54+
55+
const pid = await getServicePid(name, infos[name].socket);
56+
57+
console.log(
58+
`${infos[name].status === "Up" ? brightGreen("●") : "○"} ${name}`
59+
);
60+
61+
const table = new Table().body([
62+
[
63+
new Cell("Procfile:").align("right"),
64+
`${infos[name].procfile}\n└─ ${gray(
65+
infos[name].command + " " + infos[name].options.join(" ")
66+
)}`,
67+
],
68+
[
69+
new Cell("Active:").align("right"),
70+
infos[name].status === "Up"
71+
? bold(brightGreen("active (running)"))
72+
: "inactive (dead)",
73+
],
74+
[new Cell("Socket:").align("right"), infos[name].socket],
75+
[new Cell("Main PID:").align("right"), pid],
76+
[
77+
new Cell("WorkDir:").align("right"),
78+
infos[name].socket.replace("/.overmind.sock", ""),
79+
],
80+
]);
81+
table.render();
82+
console.log("");
83+
}

0 commit comments

Comments
 (0)