ramm 0.0.59 → 0.0.63

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/dist/ramm.js CHANGED
@@ -20,8 +20,8 @@ var printBlock = (name) => {
20
20
  class Context {
21
21
  user;
22
22
  domain;
23
- userspace;
24
- sudo;
23
+ userspace = false;
24
+ sudo = false;
25
25
  sshKey;
26
26
  params = {};
27
27
  constructor({
@@ -108,32 +108,33 @@ var execCommandMayError = async (command, props, context) => {
108
108
  return execCommandRaw(command, props, context);
109
109
  };
110
110
  var execCommand = async (command, props, context) => {
111
+ const callSiteError = new Error(command);
111
112
  const result = await execCommandMayError(command, props, context);
112
113
  if (result.spawnResult.exitCode !== 0) {
113
- console.error(`Error exit code: ${result.spawnResult.exitCode}`);
114
- console.error(`Command: ${command}`);
115
- throw new Error(command);
114
+ console.error(`<${command}> exitCode: ${result.spawnResult.exitCode}`);
115
+ callSiteError.stack = callSiteError.stack?.split(`
116
+ `).slice(1).join(`
117
+ `);
118
+ throw callSiteError;
116
119
  }
117
120
  return result;
118
121
  };
119
- var copyFilesBySsh = async (from, to, context) => {
120
- await execCommand(`rsync -avz ${from} ${context.getAddress()}:${to}`);
122
+ var copyFilesOverSsh = async (from, to, context) => {
123
+ const sshKeyPart = context.sshKey ? ` -e "ssh -i ${context.sshKey}"` : "";
124
+ await execCommand(`rsync -avz${sshKeyPart} ${from} ${context.getAddress()}:${to}`);
121
125
  };
122
126
  var execCommandOverSsh = async (command, context) => {
123
127
  const sshKeyPart = context.sshKey ? ` -i ${context.sshKey}` : "";
124
- return await execCommand(`ssh${sshKeyPart} ${context.getAddress()} '${command}'`);
128
+ const escapedCommand = command.replace(/'/g, "'\\''");
129
+ return await execCommand(`ssh${sshKeyPart} ${context.getAddress()} '${escapedCommand}'`);
125
130
  };
126
131
  // src/init.ts
127
132
  var installBunOverSsh = async (context) => {
128
133
  const bunPath = new URL(import.meta.resolve("./bun.sh")).pathname;
129
- await copyFilesBySsh(bunPath, "./bun.sh", context);
134
+ await copyFilesOverSsh(bunPath, "./bun.sh", context);
130
135
  await execCommandOverSsh("./bun.sh", context);
131
136
  };
132
- // src/podman.ts
133
- var {$: $2 } = globalThis.Bun;
134
-
135
137
  // src/packages.ts
136
- var {$ } = globalThis.Bun;
137
138
  var dnfOs = ["rocky", "fedora", "alma"];
138
139
  var aptOs = ["ubuntu"];
139
140
  var getManagerByOs = (osName) => {
@@ -180,7 +181,8 @@ var installSystemPackage = async (packageEnt, context) => {
180
181
  const errorMessage = `No package ent for: ${packageEnt}`;
181
182
  throw new Error(errorMessage);
182
183
  }
183
- const osName = (await $`cat /etc/os-release | grep ^ID= | cut -d'=' -f2`.text()).trim().replace(/"/g, "");
184
+ const { stdout: osRaw } = await execCommand("cat /etc/os-release | grep ^ID= | cut -d'=' -f2", {}, context);
185
+ const osName = osRaw.trim().replace(/"/g, "");
184
186
  const manager = getManagerByOs(osName);
185
187
  const managerConfig = getManagerConfig(manager, finalPackageEnt);
186
188
  const checkResult = await execCommandMayError(`command -v ${managerConfig.command}`, {}, context);
@@ -189,13 +191,6 @@ var installSystemPackage = async (packageEnt, context) => {
189
191
  }
190
192
  const installCommand = getInstallCommand(manager, managerConfig);
191
193
  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
194
  };
200
195
 
201
196
  // src/files.ts
@@ -225,13 +220,13 @@ var normalizeFileContent = (str) => {
225
220
  }
226
221
  return str;
227
222
  };
228
- var createDir = async (str) => {
223
+ var createDir = async (str, context = defaultContext) => {
229
224
  const dirname = str.split("/").slice(0, -1).join("/");
230
225
  const exist = await exists(dirname);
231
226
  if (exist) {
232
227
  return;
233
228
  }
234
- await execCommand(`mkdir -p ${dirname}`);
229
+ await execCommand(`mkdir -p ${dirname}`, {}, context);
235
230
  };
236
231
  var checkStrInFile = async (filePath, str) => {
237
232
  const file2 = Bun.file(filePath);
@@ -244,29 +239,24 @@ var checkStrInFile = async (filePath, str) => {
244
239
  }
245
240
  return false;
246
241
  };
247
- var createFileIfNeed = async (rawFilePath) => {
242
+ var createFileIfNeed = async (rawFilePath, context = defaultContext) => {
248
243
  const filePath = normalizePath(rawFilePath);
249
- await createDir(filePath);
244
+ await createDir(filePath, context);
250
245
  if (!await file(filePath).exists()) {
251
- await execCommand(`touch ${filePath}`);
246
+ await execCommand(`touch ${filePath}`, {}, context);
252
247
  }
253
248
  };
254
- var writeIfNewStr = async (rawFilePath, str) => {
249
+ var writeFileStrUniq = async (rawFilePath, str, context = defaultContext) => {
255
250
  const filePath = normalizePath(rawFilePath);
256
- await createFileIfNeed(filePath);
251
+ await createFileIfNeed(filePath, context);
257
252
  if (await checkStrInFile(filePath, str)) {
258
253
  return;
259
254
  }
260
255
  await appendFile(filePath, normalizeFileContent(str));
261
256
  };
262
- var writeFile = async (pathToFile, str) => {
257
+ var writeFile = async (pathToFile, str, context = defaultContext) => {
263
258
  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);
259
+ await createFileIfNeed(normalizedPath, context);
270
260
  const fileText = await file(normalizedPath).text();
271
261
  if (fileText === str) {
272
262
  return;
@@ -286,7 +276,7 @@ var reloadSystemd = async (context = defaultContext) => {
286
276
  var startSystemdUnit = async (unitName, context = defaultContext) => {
287
277
  await execCommand(formatUserspace(`systemctl start ${unitName}`, context));
288
278
  };
289
- var enabledSystemdUnit = async (unitName, context = defaultContext) => {
279
+ var enableSystemdUnit = async (unitName, context = defaultContext) => {
290
280
  await execCommand(formatUserspace(`systemctl enable ${unitName}`, context));
291
281
  };
292
282
  var restartSystemdUnit = async (name, context = defaultContext) => {
@@ -301,7 +291,7 @@ var checkSystemdUnit = async (serviceName, context = defaultContext) => {
301
291
  };
302
292
  var createSystemdUnit = async (unitName, content, context = defaultContext) => {
303
293
  const pathToSeviceTarget = getSystemdPathToUnit(unitName, context);
304
- await writeFileFull(pathToSeviceTarget, content);
294
+ await writeFile(pathToSeviceTarget, content);
305
295
  await reloadSystemd(context);
306
296
  };
307
297
  var getSystemdPathToUnit = (serviceName, context = defaultContext) => {
@@ -312,7 +302,7 @@ var getSystemdPathToUnit = (serviceName, context = defaultContext) => {
312
302
  };
313
303
  var createSystemdService = async (serviceName, content, context = defaultContext) => {
314
304
  await createSystemdUnit(serviceName, content, context);
315
- await enabledSystemdUnit(serviceName, context);
305
+ await enableSystemdUnit(serviceName, context);
316
306
  await startSystemdUnit(serviceName, context);
317
307
  };
318
308
 
@@ -356,48 +346,54 @@ var createNftTable = ({
356
346
  };
357
347
  var setupNftable = async ({
358
348
  allowedIpV4,
359
- allowedPorts
349
+ allowedPorts,
350
+ context = defaultContext
360
351
  }) => {
361
- await installSystemPackage("nftables");
362
- const listTable = await execCommandMayError("nft list table inet ramm");
352
+ await installSystemPackage("nftables", context);
353
+ const listTable = await execCommandMayError("nft list table inet ramm", {}, context);
363
354
  if (listTable.spawnResult.exitCode === 0) {
364
- await execCommand("nft delete table inet ramm");
355
+ await execCommand("nft delete table inet ramm", {}, context);
365
356
  }
366
357
  const nftTable = createNftTable({ allowedIpV4, allowedPorts });
367
358
  await execCommand(`nft -f - <<EOF
368
359
  ${nftTable}
369
- EOF`);
370
- await safeNftTable();
360
+ EOF`, {}, context);
361
+ await safeNftTable(context);
371
362
  };
372
- var safeNftTable = async () => {
373
- await execCommand("nft list ruleset > /etc/nftables.conf");
374
- await execCommand("systemctl enable nftables");
363
+ var safeNftTable = async (context = defaultContext) => {
364
+ await execCommand("nft list ruleset > /etc/nftables.conf", {}, context);
365
+ await execCommand("systemctl enable nftables", {}, context);
375
366
  };
376
367
 
377
368
  // src/podman.ts
378
- var installPodman = async () => {
379
- if ((await $2`command -v podman`.nothrow().quiet()).exitCode !== 0) {
380
- await installSystemPackage("podman");
369
+ var installPodman = async (context = defaultContext) => {
370
+ const check = await execCommandMayError("command -v podman", {}, context);
371
+ if (check.spawnResult.exitCode !== 0) {
372
+ await installSystemPackage("podman", context);
381
373
  }
382
- await createNetwork();
374
+ await createNetwork(context);
383
375
  };
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");
376
+ var createNetwork = async (context = defaultContext) => {
377
+ const netwroks = await execCommand("podman network inspect $(podman network ls -q) -f '{{.NetworkInterface}}'", {}, context);
378
+ const podmanNetworks = await execCommand("podman network ls", {}, context);
387
379
  if (podmanNetworks.stdout.includes("ramm")) {
388
380
  return;
389
381
  }
390
382
  if (netwroks.stdout.includes("podman_ramm")) {
391
383
  return;
392
384
  }
393
- await execCommand("podman network create --interface-name=podman_ramm ramm");
385
+ await execCommand("podman network create --interface-name=podman_ramm ramm", {}, context);
394
386
  };
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) || "";
387
+ var getCreateCommand = async (name, context = defaultContext) => {
388
+ const result = await execCommandMayError(`podman inspect --format '{{.Config.CreateCommand}}' ${name}`, {}, context);
389
+ const text = result.stdout.trim();
390
+ if (text.startsWith("[") && text.endsWith("]")) {
391
+ return text.slice(1, -1);
392
+ }
393
+ return text;
398
394
  };
399
- var loginPodman = async (address, login, password) => {
400
- return await execCommand(`echo "${password}" | podman login --username "${login}" --password-stdin ${address}`);
395
+ var loginPodman = async (address, login, password, context = defaultContext) => {
396
+ return await execCommand(`echo "${password}" | podman login --username "${login}" --password-stdin ${address}`, {}, context);
401
397
  };
402
398
  var createPodmanCommand = ({
403
399
  name,
@@ -432,10 +428,10 @@ var createPodmanCommand = ({
432
428
  const str = values.join(" ");
433
429
  return str;
434
430
  };
435
- var runPodmanContainer = async (name, command) => {
436
- if (await getCreateCommand(name) !== command) {
437
- await $2`podman rm -f ${name}`;
438
- await execCommand(command);
431
+ var runPodmanContainer = async (name, command, context = defaultContext) => {
432
+ if (await getCreateCommand(name, context) !== command) {
433
+ await execCommand(`podman rm -f ${name}`, {}, context);
434
+ await execCommand(command, {}, context);
439
435
  return;
440
436
  }
441
437
  console.info("Podman container is already running");
@@ -446,27 +442,27 @@ var runPodmanContainerService = async (name, command, context = defaultContext)
446
442
  if (await checkSystemdUnit(serviceName, context)) {
447
443
  await stopSystemdUnit(serviceName, context);
448
444
  }
449
- await runPodmanContainer(name, command);
450
- await execCommand(`podman generate systemd --name --new ${name} > ${filepath}`);
445
+ await runPodmanContainer(name, command, context);
446
+ await execCommand(`podman generate systemd --name --new ${name} > ${filepath}`, {}, context);
451
447
  await reloadSystemd(context);
452
448
  await startSystemdUnit(serviceName, context);
453
- await enabledSystemdUnit(serviceName, context);
449
+ await enableSystemdUnit(serviceName, context);
454
450
  };
455
- var addNftPodmanRule = async () => {
456
- const podmanNetworksResult = await execCommand("podman network inspect $(podman network ls -q) -f '{{.NetworkInterface}}'");
451
+ var addNftPodmanRule = async (context = defaultContext) => {
452
+ const podmanNetworksResult = await execCommand("podman network inspect $(podman network ls -q) -f '{{.NetworkInterface}}'", {}, context);
457
453
  const podmanNetworks = podmanNetworksResult.stdout.trim().split(`
458
454
  `);
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();
455
+ await execCommand(`nft add set inet ramm podman_interfaces '{ type ifname; flags dynamic; elements = { ${podmanNetworks.join(", ")} }; }'`, {}, context);
456
+ await execCommand("nft insert rule inet ramm prerouting iifname @podman_interfaces accept", {}, context);
457
+ await safeNftTable(context);
462
458
  };
463
459
  // src/ssh.ts
464
460
  var {file: file2 } = globalThis.Bun;
465
- var addKeyToHostConfig = async (pathToHost, address, pathToKey) => {
461
+ var addKeyToHostConfig = async (pathToHost, address, pathToKey, context = defaultContext) => {
466
462
  const text = `Host ${address}
467
463
  IdentityFile ${pathToKey}
468
464
  `;
469
- await writeIfNewStr(pathToHost, text);
465
+ await writeFileStrUniq(pathToHost, text, context);
470
466
  };
471
467
  var getServerFingerprint = async (context) => {
472
468
  const { stdout } = await execCommandOverSsh('ssh-keyscan -t ed25519 localhost | grep -v "^#"', context);
@@ -475,9 +471,9 @@ var getServerFingerprint = async (context) => {
475
471
  var saveSshFingerptint = async (filePath, context) => {
476
472
  const normalizedPath = normalizePath(filePath);
477
473
  const fingerprint = await getServerFingerprint(context);
478
- await writeFileFull(filePath, normalizedPath);
474
+ await writeFile(normalizedPath, fingerprint, context);
479
475
  };
480
- async function createSshKey(filePath, comment) {
476
+ async function createSshKey(filePath, comment, context = defaultContext) {
481
477
  printFunction(`${createSshKey.name} ${filePath}`);
482
478
  const normalizedPathToKey = normalizePath(filePath);
483
479
  const name = normalizedPathToKey.split("/").at(-1);
@@ -488,9 +484,9 @@ async function createSshKey(filePath, comment) {
488
484
  if (await pathToKeyFile.exists() && await pathToKeyPubFile.exists()) {
489
485
  return await pathToKeyPubFile.text();
490
486
  }
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}`);
487
+ await execCommand(`mkdir -p ${pathToDir}`, {}, context);
488
+ await execCommand(`ssh-keygen -t ed25519 -f ${normalizedPathToKey} -N "" -C "${comment || name}"`, {}, context);
489
+ await execCommand(`chmod 600 ${normalizedPathToKey}`, {}, context);
494
490
  const pubKey = await Bun.file(pathToKeyPub).text();
495
491
  return pubKey;
496
492
  }
@@ -504,7 +500,7 @@ var addSshKeyToAuthorizedOverSsh = async (pubKey, context) => {
504
500
  };
505
501
  var createAndAddSshKey = async (filePath, comment, context) => {
506
502
  const normalizedPath = normalizePath(filePath);
507
- const pubKey = await createSshKey(normalizedPath, comment);
503
+ const pubKey = await createSshKey(normalizedPath, comment, context);
508
504
  console.log(`Key is: ${pubKey}`);
509
505
  await addSshKeyToAuthorizedOverSsh(pubKey, context);
510
506
  };
@@ -512,38 +508,40 @@ var addSshKeyToUse = async ({
512
508
  key,
513
509
  fingerprint,
514
510
  filePath,
515
- server
511
+ server,
512
+ context = defaultContext
516
513
  }) => {
517
514
  printFunction(`${addSshKeyToUse.name} ${filePath}`);
518
515
  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);
516
+ await writeFile(normalizedFilePath, key, context);
517
+ await execCommand(`chmod 0600 ${normalizedFilePath}`, {}, context);
518
+ await writeFileStrUniq("~/.ssh/known_hosts", fingerprint, context);
519
+ await addKeyToHostConfig("~/.ssh/config", server, normalizedFilePath, context);
523
520
  };
524
521
  // src/cron.ts
525
522
  var createCron = async ({
526
523
  time,
527
- pathToFile
524
+ pathToFile,
525
+ context = defaultContext
528
526
  }) => {
529
527
  const pathToFileNorm = normalizePath(pathToFile);
530
528
  const constructedLine = `${time} ${pathToFileNorm}`;
531
- const tempFile = `/tmp/ramm_cron}`;
532
- const { stdout: cronConfig } = await execCommandMayError("crontab -l");
533
- let newCronConfig = cronConfig;
529
+ const tempFile = `/tmp/ramm_cron`;
530
+ const { stdout: cronConfig } = await execCommandMayError("crontab -l", {}, context);
534
531
  if (cronConfig.includes(constructedLine)) {
535
532
  return;
536
533
  }
534
+ let baseCronConfig = cronConfig;
537
535
  if (cronConfig.includes(pathToFileNorm)) {
538
- newCronConfig = cronConfig.split(`
536
+ baseCronConfig = cronConfig.split(`
539
537
  `).filter((str) => !str.includes(pathToFileNorm)).join(`
540
538
  `);
541
539
  }
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}`);
540
+ const newCronConfig = normalizeFileContent(normalizeFileContent(baseCronConfig) + constructedLine);
541
+ await writeFile(tempFile, newCronConfig, context);
542
+ await execCommandMayError(`cat ${tempFile}`, {}, context);
543
+ await execCommandMayError(`crontab ${tempFile}`, {}, context);
544
+ await execCommandMayError(`rm ${tempFile}`, {}, context);
547
545
  };
548
546
  // src/build.ts
549
547
  var {build, file: file3 } = globalThis.Bun;
@@ -561,27 +559,26 @@ var buildAndRunOverSsh = async ({
561
559
  });
562
560
  const pathToDistFile = outputs.outputs[0]?.path;
563
561
  const relativePathToFile = pathToDistFile.slice(distDir.length + 1);
564
- await copyFilesBySsh(`${distDir}/`, distDir, context);
562
+ await copyFilesOverSsh(`${distDir}/`, distDir, context);
565
563
  await execCommandOverSsh(`bun run ${distDir}/${relativePathToFile}`, context);
566
- await execCommand(`rm -rf ${distDir}`);
564
+ await execCommand(`rm -rf ${distDir}`, {}, context);
567
565
  };
568
566
  var pathToJson = "/tmp/ramm_json";
569
567
  var passVarsClient = async (data, context) => {
570
568
  printFunction("passVarsClient");
571
569
  const json = JSON.stringify(data);
572
- await writeFile(pathToJson, json);
573
- await copyFilesBySsh(pathToJson, pathToJson, context);
574
- await execCommand(`rm -rf ${pathToJson}`);
570
+ await writeFile(pathToJson, json, context);
571
+ await copyFilesOverSsh(pathToJson, pathToJson, context);
572
+ await execCommand(`rm -rf ${pathToJson}`, {}, context);
575
573
  };
576
- var passVarsServer = async () => {
574
+ var passVarsServer = async (context = defaultContext) => {
577
575
  printFunction("passVarsServer");
578
576
  const jsonData = await file3(pathToJson).json();
579
- await execCommand(`rm -rf ${pathToJson}`);
577
+ await execCommand(`rm -rf ${pathToJson}`, {}, context);
580
578
  return jsonData;
581
579
  };
582
580
  export {
583
- writeIfNewStr,
584
- writeFileFull,
581
+ writeFileStrUniq,
585
582
  writeFile,
586
583
  startSystemdUnit,
587
584
  setupNftable,
@@ -599,19 +596,19 @@ export {
599
596
  installSystemPackage,
600
597
  installPodman,
601
598
  installBunOverSsh,
602
- getSystemdPathToUnit as getSystemdPathToService,
599
+ getSystemdPathToUnit,
603
600
  getServerFingerprint,
604
601
  execCommandRaw,
605
602
  execCommandOverSsh,
606
603
  execCommandMayError,
607
604
  execCommand,
608
- enabledSystemdUnit,
605
+ enableSystemdUnit,
609
606
  createSystemdUnit,
610
607
  createSystemdService,
611
608
  createPodmanCommand,
612
609
  createCron,
613
610
  createAndAddSshKey,
614
- copyFilesBySsh,
611
+ copyFilesOverSsh,
615
612
  buildAndRunOverSsh,
616
613
  addSshKeyToUse,
617
614
  addNftPodmanRule,
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.63",
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
+ }
@@ -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>;