ramm 0.0.59 → 0.0.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,151 +1,381 @@
1
- ## Overview
1
+ # RAMM
2
2
 
3
- RAMM is a Bun library designed to simplify remote server management, deployment automation, and container operations with JS. Ansibsle inspired mechanism, but in JS with imperative commands
3
+ A Bun library for server management and deployment automation in TypeScript. Write your infrastructure logic in real code — no YAML, no DSL, no templating language to fight with.
4
4
 
5
- It provides utilities for:
5
+ ## Why not Ansible?
6
6
 
7
- - Remote command execution over SSH
8
- - File synchronization using rsync
9
- - System package management
10
- - Podman container management
7
+ Ansible is great, but it comes with a cost: you write infrastructure in YAML with Jinja2 templates. The moment your logic gets non-trivial conditionals, loops, dynamic values — you're fighting the format instead of solving the problem.
8
+
9
+ RAMM takes a different approach: **your deployment scripts are just TypeScript**. Full language, real abstractions, IDE support, type checking, any npm package you need. If you know JS, you already know how to write RAMM scripts.
10
+
11
+ The trade-off is explicit: Ansible gives you a huge module ecosystem and declarative guarantees. RAMM gives you simplicity and the full power of a real programming language. For developers who want to own their deployment code without learning a separate tool, RAMM is the better fit.
12
+
13
+ ## How it works
14
+
15
+ The core pattern is `buildAndRunOverSsh`: write a script that configures your server (`server.ts`), then run it from your local machine (`client.ts`). RAMM compiles the server script with `bun build` and executes it on the remote host — all commands inside run on the server.
16
+
17
+ ```
18
+ client.ts → [bun build] → server.ts runs on remote
19
+ (local machine) (all execCommand calls are local to the server)
20
+ ```
11
21
 
12
22
  ## Installation
13
23
 
14
24
  ```bash
15
25
  bun add ramm
16
- # or
17
- npm install ramm
18
26
  ```
19
27
 
20
- ## Quick Start Example
28
+ ## Quick Start
29
+
30
+ **`client.ts`** — runs on your local machine:
21
31
 
22
32
  ```ts
23
- import { $, build } from "bun";
24
- import { Context, exec, copyFiles, debug, installBun } from "ramm";
33
+ import { buildAndRunOverSsh, Context } from "ramm";
25
34
 
26
- const context = new Context("root", "example.com");
35
+ const ctx = new Context({ user: "root", address: "1.2.3.4" });
27
36
 
28
- debug("Install bun");
29
- await installBun(context);
37
+ await buildAndRunOverSsh({
38
+ entrypoint: "./server.ts",
39
+ context: ctx,
40
+ });
41
+ ```
30
42
 
31
- debug("Build project");
32
- await build({
33
- outdir: "dist",
34
- entrypoints: ["src/server.ts"],
35
- target: "bun",
43
+ **`server.ts`** — compiled and executed on the remote server:
44
+
45
+ ```ts
46
+ import {
47
+ installPodman,
48
+ createPodmanCommand,
49
+ runPodmanContainerService,
50
+ setupNftable,
51
+ printBlock,
52
+ } from "ramm";
53
+
54
+ printBlock("Firewall");
55
+ await setupNftable({
56
+ allowedIpV4: ["1.2.3.4"],
57
+ allowedPorts: [443],
36
58
  });
37
59
 
38
- debug("Deploy files");
39
- await copyFiles(context, "./dist/", "./dist");
60
+ printBlock("Container");
61
+ await installPodman();
62
+
63
+ const cmd = createPodmanCommand({
64
+ name: "app",
65
+ command: "myregistry.io/app:latest",
66
+ envs: [{ name: "PORT", value: "3000" }],
67
+ });
40
68
 
41
- debug("Start server");
42
- await exec(context, "bun run ./dist/server.js");
69
+ await runPodmanContainerService("app", cmd);
43
70
  ```
44
71
 
45
- ## Core API Reference
72
+ ---
46
73
 
47
- ### Context Class
74
+ ## API
48
75
 
49
- Data storage to connect server
76
+ ### Context
77
+
78
+ Describes the server connection.
50
79
 
51
80
  ```ts
52
- class Context {
53
- constructor(name: string, address: string);
54
- getAddress(): string;
55
- }
81
+ const ctx = new Context({
82
+ user: "root",
83
+ address: "1.2.3.4",
84
+ sshKey: "~/.ssh/id_ed25519", // optional
85
+ sudo: false, // prefix commands with sudo (default: false)
86
+ userspace: false, // use systemd --user scope (default: false)
87
+ });
88
+
89
+ ctx.getAddress(); // "root@1.2.3.4"
90
+ ```
91
+
92
+ ---
93
+
94
+ ### Commands
95
+
96
+ #### `execCommand(command, props?, context?)`
97
+
98
+ Runs a command locally. Throws on non-zero exit code.
99
+
100
+ ```ts
101
+ await execCommand("systemctl restart app");
102
+ await execCommand("mkdir -p /opt/app", {}, ctx); // with sudo if ctx.sudo = true
56
103
  ```
57
104
 
58
- - name: Server username
59
- - address: Server IP/hostname
60
- - getAddress(): Returns "user@host" format
105
+ #### `execCommandMayError(command, props?, context?)`
61
106
 
62
- ### copyFiles
107
+ Same as `execCommand` but does not throw — returns the result with exit code.
63
108
 
64
109
  ```ts
65
- copyFiles(context: Context, from: string, to: string)
110
+ const result = await execCommandMayError("command -v podman", {}, ctx);
111
+ if (result.spawnResult.exitCode !== 0) {
112
+ // podman is not installed
113
+ }
66
114
  ```
67
115
 
68
- Uses rsync to copy files to remote server. Supports directories.
116
+ #### `execCommandOverSsh(command, context)`
69
117
 
70
- Example
118
+ Runs a command on a remote server over SSH.
71
119
 
72
120
  ```ts
73
- await copyFiles(context, "./local", "/remote/path");
121
+ await execCommandOverSsh("systemctl restart app", ctx);
74
122
  ```
75
123
 
76
- ### Exec
124
+ #### `copyFilesOverSsh(from, to, context)`
125
+
126
+ Copies files to the remote server using rsync.
77
127
 
78
128
  ```ts
79
- exec(context: Context, command: string)
129
+ await copyFilesOverSsh("./dist/", "/opt/app/dist", ctx);
80
130
  ```
81
131
 
82
- Executes command on remote server via SSH.
132
+ ---
133
+
134
+ ### Build & Deploy
83
135
 
84
- Example
136
+ #### `buildAndRunOverSsh({ entrypoint, context })`
137
+
138
+ Compiles a TypeScript script with `bun build` and runs it on the remote server. This is the primary usage pattern.
85
139
 
86
140
  ```ts
87
- await exec(ctx, "systemctl restart nginx");
141
+ await buildAndRunOverSsh({
142
+ entrypoint: "./server.ts",
143
+ context: ctx,
144
+ });
88
145
  ```
89
146
 
90
- ### Install system package
147
+ #### `passVarsClient(data, context)` / `passVarsServer(context?)`
148
+
149
+ Passes variables from the local environment into the server script. Call `passVarsClient` before `buildAndRunOverSsh`, then `passVarsServer` inside the server script.
91
150
 
92
151
  ```ts
93
- installSystemPackage(packageName: string)
152
+ // client.ts
153
+ await passVarsClient({ DB_URL: process.env.DB_URL }, ctx);
154
+ await buildAndRunOverSsh({ entrypoint: "./server.ts", context: ctx });
155
+
156
+ // server.ts
157
+ const vars = await passVarsServer();
158
+ console.log(vars.DB_URL);
94
159
  ```
95
160
 
96
- Auto-detects OS and uses appropriate package manager.
161
+ ---
97
162
 
98
- Example
163
+ ### Packages
164
+
165
+ #### `installSystemPackage(package, context?)`
166
+
167
+ Installs a system package. Auto-detects the OS and uses `apt` or `dnf`. Skips if already installed.
99
168
 
100
169
  ```ts
101
- await installSystemPackage("nginx");
170
+ await installSystemPackage("nftables");
171
+
172
+ // with explicit config for a custom package
173
+ await installSystemPackage({
174
+ name: "nginx",
175
+ command: "nginx",
176
+ });
102
177
  ```
103
178
 
104
- ### Install podman
179
+ #### `installBunOverSsh(context)`
180
+
181
+ Installs Bun on a remote server.
105
182
 
106
183
  ```ts
107
- installPodman(packageName: string)
184
+ await installBunOverSsh(ctx);
108
185
  ```
109
186
 
110
- Installs Podman if not present.
187
+ ---
188
+
189
+ ### Podman
111
190
 
112
- Example
191
+ #### `installPodman(context?)`
192
+
193
+ Installs Podman and creates the `ramm` network. Skips if already installed.
113
194
 
114
195
  ```ts
115
196
  await installPodman();
116
197
  ```
117
198
 
118
- ### Install podman
199
+ #### `createPodmanCommand(options)`
200
+
201
+ Builds a `podman run` command string from structured options.
119
202
 
120
203
  ```ts
121
- runPodmanContainer(command: string, name: string)
204
+ const cmd = createPodmanCommand({
205
+ name: "app",
206
+ command: "myregistry.io/app:latest",
207
+ networks: ["ramm"], // default: ["ramm"]
208
+ replace: true, // default: true
209
+ background: true, // default: true
210
+ envs: [{ name: "PORT", value: "3000" }],
211
+ volumes: [{ from: "/data", to: "/data" }],
212
+ });
213
+ // "podman run --name app --replace -d --network ramm -e PORT=3000 -v /data:/data myregistry.io/app:latest"
122
214
  ```
123
215
 
124
- Smart container management:
216
+ #### `runPodmanContainer(name, command, context?)`
125
217
 
126
- - Checks if container exists
127
- - Recreates if configuration changed
128
- - Skips if already running
218
+ Runs a container. Skips if already running with the same command. Recreates if the command changed.
129
219
 
130
- Example
220
+ ```ts
221
+ await runPodmanContainer("app", cmd);
222
+ ```
223
+
224
+ #### `runPodmanContainerService(name, command, context?)`
225
+
226
+ Runs a container and registers it as a systemd service for autostart on reboot.
227
+
228
+ ```ts
229
+ await runPodmanContainerService("app", cmd);
230
+ ```
231
+
232
+ #### `loginPodman(address, login, password, context?)`
233
+
234
+ Authenticates with a container registry.
131
235
 
132
236
  ```ts
133
- await runPodmanContainer("podman run -d --name web nginx", "web");
237
+ await loginPodman("registry.example.com", "user", "password");
134
238
  ```
135
239
 
136
- ### Install bun
240
+ #### `addNftPodmanRule(context?)`
241
+
242
+ Adds an nftables rule to allow Podman network traffic through the firewall.
137
243
 
138
244
  ```ts
139
- installBun(context: Context)
245
+ await addNftPodmanRule();
140
246
  ```
141
247
 
142
- Smart container management:
248
+ ---
249
+
250
+ ### Systemd
251
+
252
+ #### `createSystemdService(name, content, context?)`
253
+
254
+ Writes a unit file, enables and starts the service.
255
+
256
+ ```ts
257
+ await createSystemdService(
258
+ "app.service",
259
+ `[Unit]
260
+ Description=My App
261
+
262
+ [Service]
263
+ ExecStart=bun run /opt/app/server.js
264
+ Restart=always
265
+
266
+ [Install]
267
+ WantedBy=multi-user.target`
268
+ );
269
+ ```
270
+
271
+ #### `createSystemdUnit(name, content, context?)`
272
+
273
+ Writes a unit file and reloads systemd. Does not start the service.
274
+
275
+ #### `startSystemdUnit(name, context?)` / `restartSystemdUnit(name, context?)` / `enableSystemdUnit(name, context?)` / `reloadSystemd(context?)`
276
+
277
+ Systemd unit management.
278
+
279
+ ```ts
280
+ await reloadSystemd();
281
+ await enableSystemdUnit("app.service");
282
+ await startSystemdUnit("app.service");
283
+ ```
284
+
285
+ #### `getSystemdPathToUnit(name, context?)`
286
+
287
+ Returns the path to the unit file — `/etc/systemd/system/` or `~/.config/systemd/user/` for userspace context.
288
+
289
+ ---
290
+
291
+ ### Firewall (nftables)
292
+
293
+ #### `setupNftable({ allowedIpV4, allowedPorts, context? })`
294
+
295
+ Creates an `inet ramm` nftables table. Closes all ports except the ones listed, rate-limits SSH, saves the config, and enables autostart.
296
+
297
+ ```ts
298
+ await setupNftable({
299
+ allowedIpV4: ["1.2.3.4"], // IPs with unrestricted SSH access
300
+ allowedPorts: [80, 443],
301
+ });
302
+ ```
303
+
304
+ ---
305
+
306
+ ### SSH Keys
307
+
308
+ #### `createAndAddSshKey(filePath, comment, context)`
309
+
310
+ Creates an ed25519 key (if it doesn't exist) and adds the public key to `authorized_keys` on the server.
311
+
312
+ ```ts
313
+ await createAndAddSshKey("~/.ssh/deploy_key", "deploy", ctx);
314
+ ```
315
+
316
+ #### `addSshKeyToUse({ key, fingerprint, filePath, server, context? })`
317
+
318
+ Saves a private key locally, adds the fingerprint to `known_hosts`, and configures `~/.ssh/config`.
319
+
320
+ #### `saveSshFingerptint(filePath, context)`
321
+
322
+ Fetches the server's fingerprint and saves it to a file.
323
+
324
+ #### `addKeyToHostConfig(pathToHost, address, pathToKey, context?)`
325
+
326
+ Adds a `Host` block to the SSH config.
327
+
328
+ ---
329
+
330
+ ### Files
331
+
332
+ #### `writeFile(path, content, context?)`
333
+
334
+ Writes a file. Skips if the content is already the same.
335
+
336
+ ```ts
337
+ await writeFile("/etc/app/config.json", JSON.stringify(config));
338
+ ```
339
+
340
+ #### `writeFileStrUniq(path, str, context?)`
341
+
342
+ Appends a string to a file only if it is not already present.
343
+
344
+ ```ts
345
+ await writeFileStrUniq("~/.bashrc", 'export PATH="$PATH:/opt/bin"');
346
+ ```
347
+
348
+ ---
349
+
350
+ ### Cron
351
+
352
+ #### `createCron({ time, pathToFile, context? })`
353
+
354
+ Adds a crontab entry. Skips if the entry already exists. Updates the schedule if the file is already in crontab with a different time.
355
+
356
+ ```ts
357
+ await createCron({
358
+ time: "0 3 * * *",
359
+ pathToFile: "/opt/scripts/backup.sh",
360
+ });
361
+ ```
362
+
363
+ ---
364
+
365
+ ### Utilities
366
+
367
+ #### `printBlock(name)`
368
+
369
+ Prints a named block to the console — useful for structuring deployment output.
370
+
371
+ ```ts
372
+ printBlock("Database"); // → Block: Database
373
+ ```
143
374
 
144
- - Copies installation script to server
145
- - Executes bun.sh on remote host
375
+ #### `normalizePath(path)`
146
376
 
147
- Example
377
+ Expands `~/` to an absolute path.
148
378
 
149
379
  ```ts
150
- await installBun(context);
380
+ normalizePath("~/.ssh/config"); // "/home/user/.ssh/config"
151
381
  ```
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ramm",
3
3
  "type": "module",
4
- "version": "0.0.59",
4
+ "version": "0.0.62",
5
5
  "scripts": {
6
6
  "build": "bun build ./src/ramm.ts --target bun --outdir ./dist && cp ./src/bun.sh ./dist/bun.sh",
7
7
  "types": "tsc --project tsconfig.types.json"
@@ -14,9 +14,13 @@
14
14
  "engines": {
15
15
  "bun": ">=1.0.0"
16
16
  },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/utftu/ramm.git"
20
+ },
17
21
  "devDependencies": {
18
22
  "@types/bun": "latest",
19
23
  "typescript": "^5.8.2",
20
24
  "dapes": "^0.0.26"
21
25
  }
22
- }
26
+ }
package/dist/bun.sh DELETED
@@ -1,23 +0,0 @@
1
- #!/bin/bash
2
-
3
- if ! command -v unzip &> /dev/null; then
4
- . /etc/os-release
5
-
6
- if [[ "$ID_LIKE" == *"rhel"* ]]; then
7
- sudo dnf install -y unzip
8
- fi
9
-
10
- if [[ "$ID" == "ubuntu" ]]; then
11
- sudo apt update
12
- sudo apt install -y unzip
13
- fi
14
- fi
15
-
16
- if ! command -v bun &> /dev/null; then
17
- echo "Installing bun..."
18
-
19
- curl -fsSL https://bun.sh/install | bash
20
- ln -s "$HOME/.bun/bin/bun" /usr/local/bin/bun
21
- else
22
- echo "Already installed bun."
23
- fi
package/dist/ramm.js DELETED
@@ -1,620 +0,0 @@
1
- // @bun
2
- // src/base/base.ts
3
- var {spawn } = globalThis.Bun;
4
-
5
- // src/print.ts
6
- var printInternal = (left, right) => {
7
- console.info(`\x1B[32m${left}:\x1B[0m \x1B[38;5;85m${right}\x1B[0m`);
8
- };
9
- var printCommand = (command) => {
10
- printInternal("Command", command);
11
- };
12
- var printFunction = (func) => {
13
- printInternal("Function", func);
14
- };
15
- var printBlock = (name) => {
16
- console.info(`\x1B[34mBlock:\x1B[0m \x1B[38;5;81m${name}\x1B[0m`);
17
- };
18
-
19
- // src/context.ts
20
- class Context {
21
- user;
22
- domain;
23
- userspace;
24
- sudo;
25
- sshKey;
26
- params = {};
27
- constructor({
28
- user,
29
- address,
30
- userspace = false,
31
- sudo = false,
32
- sshKey
33
- }) {
34
- this.user = user;
35
- this.domain = address;
36
- this.sudo = sudo;
37
- this.userspace = userspace;
38
- this.sshKey = sshKey;
39
- }
40
- getAddress() {
41
- return `${this.user}@${this.domain}`;
42
- }
43
- }
44
-
45
- // src/base/tee.ts
46
- var tee = async (read, write, prefix) => {
47
- const reader = read.getReader();
48
- let leftover = "";
49
- let output = "";
50
- const decoder = new TextDecoder("utf-8");
51
- while (true) {
52
- const { value, done } = await reader.read();
53
- if (done) {
54
- if (leftover)
55
- write(prefix + leftover + `
56
- `);
57
- return output;
58
- }
59
- const decodedOutput = decoder.decode(value, { stream: true });
60
- output += decodedOutput;
61
- const lines = (leftover + decodedOutput).split(`
62
- `);
63
- leftover = lines.pop() ?? "";
64
- for (const line of lines) {
65
- write(prefix + line + `
66
- `);
67
- }
68
- }
69
- };
70
- var teeStdout = (read, prefix) => {
71
- return tee(read, (text) => process.stdout.write(text), prefix);
72
- };
73
- var teeStderr = (read, prefix) => {
74
- return tee(read, (text) => process.stderr.write(text), prefix);
75
- };
76
-
77
- // src/base/base.ts
78
- var defaultContext = new Context({
79
- user: "root",
80
- address: "0.0.0.0",
81
- userspace: false,
82
- sudo: false
83
- });
84
- var execCommandRaw = async (command, { store = {}, signal, env, cwd, prefix = "" } = {}, ctx) => {
85
- const finalCommand = ctx?.sudo ? `sudo ${command}` : command;
86
- const spawnResult = spawn(["bash", "-c", finalCommand], {
87
- stdin: "inherit",
88
- stdout: "pipe",
89
- stderr: "pipe",
90
- signal,
91
- cwd,
92
- env
93
- });
94
- store.spawnResult = spawnResult;
95
- const [stdout, stderr] = await Promise.all([
96
- teeStdout(spawnResult.stdout, prefix),
97
- teeStderr(spawnResult.stderr, prefix)
98
- ]);
99
- await spawnResult.exited;
100
- return {
101
- stderr,
102
- stdout,
103
- spawnResult
104
- };
105
- };
106
- var execCommandMayError = async (command, props, context) => {
107
- printCommand(command);
108
- return execCommandRaw(command, props, context);
109
- };
110
- var execCommand = async (command, props, context) => {
111
- const result = await execCommandMayError(command, props, context);
112
- if (result.spawnResult.exitCode !== 0) {
113
- console.error(`Error exit code: ${result.spawnResult.exitCode}`);
114
- console.error(`Command: ${command}`);
115
- throw new Error(command);
116
- }
117
- return result;
118
- };
119
- var copyFilesBySsh = async (from, to, context) => {
120
- await execCommand(`rsync -avz ${from} ${context.getAddress()}:${to}`);
121
- };
122
- var execCommandOverSsh = async (command, context) => {
123
- const sshKeyPart = context.sshKey ? ` -i ${context.sshKey}` : "";
124
- return await execCommand(`ssh${sshKeyPart} ${context.getAddress()} '${command}'`);
125
- };
126
- // src/init.ts
127
- var installBunOverSsh = async (context) => {
128
- const bunPath = new URL(import.meta.resolve("./bun.sh")).pathname;
129
- await copyFilesBySsh(bunPath, "./bun.sh", context);
130
- await execCommandOverSsh("./bun.sh", context);
131
- };
132
- // src/podman.ts
133
- var {$: $2 } = globalThis.Bun;
134
-
135
- // src/packages.ts
136
- var {$ } = globalThis.Bun;
137
- var dnfOs = ["rocky", "fedora", "alma"];
138
- var aptOs = ["ubuntu"];
139
- var getManagerByOs = (osName) => {
140
- if (aptOs.includes(osName)) {
141
- return "apt";
142
- } else if (dnfOs.includes(osName)) {
143
- return "dnf";
144
- } else {
145
- throw new Error(`Unsupported OS: ${osName}`);
146
- }
147
- };
148
- var getManagerConfig = (manager, packageConfig) => {
149
- if (packageConfig.managers && manager in packageConfig.managers) {
150
- const managerEnt = packageConfig.managers[manager];
151
- return managerEnt;
152
- }
153
- return {
154
- name: packageConfig.name,
155
- command: packageConfig.command
156
- };
157
- };
158
- var getInstallCommand = (manager, config) => {
159
- if (manager === "apt") {
160
- return `apt install -y ${config.name}`;
161
- } else if (manager === "dnf") {
162
- return `dnf install -y ${config.name}`;
163
- } else {
164
- throw new Error("Unknow manager");
165
- }
166
- };
167
- var packages = {
168
- nftables: {
169
- name: "nftables",
170
- command: "nft"
171
- },
172
- podman: {
173
- name: "podman",
174
- command: "podman"
175
- }
176
- };
177
- var installSystemPackage = async (packageEnt, context) => {
178
- const finalPackageEnt = typeof packageEnt === "string" ? packages[packageEnt] : packageEnt;
179
- if (!finalPackageEnt) {
180
- const errorMessage = `No package ent for: ${packageEnt}`;
181
- throw new Error(errorMessage);
182
- }
183
- const osName = (await $`cat /etc/os-release | grep ^ID= | cut -d'=' -f2`.text()).trim().replace(/"/g, "");
184
- const manager = getManagerByOs(osName);
185
- const managerConfig = getManagerConfig(manager, finalPackageEnt);
186
- const checkResult = await execCommandMayError(`command -v ${managerConfig.command}`, {}, context);
187
- if (checkResult.spawnResult.exitCode === 0) {
188
- return;
189
- }
190
- const installCommand = getInstallCommand(manager, managerConfig);
191
- await execCommand(installCommand, {}, context);
192
- if (osName === "ubuntu") {
193
- await execCommand(`apt-get install -y ${managerConfig.name}`, {}, context);
194
- } else if (dnfOs.includes(osName)) {
195
- await execCommand(`dnf install -y ${managerConfig.name}`, {}, context);
196
- } else {
197
- throw new Error(`Unsupported OS: ${osName}`);
198
- }
199
- };
200
-
201
- // src/files.ts
202
- import { appendFile, exists } from "fs/promises";
203
- var {file, write } = globalThis.Bun;
204
-
205
- // src/path.ts
206
- import { homedir } from "os";
207
- import { join } from "path";
208
- function normalizePath(rawFilePath) {
209
- let finalFilePath = rawFilePath.trim();
210
- if (finalFilePath.startsWith("~/")) {
211
- finalFilePath = join(homedir(), finalFilePath.slice(2));
212
- }
213
- return finalFilePath;
214
- }
215
-
216
- // src/files.ts
217
- var normalizeFileContent = (str) => {
218
- if (str === "") {
219
- return str;
220
- }
221
- if (str.at(-1) !== `
222
- `) {
223
- return str + `
224
- `;
225
- }
226
- return str;
227
- };
228
- var createDir = async (str) => {
229
- const dirname = str.split("/").slice(0, -1).join("/");
230
- const exist = await exists(dirname);
231
- if (exist) {
232
- return;
233
- }
234
- await execCommand(`mkdir -p ${dirname}`);
235
- };
236
- var checkStrInFile = async (filePath, str) => {
237
- const file2 = Bun.file(filePath);
238
- if (!await file2.exists()) {
239
- return false;
240
- }
241
- const fileText = await file2.text();
242
- if (fileText.includes(str)) {
243
- return true;
244
- }
245
- return false;
246
- };
247
- var createFileIfNeed = async (rawFilePath) => {
248
- const filePath = normalizePath(rawFilePath);
249
- await createDir(filePath);
250
- if (!await file(filePath).exists()) {
251
- await execCommand(`touch ${filePath}`);
252
- }
253
- };
254
- var writeIfNewStr = async (rawFilePath, str) => {
255
- const filePath = normalizePath(rawFilePath);
256
- await createFileIfNeed(filePath);
257
- if (await checkStrInFile(filePath, str)) {
258
- return;
259
- }
260
- await appendFile(filePath, normalizeFileContent(str));
261
- };
262
- var writeFile = async (pathToFile, str) => {
263
- const normalizedPath = normalizePath(pathToFile);
264
- await createFileIfNeed(normalizedPath);
265
- await write(normalizedPath, str);
266
- };
267
- var writeFileFull = async (pathToFile, str) => {
268
- const normalizedPath = normalizePath(pathToFile);
269
- await createFileIfNeed(normalizedPath);
270
- const fileText = await file(normalizedPath).text();
271
- if (fileText === str) {
272
- return;
273
- }
274
- await write(normalizedPath, str);
275
- };
276
-
277
- // src/systemd.ts
278
- var systemctlWordLangth = "systemctl ".length;
279
- var formatUserspace = (command, context = defaultContext) => {
280
- const userPart = context.userspace ? " --user " : "";
281
- return "systemctl" + userPart + " " + command.slice(systemctlWordLangth);
282
- };
283
- var reloadSystemd = async (context = defaultContext) => {
284
- await execCommand(formatUserspace("systemctl daemon-reload", context));
285
- };
286
- var startSystemdUnit = async (unitName, context = defaultContext) => {
287
- await execCommand(formatUserspace(`systemctl start ${unitName}`, context));
288
- };
289
- var enabledSystemdUnit = async (unitName, context = defaultContext) => {
290
- await execCommand(formatUserspace(`systemctl enable ${unitName}`, context));
291
- };
292
- var restartSystemdUnit = async (name, context = defaultContext) => {
293
- await execCommand(formatUserspace(`systemctl restart ${name}`, context));
294
- };
295
- var stopSystemdUnit = async (name, context = defaultContext) => {
296
- await execCommand(formatUserspace(`systemctl stop ${name}`, context));
297
- };
298
- var checkSystemdUnit = async (serviceName, context = defaultContext) => {
299
- const { spawnResult } = await execCommandMayError(formatUserspace(`systemctl is-active ${serviceName}`, context));
300
- return spawnResult.exitCode === 0;
301
- };
302
- var createSystemdUnit = async (unitName, content, context = defaultContext) => {
303
- const pathToSeviceTarget = getSystemdPathToUnit(unitName, context);
304
- await writeFileFull(pathToSeviceTarget, content);
305
- await reloadSystemd(context);
306
- };
307
- var getSystemdPathToUnit = (serviceName, context = defaultContext) => {
308
- if (context.userspace) {
309
- return `~/.config/systemd/user/${serviceName}`;
310
- }
311
- return `/etc/systemd/system/${serviceName}`;
312
- };
313
- var createSystemdService = async (serviceName, content, context = defaultContext) => {
314
- await createSystemdUnit(serviceName, content, context);
315
- await enabledSystemdUnit(serviceName, context);
316
- await startSystemdUnit(serviceName, context);
317
- };
318
-
319
- // src/nft.ts
320
- var createNftTable = ({
321
- allowedIpV4,
322
- allowedPorts = []
323
- }) => {
324
- const nftTable = `table inet ramm {
325
- set allowed_ipv4 {
326
- type ipv4_addr
327
- flags dynamic
328
- elements = { ${allowedIpV4.join(`
329
- `)} }
330
- }
331
-
332
- chain local_chain_base {
333
- iif "lo" accept
334
- ct state established,related accept
335
- ip saddr @allowed_ipv4 tcp dport 22 accept
336
- tcp dport 22 ct state new limit rate over 700/minute burst 5 packets drop
337
- tcp dport 22 accept
338
- ${allowedPorts.map((port) => {
339
- return `tcp dport ${port} accept`;
340
- }).join(`
341
- `)}
342
- }
343
-
344
- chain local_chain {
345
- jump local_chain_base
346
- drop
347
- }
348
-
349
- chain prerouting {
350
- type filter hook prerouting priority mangle; policy accept;
351
- fib daddr type local jump local_chain
352
- }
353
- }
354
- `;
355
- return nftTable;
356
- };
357
- var setupNftable = async ({
358
- allowedIpV4,
359
- allowedPorts
360
- }) => {
361
- await installSystemPackage("nftables");
362
- const listTable = await execCommandMayError("nft list table inet ramm");
363
- if (listTable.spawnResult.exitCode === 0) {
364
- await execCommand("nft delete table inet ramm");
365
- }
366
- const nftTable = createNftTable({ allowedIpV4, allowedPorts });
367
- await execCommand(`nft -f - <<EOF
368
- ${nftTable}
369
- EOF`);
370
- await safeNftTable();
371
- };
372
- var safeNftTable = async () => {
373
- await execCommand("nft list ruleset > /etc/nftables.conf");
374
- await execCommand("systemctl enable nftables");
375
- };
376
-
377
- // src/podman.ts
378
- var installPodman = async () => {
379
- if ((await $2`command -v podman`.nothrow().quiet()).exitCode !== 0) {
380
- await installSystemPackage("podman");
381
- }
382
- await createNetwork();
383
- };
384
- var createNetwork = async () => {
385
- const netwroks = await execCommand("podman network inspect $(podman network ls -q) -f '{{.NetworkInterface}}'");
386
- const podmanNetworks = await execCommand("podman network ls");
387
- if (podmanNetworks.stdout.includes("ramm")) {
388
- return;
389
- }
390
- if (netwroks.stdout.includes("podman_ramm")) {
391
- return;
392
- }
393
- await execCommand("podman network create --interface-name=podman_ramm ramm");
394
- };
395
- var getCreateCommand = async (name) => {
396
- const podmanCreateCommand = await $2`podman inspect --format '{{.Config.CreateCommand}}' ${name}`.nothrow().quiet();
397
- return podmanCreateCommand.text().slice(0, -1).slice(1, -1) || "";
398
- };
399
- var loginPodman = async (address, login, password) => {
400
- return await execCommand(`echo "${password}" | podman login --username "${login}" --password-stdin ${address}`);
401
- };
402
- var createPodmanCommand = ({
403
- name,
404
- replace = true,
405
- background = true,
406
- networks = ["ramm"],
407
- envs = [],
408
- volumes = [],
409
- command
410
- }) => {
411
- const values = [];
412
- values.push("podman", "run");
413
- if (name) {
414
- values.push(`--name ${name}`);
415
- }
416
- if (replace) {
417
- values.push("--replace");
418
- }
419
- if (background) {
420
- values.push("-d");
421
- }
422
- for (const network of networks) {
423
- values.push(`--network ${network}`);
424
- }
425
- for (const env of envs) {
426
- values.push(`-e ${env.name}=${env.value}`);
427
- }
428
- for (const volume of volumes) {
429
- values.push(`-v ${volume.from}:${volume.to}`);
430
- }
431
- values.push(command);
432
- const str = values.join(" ");
433
- return str;
434
- };
435
- var runPodmanContainer = async (name, command) => {
436
- if (await getCreateCommand(name) !== command) {
437
- await $2`podman rm -f ${name}`;
438
- await execCommand(command);
439
- return;
440
- }
441
- console.info("Podman container is already running");
442
- };
443
- var runPodmanContainerService = async (name, command, context = defaultContext) => {
444
- const serviceName = `${name}.service`;
445
- const filepath = getSystemdPathToUnit(serviceName);
446
- if (await checkSystemdUnit(serviceName, context)) {
447
- await stopSystemdUnit(serviceName, context);
448
- }
449
- await runPodmanContainer(name, command);
450
- await execCommand(`podman generate systemd --name --new ${name} > ${filepath}`);
451
- await reloadSystemd(context);
452
- await startSystemdUnit(serviceName, context);
453
- await enabledSystemdUnit(serviceName, context);
454
- };
455
- var addNftPodmanRule = async () => {
456
- const podmanNetworksResult = await execCommand("podman network inspect $(podman network ls -q) -f '{{.NetworkInterface}}'");
457
- const podmanNetworks = podmanNetworksResult.stdout.trim().split(`
458
- `);
459
- await execCommand(`nft add set inet ramm podman_interfaces '{ type ifname; flags dynamic; elements = { ${podmanNetworks.join(", ")} }; }'`);
460
- await execCommand("nft insert rule inet ramm prerouting iifname @podman_interfaces accept");
461
- await safeNftTable();
462
- };
463
- // src/ssh.ts
464
- var {file: file2 } = globalThis.Bun;
465
- var addKeyToHostConfig = async (pathToHost, address, pathToKey) => {
466
- const text = `Host ${address}
467
- IdentityFile ${pathToKey}
468
- `;
469
- await writeIfNewStr(pathToHost, text);
470
- };
471
- var getServerFingerprint = async (context) => {
472
- const { stdout } = await execCommandOverSsh('ssh-keyscan -t ed25519 localhost | grep -v "^#"', context);
473
- return stdout.replace("localhost", context.domain);
474
- };
475
- var saveSshFingerptint = async (filePath, context) => {
476
- const normalizedPath = normalizePath(filePath);
477
- const fingerprint = await getServerFingerprint(context);
478
- await writeFileFull(filePath, normalizedPath);
479
- };
480
- async function createSshKey(filePath, comment) {
481
- printFunction(`${createSshKey.name} ${filePath}`);
482
- const normalizedPathToKey = normalizePath(filePath);
483
- const name = normalizedPathToKey.split("/").at(-1);
484
- const pathToKeyPub = `${normalizedPathToKey}.pub`;
485
- const pathToDir = normalizedPathToKey.split("/").slice(0, -1).join("/");
486
- const pathToKeyFile = file2(normalizedPathToKey);
487
- const pathToKeyPubFile = file2(pathToKeyPub);
488
- if (await pathToKeyFile.exists() && await pathToKeyPubFile.exists()) {
489
- return await pathToKeyPubFile.text();
490
- }
491
- await execCommand(`mkdir -p ${pathToDir}`);
492
- await execCommand(`ssh-keygen -t ed25519 -f ${normalizedPathToKey} -N "" -C "${comment || name}"`);
493
- await execCommand(`chmod 600 ${normalizedPathToKey}`);
494
- const pubKey = await Bun.file(pathToKeyPub).text();
495
- return pubKey;
496
- }
497
- var addSshKeyToAuthorizedOverSsh = async (pubKey, context) => {
498
- printFunction(`${addSshKeyToAuthorizedOverSsh.name} ${pubKey}`);
499
- const { stdout: keys } = await execCommandOverSsh("cat .ssh/authorized_keys", context);
500
- if (keys.includes(pubKey)) {
501
- return;
502
- }
503
- await execCommandOverSsh(`echo "${pubKey}" >> .ssh/authorized_keys`, context);
504
- };
505
- var createAndAddSshKey = async (filePath, comment, context) => {
506
- const normalizedPath = normalizePath(filePath);
507
- const pubKey = await createSshKey(normalizedPath, comment);
508
- console.log(`Key is: ${pubKey}`);
509
- await addSshKeyToAuthorizedOverSsh(pubKey, context);
510
- };
511
- var addSshKeyToUse = async ({
512
- key,
513
- fingerprint,
514
- filePath,
515
- server
516
- }) => {
517
- printFunction(`${addSshKeyToUse.name} ${filePath}`);
518
- const normalizedFilePath = normalizePath(filePath);
519
- await writeFileFull(normalizedFilePath, key);
520
- await execCommand(`chmod 0600 ${normalizedFilePath}`);
521
- await writeIfNewStr("~/.ssh/known_hosts", fingerprint);
522
- await addKeyToHostConfig("~/.ssh/config", server, normalizedFilePath);
523
- };
524
- // src/cron.ts
525
- var createCron = async ({
526
- time,
527
- pathToFile
528
- }) => {
529
- const pathToFileNorm = normalizePath(pathToFile);
530
- const constructedLine = `${time} ${pathToFileNorm}`;
531
- const tempFile = `/tmp/ramm_cron}`;
532
- const { stdout: cronConfig } = await execCommandMayError("crontab -l");
533
- let newCronConfig = cronConfig;
534
- if (cronConfig.includes(constructedLine)) {
535
- return;
536
- }
537
- if (cronConfig.includes(pathToFileNorm)) {
538
- newCronConfig = cronConfig.split(`
539
- `).filter((str) => !str.includes(pathToFileNorm)).join(`
540
- `);
541
- }
542
- newCronConfig = normalizeFileContent(normalizeFileContent(cronConfig) + constructedLine);
543
- await writeFile(tempFile, newCronConfig);
544
- await execCommandMayError(`cat ${tempFile}`);
545
- await execCommandMayError(`crontab ${tempFile}`);
546
- await execCommandMayError(`rm ${tempFile}`);
547
- };
548
- // src/build.ts
549
- var {build, file: file3 } = globalThis.Bun;
550
- var buildAndRunOverSsh = async ({
551
- entrypoint,
552
- context
553
- }) => {
554
- const normalizedEntrypoint = normalizePath(entrypoint);
555
- const distName = `ramm_dist`;
556
- const distDir = `/tmp/${distName}`;
557
- const outputs = await build({
558
- outdir: distDir,
559
- entrypoints: [normalizedEntrypoint],
560
- target: "bun"
561
- });
562
- const pathToDistFile = outputs.outputs[0]?.path;
563
- const relativePathToFile = pathToDistFile.slice(distDir.length + 1);
564
- await copyFilesBySsh(`${distDir}/`, distDir, context);
565
- await execCommandOverSsh(`bun run ${distDir}/${relativePathToFile}`, context);
566
- await execCommand(`rm -rf ${distDir}`);
567
- };
568
- var pathToJson = "/tmp/ramm_json";
569
- var passVarsClient = async (data, context) => {
570
- printFunction("passVarsClient");
571
- const json = JSON.stringify(data);
572
- await writeFile(pathToJson, json);
573
- await copyFilesBySsh(pathToJson, pathToJson, context);
574
- await execCommand(`rm -rf ${pathToJson}`);
575
- };
576
- var passVarsServer = async () => {
577
- printFunction("passVarsServer");
578
- const jsonData = await file3(pathToJson).json();
579
- await execCommand(`rm -rf ${pathToJson}`);
580
- return jsonData;
581
- };
582
- export {
583
- writeIfNewStr,
584
- writeFileFull,
585
- writeFile,
586
- startSystemdUnit,
587
- setupNftable,
588
- saveSshFingerptint,
589
- runPodmanContainerService,
590
- runPodmanContainer,
591
- restartSystemdUnit,
592
- reloadSystemd,
593
- printBlock,
594
- passVarsServer,
595
- passVarsClient,
596
- normalizePath,
597
- normalizeFileContent,
598
- loginPodman,
599
- installSystemPackage,
600
- installPodman,
601
- installBunOverSsh,
602
- getSystemdPathToUnit as getSystemdPathToService,
603
- getServerFingerprint,
604
- execCommandRaw,
605
- execCommandOverSsh,
606
- execCommandMayError,
607
- execCommand,
608
- enabledSystemdUnit,
609
- createSystemdUnit,
610
- createSystemdService,
611
- createPodmanCommand,
612
- createCron,
613
- createAndAddSshKey,
614
- copyFilesBySsh,
615
- buildAndRunOverSsh,
616
- addSshKeyToUse,
617
- addNftPodmanRule,
618
- addKeyToHostConfig,
619
- Context
620
- };
@@ -1,35 +0,0 @@
1
- import { type Subprocess } from "bun";
2
- import { Context } from "../context.ts";
3
- export declare const defaultContext: Context;
4
- export type ExecCommandStore = {
5
- spawnResult?: Subprocess<"inherit", "pipe", "pipe">;
6
- };
7
- type ExecProps = {
8
- store?: ExecCommandStore;
9
- env?: Record<string, string>;
10
- cwd?: string;
11
- prefix?: string;
12
- signal?: AbortSignal;
13
- } | void;
14
- export declare const execCommandRaw: (command: string, { store, signal, env, cwd, prefix }?: ExecProps, ctx?: Context) => Promise<{
15
- stderr: string;
16
- stdout: string;
17
- spawnResult: Subprocess<"inherit", "pipe", "pipe">;
18
- }>;
19
- export declare const execCommandMayError: (command: string, props: ExecProps, context?: Context) => Promise<{
20
- stderr: string;
21
- stdout: string;
22
- spawnResult: Subprocess<"inherit", "pipe", "pipe">;
23
- }>;
24
- export declare const execCommand: (command: string, props: ExecProps, context?: Context) => Promise<{
25
- stderr: string;
26
- stdout: string;
27
- spawnResult: Subprocess<"inherit", "pipe", "pipe">;
28
- }>;
29
- export declare const copyFilesBySsh: (from: string, to: string, context: Context) => Promise<void>;
30
- export declare const execCommandOverSsh: (command: string, context: Context) => Promise<{
31
- stderr: string;
32
- stdout: string;
33
- spawnResult: Subprocess<"inherit", "pipe", "pipe">;
34
- }>;
35
- export {};
@@ -1,2 +0,0 @@
1
- export declare const teeStdout: (read: ReadableStream, prefix: string) => Promise<string>;
2
- export declare const teeStderr: (read: ReadableStream, prefix: string) => Promise<string>;
@@ -1,7 +0,0 @@
1
- import type { Context } from "./context.ts";
2
- export declare const buildAndRunOverSsh: ({ entrypoint, context, }: {
3
- entrypoint: string;
4
- context: Context;
5
- }) => Promise<void>;
6
- export declare const passVarsClient: (data: Record<string, any>, context: Context) => Promise<void>;
7
- export declare const passVarsServer: () => Promise<any>;
@@ -1,16 +0,0 @@
1
- export declare class Context {
2
- user: string;
3
- domain: string;
4
- userspace: boolean;
5
- sudo: boolean;
6
- sshKey?: string;
7
- params: Record<string, string>;
8
- constructor({ user, address, userspace, sudo, sshKey, }: {
9
- user: string;
10
- address: string;
11
- userspace?: boolean;
12
- sudo?: boolean;
13
- sshKey?: string;
14
- });
15
- getAddress(): string;
16
- }
@@ -1,4 +0,0 @@
1
- export declare const createCron: ({ time, pathToFile, }: {
2
- time: string;
3
- pathToFile: string;
4
- }) => Promise<void>;
@@ -1,4 +0,0 @@
1
- export declare const normalizeFileContent: (str: string) => string;
2
- export declare const writeIfNewStr: (rawFilePath: string, str: string) => Promise<void>;
3
- export declare const writeFile: (pathToFile: string, str: string) => Promise<void>;
4
- export declare const writeFileFull: (pathToFile: string, str: string) => Promise<void>;
@@ -1,2 +0,0 @@
1
- import type { Context } from "./context.ts";
2
- export declare const installBunOverSsh: (context: Context) => Promise<void>;
@@ -1,6 +0,0 @@
1
- export declare const localNftChainName = "local_chain";
2
- export declare const setupNftable: ({ allowedIpV4, allowedPorts, }: {
3
- allowedIpV4: string[];
4
- allowedPorts: string[];
5
- }) => Promise<void>;
6
- export declare const safeNftTable: () => Promise<void>;
@@ -1,13 +0,0 @@
1
- import type { Context } from "./context.ts";
2
- type Manager = "dnf" | "apt";
3
- type PackageConfig = {
4
- name: string;
5
- command: string;
6
- managers?: Record<Manager, ManagerConfig>;
7
- };
8
- type ManagerConfig = {
9
- name: string;
10
- command: string;
11
- };
12
- export declare const installSystemPackage: (packageEnt: string | PackageConfig, context?: Context) => Promise<void>;
13
- export {};
@@ -1 +0,0 @@
1
- export declare function normalizePath(rawFilePath: string): string;
@@ -1,25 +0,0 @@
1
- import type { Context } from "./context.ts";
2
- export declare const installPodman: () => Promise<void>;
3
- export declare const loginPodman: (address: string, login: string, password: string) => Promise<{
4
- stderr: string;
5
- stdout: string;
6
- spawnResult: import("bun").Subprocess<"inherit", "pipe", "pipe">;
7
- }>;
8
- export declare const createPodmanCommand: ({ name, replace, background, networks, envs, volumes, command, }: {
9
- name?: string;
10
- replace?: boolean;
11
- background?: boolean;
12
- networks?: string[];
13
- envs?: {
14
- name: string;
15
- value: string;
16
- }[];
17
- volumes?: {
18
- from: string;
19
- to: string;
20
- }[];
21
- command: string;
22
- }) => string;
23
- export declare const runPodmanContainer: (name: string, command: string) => Promise<void>;
24
- export declare const runPodmanContainerService: (name: string, command: string, context?: Context) => Promise<void>;
25
- export declare const addNftPodmanRule: () => Promise<void>;
@@ -1,3 +0,0 @@
1
- export declare const printCommand: (command: string) => void;
2
- export declare const printFunction: (func: string) => void;
3
- export declare const printBlock: (name: string) => void;
@@ -1,13 +0,0 @@
1
- export { execCommandOverSsh, execCommand, execCommandMayError, copyFilesBySsh, execCommandRaw, } from "./base/base.ts";
2
- export { Context } from "./context.ts";
3
- export { installBunOverSsh } from "./init.ts";
4
- export { installPodman, runPodmanContainer, loginPodman, runPodmanContainerService, addNftPodmanRule, createPodmanCommand, } from "./podman.ts";
5
- export { startSystemdUnit, enabledSystemdUnit, restartSystemdUnit, createSystemdService, getSystemdPathToUnit as getSystemdPathToService, createSystemdUnit, reloadSystemd, } from "./systemd.ts";
6
- export { installSystemPackage } from "./packages.ts";
7
- export { printBlock } from "./print.ts";
8
- export { setupNftable } from "./nft.ts";
9
- export { writeIfNewStr, writeFile, writeFileFull, normalizeFileContent, } from "./files.ts";
10
- export { createAndAddSshKey, getServerFingerprint, addKeyToHostConfig, addSshKeyToUse, saveSshFingerptint, } from "./ssh.ts";
11
- export { normalizePath } from "./path.ts";
12
- export { createCron } from "./cron.ts";
13
- export { buildAndRunOverSsh, passVarsClient, passVarsServer } from "./build.ts";
@@ -1,11 +0,0 @@
1
- import type { Context } from "./context.ts";
2
- export declare const addKeyToHostConfig: (pathToHost: string, address: string, pathToKey: string) => Promise<void>;
3
- export declare const getServerFingerprint: (context: Context) => Promise<string>;
4
- export declare const saveSshFingerptint: (filePath: string, context: Context) => Promise<void>;
5
- export declare const createAndAddSshKey: (filePath: string, comment: string, context: Context) => Promise<void>;
6
- export declare const addSshKeyToUse: ({ key, fingerprint, filePath, server, }: {
7
- key: string;
8
- fingerprint: string;
9
- filePath: string;
10
- server: string;
11
- }) => Promise<void>;
@@ -1,10 +0,0 @@
1
- import { Context } from "./context.ts";
2
- export declare const reloadSystemd: (context?: Context) => Promise<void>;
3
- export declare const startSystemdUnit: (unitName: string, context?: Context) => Promise<void>;
4
- export declare const enabledSystemdUnit: (unitName: string, context?: Context) => Promise<void>;
5
- export declare const restartSystemdUnit: (name: string, context?: Context) => Promise<void>;
6
- export declare const stopSystemdUnit: (name: string, context?: Context) => Promise<void>;
7
- export declare const checkSystemdUnit: (serviceName: string, context?: Context) => Promise<boolean>;
8
- export declare const createSystemdUnit: (unitName: string, content: string, context?: Context) => Promise<void>;
9
- export declare const getSystemdPathToUnit: (serviceName: string, context?: Context) => string;
10
- export declare const createSystemdService: (serviceName: string, content: string, context?: Context) => Promise<void>;