diff --git a/docs/bun.lock b/docs/bun.lock index e0ba7ab4c..63cfdd44e 100644 --- a/docs/bun.lock +++ b/docs/bun.lock @@ -1,9 +1,12 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "dependencies": { "@types/bun": "^1.3.6", + "@types/pako": "^2.0.4", + "pako": "^2.1.0", }, }, }, @@ -12,8 +15,12 @@ "@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="], + "@types/pako": ["@types/pako@2.0.4", "", {}, "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw=="], + "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="], + "pako": ["pako@2.1.0", "", {}, "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="], + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], } } diff --git a/docs/github-corner.ts b/docs/github-corner.ts new file mode 100644 index 000000000..39f6d1b64 --- /dev/null +++ b/docs/github-corner.ts @@ -0,0 +1,44 @@ +export const icon = ["svg", { + width: "80", + height: "80", + viewBox: "0 0 250 250", + style: [ + // TODO: depend on style + "fill: #333333;", + "color: #ccc8b1;", + "position: absolute;", + "top: 0;", + "border: 0;", + "right: 0;", + ].join(" "), + "aria-hidden": "true", +}, + ["path", {d: "M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"}], + ["path", {fill: "currentColor", style: "transform-origin: 130px 106px;", class: "octo-arm", d: "M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"}], + ["path", {fill: "currentColor", class: "octo-body", d: "M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"}], +] as const; + +export const style = ["style", {}, ` + .github-corner:hover .octo-arm { + animation:octocat-wave 560ms ease-in-out; + } + @keyframes octocat-wave { + 0%,100% { + transform:rotate(0); + } + 20%,60% { + transform:rotate(-25deg); + } + 40%,80% { + transform:rotate(10deg); + } + } + @media (max-width:500px){ + .github-corner:hover .octo-arm{ + animation:none; + } + .github-corner .octo-arm{ + animation:octocat-wave 560ms ease-in-out; + } + } +`] as const; diff --git a/docs/index.html b/docs/index.html index fd23545bd..0c8ae0d26 100644 --- a/docs/index.html +++ b/docs/index.html @@ -11,7 +11,7 @@ .markdown-section h1 code, .markdown-section h2 code, .markdown-section h3 code, .markdown-section h4 code, .markdown-section h5 code, .markdown-section h6 code { font-size: .875em; } .markdown-section h1+h2, .markdown-section h1+h3, .markdown-section h1+h4, .markdown-section h1+h5, .markdown-section h1+h6, .markdown-section h2+h3, .markdown-section h2+h4, .markdown-section h2+h5, .markdown-section h2+h6, .markdown-section h3+h4, .markdown-section h3+h5, .markdown-section h3+h6, .markdown-section h4+h5, .markdown-section h4+h6, .markdown-section h5+h6 { margin-top: 1rem; } .markdown-section h1:first-child, .markdown-section h2:first-child, .markdown-section h3:first-child, .markdown-section h4:first-child, .markdown-section h5:first-child, .markdown-section h6:first-child { margin-top: 0; } -.markdown-section h2 { border-color: #e3e3e3; border-style: solid; border-width: 0 0 1px 0; color: var(--heading-color); font-size: var(--heading-h2-font-size); font-weight: 400; line-height: 1.55rem; margin: 2.5rem 0 1rem; padding: 0 0 0.75rem 0; } +.markdown-section h2 { border-color: #454138; border-style: solid; border-width: 0 0 1px 0; color: var(--heading-color); font-size: var(--heading-h2-font-size); font-weight: 400; line-height: 1.55rem; margin: 2.5rem 0 1rem; padding: 0 0 0.75rem 0; } .markdown-section h3 { color: var(--heading-color); font-size: var(--heading-h3-font-size); font-weight: 400; line-height: 1.35rem; margin: 1rem 0rem -.5rem 0rem; } .markdown-section h4 { color: var(--heading-color); font-size: var(--heading-h4-font-size); font-weight: 400; line-height: 1.30rem; margin: 1rem 0rem -.5rem 0rem; } .markdown-section h5 { color: var(--heading-color); font-size: var(--heading-h5-font-size); font-weight: 400; line-height: 1.25rem; margin: 1rem 0rem -.5rem 0rem; } @@ -48,7 +48,7 @@ .sidebar>h1 { background: var(--sidebar-name-background); color: var(--sidebar-name-color); font-family: var(--sidebar-name-font-family); font-size: var(--sidebar-name-font-size); font-weight: var(--sidebar-name-font-weight); margin: 0; padding: var(--sidebar-name-padding); text-align: var(--sidebar-name-text-align); } .sidebar>h1 img { max-width: 100%; } ::selection { background: var(--selection-color); } -:root { --base-background-color: #ffffff; --base-color: #4d4d4d; --base-font-size: 1.125rem; --base-font-weight: normal; --base-line-height: 1.6; --border-radius-l: 8px; --border-radius-m: 4px; --border-radius-s: 2px; --code-block-border-radius: var(--border-radius-m); --code-block-margin: 1em 0; --code-font-family: Inconsolata, Consolas, Menlo, Monaco, "Andale Mono WT", "Andale Mono", "Lucida Console", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace; --code-font-size: calc(var(--font-size-m) * 0.95); --code-font-weight: normal; --code-inline-background: #e3e3e3; --code-inline-border-radius: var(--border-radius-s); --code-inline-color: var(--code-theme-text); --code-inline-margin: 0 0.15em; --code-inline-padding: 0.125em 0.4em; --code-tab-size: 4; --code-theme-background: #F7F7F7; --code-theme-comment: #6e8090; --code-theme-function: #333333; --code-theme-keyword: #690; --code-theme-operator: #4d4d4d; --code-theme-punctuation: #999; --code-theme-selector: #690; --code-theme-tag: #905; --code-theme-text: #333; --code-theme-variable: #905; --font-size-l: var(--modular-scale-2); --font-size-m: var(--modular-scale-1); --font-size-s: var(--modular-scale--1); --font-size-xl: var(--modular-scale-3); --font-size-xs: var(--modular-scale--2); --font-size-xxl: var(--modular-scale-4); --heading-color: #333333; --heading-h1-font-size: var(--font-size-xxl); --heading-h2-font-size: var(--font-size-xl); --heading-h3-font-size: var(--font-size-l); --heading-h4-font-size: var(--font-size-m); --heading-h5-font-size: var(--font-size-s); --heading-h6-font-size: var(--font-size-xs); --hr-border: 1px solid var(--mono-tint2); --link-color: #0374b5; --modular-scale: 1.333; --modular-scale--1: calc(var(--modular-scale-1) / var(--modular-scale)); --modular-scale--2: calc(var(--modular-scale--1) / var(--modular-scale)); --modular-scale-1: 1rem; --modular-scale-2: calc(var(--modular-scale-1) * var(--modular-scale)); --modular-scale-3: calc(var(--modular-scale-2) * var(--modular-scale)); --modular-scale-4: calc(var(--modular-scale-3) * var(--modular-scale)); --mono-base: hsl(var(--mono-hue), var(--mono-saturation), 50%); --mono-hue: 113; --mono-saturation: 0%; --mono-shade1: hsl(var(--mono-hue), var(--mono-saturation), 40%); --mono-shade2: hsl(var(--mono-hue), var(--mono-saturation), 30%); --mono-shade3: hsl(var(--mono-hue), var(--mono-saturation), 20%); --mono-tint1: hsl(var(--mono-hue), var(--mono-saturation), 70%); --mono-tint2: hsl(var(--mono-hue), var(--mono-saturation), 89%); --mono-tint3: hsl(var(--mono-hue), var(--mono-saturation), 97%); --navbar-menu-background: var(--base-background-color); --navbar-menu-box-shadow: rgba(45, 45, 45, 0.05) 0px 0px 1px, rgba(49, 49, 49, 0.05) 0px 1px 2px, rgba(42, 42, 42, 0.05) 0px 2px 4px, rgba(32, 32, 32, 0.05) 0px 4px 8px, rgba(49, 49, 49, 0.05) 0px 8px 16px, rgba(35, 35, 35, 0.05) 0px 16px 32px; --navbar-menu-link-border-style: solid; --navbar-menu-link-margin: 0.75em 0.5em; --navbar-menu-link-padding: 0.2em 0; --navbar-menu-padding: 0.5em; --navbar-root-border-style: solid; --navbar-root-color--active: var(--theme-color); --pre-font-size: var(--code-font-size); --selection-color: #b4d5fe; --sidebar-background: #F7F7F7; --sidebar-border-color: #e3e3e3; --sidebar-border-width: 0 1px 0 0; --sidebar-name-color: var(--theme-color); --sidebar-name-font-size: var(--font-size-l); --sidebar-name-font-weight: 300; --sidebar-name-margin: 1.5rem 0 0; --sidebar-name-text-align: center; --sidebar-nav-indent: 1em; --sidebar-nav-link-before-margin: 0 0.35em 0 0; --sidebar-nav-link-border-color: transparent; --sidebar-nav-link-border-color--active: #0374B5; --sidebar-nav-link-border-style: solid; --sidebar-nav-link-border-width: 0 4px 0 0; --sidebar-nav-link-color: var(--base-color); --sidebar-nav-link-color--active: var(--theme-color); --sidebar-nav-link-font-weight: normal; --sidebar-nav-link-font-weight--active: bold; --sidebar-nav-link-margin: 0 -25px 0 0; --sidebar-nav-link-padding: 0.25em 0; --sidebar-nav-link-text-decoration: none; --sidebar-nav-link-text-decoration--active: none; --sidebar-nav-link-text-decoration--hover: underline; --sidebar-nav-margin: 1.5rem 0 0; --sidebar-nav-pagelink-background: no-repeat 2px calc(50% - 2.5px) / 6px 5px linear-gradient(45deg, transparent 2.75px, var(--mono-tint1) 2.75px 4.25px, transparent 4px), no-repeat 2px calc(50% + 2.5px) / 6px 5px linear-gradient(135deg, transparent 2.75px, var(--mono-tint1) 2.75px 4.25px, transparent 4px); --sidebar-nav-pagelink-background--active: no-repeat 0px center / 5px 6px linear-gradient(225deg, transparent 2.75px, var(--theme-color) 2.75px 4.25px, transparent 4.25px), no-repeat 5px center / 5px 6px linear-gradient(135deg, transparent 2.75px, var(--theme-color) 2.75px 4.25px, transparent 4.25px); --sidebar-nav-pagelink-background--collapse: no-repeat 2px calc(50% - 2.5px) / 6px 5px linear-gradient(45deg, transparent 2.75px, var(--theme-color) 2.75px 4.25px, transparent 4px), no-repeat 2px calc(50% + 2.5px) / 6px 5px linear-gradient(135deg, transparent 2.75px, var(--theme-color) 2.75px 4.25px, transparent 4px); --sidebar-nav-pagelink-background--loaded: no-repeat 0px center / 5px 6px linear-gradient(225deg, transparent 2.75px, var(--mono-tint1) 2.75px 4.25px, transparent 4.25px), no-repeat 5px center / 5px 6px linear-gradient(135deg, transparent 2.75px, var(--mono-tint1) 2.75px 4.25px, transparent 4.25px); --sidebar-nav-pagelink-padding: 0.25em 0 0.25em 20px; --sidebar-nav-strong-border-color: #e3e3e3; --sidebar-nav-strong-border-width: 0 0 1px 0; --sidebar-nav-strong-color: var(--heading-color); --sidebar-nav-strong-font-size: smaller; --sidebar-nav-strong-font-weight: var(--strong-font-weight); --sidebar-nav-strong-margin: 1.5em 0 0.5em; --sidebar-nav-strong-padding: 0.25em 0; --sidebar-nav-strong-text-transform: uppercase; --sidebar-padding: 0 25px; --sidebar-width: 17rem; --small-font-size: var(--font-size-s); --strong-color: var(--heading-color); --strong-font-weight: 600; --subsup-font-size: var(--font-size-s); --theme-color: hsl(204, 90%, 45%); background-color: var(--base-background-color); box-sizing: border-box; color: var(--base-color); font-size: var(--base-font-size); font-weight: var(--base-font-weight); letter-spacing: var(--base-letter-spacing); line-height: var(--base-line-height); } +:root { --base-background-color: #d1cdb7; --base-color: #4d4d4d; --base-font-size: 1.125rem; --base-font-weight: normal; --base-line-height: 1.6; --border-radius-l: 8px; --border-radius-m: 4px; --border-radius-s: 2px; --code-block-border-radius: var(--border-radius-m); --code-block-margin: 1em 0; --code-font-family: Inconsolata, Consolas, Menlo, Monaco, "Andale Mono WT", "Andale Mono", "Lucida Console", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace; --code-font-size: calc(var(--font-size-m) * 0.95); --code-font-weight: normal; --code-inline-background: #454138; --code-inline-border-radius: var(--border-radius-s); --code-inline-color: #dcd8c0; --code-inline-margin: 0 0.15em; --code-inline-padding: 0.125em 0.4em; --code-tab-size: 4; --code-theme-background: #bab5a1; --code-theme-comment: #508b57; --code-theme-function: #333333; --code-theme-keyword: #669900; --code-theme-operator: #4d4d4d; --code-theme-punctuation: #999999; --code-theme-selector: #669900; --code-theme-tag: #8a3f3a; --code-theme-text: #454138; --code-theme-variable: #8a3f3a; --font-size-l: var(--modular-scale-2); --font-size-m: var(--modular-scale-1); --font-size-s: var(--modular-scale--1); --font-size-xl: var(--modular-scale-3); --font-size-xs: var(--modular-scale--2); --font-size-xxl: var(--modular-scale-4); --heading-color: #333333; --heading-h1-font-size: var(--font-size-xxl); --heading-h2-font-size: var(--font-size-xl); --heading-h3-font-size: var(--font-size-l); --heading-h4-font-size: var(--font-size-m); --heading-h5-font-size: var(--font-size-s); --heading-h6-font-size: var(--font-size-xs); --hr-border: 1px solid var(--mono-tint2); --link-color: #454138; --modular-scale: 1.333; --modular-scale--1: calc(var(--modular-scale-1) / var(--modular-scale)); --modular-scale--2: calc(var(--modular-scale--1) / var(--modular-scale)); --modular-scale-1: 1rem; --modular-scale-2: calc(var(--modular-scale-1) * var(--modular-scale)); --modular-scale-3: calc(var(--modular-scale-2) * var(--modular-scale)); --modular-scale-4: calc(var(--modular-scale-3) * var(--modular-scale)); --mono-base: hsl(var(--mono-hue), var(--mono-saturation), 50%); --mono-hue: 113; --mono-saturation: 0%; --mono-shade1: hsl(var(--mono-hue), var(--mono-saturation), 40%); --mono-shade2: hsl(var(--mono-hue), var(--mono-saturation), 30%); --mono-shade3: hsl(var(--mono-hue), var(--mono-saturation), 20%); --mono-tint1: hsl(var(--mono-hue), var(--mono-saturation), 70%); --mono-tint2: hsl(var(--mono-hue), var(--mono-saturation), 89%); --mono-tint3: hsl(var(--mono-hue), var(--mono-saturation), 97%); --navbar-menu-background: var(--base-background-color); --navbar-menu-box-shadow: rgba(45, 45, 45, 0.05) 0px 0px 1px, rgba(49, 49, 49, 0.05) 0px 1px 2px, rgba(42, 42, 42, 0.05) 0px 2px 4px, rgba(32, 32, 32, 0.05) 0px 4px 8px, rgba(49, 49, 49, 0.05) 0px 8px 16px, rgba(35, 35, 35, 0.05) 0px 16px 32px; --navbar-menu-link-border-style: solid; --navbar-menu-link-margin: 0.75em 0.5em; --navbar-menu-link-padding: 0.2em 0; --navbar-menu-padding: 0.5em; --navbar-root-border-style: solid; --navbar-root-color--active: var(--theme-color); --pre-font-size: var(--code-font-size); --selection-color: #454138; --sidebar-background: #bab5a1; --sidebar-border-color: #454138; --sidebar-border-width: 0 1px 0 0; --sidebar-name-color: var(--theme-color); --sidebar-name-font-size: var(--font-size-l); --sidebar-name-font-weight: 300; --sidebar-name-margin: 1.5rem 0 0; --sidebar-name-text-align: center; --sidebar-nav-indent: 1em; --sidebar-nav-link-before-margin: 0 0.35em 0 0; --sidebar-nav-link-border-color: transparent; --sidebar-nav-link-border-color--active: #0374B5; --sidebar-nav-link-border-style: solid; --sidebar-nav-link-border-width: 0 4px 0 0; --sidebar-nav-link-color: var(--base-color); --sidebar-nav-link-color--active: var(--theme-color); --sidebar-nav-link-font-weight: normal; --sidebar-nav-link-font-weight--active: bold; --sidebar-nav-link-margin: 0 -25px 0 0; --sidebar-nav-link-padding: 0.25em 0; --sidebar-nav-link-text-decoration: none; --sidebar-nav-link-text-decoration--active: none; --sidebar-nav-link-text-decoration--hover: underline; --sidebar-nav-margin: 1.5rem 0 0; --sidebar-nav-pagelink-background: no-repeat 2px calc(50% - 2.5px) / 6px 5px linear-gradient(45deg, transparent 2.75px, var(--mono-tint1) 2.75px 4.25px, transparent 4px), no-repeat 2px calc(50% + 2.5px) / 6px 5px linear-gradient(135deg, transparent 2.75px, var(--mono-tint1) 2.75px 4.25px, transparent 4px); --sidebar-nav-pagelink-background--active: no-repeat 0px center / 5px 6px linear-gradient(225deg, transparent 2.75px, var(--theme-color) 2.75px 4.25px, transparent 4.25px), no-repeat 5px center / 5px 6px linear-gradient(135deg, transparent 2.75px, var(--theme-color) 2.75px 4.25px, transparent 4.25px); --sidebar-nav-pagelink-background--collapse: no-repeat 2px calc(50% - 2.5px) / 6px 5px linear-gradient(45deg, transparent 2.75px, var(--theme-color) 2.75px 4.25px, transparent 4px), no-repeat 2px calc(50% + 2.5px) / 6px 5px linear-gradient(135deg, transparent 2.75px, var(--theme-color) 2.75px 4.25px, transparent 4px); --sidebar-nav-pagelink-background--loaded: no-repeat 0px center / 5px 6px linear-gradient(225deg, transparent 2.75px, var(--mono-tint1) 2.75px 4.25px, transparent 4.25px), no-repeat 5px center / 5px 6px linear-gradient(135deg, transparent 2.75px, var(--mono-tint1) 2.75px 4.25px, transparent 4.25px); --sidebar-nav-pagelink-padding: 0.25em 0 0.25em 20px; --sidebar-nav-strong-border-color: #454138; --sidebar-nav-strong-border-width: 0 0 1px 0; --sidebar-nav-strong-color: var(--heading-color); --sidebar-nav-strong-font-size: smaller; --sidebar-nav-strong-font-weight: var(--strong-font-weight); --sidebar-nav-strong-margin: 1.5em 0 0.5em; --sidebar-nav-strong-padding: 0.25em 0; --sidebar-nav-strong-text-transform: uppercase; --sidebar-padding: 0 25px; --sidebar-width: 17rem; --small-font-size: var(--font-size-s); --strong-color: var(--heading-color); --strong-font-weight: 600; --subsup-font-size: var(--font-size-s); --theme-color: hsl(204, 90%, 45%); background-color: var(--base-background-color); box-sizing: border-box; color: var(--base-color); font-size: var(--base-font-size); font-weight: var(--base-font-weight); letter-spacing: var(--base-letter-spacing); line-height: var(--base-line-height); } a { text-decoration: none; text-decoration-skip-ink: auto; } body * { scrollbar-color: hsla(var(--mono-hue), var(--mono-saturation), 50%, 0.3) hsla(var(--mono-hue), var(--mono-saturation), 50%, 0.1); scrollbar-width: thin; } body .sidebar { padding: var(--sidebar-padding); } @@ -58,7 +58,33 @@ html { font-family: "Source Sans Pro", "Helvetica Neue", Arial, sans-serif; } main { display: block; min-height: 100vh; overflow-x: hidden; position: relative; } pre { font-family: var(--code-font-family); font-size: var(--pre-font-size); font-weight: normal; } -pre[data-lang]::selection, code[class*=lang-]::selection { background: var(--code-theme-selection, var(--selection-color)); }

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:

FieldTypeDescriptionRequired
namestringProcess name (auto-generated if omitted)No
commandstringCommand to executeYes
argsarray(string)Command argumentsNo
cwdstringWorking directoryNo
envmap(string, string)Environment variables (name: value pairs)No
tagsarray(string)Process tags for filteringNo
watchstringFile pattern to watch for restarts (regex)No
startupbooleanStart process on system startupNo
depends_onarray(string)Process names that must start firstNo
cronstringCron expression for scheduled executionNo
stdout_filestringFile to redirect stdout toNo
stderr_filestringFile to redirect stderr toNo
kill_timeoutdurationTime before SIGKILL after SIGINTNo
autorestartbooleanAuto-restart on process deathNo
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

Running
new process
process started
process died
yes
no
start
stop
Running
Created
autorestart/watch enabled?
Stopped

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

Running

new process

process started

process died

yes

no

start

stop

Stopped

Created

Running

autorestart/watch enabled?

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}`;
 }
@@ -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) => `![${alt}](${src})`,
+  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 ```