PM (process manager)
Installation
PM is available only for linux due to heavy usage of linux mechanisms. Go to the releases page to download the latest binary.
# download binary
+pre[data-lang]::selection, code[class*=lang-]::selection { background: var(--code-theme-selection, var(--selection-color)); }
+table { border-spacing: 0; }
+th { border-bottom: 0.1rem solid var(--sidebar-border-color); }
+body { background-image: linear-gradient(to right, #ccc8b1 1px, rgba(204,200,177,0) 1px), linear-gradient(to bottom, #ccc8b1 1px, rgba(204,200,177,0) 1px); background-size: 0.3rem 0.3rem; }PM (process manager)

Installation
PM is available only for linux due to heavy usage of linux mechanisms. Go to the releases page to download the latest binary.
# download binary
wget https://github.com/rprtr258/pm/releases/latest/download/pm_linux_amd64
# make binary executable
chmod +x pm_linux_amd64
@@ -68,7 +94,72 @@
sudo ln -s ~/go/bin/pm /usr/bin/pm
# install systemd service, copy/paste output of following command
pm startup
-
After these commands, processes with startup: true config option will be started on system startup.
Configuration
jsonnet configuration language is used. It is also fully compatible with plain JSON, so you can write JSON instead.
See example configuration file. Other examples can be found in tests directory.
Usage
Most fresh usage descriptions can be seen using pm <command> --help.
Run process
# run process using command
+
After these commands, processes with startup: true config option will be started on system startup.
Configuration
PM supports multiple configuration formats for defining processes. The original jsonnet format is supported, along with several additional formats for flexibility:
Supported Formats
The primary configuration format. JSONNet is fully compatible with plain JSON.
[
+ {
+ name: "web-server",
+ command: "node",
+ args: ["server.js"],
+ env: {
+ PORT: "3000",
+ NODE_ENV: "production"
+ },
+ tags: ["web"],
+ startup: true
+ }
+]
Configuration Schema
All formats define list of processes with following fields:
Field Type Description Required namestringProcess name (auto-generated if omitted) No commandstringCommand to execute Yes argsarray(string)Command arguments No cwdstringWorking directory No envmap(string, string)Environment variables (name: value pairs) No tagsarray(string)Process tags for filtering No watchstringFile pattern to watch for restarts (regex) No startupbooleanStart process on system startup No depends_onarray(string)Process names that must start first No cronstringCron expression for scheduled execution No stdout_filestringFile to redirect stdout to No stderr_filestringFile to redirect stderr to No kill_timeoutdurationTime before SIGKILL after SIGINT No autorestartbooleanAuto-restart on process death No max_restartsnumberMaximum restart limit (0 = unlimited) No
See example configuration file. Other examples can be found in tests directory.
Usage
Most fresh usage descriptions can be seen using pm <command> --help.
Run process
# run process using command
pm run go run main.go
# run processes from config file
@@ -83,7 +174,7 @@
# e.g. delete all processes
pm delete all
-
Process state diagram
Development
Architecture
pm consists of two parts:
- cli client - requests server, launches/stops shim processes
- shim - monitors and restarts processes, handle watches, signals and shutdowns
PM directory structure
pm uses XDG specification, so db and logs are in ~/.local/share/pm and config is ~/.config/pm.json. XDG_DATA_HOME and XDG_CONFIG_HOME environment variables can be used to change this. Layout is following:
~/.config/pm.json # pm config file
+
Process state diagram
Development
Architecture
pm consists of two parts:
- cli client - requests server, launches/stops shim processes
- shim - monitors and restarts processes, handle watches, signals and shutdowns
PM directory structure
pm uses XDG specification, so db and logs are in ~/.local/share/pm and config is ~/.config/pm.json. XDG_DATA_HOME and XDG_CONFIG_HOME environment variables can be used to change this. Layout is following:
~/.config/pm.json # pm config file
~/.local/share/pm/
├──db/ # database tables
│ └──<ID> # process info
diff --git a/docs/main.ts b/docs/main.ts
index 9d69489d8..edc8f6647 100644
--- a/docs/main.ts
+++ b/docs/main.ts
@@ -1,3 +1,8 @@
+import {styleText} from "util";
+import {join} from "path";
+import {stat} from "fs/promises";
+import {deflate} from "pako";
+
function dedent(s: string): string {
const lines = s.substring(1).trimEnd().split("\n");
@@ -24,7 +29,6 @@ function manifestXmlJsonml(x: HTMLNode): string {
const content = children
.map(manifestXmlJsonml)
.filter(s => s !== "")
- // .join(tag === "span" ? " " : "");
.join("");
return `<${tag}${props}>${content}${tag}>`;
}
@@ -45,28 +49,24 @@ const std = {
joinList: (sep: T[], xs: T[][]): T[] => xs.flatMap((x, i) => [...(i > 0 ? sep : []), ...x]),
};
+type Lang = "sh" | "jsonnet" | "yaml" | "toml" | "ini" | "hcl" | "json";
+
type Adapter = {
render: (doc: (a: Adapter) => R[]) => X,
h1: (title: string) => T,
h2: (title: string) => T,
h3: (title: string) => T,
- h4: (title: string) => T,
- p: (xs: (string | T)[]) => T,
+ tabs: (tabs: [string, ...T[]][]) => T,
+ p: (...xs: (string | T)[]) => T,
b: (s: string) => T,
a: (text: string, href: string) => T,
a_external: (text: string, href: string) => T,
code: (code: string) => T,
- codeblock_sh: (code: string) => T,
- codeblock_jsonnet: (code: string) => T,
- codeblock_yaml: (code: string) => T,
- codeblock_toml: (code: string) => T,
- codeblock_ini: (code: string) => T,
- codeblock_hcl: (code: string) => T,
- codeblock_json: (code: string) => T,
- codeblock: (code: string) => T,
- ul: (xs: (string | T)[][]) => T,
+ codeblock: (code: string, lang: Lang) => T,
+ ul: (...xs: (string | T)[][]) => T,
icon: () => T,
- process_state_diagram: T,
+ table: (headers: string[], ...xs: (string | T)[][]) => T,
+ process_state_diagram: (source: string) => T,
};
type TOCItem = {title: string, level: 0|1|2|3};
@@ -95,92 +95,93 @@ const toc: Adapter & {compose: (xs: (TOCItem | [])[])
}];
} else return acc;
}, []),
- h1: (title: string) => ({title: title, level: 0}),
- h2: (title: string) => ({title: title, level: 1}),
- h3: (title: string) => ({title: title, level: 2}),
- h4: (title: string) => ({title: title, level: 3}),
- ul: (xs) => [],
- p: (xs) => [],
- b: (s) => [],
+ h1: title => ({title: title, level: 0}),
+ h2: title => ({title: title, level: 1}),
+ h3: title => ({title: title, level: 2}),
+ tabs: tabs => [],
+ ul: xs => [],
+ p: xs => [],
+ b: s => [],
a: (text, href) => [],
a_external: (text, href) => [],
- code: (code) => [],
- codeblock_sh: (code) => [],
- codeblock_jsonnet: (code) => [],
- codeblock_yaml: (code) => [],
- codeblock_toml: (code) => [],
- codeblock_ini: (code) => [],
- codeblock_hcl: (code) => [],
- codeblock_json: (code) => [],
- codeblock: (code) => [],
+ code: code => [],
+ codeblock: (code, lang) => [],
icon: () => [],
- process_state_diagram: [],
+ table: (hs, xs) => [],
+ process_state_diagram: source => [],
};
const renderer_markdown = {
compose: (xs: string[]) => xs.join("\n"),
- h1: (title: string) => "# "+title,
- h2: (title: string) => "## "+title,
- h3: (title: string) => "### "+title,
- h4: (title: string) => "#### "+title,
- p: (xs: string[]) => xs.join("")+"\n",
+ h: (title: string, level: 1|2|3|4) => "#".repeat(level)+" "+title,
+ p: (...xs: string[]) => xs.join("")+"\n",
code: (code: string) => "`"+code+"`",
codeblock: (lang: string, code: string) => "```"+lang+"\n"+code+"\n```\n",
a: (text: string, href: string) => `[${text}](${href})`,
bold: (text: string) => `**${text}**`,
italic: (text: string) => `_${text}_`,
- ul: (lines: string[][]) => lines.map(line => renderer_markdown.li(line)).join("\n")+"\n", // TODO: move out li
- li: (x: string[]) => "- "+x.join(""),
+ ul: (...lines: string[][]) => lines.map(line => renderer_markdown.li(...line)).join("\n")+"\n", // TODO: move out li
+ li: (...xs: string[]) => "- "+xs.join(""),
img: (src: string, alt: string) => ``,
+ table: (headers: string[], ...xs: string[][]) => {
+ const row = (xs: string[]) => "| "+xs.join(" | ")+" |";
+ const header = row(headers);
+ const sep = "|"+headers.map(s => (" "+s).split("").map(() => "-").join("")).join("|",)+"|";
+ const rows = xs.map(r => row(r));
+ return [header, sep, ...rows].join("\n") + "\n";
+ },
hr: "---",
};
const markdown_adapter: Adapter = {
render: (doc) => renderer_markdown.compose(doc(markdown_adapter)),
- h1: (title: string) => renderer_markdown.h1(title),
- h2: (title: string) => renderer_markdown.h2(title),
- h3: (title: string) => renderer_markdown.h3(title),
- h4: (title: string) => renderer_markdown.h4(title),
- p: (xs: string[]) => renderer_markdown.p(xs),
+ h1: (title: string) => renderer_markdown.h(title, 1),
+ h2: (title: string) => renderer_markdown.h(title, 2),
+ h3: (title: string) => renderer_markdown.h(title, 3),
+ tabs: tabs => tabs.map(([title, ...content]) => {
+ const header = renderer_markdown.h(title, 4);
+ return header + "\n" + content.join("\n");
+ }).join("\n"),
+ p: (...xs: string[]) => renderer_markdown.p(...xs),
b: (s: string) => renderer_markdown.bold(s),
a: (text: string, href: string) => renderer_markdown.a(text, href), // TODO: local links should work
a_external: (text: string, href: string) => renderer_markdown.a(text, href),
code: (code: string) => renderer_markdown.code(code),
- codeblock_sh: (code: string) => renderer_markdown.codeblock("sh", code),
- codeblock_jsonnet: (code: string) => renderer_markdown.codeblock("jsonnet", code),
- codeblock_yaml: (code: string) => renderer_markdown.codeblock("yaml", code),
- codeblock_toml: (code: string) => renderer_markdown.codeblock("toml", code),
- codeblock_ini: (code: string) => renderer_markdown.codeblock("ini", code),
- codeblock_hcl: (code: string) => renderer_markdown.codeblock("hcl", code),
- codeblock_json: (code: string) => renderer_markdown.codeblock("json", code),
- codeblock: (code: string) => renderer_markdown.codeblock("", code),
- ul: (xs: string[][]) => renderer_markdown.ul(xs),
+ codeblock: (code: string, lang: Lang) => renderer_markdown.codeblock(lang, code),
+ ul: (...xs: string[][]) => renderer_markdown.ul(...xs),
icon: () => '
\n',
- process_state_diagram: renderer_markdown.codeblock("mermaid", dedent(`
- flowchart TB
- 0( )
- S(Stopped)
- C(Created)
- R(Running)
- A{{autorestart/watch enabled?}}
- 0 -->|new process| S
- subgraph Running
- direction TB
- C -->|process started| R
- R -->|process died| A
- end
- A -->|yes| C
- A -->|no| S
- Running -->|stop| S
- S -->|start| C
- `)),
+ table: (headers: string[], ...xs: string[][]) => renderer_markdown.table(headers, ...xs),
+ process_state_diagram: source => renderer_markdown.codeblock("mermaid", source),
};
-import process_state_diagram from "./process-state-diagram.ts"; // TODO: render from mermaid
+import {icon as github_corner, style as github_corner_style} from "./github-corner.ts";
import css from "./styles.ts";
+const diagram = `flowchart TB
+ 0( )
+ S(Stopped)
+ C(Created)
+ R(Running)
+ A{{autorestart/watch enabled?}}
+ 0 -->|new process| S
+ subgraph Running
+ direction TB
+ C -->|process started| R
+ R -->|process died| A
+ end
+ A -->|yes| C
+ A -->|no| S
+ Running -->|stop| S
+ S -->|start| C`;
+const diagurl = "https://kroki.io/mermaid/svg/" + deflate(diagram, {level: 9})
+ .toBase64()
+ .replace(/\+/g, '-')
+ .replace(/\//g, '_');
+const diagresponse = await fetch(diagurl);
+const diagsvg = await diagresponse.text();
+
type HTMLNode = string | HTMLElem;
-type HTMLElem = [string, Record, ...HTMLNode[]];
+type HTMLElem = readonly [string, Record, ...HTMLNode[]];
const html_adapter: Adapter = ((): Adapter => {
const join = (
sep: string,
@@ -193,17 +194,6 @@ const html_adapter: Adapter = ((): Adapter =
const span = (s: string): HTMLNode => ["span", {}, s];
const li = (xs: HTMLNode[]): HTMLNode => ["li", {}, ...xs];
- const img = (src: string, width: number, height: number): HTMLNode => ["img", {src: src, width: width, height: height, style: renderCSSProps({border: "0"})}];
- const codeblock_generic = (lang: string, code: string): HTMLNode => {
- const punctuation = (s: string): HTMLElem => ["span", {style: renderCSSProps({color: "var(--code-theme-tag)"})}, s];
- const escape = (s: string) => s.split("").map(c => {
- if (c == "<") return "<";
- else if (c == ">") return ">";
- else if (c == "[" || c == "]" || c == "{" || c == "}" || c == "=") return punctuation(c);
- else return c;});
- return ["pre", {"data-lang": lang, style: renderCSSProps({background: "var(--code-theme-background)"})},
- ["code", {class: "language-" + lang}, ...escape(code)]];
- };
const self: Adapter = {
render: (doc): string => {
const TOC: TOCItem2[] = toc.render(doc)[0].children; // NOTE: skip h1
@@ -222,11 +212,12 @@ const html_adapter: Adapter = ((): Adapter =
["style", {}, renderCSS(css)],
],
["body", {class: "sticky", style: renderCSSProps({margin: "0"})},
+ ["a", {href: link_github, class: "github-corner", "aria-label": "View source on GitHub"}, github_corner], github_corner_style,
["main", {role: "presentation"},
["aside", {class: "sidebar", role: "none"},
["div", {class: "sidebar-nav", role: "navigation", "aria-label": "primary"}, (() => {
const a = (id: string, title: string): HTMLElem => ["a", {class: "section-link", href: "#"+id, title: title}, title];
- const toc_render = (xs: TOCItem2[]): HTMLNode => self.ul(xs.reduce(
+ const toc_render = (xs: TOCItem2[]): HTMLNode => self.ul(...xs.reduce(
(acc: HTMLNode[][], x: TOCItem2): HTMLNode[][] => [...acc, [a(x.title, x.title)], [toc_render(x.children)]],
[],
));
@@ -240,208 +231,363 @@ const html_adapter: Adapter = ((): Adapter =
],
]);
},
- process_state_diagram: process_state_diagram as HTMLElem,
- code: (s) => {
+ process_state_diagram: source => diagsvg, // TODO: solve sync/async
+ code: s => {
const escape = (s: string) => s.split("").map((c) => {
if (c == "<") return "<";
else if (c == ">") return ">";
else return c;}).join("");
return ["code", {}, escape(s)];
},
- b: (s) => ["b", {}, s],
- p: (xs: HTMLNode[]) => ["p", {}, ...xs],
- ul: (xs: HTMLNode[][]) => ["ul", {}, ...xs.map(x => li(x))],
- a: (text, href) => ["a", {href: "https://github.com/rprtr258/pm/blob/master/" + href}, text],
+ b: s => ["b", {}, s],
+ p: (...xs: HTMLNode[]) => ["p", {}, ...xs],
+ ul: (...xs: HTMLNode[][]) => ["ul", {}, ...xs.map(x => li(x))],
+ a: (text, href) => ["a", {href: link_github + "/blob/master/" + href}, text],
a_external: (text, href) => ["a", {href: href, target: "_top"}, text],
- h1: (title) => ["h1", {id: title}, ["a", {href: "#"+title, class: "anchor"}, span(title)]],
- h2: (title) => ["h2", {id: title}, ["a", {href: "#"+title, class: "anchor"}, span(title)]],
- h3: (title) => ["h3", {id: title}, ["a", {href: "#"+title, class: "anchor"}, span(title)]],
- h4: (title) => ["h4", {id: title}, ["a", {href: "#"+title, class: "anchor"}, span(title)]],
+ h1: title => ["h1", {id: title}, ["a", {href: "#"+title, class: "anchor"}, span(title)]],
+ h2: title => ["h2", {id: title}, ["a", {href: "#"+title, class: "anchor"}, span(title)]],
+ h3: title => ["h3", {id: title}, ["a", {href: "#"+title, class: "anchor"}, span(title)]],
+ tabs: tabs => {
+ return ["p", {},
+ ["style", {}, `
+ /* Style the tab */
+ .tab {
+ overflow: hidden;
+ background-color: var(--code-theme-background);
+ }
+
+ /* Style the buttons that are used to open the tab content */
+ .tab button {
+ background-color: inherit;
+ float: left;
+ border: 1px solid transparent;
+ outline: none;
+ cursor: pointer;
+ padding: 8px 14px;
+ }
+
+ /* Change background color of buttons on hover */
+ .tab button:hover {
+ background-color: #ccc8b1;
+ border: 1px solid var(--code-theme-background);
+ }
+
+ /* Create an active/current tablink class */
+ .tab button.active {
+ background-color: #ccc8b1;
+ border: 1px solid #454138;
+ }
+
+ /* Style the tab content */
+ .tabcontent {
+ display: none;
+ padding: 1px 12px;
+ background-color: #45413810;
+ }`,
+ ],
+ ["div", {class: "tab"}, ...tabs.map(([title, ..._]): HTMLElem =>
+ ["button", {class: "tablinks", onclick: `openCity(event, '${title}')`}, title],
+ )],
+ // Tab content
+ ...tabs.map(([title, ...content]): HTMLElem =>
+ ["div", {id: title, class: "tabcontent"},
+ ["p", {}, ...content],
+ ],
+ ),
+ ["script", {}, `
+ function openCity(evt, cityName) {
+ // Get all elements with class="tabcontent" and hide them
+ const tabcontent = document.getElementsByClassName("tabcontent");
+ for (let i = 0; i < tabcontent.length; i++) {
+ tabcontent[i].style.display = "none";
+ }
+
+ // Get all elements with class="tablinks" and remove the class "active"
+ const tablinks = document.getElementsByClassName("tablinks");
+ for (let i = 0; i < tablinks.length; i++) {
+ tablinks[i].className = tablinks[i].className.replace(" active", "");
+ }
+
+ // Show the current tab, and add an "active" class to the button that opened the tab
+ document.getElementById(cityName).style.display = "block";
+ evt.currentTarget.className += " active";
+ }
+ document.querySelector(".tab > button:nth-child(1)").click();
+ `],
+ ];
+ },
icon: () => ["p", {align: "center"}, ["img", {src: "icon.svg", width: 250, height: 250, style: renderCSSProps({border: "0"})}]],
- codeblock_sh: (code) => {
- const functionn = (s: string): HTMLElem => ["span", {style: renderCSSProps({color: "var(--code-theme-function)"})}, s];
- const variable = (s: string): HTMLElem => ["span", {style: renderCSSProps({color: "var(--code-theme-variable)"})}, s];
- const comment = (s: string): HTMLElem => ["span", {style: renderCSSProps({color: "var(--code-theme-comment)"})}, s];
- const operator = (s: string): HTMLElem => ["span", {style: renderCSSProps({color: "var(--code-theme-operator)"})}, s];
- const env = (s: string): HTMLElem => ["span", {style: renderCSSProps({color: "var(--code-theme-tag)"})}, s];
- const number = (s: string): HTMLElem => ["span", {style: renderCSSProps({color: "var(--code-theme-tag)"})}, s];
- const punctuation = (s: string): HTMLElem => ["span", {style: renderCSSProps({color: "var(--code-theme-punctuation)"})}, s];
- const render_word = (word: string): HTMLNode[] => {
- const functionns = ["wget", "chmod", "chown", "mv", "cp", "ln", "sudo", "git"];
- const lt = std.findSubstr("<", word);
- const gt = std.findSubstr(">", word);
- const lsb = std.findSubstr("[", word);
- const rsb = std.findSubstr("]", word);
- const ellipsis = std.findSubstr("...", word);
- if (word == "") return [];
- else if (lt.length > 0) return [...render_word(std.substr(word, 0, lt[0])), operator("<"), ...render_word(std.substr(word, lt[0]+1, word.length))];
- else if (gt.length > 0) return [...render_word(std.substr(word, 0, gt[0])), operator(">"), ...render_word(std.substr(word, gt[0]+1, word.length))];
- else if (lsb.length > 0) return [
- ...render_word(word.substring(0, lsb[0])),
- punctuation("["),
- ...render_word(word.substring(lsb[0]+1, word.length)),
- ];
- else if (rsb.length > 0) return [...render_word(std.substr(word, 0, rsb[0])), punctuation("]"), ...render_word(std.substr(word, rsb[0]+1, word.length))];
- else if (ellipsis.length > 0) return [...render_word(std.substr(word, 0, ellipsis[0])), punctuation("..."), ...render_word(std.substr(word, ellipsis[0]+3, word.length))];
- else if (functionns.some(x => word == x)) return [functionn(word)];
- else if (word == "[") return [punctuation(word)];
- else if (word == "]") return [punctuation(word)];
- else if (word == "644") return [number(word)];
- else if (word == "enable") return [["span", {class: "token class-name", style: renderCSSProps({color: "var(--code-theme-selector)"})}, "enable"]];
- else if (word[0] == "-") return [variable(word)];
- else if (word == "$HOME/.pm/") return [env("$HOME"), "/.pm/"];
- else return [word];
- };
- const render = (line: string): HTMLNode[] => { // TODO: use sh parser actually
- if (line === "")
- return [];
- else if (line.indexOf("#") !== -1) {
- const hash = line.indexOf("#");
- const before = line.substring(0, hash);
- const after = line.substring(hash, line.length);
- return [...render(before), comment(after)];
- } else return std.joinList([" "], line.split(" ").map(render_word));
- };
- const lines = code.split("\n").map(render);
- return ["pre", {"data-lang": "sh", style: renderCSSProps({background: "var(--code-theme-background)"})},
- ["code", {class: "language-sh"}, ...std.joinList(["\n"], lines), "\n"]];
+ codeblock: (code, lang) => {
+ if (lang === "sh") {
+ const functionn = (s: string): HTMLElem => ["span", {style: renderCSSProps({color: "var(--code-theme-function)"})}, s];
+ const variable = (s: string): HTMLElem => ["span", {style: renderCSSProps({color: "var(--code-theme-variable)"})}, s];
+ const comment = (s: string): HTMLElem => ["span", {style: renderCSSProps({color: "var(--code-theme-comment)"})}, s];
+ const operator = (s: string): HTMLElem => ["span", {style: renderCSSProps({color: "var(--code-theme-operator)"})}, s];
+ const env = (s: string): HTMLElem => ["span", {style: renderCSSProps({color: "var(--code-theme-tag)"})}, s];
+ const number = (s: string): HTMLElem => ["span", {style: renderCSSProps({color: "var(--code-theme-tag)"})}, s];
+ const punctuation = (s: string): HTMLElem => ["span", {style: renderCSSProps({color: "var(--code-theme-punctuation)"})}, s];
+ const render_word = (word: string): HTMLNode[] => {
+ const functionns = ["wget", "chmod", "chown", "mv", "cp", "ln", "sudo", "git"];
+ const lt = std.findSubstr("<", word);
+ const gt = std.findSubstr(">", word);
+ const lsb = std.findSubstr("[", word);
+ const rsb = std.findSubstr("]", word);
+ const ellipsis = std.findSubstr("...", word);
+ if (word == "") return [];
+ else if (lt.length > 0) return [...render_word(std.substr(word, 0, lt[0])), operator("<"), ...render_word(std.substr(word, lt[0]+1, word.length))];
+ else if (gt.length > 0) return [...render_word(std.substr(word, 0, gt[0])), operator(">"), ...render_word(std.substr(word, gt[0]+1, word.length))];
+ else if (lsb.length > 0) return [
+ ...render_word(word.substring(0, lsb[0])),
+ punctuation("["),
+ ...render_word(word.substring(lsb[0]+1, word.length)),
+ ];
+ else if (rsb.length > 0) return [...render_word(std.substr(word, 0, rsb[0])), punctuation("]"), ...render_word(std.substr(word, rsb[0]+1, word.length))];
+ else if (ellipsis.length > 0) return [...render_word(std.substr(word, 0, ellipsis[0])), punctuation("..."), ...render_word(std.substr(word, ellipsis[0]+3, word.length))];
+ else if (functionns.some(x => word == x)) return [functionn(word)];
+ else if (word == "[") return [punctuation(word)];
+ else if (word == "]") return [punctuation(word)];
+ else if (word == "644") return [number(word)];
+ else if (word == "enable") return [["span", {class: "token class-name", style: renderCSSProps({color: "var(--code-theme-selector)"})}, "enable"]];
+ else if (word[0] == "-") return [variable(word)];
+ else if (word == "$HOME/.pm/") return [env("$HOME"), "/.pm/"];
+ else return [word];
+ };
+ const render = (line: string): HTMLNode[] => { // TODO: use sh parser actually
+ if (line === "")
+ return [];
+ else if (line.indexOf("#") !== -1) {
+ const hash = line.indexOf("#");
+ const before = line.substring(0, hash);
+ const after = line.substring(hash, line.length);
+ return [...render(before), comment(after)];
+ } else return std.joinList([" "], line.split(" ").map(render_word));
+ };
+ const lines = code.split("\n").map(render);
+ return ["pre", {"data-lang": lang, style: renderCSSProps({background: "var(--code-theme-background)"})},
+ ["code", {class: "language-" + lang}, ...std.joinList(["\n"], lines), "\n"]];
+ }
+
+ const punctuation = (s: string): HTMLElem => ["span", {style: renderCSSProps({color: "var(--code-theme-tag)"})}, s];
+ const escape = (s: string) => s.split("").map(c => {
+ if (c == "<") return "<";
+ else if (c == ">") return ">";
+ else if (c == "[" || c == "]" || c == "{" || c == "}" || c == "=") return punctuation(c);
+ else return c;});
+ return ["pre", {"data-lang": lang, style: renderCSSProps({background: "var(--code-theme-background)"})},
+ ["code", {class: "language-" + lang}, ...escape(code)]];
},
- codeblock_jsonnet: (code) => codeblock_generic("jsonnet", code),
- codeblock_yaml: (code) => codeblock_generic("yaml", code),
- codeblock_toml: (code) => codeblock_generic("toml", code),
- codeblock_ini: (code) => codeblock_generic("ini", code),
- codeblock_hcl: (code) => codeblock_generic("hcl", code),
- codeblock_json: (code) => codeblock_generic("json", code),
- codeblock: (code) => codeblock_generic("", code),
+ table: (headers, ...xs) => ["table", {},
+ ["thead", {},
+ ["tr", {}, ...headers.map((x): HTMLElem => ["th", {}, x])],
+ ],
+ ["tbody", {}, ...xs.map((r): HTMLElem =>
+ ["tr", {}, ...r.map((x): HTMLElem => ["td", {}, x])]
+ )],
+ ],
};
return self;
})();
-const link_release = "https://github.com/rprtr258/pm/releases/latest";
-
-const docs = (R: Adapter): T[] => [
- R.h1("PM (process manager)"),
-
- // ["div", {}, R.a("https://github.com/rprtr258/pm", R.img("https://img.shields.io/badge/source-code?logo=github&label=github"))],
- R.icon(),
- R.h2("Installation"),
- R.p(["PM is available only for linux due to heavy usage of linux mechanisms. Go to the ", R.a_external("releases", link_release), " page to download the latest binary."]),
- R.codeblock_sh(dedent(`
- # download binary
- wget ${link_release}/download/pm_linux_amd64
- # make binary executable
- chmod +x pm_linux_amd64
- # move binary to $PATH, here just local
- mv pm_linux_amd64 pm
- `)),
- R.h3("Systemd service"),
- R.p(["To enable running processes on system startup:"]),
- R.codeblock_sh(dedent(`
- # soft link /usr/bin/pm binary to whenever it is installed
- sudo ln -s ~/go/bin/pm /usr/bin/pm
- # install systemd service, copy/paste output of following command
- pm startup
- `)),
- R.p(["After these commands, processes with ", R.code("startup: true"), " config option will be started on system startup."]),
-
- R.h2("Configuration"),
- R.p([R.a_external("jsonnet", "https://jsonnet.org/"), " configuration language is used. It is also fully compatible with plain JSON, so you can write JSON instead."]),
-
- R.p(["See ", R.a("example configuration file", "./config.jsonnet"), ". Other examples can be found in ", R.a("tests", "./e2e/tests"), " directory."]),
-
- R.h2("Usage"),
- R.p(["Most fresh usage descriptions can be seen using ", R.code("pm --help"), "."]),
- R.h3("Run process"),
- R.codeblock_sh(dedent(`
- # run process using command
- pm run go run main.go
-
- # run processes from config file
- pm run --config config.jsonnet
- `)),
- R.h3("List processes"),
- R.codeblock_sh(dedent(`
- pm list
- `)),
-
- R.h3("Start already added processes"),
- R.codeblock_sh(dedent(`
- pm start [ID/NAME/TAG]...
- `)),
-
- R.h3("Stop processes"),
- R.codeblock_sh(dedent(`
- pm stop [ID/NAME/TAG]...
-
- # e.g. stop all added processes (all processes has tag `+"`all`"+` by default)
- pm stop all
- `)),
- R.h3("Delete processes"),
- R.p(["When deleting process, they are first stopped, then removed from ", R.code("pm"), "."]),
- R.codeblock_sh(dedent(`
- pm delete [ID/NAME/TAG]...
-
- # e.g. delete all processes
- pm delete all
- `)),
-
- R.h2("Process state diagram"),
- R.process_state_diagram,
-
- R.h2("Development"),
- R.h3("Architecture"),
- R.p([R.code("pm"), " consists of two parts:"]),
- R.ul([
- [R.b("cli client"), " - requests server, launches/stops shim processes"],
- [R.b("shim"), " - monitors and restarts processes, handle watches, signals and shutdowns"],
- ]),
-
- R.h3("PM directory structure"),
- R.p([
- R.code("pm"),
- " uses ",
- R.a("XDG", "https://specifications.freedesktop.org/basedir-spec/latest/"),
- " specification, so db and logs are in ",
- R.code("~/.local/share/pm"),
- " and config is ",
- R.code("~/.config/pm.json"),
- ". ",
- R.code("XDG_DATA_HOME"), " and ", R.code("XDG_CONFIG_HOME"),
- " environment variables can be used to change this. Layout is following:"]),
- R.codeblock_sh(dedent(`
- ~/.config/pm.json # pm config file
- ~/.local/share/pm/
- ├──db/ # database tables
- │ └── # process info
- └──logs/ # processes logs
- ├──.stdout # stdout of process with id ID
- └──.stderr # stderr of process with id ID
- `)),
-
- R.h3("Differences from pm2"),
- R.ul([
- [R.code("pm"), " is just a single binary, not dependent on ", R.code("nodejs"), " and bunch of ", R.code("js"), " scripts"],
- [R.a_external("jsonnet", "https://jsonnet.org/"), " configuration language, back compatible with ", R.code("JSON"), " and allows to thoroughly configure processes, e.g. separate environments without requiring corresponding mechanism in ", R.code("pm"), " (others configuration languages might be added in future such as ", R.code("Procfile"), ", ", R.code("HCL"), ", etc.)"],
- ["supports only ", R.code("linux"), " now"],
- ["I can fix problems/add features as I need, independent of whether they work or not in ", R.code("pm2"), " because I don't know ", R.code("js")],
- ["fast and convenient (I hope so)"],
- ["no specific integrations for ", R.code("js")],
- ]),
-
- R.h3("Release"),
- R.p(["On ", R.code("master"), " branch:"]),
- R.codeblock_sh(dedent(`
- git tag v1.2.3
- git push --tags
- GITHUB_TOKEN= goreleaser release --clean
- `)),
+const link_github = "https://github.com/rprtr258/pm";
+const link_release = link_github + "/releases/latest";
+
+const docs = ({
+ h1, h2, h3, tabs,
+ p, b, code, a, a_external,
+ codeblock, ul, table,
+ icon, process_state_diagram,
+}: Adapter): T[] => [
+ h1("PM (process manager)"),
+ icon(),
+ h2("Installation"),
+ p("PM is available only for linux due to heavy usage of linux mechanisms. Go to the ", a_external("releases", link_release), " page to download the latest binary."),
+ codeblock(dedent(`
+ # download binary
+ wget ${link_release}/download/pm_linux_amd64
+ # make binary executable
+ chmod +x pm_linux_amd64
+ # move binary to $PATH, here just local
+ mv pm_linux_amd64 pm
+ `), "sh"),
+ h3("Systemd service"),
+ p("To enable running processes on system startup:"),
+ codeblock(dedent(`
+ # soft link /usr/bin/pm binary to whenever it is installed
+ sudo ln -s ~/go/bin/pm /usr/bin/pm
+ # install systemd service, copy/paste output of following command
+ pm startup
+ `), "sh"),
+ p("After these commands, processes with ", code("startup: true"), " config option will be started on system startup."),
+ h2("Configuration"),
+ p("PM supports multiple configuration formats for defining processes. The original ", a_external("jsonnet", "https://jsonnet.org/"), " format is supported, along with several additional formats for flexibility:"),
+ h3("Supported Formats"), tabs([
+ ["JSONNet (.jsonnet)",
+ p("The primary configuration format. JSONNet is fully compatible with plain JSON."),
+ codeblock(dedent(`
+ [
+ {
+ name: "web-server",
+ command: "node",
+ args: ["server.js"],
+ env: {
+ PORT: "3000",
+ NODE_ENV: "production"
+ },
+ tags: ["web"],
+ startup: true
+ }
+ ]
+ `), "jsonnet")],
+ ]),
+ h3("Configuration Schema"),
+ p("All formats define list of processes with following fields:"),
+ table(
+ ["Field", "Type", "Description", "Required"],
+ [code("name"), code("string"), "Process name (auto-generated if omitted)", "No"],
+ [code("command"), code("string"), "Command to execute", "Yes"],
+ [code("args"), code("array(string)"), "Command arguments", "No"],
+ [code("cwd"), code("string"), "Working directory", "No"],
+ [code("env"), code("map(string, string)"), "Environment variables (name: value pairs)", "No"],
+ [code("tags"), code("array(string)"), "Process tags for filtering", "No"],
+ [code("watch"), code("string"), "File pattern to watch for restarts (regex)", "No"],
+ [code("startup"), code("boolean"), "Start process on system startup", "No"],
+ [code("depends_on"), code("array(string)"), "Process names that must start first", "No"],
+ [code("cron"), code("string"), "Cron expression for scheduled execution", "No"],
+ [code("stdout_file"), code("string"), "File to redirect stdout to", "No"],
+ [code("stderr_file"), code("string"), "File to redirect stderr to", "No"],
+ [code("kill_timeout"), code("duration"), "Time before SIGKILL after SIGINT", "No"],
+ [code("autorestart"), code("boolean"), "Auto-restart on process death", "No"],
+ [code("max_restarts"), code("number"), "Maximum restart limit (0 = unlimited)", "No"],
+ ),
+ p("See ", a("example configuration file", "./config.jsonnet"), ". Other examples can be found in ", a("tests", "./e2e/tests"), " directory."),
+ h2("Usage"),
+ p("Most fresh usage descriptions can be seen using ", code("pm --help"), "."),
+ h3("Run process"),
+ codeblock(dedent(`
+ # run process using command
+ pm run go run main.go
+
+ # run processes from config file
+ pm run --config config.jsonnet
+ `), "sh"),
+ h3("List processes"),
+ codeblock(dedent(`
+ pm list
+ `), "sh"),
+ h3("Start already added processes"),
+ codeblock(dedent(`
+ pm start [ID/NAME/TAG]...
+ `), "sh"),
+ h3("Stop processes"),
+ codeblock(dedent(`
+ pm stop [ID/NAME/TAG]...
+
+ # e.g. stop all added processes (all processes has tag `+"`all`"+` by default)
+ pm stop all
+ `), "sh"),
+ h3("Delete processes"),
+ p("When deleting process, they are first stopped, then removed from ", code("pm"), "."),
+ codeblock(dedent(`
+ pm delete [ID/NAME/TAG]...
+
+ # e.g. delete all processes
+ pm delete all
+ `), "sh"),
+ h2("Process state diagram"),
+ process_state_diagram(diagram),
+ h2("Development"),
+ h3("Architecture"),
+ p(code("pm"), " consists of two parts:"),
+ ul(
+ [b("cli client"), " - requests server, launches/stops shim processes"],
+ [b("shim"), " - monitors and restarts processes, handle watches, signals and shutdowns"],
+ ),
+ h3("PM directory structure"),
+ p(
+ code("pm"),
+ " uses ",
+ a_external("XDG", "https://specifications.freedesktop.org/basedir-spec/latest/"),
+ " specification, so db and logs are in ",
+ code("~/.local/share/pm"),
+ " and config is ",
+ code("~/.config/pm.json"),
+ ". ",
+ code("XDG_DATA_HOME"), " and ", code("XDG_CONFIG_HOME"),
+ " environment variables can be used to change this. Layout is following:"),
+ codeblock(dedent(`
+ ~/.config/pm.json # pm config file
+ ~/.local/share/pm/
+ ├──db/ # database tables
+ │ └── # process info
+ └──logs/ # processes logs
+ ├──.stdout # stdout of process with id ID
+ └──.stderr # stderr of process with id ID
+ `), "sh"),
+ h3("Differences from pm2"),
+ ul(
+ [code("pm"), " is just a single binary, not dependent on ", code("nodejs"), " and bunch of ", code("js"), " scripts"],
+ [a_external("jsonnet", "https://jsonnet.org/"), " configuration language, back compatible with ", code("JSON"), " and allows to thoroughly configure processes, e.g. separate environments without requiring corresponding mechanism in ", code("pm"), " (others configuration languages might be added in future such as ", code("Procfile"), ", ", code("HCL"), ", etc.)"],
+ ["supports only ", code("linux"), " now"],
+ ["I can fix problems/add features as I need, independent of whether they work or not in ", code("pm2"), " because I don't know ", code("js")],
+ ["fast and convenient (I hope so)"],
+ ["no specific integrations for ", code("js")],
+ ),
+ h3("Release"),
+ p("On ", code("master"), " branch:"),
+ codeblock(dedent(`
+ git tag v1.2.3
+ git push --tags
+ GITHUB_TOKEN= goreleaser release --clean
+ `), "sh"),
];
+type Link = {
+ link: string,
+ isExternal: boolean,
+};
+
+const links_collect: Adapter = {
+ render: (doc) => doc(links_collect).flatMap(ss => ss),
+ h1: title => [],
+ h2: title => [],
+ h3: title => [],
+ tabs: title => [],
+ ul: (...xs) => xs.flatMap(ss => ss.flatMap(s => typeof s === "string" ? [] : s)),
+ p: (...xs) => xs.flatMap(s => typeof s === "string" ? [] : s),
+ b: s => [],
+ a: (text, link) => [{link, isExternal: false}],
+ a_external: (text, link) => [{link, isExternal: true}],
+ code: code => [],
+ codeblock: (code, lang) => [],
+ icon: () => [],
+ table: (...xs) => xs.flatMap(ss => ss.flatMap(s => typeof s === "string" ? [] : s)),
+ process_state_diagram: source => [],
+};
+
async function writeFile(filename: string, content: string): Promise {
- const dir = import.meta.dir;
- console.log(filename);
- await Bun.write(dir + "/" + filename, content);
+ const dir = import.meta.dir;
+ console.log("Writing", filename + "...");
+ await Bun.write(dir + "/" + filename, content);
}
+const workspaceDir = join(import.meta.dir, "..");
+console.log("Checking links...");
+await Promise.all([...new Set(links_collect.render(docs))].map(async ({link, isExternal}) => {
+ if (isExternal) {
+ const res = await fetch(link);
+ if (res.status < 200 || res.status >= 300)
+ throw new Error(`Broken link: ${link}`);
+ console.log(styleText("greenBright", "OK") + ":", styleText(["underline", "blueBright"], link), "=>", styleText("greenBright", `${res.status}`));
+ } else {
+ const info = await stat(join(workspaceDir, link));
+ const type = info.isDirectory() ? "DIR" :
+ info.isFile() ? "FILE" :
+ (() => {throw new Error(`Broken local name: ${link}`)})();
+ console.log(styleText("greenBright", "OK") + ":", link, "=>", styleText("blue", type));
+ }
+}))
+console.log("All links OK!");
+console.log();
+
await writeFile("index.html", html_adapter.render(docs));
await writeFile("../readme.md", markdown_adapter.render(docs));
diff --git a/docs/package.json b/docs/package.json
index 2230bd2cb..e46122da1 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -1,5 +1,7 @@
{
"dependencies": {
- "@types/bun": "^1.3.6"
+ "@types/bun": "^1.3.6",
+ "@types/pako": "^2.0.4",
+ "pako": "^2.1.0"
}
}
\ No newline at end of file
diff --git a/docs/process-state-diagram.ts b/docs/process-state-diagram.ts
deleted file mode 100644
index e4b4daf39..000000000
--- a/docs/process-state-diagram.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-export default ["div", {class: "mermaid", "data-processed": "true"},
- ["svg", {"aria-roledescription": "flowchart-v2", role: "graphics-document document", viewBox: "-8 -8 370.84375 520.125", style: "max-width: 370.84375px;", "xmlns:xlink": "http://www.w3.org/1999/xlink", xmlns: "http://www.w3.org/2000/svg", width: "100%", id: "mermaid-1722776679139"},
- ["style", {}, '#mermaid-1722776679139{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-1722776679139 .error-icon{fill:#552222;}#mermaid-1722776679139 .error-text{fill:#552222;stroke:#552222;}#mermaid-1722776679139 .edge-thickness-normal{stroke-width:2px;}#mermaid-1722776679139 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-1722776679139 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-1722776679139 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-1722776679139 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-1722776679139 .marker{fill:#333333;stroke:#333333;}#mermaid-1722776679139 .marker.cross{stroke:#333333;}#mermaid-1722776679139 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-1722776679139 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-1722776679139 .cluster-label text{fill:#333;}#mermaid-1722776679139 .cluster-label span,#mermaid-1722776679139 p{color:#333;}#mermaid-1722776679139 .label text,#mermaid-1722776679139 span,#mermaid-1722776679139 p{fill:#333;color:#333;}#mermaid-1722776679139 .node rect,#mermaid-1722776679139 .node circle,#mermaid-1722776679139 .node ellipse,#mermaid-1722776679139 .node polygon,#mermaid-1722776679139 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-1722776679139 .flowchart-label text{text-anchor:middle;}#mermaid-1722776679139 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-1722776679139 .node .label{text-align:center;}#mermaid-1722776679139 .node.clickable{cursor:pointer;}#mermaid-1722776679139 .arrowheadPath{fill:#333333;}#mermaid-1722776679139 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-1722776679139 .flowchart-link{stroke:#333333;fill:none;}#mermaid-1722776679139 .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-1722776679139 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-1722776679139 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-1722776679139 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-1722776679139 .cluster text{fill:#333;}#mermaid-1722776679139 .cluster span,#mermaid-1722776679139 p{color:#333;}#mermaid-1722776679139 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-1722776679139 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-1722776679139 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}'],
- ["g", {},
- ["marker", {orient: "auto", markerHeight: "12", markerWidth: "12", markerUnits: "userSpaceOnUse", refY: "5", refX: "6", viewBox: "0 0 10 10", class: "marker flowchart", id: "mermaid-1722776679139_flowchart-pointEnd"}, ["path", {style: "stroke-width: 1; stroke-dasharray: 1, 0;", class: "arrowMarkerPath", d: "M 0 0 L 10 5 L 0 10 z"}]],
- ["marker", {orient: "auto", markerHeight: "12", markerWidth: "12", markerUnits: "userSpaceOnUse", refY: "5", refX: "4.5", viewBox: "0 0 10 10", class: "marker flowchart", id: "mermaid-1722776679139_flowchart-pointStart"}, ["path", {style: "stroke-width: 1; stroke-dasharray: 1, 0;", class: "arrowMarkerPath", d: "M 0 5 L 10 10 L 10 0 z"}]],
- ["marker", {orient: "auto", markerHeight: "11", markerWidth: "11", markerUnits: "userSpaceOnUse", refY: "5", refX: "11", viewBox: "0 0 10 10", class: "marker flowchart", id: "mermaid-1722776679139_flowchart-circleEnd"}, ["circle", {style: "stroke-width: 1; stroke-dasharray: 1, 0;", class: "arrowMarkerPath", r: "5", cy: "5", cx: "5"}]],
- ["marker", {orient: "auto", markerHeight: "11", markerWidth: "11", markerUnits: "userSpaceOnUse", refY: "5", refX: "-1", viewBox: "0 0 10 10", class: "marker flowchart", id: "mermaid-1722776679139_flowchart-circleStart"}, ["circle", {style: "stroke-width: 1; stroke-dasharray: 1, 0;", class: "arrowMarkerPath", r: "5", cy: "5", cx: "5"}]],
- ["marker", {orient: "auto", markerHeight: "11", markerWidth: "11", markerUnits: "userSpaceOnUse", refY: "5.2", refX: "12", viewBox: "0 0 11 11", class: "marker cross flowchart", id: "mermaid-1722776679139_flowchart-crossEnd"}, ["path", {style: "stroke-width: 2; stroke-dasharray: 1, 0;", class: "arrowMarkerPath", d: "M 1,1 l 9,9 M 10,1 l -9,9"}]],
- ["marker", {orient: "auto", markerHeight: "11", markerWidth: "11", markerUnits: "userSpaceOnUse", refY: "5.2", refX: "-1", viewBox: "0 0 11 11", class: "marker cross flowchart", id: "mermaid-1722776679139_flowchart-crossStart"}, ["path", {style: "stroke-width: 2; stroke-dasharray: 1, 0;", class: "arrowMarkerPath", d: "M 1,1 l 9,9 M 10,1 l -9,9"}]],
- ["g", {class: "root"},
- ["g", {class: "clusters"},
- ["g", {id: "Running", class: "cluster default flowchart-label"}, ["rect", {height: "306.953125", width: "354.84375", y: "197.171875", x: "0", ry: "0", rx: "0"}], ["g", {transform: "translate(147.6171875, 197.171875)", class: "cluster-label"}, ["foreignobject", {height: "22.390625", width: "59.609375"}, ["div", {style: "display: inline-block; white-space: nowrap;", xmlns: "http://www.w3.org/1999/xhtml"}, ["span", {class: "nodeLabel"}, "Running"]]]]],
- ],
- ["g", {class: "edgePaths"},
- ["path", {"marker-end": "url(#mermaid-1722776679139_flowchart-pointEnd)", style: "fill:none;", class: "edge-thickness-normal edge-pattern-solid flowchart-link LS-0 LE-S", id: "L-0-S-0", d: "M176.344,15L176.344,21.033C176.344,27.065,176.344,39.13,176.344,50.312C176.344,61.494,176.344,71.792,176.344,76.941L176.344,82.091"}],
- ["path", {"marker-end": "url(#mermaid-1722776679139_flowchart-pointEnd)", style: "fill:none;", class: "edge-thickness-normal edge-pattern-solid flowchart-link LS-C LE-R", id: "L-C-R-0", d: "M74.695,259.563L74.695,265.595C74.695,271.628,74.695,283.693,78.881,295.063C83.066,306.433,91.437,317.108,95.622,322.445L99.808,327.782"}],
- ["path", {"marker-end": "url(#mermaid-1722776679139_flowchart-pointEnd)", style: "fill:none;", class: "edge-thickness-normal edge-pattern-solid flowchart-link LS-R LE-A", id: "L-R-A-0", d: "M117.738,369.344L117.738,375.376C117.738,381.409,117.738,393.474,126.825,405.156C135.912,416.838,154.086,428.137,163.173,433.787L172.26,439.436"}],
- ["path", {"marker-end": "url(#mermaid-1722776679139_flowchart-pointEnd)", style: "fill:none;", class: "edge-thickness-normal edge-pattern-solid flowchart-link LS-A LE-C", id: "L-A-C-0", d: "M209.261,442.234L209.911,436.118C210.56,430.003,211.86,417.771,212.51,402.507C213.16,387.242,213.16,368.945,213.16,350.648C213.16,332.352,213.16,314.055,196.897,298.459C180.635,282.864,148.109,269.97,131.846,263.523L115.583,257.076"}],
- ["path", {"marker-end": "url(#mermaid-1722776679139_flowchart-pointEnd)", style: "fill:none;", class: "edge-thickness-normal edge-pattern-solid flowchart-link LS-A LE-S", id: "L-A-S-0", d: "M232.858,442.234L241.122,436.118C249.386,430.003,265.915,417.771,274.179,402.507C282.443,387.242,282.443,368.945,282.443,350.648C282.443,332.352,282.443,314.055,282.443,295.758C282.443,277.461,282.443,259.164,282.443,242.733C282.443,226.302,282.443,211.737,282.443,198.422C282.443,185.107,282.443,173.042,271.567,161.382C260.692,149.723,238.94,138.47,228.064,132.843L217.188,127.217"}],
- ["path", {"marker-end": "url(#mermaid-1722776679139_flowchart-pointEnd)", style: "fill:none;", class: "edge-thickness-normal edge-pattern-solid flowchart-link LS-S LE-C", id: "L-S-C-0", d: "M141.723,124.781L130.552,130.814C119.381,136.846,97.038,148.911,85.867,160.977C74.695,173.042,74.695,185.107,74.695,194.423C74.695,203.739,74.695,210.305,74.695,213.589L74.695,216.872"}],
- ["path", {"marker-end": "url(#mermaid-1722776679139_flowchart-pointEnd)", style: "fill:none;", class: "edge-thickness-normal edge-pattern-solid flowchart-link LS-Running LE-S", id: "L-Running-S-0", d: "M160.781,197.172L160.781,191.139C160.781,185.107,160.781,173.042,162.251,161.826C163.72,150.611,166.659,140.246,168.128,135.063L169.598,129.88"}],
- ],
- (() => {
- const edgeLabel = (p1: string, p2: string, h: string, w: string, label: string) =>
- ["g", {transform: "translate("+p1+")", class: "edgeLabel"},
- ["g", {transform: "translate("+p2+")", class: "label"},
- ["foreignobject", {height: h, width: w},
- ["div", {style: "display: inline-block; white-space: nowrap;", xmlns: "http://www.w3.org/1999/xhtml"},
- ["span", {class: "edgeLabel"}, label]]]]];
- return ["g", {class: "edgeLabels"},
- edgeLabel("176.34375, 51.1953125", "-44.9140625, -11.1953125", "22.390625", "89.828125", "new process"),
- edgeLabel("74.6953125, 295.7578125", "-54.6953125, -11.1953125", "22.390625", "109.390625", "process started"),
- edgeLabel("117.73828125, 405.5390625", "-45.359375, -11.1953125", "22.390625", "90.71875", "process died"),
- edgeLabel("213.16015625, 350.6484375", "-12.453125, -11.1953125", "22.390625", "24.90625", "yes"),
- edgeLabel("282.443359375, 295.7578125", "-8.8984375, -11.1953125", "22.390625", "17.796875", "no"),
- edgeLabel("74.6953125, 160.9765625", "-15.5625, -11.1953125", "22.390625", "31.125", "start"),
- edgeLabel("160.97582, 160.2903", "-15.125, -11.1953125", "22.390625", "30.25", "stop"),
- ];
- })(),
- ["g", {class: "nodes"},
- ["g", {transform: "translate(176.34375, 7.5)", "data-node": "true", id: "flowchart-0-0", class: "node default flowchart-label"}, ["rect", {height: "15", width: "15", y: "-7.5", x: "-7.5", ry: "5", rx: "5", class: "basic label-container"}], ["g", {transform: "translate(0, 0)", class: "label"}, ["rect", {}], ["foreignobject", {height: "0", width: "0"}, ["div", {style: "display: inline-block; white-space: nowrap;", xmlns: "http://www.w3.org/1999/xhtml"}, ["span", {class: "nodeLabel"}]]]]],
- ["g", {transform: "translate(117.73828125, 350.6484375)", "data-node": "true", id: "flowchart-R-3", class: "node default default flowchart-label"}, ["rect", {height: "37.390625", width: "74.609375", y: "-18.6953125", x: "-37.3046875", ry: "5", rx: "5", class: "basic label-container"}], ["g", {transform: "translate(-29.8046875, -11.1953125)", class: "label"}, ["rect", {}], ["foreignobject", {height: "22.390625", width: "59.609375"}, ["div", {style: "display: inline-block; white-space: nowrap;", xmlns: "http://www.w3.org/1999/xhtml"}, ["span", {class: "nodeLabel"}, "Running"]]]]],
- ["g", {transform: "translate(74.6953125, 240.8671875)", "data-node": "true", id: "flowchart-C-2", class: "node default default flowchart-label"}, ["rect", {height: "37.390625", width: "71.921875", y: "-18.6953125", x: "-35.9609375", ry: "5", rx: "5", class: "basic label-container"}], ["g", {transform: "translate(-28.4609375, -11.1953125)", class: "label"}, ["rect", {}], ["foreignobject", {height: "22.390625", width: "56.921875"}, ["div", {style: "display: inline-block; white-space: nowrap;", xmlns: "http://www.w3.org/1999/xhtml"}, ["span", {class: "nodeLabel"}, "Created"]]]]],
- ["g", {transform: "translate(206.48828125, 460.4296875)", "data-node": "true", id: "flowchart-A-4", class: "node default default flowchart-label"}, ["polygon", {transform: "translate(-113.35546875,18.6953125)", class: "label-container", points: "9.34765625,0 217.36328125,0 226.7109375,-18.6953125 217.36328125,-37.390625 9.34765625,-37.390625 0,-18.6953125"}], ["g", {transform: "translate(-96.5078125, -11.1953125)", class: "label"}, ["rect", {}], ["foreignobject", {height: "22.390625", width: "193.015625"}, ["div", {style: "display: inline-block; white-space: nowrap;", xmlns: "http://www.w3.org/1999/xhtml"}, ["span", {class: "nodeLabel"}, "autorestart/watch enabled?"]]]]],
- ["g", {transform: "translate(176.34375, 106.0859375)", "data-node": "true", id: "flowchart-S-1", class: "node default default flowchart-label"}, ["rect", {height: "37.390625", width: "74.609375", y: "-18.6953125", x: "-37.3046875", ry: "5", rx: "5", class: "basic label-container"}], ["g", {transform: "translate(-29.8046875, -11.1953125)", class: "label"}, ["rect", {}], ["foreignobject", {height: "22.390625", width: "59.609375"}, ["div", {style: "display: inline-block; white-space: nowrap;", xmlns: "http://www.w3.org/1999/xhtml"}, ["span", {class: "nodeLabel"}, "Stopped"]]]]],
- ],
- ]
- ],
- ],
-];
\ No newline at end of file
diff --git a/docs/styles.ts b/docs/styles.ts
index 650c8c5cf..bdf2e4c48 100644
--- a/docs/styles.ts
+++ b/docs/styles.ts
@@ -44,7 +44,7 @@ const themes: Record = {
"simple": simpletheme,
"yorha": yorhatheme,
};
-const theme = "simple";
+const theme = "yorha";
const colors = Object.fromEntries(Object.entries(themes[theme]).map(([k, v]) => [k, "#"+v]));
export default [
@@ -504,7 +504,7 @@ export default [
"--code-block-margin": "1em 0",
"--code-inline-background": colors.base07,
"--code-inline-border-radius": "var(--border-radius-s)",
- "--code-inline-color": "var(--code-theme-text)",
+ "--code-inline-color": colors.base06,
"--code-inline-margin": "0 0.15em",
"--code-inline-padding": "0.125em 0.4em",
@@ -552,16 +552,16 @@ export default [
["pre[data-lang]::selection, code[class*=lang-]::selection", {
"background": "var(--code-theme-selection, var(--selection-color))",
}],
- // ["table", {
- // "border-spacing": "0",
- // }],
- // ["th", {
- // "border-bottom": "0.1rem solid var(--sidebar-border-color)",
- // }],
- // ["body", {
- // "background-image": "linear-gradient(to right, #ccc8b1 1px, rgba(204,200,177,0) 1px), linear-gradient(to bottom, #ccc8b1 1px, rgba(204,200,177,0) 1px)",
- // "background-size": "0.3rem 0.3rem",
- // }],
+ ["table", {
+ "border-spacing": "0",
+ }],
+ ["th", {
+ "border-bottom": "0.1rem solid var(--sidebar-border-color)",
+ }],
+ ["body", {
+ "background-image": "linear-gradient(to right, #ccc8b1 1px, rgba(204,200,177,0) 1px), linear-gradient(to bottom, #ccc8b1 1px, rgba(204,200,177,0) 1px)",
+ "background-size": "0.3rem 0.3rem",
+ }],
// @media(min-width: 48em) { body.sticky .sidebar { position: fixed } }
// @media print {
// .sidebar {
diff --git a/docs/themes/yorha.ts b/docs/themes/yorha.ts
index ec3f831aa..15a51ea70 100644
--- a/docs/themes/yorha.ts
+++ b/docs/themes/yorha.ts
@@ -2,10 +2,10 @@ export default {
base00: "d1cdb7",
base01: "bab5a1",
base02: "454138",
-base03: "6e8090", // not set
+base03: "508b57",
base04: "064048", // not set
base05: "4d4d4d", // not set
-base06: "0c7c8c", // not set
+base06: "dcd8c0",
base07: "454138",
base08: "8a3f3a",
base09: "454138",
diff --git a/readme.md b/readme.md
index ad659f8d7..126b37d67 100644
--- a/readme.md
+++ b/readme.md
@@ -26,7 +26,48 @@ pm startup
After these commands, processes with `startup: true` config option will be started on system startup.
## Configuration
-[jsonnet](https://jsonnet.org/) configuration language is used. It is also fully compatible with plain JSON, so you can write JSON instead.
+PM supports multiple configuration formats for defining processes. The original [jsonnet](https://jsonnet.org/) format is supported, along with several additional formats for flexibility:
+
+### Supported Formats
+#### JSONNet (.jsonnet)
+The primary configuration format. JSONNet is fully compatible with plain JSON.
+
+```jsonnet
+[
+ {
+ name: "web-server",
+ command: "node",
+ args: ["server.js"],
+ env: {
+ PORT: "3000",
+ NODE_ENV: "production"
+ },
+ tags: ["web"],
+ startup: true
+ }
+]
+```
+
+### Configuration Schema
+All formats define list of processes with following fields:
+
+| Field | Type | Description | Required |
+|-------|------|-------------|----------|
+| `name` | `string` | Process name (auto-generated if omitted) | No |
+| `command` | `string` | Command to execute | Yes |
+| `args` | `array(string)` | Command arguments | No |
+| `cwd` | `string` | Working directory | No |
+| `env` | `map(string, string)` | Environment variables (name: value pairs) | No |
+| `tags` | `array(string)` | Process tags for filtering | No |
+| `watch` | `string` | File pattern to watch for restarts (regex) | No |
+| `startup` | `boolean` | Start process on system startup | No |
+| `depends_on` | `array(string)` | Process names that must start first | No |
+| `cron` | `string` | Cron expression for scheduled execution | No |
+| `stdout_file` | `string` | File to redirect stdout to | No |
+| `stderr_file` | `string` | File to redirect stderr to | No |
+| `kill_timeout` | `duration` | Time before SIGKILL after SIGINT | No |
+| `autorestart` | `boolean` | Auto-restart on process death | No |
+| `max_restarts` | `number` | Maximum restart limit (0 = unlimited) | No |
See [example configuration file](./config.jsonnet). Other examples can be found in [tests](./e2e/tests) directory.
@@ -86,7 +127,7 @@ flowchart TB
end
A -->|yes| C
A -->|no| S
- Running -->|stop| S
+ Running -->|stop| S
S -->|start| C
```