Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add commands for managing services (up, down, status, ps) #56

Merged
merged 4 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
jobs:
build:
name: release
runs-on: ubuntu-latest
runs-on: macos-latest
strategy:
matrix:
target:
Expand Down
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ Requirements:

**Latest (Desktop):**

- `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)
- `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)
- `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)
- `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)

**Latest (CLI):**

Expand Down Expand Up @@ -110,7 +110,7 @@ fluentci studio
fluentci --help

Usage: fluentci [pipeline] [jobs...]
Version: 0.15.2
Version: 0.15.3

Description:

Expand Down Expand Up @@ -148,8 +148,16 @@ Commands:
whoami - Show current logged in user
repl [pipelines...] - Start FluentCI REPL
studio - Start FluentCI Studio, a web-based user interface
project - Manage projects
server - Start FluentCI GraphQL Server
project - Manage projects
server - Start FluentCI GraphQL Server
up - Start services
down - Stop services
ps - List services
status <service> - Show status of a service
start <service> - Start a service
restart <service> - Restart a service
stop <service> - Stop a service
echo <service> - Stream logs of a service
```

## 📚 Documentation
Expand Down
2 changes: 2 additions & 0 deletions deps.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as semver from "jsr:@std/semver@0.224.0";
export { semver };
import procfile from "npm:procfile";
export { procfile };
export {
bold,
brightGreen,
Expand Down
44 changes: 44 additions & 0 deletions main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ import repl from "./src/cmd/repl.ts";
import studio from "./src/cmd/studio.ts";
import * as projects from "./src/cmd/project.ts";
import server from "./src/cmd/server.ts";
import down from "./src/cmd/down.ts";
import up from "./src/cmd/up.ts";
import listServices from "./src/cmd/ps.ts";
import status from "./src/cmd/status.ts";
import restart from "./src/cmd/restart.ts";
import stop from "./src/cmd/stop.ts";
import echo from "./src/cmd/echo.ts";

export async function main() {
Deno.env.set(
Expand Down Expand Up @@ -205,6 +212,43 @@ export async function main() {
.action(function (options) {
server(options);
})
.command("up", "Start services")
.action(async function () {
await up();
})
.command("down", "Stop services")
.action(async function () {
await down();
})
.command("ps", "List services")
.action(async function () {
await listServices();
})
.command("status", "Show status of a service")
.arguments("<service:string>")
.action(async function (_, service) {
await status(service);
})
.command("start", "Start a service")
.arguments("<service:string>")
.action(async function (_, service) {
await restart(service);
})
.command("restart", "Restart a service")
.arguments("<service:string>")
.action(async function (_, service) {
await restart(service);
})
.command("stop", "Stop a service")
.arguments("<service:string>")
.action(async function (_, service) {
await stop(service);
})
.command("echo", "Stream logs of a service")
.arguments("<service:string>")
.action(async function (_, service) {
await echo(service);
})
.globalOption("--check-update <checkUpdate:boolean>", "check for update", {
default: true,
})
Expand Down
34 changes: 34 additions & 0 deletions src/cmd/down.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { green, procfile } from "../../deps.ts";
import { getProcfiles } from "../utils.ts";

export default async function down() {
const files = await getProcfiles();
const services = [];
// deno-lint-ignore no-explicit-any
let infos: Record<string, any> = {};

for (const file of files) {
const manifest = procfile.parse(Deno.readTextFileSync(file));
services.push(...Object.keys(manifest));
infos = {
...infos,
...manifest,
};

for (const service of Object.keys(manifest)) {
const socket = file.replace("Procfile", ".overmind.sock");
infos[service].socket = socket;
const command = new Deno.Command("sh", {
args: ["-c", `echo stop | nc -U -w 1 ${socket}`],
stdout: "piped",
});
const process = await command.spawn();
const { success } = await process.output();
if (!success) {
console.log(`Failed to stop ${green(service)}`);
continue;
}
console.log(`Successfully stopped ${green(service)}`);
}
}
}
41 changes: 41 additions & 0 deletions src/cmd/echo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { green, procfile } from "../../deps.ts";
import { getProcfiles } from "../utils.ts";

export default async function echo(name: string) {
const files = await getProcfiles();
const services = [];
// deno-lint-ignore no-explicit-any
let infos: Record<string, any> = {};

for (const file of files) {
const manifest = procfile.parse(Deno.readTextFileSync(file));
services.push(...Object.keys(manifest));
infos = {
...infos,
...manifest,
};

for (const service of Object.keys(manifest)) {
const socket = file.replace("Procfile", ".overmind.sock");
infos[service].socket = socket;
}
}

if (!infos[name]) {
console.log("Service not found in Procfile");
Deno.exit(1);
}

const socket = infos[name].socket;
const command = new Deno.Command("sh", {
args: ["-c", `echo echo | nc -U ${socket}`],
stdout: "inherit",
stderr: "inherit",
});
const process = await command.spawn();
const { success } = await process.output();
if (!success) {
console.log(`Failed to stream logs for ${green(name)}`);
Deno.exit(1);
}
}
52 changes: 52 additions & 0 deletions src/cmd/ps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { procfile, Table } from "../../deps.ts";
import { getProcfiles, getServicePid } from "../utils.ts";

export default async function listServices() {
const files = await getProcfiles();
const services = [];
// deno-lint-ignore no-explicit-any
let infos: Record<string, any> = {};

for (const file of files) {
const manifest = procfile.parse(Deno.readTextFileSync(file));
services.push(...Object.keys(manifest));
infos = {
...infos,
...manifest,
};

for (const service of Object.keys(manifest)) {
const socket = file.replace("Procfile", ".overmind.sock");
infos[service].socket = socket;
const command = new Deno.Command("sh", {
args: ["-c", `echo status | nc -U -w 1 ${socket}`],
stdout: "piped",
});
const process = await command.spawn();
const { stdout, success } = await process.output();
if (!success) {
infos[service].status = "Stopped";
continue;
}
const decoder = new TextDecoder();
infos[service].status = decoder.decode(stdout).includes("running")
? "Up"
: "Stopped";
}
}

services.sort();

const table = new Table();
table.header(["PROCESS", "PID", "STATUS", "COMMAND"]);
for (const service of services) {
const pid = await getServicePid(service, infos[service].socket);
table.push([
service,
pid,
infos[service].status,
infos[service].command + " " + infos[service].options.join(" "),
]);
}
table.render();
}
41 changes: 41 additions & 0 deletions src/cmd/restart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { green, procfile } from "../../deps.ts";
import { getProcfiles } from "../utils.ts";

export default async function restart(name: string) {
const files = await getProcfiles();
const services = [];
// deno-lint-ignore no-explicit-any
let infos: Record<string, any> = {};

for (const file of files) {
const manifest = procfile.parse(Deno.readTextFileSync(file));
services.push(...Object.keys(manifest));
infos = {
...infos,
...manifest,
};

for (const service of Object.keys(manifest)) {
const socket = file.replace("Procfile", ".overmind.sock");
infos[service].socket = socket;
}
}

if (!infos[name]) {
console.log("Service not found in Procfile");
Deno.exit(1);
}

const socket = infos[name].socket;
const command = new Deno.Command("sh", {
args: ["-c", `echo restart | nc -U -w 1 ${socket}`],
stdout: "piped",
});
const process = await command.spawn();
const { success } = await process.output();
if (!success) {
console.log(`Failed to restart ${green(name)}`);
return;
}
console.log(`Successfully restarted ${green(name)}`);
}
83 changes: 83 additions & 0 deletions src/cmd/status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { brightGreen, gray, bold, procfile, Table, Cell } from "../../deps.ts";
import { getServicePid } from "../utils.ts";

export default async function status(name: string) {
const command = new Deno.Command("bash", {
args: ["-c", "ls .fluentci/*/Procfile"],
stdout: "piped",
});
const process = await command.spawn();
const { stdout, success } = await process.output();
if (!success) {
console.log("No services running");
Deno.exit(0);
}
const decoder = new TextDecoder();
const files = decoder.decode(stdout).trim().split("\n");
const services = [];
// deno-lint-ignore no-explicit-any
let infos: Record<string, any> = {};

for (const file of files) {
const manifest = procfile.parse(Deno.readTextFileSync(file));
services.push(...Object.keys(manifest));
infos = {
...infos,
...manifest,
};

for (const service of Object.keys(manifest)) {
infos[service].procfile = file;
const socket = file.replace("Procfile", ".overmind.sock");
infos[service].socket = socket;
const command = new Deno.Command("sh", {
args: ["-c", `echo status | nc -U -w 1 ${socket}`],
stdout: "piped",
});
const process = await command.spawn();
const { stdout, success } = await process.output();
if (!success) {
infos[service].status = "Stopped";
continue;
}
const decoder = new TextDecoder();
infos[service].status = decoder.decode(stdout).includes("running")
? "Up"
: "Stopped";
}
}

if (!infos[name]) {
console.log("Service not found in Procfile");
Deno.exit(1);
}

const pid = await getServicePid(name, infos[name].socket);

console.log(
`${infos[name].status === "Up" ? brightGreen("●") : "○"} ${name}`
);

const table = new Table().body([
[
new Cell("Procfile:").align("right"),
`${infos[name].procfile}\n└─ ${gray(
infos[name].command + " " + infos[name].options.join(" ")
)}`,
],
[
new Cell("Active:").align("right"),
infos[name].status === "Up"
? bold(brightGreen("active (running)"))
: "inactive (dead)",
],
[new Cell("Socket:").align("right"), infos[name].socket],
[new Cell("Main PID:").align("right"), pid],
[
new Cell("WorkDir:").align("right"),
infos[name].socket.replace("/.overmind.sock", ""),
],
]);
table.render();
console.log("");
}
Loading