21 Commits

Author SHA1 Message Date
c3a6957a9e chore(changelog): update changelog for v0.1.0
All checks were successful
Upload Assets / upload-assets (release) Successful in 15s
2025-05-21 01:48:51 +00:00
10060db8cb chore(version): add initial version file
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 3s
Auto Changelog & Release / changelog-only (push) Has been skipped
Auto Changelog & Release / release (push) Successful in 6s
- Introduces a VERSION file to track the project's version
- Sets the initial version to 0.1.0
2025-05-21 03:48:39 +02:00
283a0d3905 chore(changelog): update unreleased changelog 2025-05-21 01:47:52 +00:00
1012ca5378 feat(workflows): add release asset upload workflow
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 3s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 5s
- Introduces a workflow to upload release assets in Gitea
- Executes on published release events to associate artifacts
- Ensures artifacts match the release state using tagged checkout
2025-05-21 03:47:34 +02:00
6e00e89bb0 chore(tasks): include version file in build process
- Adds the `--include=VERSION` flag to ensure the version file is part
  of the build output for consistency.
2025-05-21 03:47:24 +02:00
403e047c0c feat(cli): use dynamic version retrieval
- Replace hardcoded version with dynamic retrieval using `getVersion`
- Improves maintainability by avoiding manual version updates
2025-05-21 03:47:14 +02:00
56fb554f13 feat(utils): add version retrieval utility
- Introduces a function to retrieve the application version
- Returns 'dev' if the version file is missing and 'unknown' for other errors
- Exports the new utility for use in other modules
2025-05-21 03:47:02 +02:00
8ed98cc998 chore(changelog): update unreleased changelog 2025-05-21 01:13:51 +00:00
f81bb53353 feat(generator): add systemctl usage instructions
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 3s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 6s
- Log systemctl commands for user and root configurations
- Help users reload and enable timers more easily
2025-05-21 03:13:27 +02:00
db1f56c539 docs: add README for systemd-timer CLI tool
- Introduces a README file describing the systemd-timer tool
- Highlights features, installation steps, and usage examples
- Provides details on testing, development, and required permissions
2025-05-21 03:10:54 +02:00
e1cd5dfd35 docs: add MIT license file 2025-05-21 03:09:40 +02:00
67f302c2e9 chore(changelog): add initial changelog file to document project updates
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 3s
Auto Changelog & Release / changelog-only (push) Successful in 1m59s
Auto Changelog & Release / release (push) Has been skipped
2025-05-21 03:02:20 +02:00
97dc3fe23a feat(cli): add command to generate systemd unit files
- Introduces a CLI tool for creating systemd .timer and .service units
- Adds options for configuring unit names, commands, scheduling, and more
- Supports dry-run mode and user-level unit file generation
2025-05-21 03:01:21 +02:00
569b14d574 test(generate): add unit tests for service and timer generation
- Introduces tests to validate service and timer unit generation
- Covers description, dependencies, environment variables, and logging
- Ensures generated units meet expected configurations
2025-05-21 03:01:12 +02:00
316f3af04e refactor(utils): update import path for TimerOptions
- Adjusts the import path for TimerOptions to align with the new module structure.
- Simplifies dependency management by consolidating imports.
2025-05-21 03:01:03 +02:00
428e84927f feat(utils): export utility functions for filesystem and naming 2025-05-21 03:00:13 +02:00
ba4b933f78 feat(types): add TimerOptions interface for timer configuration 2025-05-21 02:58:53 +02:00
d5a383a62c feat(cli): add entry point for CLI commands 2025-05-21 02:58:30 +02:00
9539fe0532 feat(utils): add function to derive sanitized job names
- Introduces a utility function to extract and sanitize job names
  from executable paths by removing paths, extensions, and special
  characters.
- Adds unit tests to validate function behavior with various inputs.
2025-05-21 02:58:05 +02:00
ef2ac416d9 test(utils): add unit tests for systemd file handling
- Add tests for writing .service and .timer files with `writeUnitFiles`
- Add tests for resolving target paths with `resolveUnitTargetPath`
- Ensure proper file creation, content validation, and path resolution
2025-05-21 02:57:53 +02:00
6608f48840 chore: add VSCode settings for color customizations and folder listener 2025-05-21 02:57:32 +02:00
20 changed files with 546 additions and 1 deletions

View File

@@ -0,0 +1,47 @@
# ========================
# 📦 Upload Assets Template
# ========================
# Dieser Workflow wird automatisch ausgelöst, wenn ein Release
# in Gitea veröffentlicht wurde (event: release.published).
#
# Er dient dem Zweck, Release-Artefakte (wie z. B. Binary-Dateien,
# Changelogs oder Build-Zips) nachträglich mit dem Release zu verknüpfen.
#
# Voraussetzung: Zwei Shell-Skripte liegen im Projekt:
# - .gitea/scripts/get-release-id.sh → ermittelt Release-ID per Tag
# - .gitea/scripts/upload-asset.sh → lädt Datei als Release-Asset hoch
#
# --------------------------------------
name: Upload Assets
on:
release:
types: [published] # Nur bei Veröffentlichung eines Releases (nicht bei Entwürfen)
jobs:
upload-assets:
runs-on: ubuntu-latest
steps:
# 📥 Checke den Stand des Repos aus, exakt auf dem veröffentlichten Tag
# So ist garantiert, dass die Artefakte dem Zustand des Releases entsprechen.
- uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name }} # z. B. "v1.2.3"
fetch-depth: 0 # vollständige Git-Historie (für z. B. git-cliff, logs etc.)
# 🆔 Hole die Release-ID basierend auf dem Tag
# Die ID wird als Umgebungsvariable RELEASE_ID über $GITHUB_ENV verfügbar gemacht.
- name: Get Release ID from tag
run: .gitea/scripts/get-release-id.sh "${{ github.event.release.tag_name }}"
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Build application
run: deno task build
- name: Upload CHANGELOG.md as RELEASE-NOTES.md
run: .gitea/scripts/upload-asset.sh ./dist/systemd-timer systemd-timer

15
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,15 @@
{
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#d48282",
"activityBar.background": "#d48282",
"activityBar.foreground": "#15202b",
"activityBar.inactiveForeground": "#15202b99",
"activityBarBadge.background": "#b8e7b8",
"activityBarBadge.foreground": "#15202b"
},
"peacock.color": "#c75c5c",
"exportall.config.folderListener": [
"/src/utils",
"/src/types"
]
}

42
CHANGELOG.md Normal file
View File

@@ -0,0 +1,42 @@
# Changelog
All notable changes to this project will be documented in this file.
## [0.1.0] - 2025-05-21
### 🚀 Features
- *(workflows)* Add release asset upload workflow - ([1012ca5](https://git.0xmax42.io/maxp/systemd-timer/commit/1012ca53781c36131a8b7aa43a9134f7b8565599))
- *(cli)* Use dynamic version retrieval - ([403e047](https://git.0xmax42.io/maxp/systemd-timer/commit/403e047c0c376229244a5605d5c52eb1699acd4a))
- *(utils)* Add version retrieval utility - ([56fb554](https://git.0xmax42.io/maxp/systemd-timer/commit/56fb554f132a53d74b2e9a1a02cc973c5420e73c))
- *(generator)* Add systemctl usage instructions - ([f81bb53](https://git.0xmax42.io/maxp/systemd-timer/commit/f81bb533536810fc34656d572369b94ab669a181))
- *(cli)* Add command to generate systemd unit files - ([97dc3fe](https://git.0xmax42.io/maxp/systemd-timer/commit/97dc3fe23acf2c35053aced7b34918bab7778c35))
- *(utils)* Export utility functions for filesystem and naming - ([428e849](https://git.0xmax42.io/maxp/systemd-timer/commit/428e84927f8a9a379fa014ea763dd61115be34d6))
- *(types)* Add TimerOptions interface for timer configuration - ([ba4b933](https://git.0xmax42.io/maxp/systemd-timer/commit/ba4b933f78c48a52b1c199fe28dc82d7ebabd7fe))
- *(cli)* Add entry point for CLI commands - ([d5a383a](https://git.0xmax42.io/maxp/systemd-timer/commit/d5a383a62c965b60de7429ac1cb89f02639935f6))
- *(utils)* Add function to derive sanitized job names - ([9539fe0](https://git.0xmax42.io/maxp/systemd-timer/commit/9539fe053245e9fea10ceda0e46fe61e9de80797))
### 🚜 Refactor
- *(utils)* Update import path for TimerOptions - ([316f3af](https://git.0xmax42.io/maxp/systemd-timer/commit/316f3af04ef7fe4c08963cfe3ad7780ed3bc262c))
### 📚 Documentation
- Add README for systemd-timer CLI tool - ([db1f56c](https://git.0xmax42.io/maxp/systemd-timer/commit/db1f56c539309b8a02adff114d765c725ac5ff8a))
- Add MIT license file - ([e1cd5df](https://git.0xmax42.io/maxp/systemd-timer/commit/e1cd5dfd353c7cd7ca770daae5fc40405e461d1d))
### 🧪 Testing
- *(generate)* Add unit tests for service and timer generation - ([569b14d](https://git.0xmax42.io/maxp/systemd-timer/commit/569b14d57432589107a0f33e52881b605c5f79f9))
- *(utils)* Add unit tests for systemd file handling - ([ef2ac41](https://git.0xmax42.io/maxp/systemd-timer/commit/ef2ac416d92f59efe3390317af46e943549adc47))
### ⚙️ Miscellaneous Tasks
- *(tasks)* Include version file in build process - ([6e00e89](https://git.0xmax42.io/maxp/systemd-timer/commit/6e00e89bb086672b9c3276ffeebcb1ded28c836f))
- Add VSCode settings for color customizations and folder listener - ([6608f48](https://git.0xmax42.io/maxp/systemd-timer/commit/6608f488405adefc7993f47a137a824e5de62154))
- *(config)* Add deno configuration and lockfile - ([0b72050](https://git.0xmax42.io/maxp/systemd-timer/commit/0b720500e0fe34db087b3277c38fa6bb07875e80))
- Add automated release workflow and scripts for version management - ([a058e7b](https://git.0xmax42.io/maxp/systemd-timer/commit/a058e7b6838d41a98f3269db9a9d1e31f752121f))
- *(gitignore)* Add dist/ and .env files to ignore list - ([2da372d](https://git.0xmax42.io/maxp/systemd-timer/commit/2da372d20dd0e023feb7e2da391dd0971da6a73d))
- *(gitignore)* Add common build and coverage directories - ([2990af3](https://git.0xmax42.io/maxp/systemd-timer/commit/2990af3628b036c1d61daaf3d8efd3d2f0d4b761))

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 0xMax42
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

89
README.md Normal file
View File

@@ -0,0 +1,89 @@
# systemd-timer
Ein einfaches CLI-Tool zum schnellen Erzeugen von systemd `.service` und `.timer` Units – als Ersatz oder moderne Ergänzung zu klassischen `cron`-Jobs.
---
## 🚀 Features
- Erzeugt `.service` und `.timer` Dateien per CLI
- Unterstützt `--user` Timer (für `~/.config/systemd/user/`)
- Optionales Logging (`StandardOutput/StandardError`)
- Unterstützt:
- `--calendar`
- `--exec`
- `--after`
- `--environment`
- `--output`
- `--dry-run`
- Getestet und typisiert mit Deno + Cliffy
---
## 🛠️ Installation
```bash
git clone https://git.0xmax42.io/maxp/systemd-timer.git
cd systemd-timer
deno task build
# Binary liegt nun unter ./systemd-timer
./systemd-timer --help
```
---
## 📦 Beispiel
```bash
./systemd-timer create \
--exec "/usr/local/bin/backup.sh" \
--calendar "Mon..Fri 02:00" \
--description "Backup Job" \
--user \
--logfile "/var/log/backup.log"
```
Erzeugt:
- `~/.config/systemd/user/backup.service`
- `~/.config/systemd/user/backup.timer`
Anschließend aktivieren:
```bash
systemctl --user daemon-reload
systemctl --user enable --now backup.timer
```
---
## 🧪 Tests ausführen
```bash
deno task test
```
---
## 🧰 Entwickeln
```bash
deno task start create --exec "/bin/true" --calendar "daily" --dry-run
```
---
## 🔒 Rechte / Flags
Das Tool benötigt beim Ausführen bzw. Kompilieren:
- `--allow-env` (für `$HOME`)
- `--allow-write` (zum Schreiben von `.service`/`.timer`)
Beim Entwickeln wird meist `-A` (allow all) verwendet.
---
## 📝 Lizenz
[MIT License](LICENSE)

1
VERSION Normal file
View File

@@ -0,0 +1 @@
0.1.0

View File

@@ -2,7 +2,7 @@
"tasks": { "tasks": {
"start": "deno run -A src/mod.ts", "start": "deno run -A src/mod.ts",
"test": "deno test -A --coverage **/__tests__/*.test.ts", "test": "deno test -A --coverage **/__tests__/*.test.ts",
"build": "deno compile --allow-env --allow-write --output dist/systemd-timer src/mod.ts" "build": "deno compile --include=VERSION --allow-env --allow-write --output dist/systemd-timer src/mod.ts"
}, },
"compilerOptions": {}, "compilerOptions": {},
"fmt": { "fmt": {

39
src/cli/create.ts Normal file
View File

@@ -0,0 +1,39 @@
import { Command } from '@cliffy/command';
import { generateUnitFiles } from '../templates/unit-generator.ts';
import { TimerOptions } from '../types/options.ts';
export const createCommand = new Command()
.description('Erzeugt eine systemd .service und .timer Unit')
.option(
'--name <name:string>',
'Name der Unit-Dateien (optional, wird sonst aus dem Exec generiert)',
)
.option(
'--exec <cmd:string>',
'Kommando, das durch systemd ausgeführt werden soll',
{ required: true },
)
.option('--calendar <time:string>', 'OnCalendar-Ausdruck für den Timer', {
required: true,
})
.option('--description <desc:string>', 'Beschreibung des Timers')
.option('--user', 'Erstellt die Unit als User-Timer')
.option('--output <dir:string>', 'Zielverzeichnis der Unit-Dateien')
.option(
'--after <target:string>',
'Optionales After= für die Service-Unit',
{ collect: true },
)
.option(
'--environment <env:string>',
'Environment-Variablen im Format KEY=VALUE',
{ collect: true },
)
.option(
'--logfile <file:string>',
'Dateipfad für Log-Ausgabe (stdout/stderr)',
)
.option('--dry-run', 'Gibt die Unit-Dateien nur aus, ohne sie zu schreiben')
.action(async (options: TimerOptions) => {
await generateUnitFiles(options);
});

10
src/cli/main.ts Normal file
View File

@@ -0,0 +1,10 @@
import { Command } from '@cliffy/command';
import { createCommand } from './create.ts';
import { getVersion } from '../utils/mod.ts';
await new Command()
.name('systemd-timer')
.version(await getVersion())
.description('CLI Tool zum Erzeugen von systemd .timer und .service Units')
.command('create', createCommand)
.parse(Deno.args);

5
src/mod.ts Normal file
View File

@@ -0,0 +1,5 @@
import './cli/main.ts';
// ────────────────────────────────────────────────
// Entry Point for CLI
// Delegates to src/cli/main.ts, which registers all CLI commands

View File

@@ -0,0 +1,55 @@
import {
assertStringIncludes,
} from 'https://deno.land/std@0.224.0/assert/mod.ts';
import { TimerOptions } from '../../types/mod.ts';
import { generateUnits } from '../unit-generator.ts';
Deno.test('generateUnits erzeugt Basis-Service und Timer korrekt', () => {
const opts: TimerOptions = {
exec: '/usr/bin/my-script',
calendar: 'daily',
};
const { serviceUnit, timerUnit } = generateUnits('myjob', opts);
assertStringIncludes(serviceUnit, 'ExecStart=/usr/bin/my-script');
assertStringIncludes(timerUnit, 'OnCalendar=daily');
assertStringIncludes(timerUnit, '[Install]');
});
Deno.test('generateUnits setzt Beschreibung aus Option', () => {
const opts: TimerOptions = {
exec: '/bin/true',
calendar: 'daily',
description: 'Meine Unit',
};
const { serviceUnit } = generateUnits('job', opts);
assertStringIncludes(serviceUnit, 'Description=Meine Unit');
});
Deno.test('generateUnits berücksichtigt after=', () => {
const opts: TimerOptions = {
exec: '/bin/true',
calendar: 'daily',
after: ['network-online.target', 'docker.service'],
};
const { serviceUnit } = generateUnits('job', opts);
assertStringIncludes(serviceUnit, 'After=network-online.target');
assertStringIncludes(serviceUnit, 'After=docker.service');
});
Deno.test('generateUnits berücksichtigt environment und logfile', () => {
const opts: TimerOptions = {
exec: '/bin/true',
calendar: 'daily',
environment: ['FOO=bar', 'DEBUG=1'],
logfile: '/var/log/job.log',
};
const { serviceUnit } = generateUnits('job', opts);
assertStringIncludes(serviceUnit, 'Environment=FOO=bar');
assertStringIncludes(serviceUnit, 'Environment=DEBUG=1');
assertStringIncludes(serviceUnit, 'StandardOutput=append:/var/log/job.log');
assertStringIncludes(serviceUnit, 'StandardError=append:/var/log/job.log');
});

View File

@@ -0,0 +1,73 @@
import { TimerOptions } from '../types/mod.ts';
import { deriveNameFromExec, writeUnitFiles } from '../utils/mod.ts';
export async function generateUnitFiles(options: TimerOptions): Promise<void> {
const name = options.name || deriveNameFromExec(options.exec);
const { serviceUnit, timerUnit } = generateUnits(name, options);
if (options.dryRun) {
console.log(`===== ${name}.service =====`);
console.log(serviceUnit);
console.log(`\n===== ${name}.timer =====`);
console.log(timerUnit);
} else {
const { servicePath, timerPath } = await writeUnitFiles(
name,
serviceUnit,
timerUnit,
options,
);
console.log(`Service unit written to: ${servicePath}`);
console.log(`Timer unit written to: ${timerPath}`);
console.log(`\nℹ️ Hinweis:`);
if (options.user) {
console.log(` systemctl --user daemon-reload`);
console.log(` systemctl --user enable --now ${name}.timer`);
} else {
console.log(` sudo systemctl daemon-reload`);
console.log(` sudo systemctl enable --now ${name}.timer`);
}
}
}
export function generateUnits(name: string, options: TimerOptions): {
serviceUnit: string;
timerUnit: string;
} {
const unitParts = [
`[Unit]`,
`Description=${options.description ?? name}`,
...(options.after?.map((a) => `After=${a}`) ?? []),
``,
`[Service]`,
`Type=oneshot`,
`ExecStart=${options.exec}`,
...(options.environment?.map((e) => `Environment=${e}`) ?? []),
];
if (options.logfile) {
unitParts.push(`StandardOutput=append:${options.logfile}`);
unitParts.push(`StandardError=append:${options.logfile}`);
}
const serviceUnit = unitParts.join('\n');
const timerParts = [
`[Unit]`,
`Description=Timer for ${name}`,
``,
`[Timer]`,
`OnCalendar=${options.calendar}`,
`Persistent=true`,
``,
`[Install]`,
`WantedBy=timers.target`,
];
const timerUnit = timerParts.join('\n');
return { serviceUnit, timerUnit };
}

1
src/types/mod.ts Normal file
View File

@@ -0,0 +1 @@
export type { TimerOptions } from './options.ts';

12
src/types/options.ts Normal file
View File

@@ -0,0 +1,12 @@
export interface TimerOptions {
name?: string;
exec: string;
calendar: string;
description?: string;
user?: boolean;
output?: string;
after?: string[];
environment?: string[];
logfile?: string;
dryRun?: boolean;
}

View File

@@ -0,0 +1,59 @@
import {
assertEquals,
assertExists,
assertStringIncludes,
} from 'https://deno.land/std@0.224.0/assert/mod.ts';
import { join } from 'https://deno.land/std@0.224.0/path/mod.ts';
import { resolveUnitTargetPath, writeUnitFiles } from '../mod.ts';
import { TimerOptions } from '../../types/options.ts';
Deno.test('writeUnitFiles schreibt .service und .timer korrekt', async () => {
const tmp = await Deno.makeTempDir({ prefix: 'test-units-' });
const options = {
output: tmp,
user: false,
};
const name = 'test-backup';
const serviceContent = '[Service]\nExecStart=/bin/true';
const timerContent = '[Timer]\nOnCalendar=daily';
const { servicePath, timerPath } = await writeUnitFiles(
name,
serviceContent,
timerContent,
options as TimerOptions,
);
// Überprüfe Pfade
assertEquals(servicePath, join(tmp, 'test-backup.service'));
assertEquals(timerPath, join(tmp, 'test-backup.timer'));
// Existieren Dateien?
assertExists(await Deno.stat(servicePath));
assertExists(await Deno.stat(timerPath));
// Enthält die Datei den erwarteten Inhalt?
const readService = await Deno.readTextFile(servicePath);
const readTimer = await Deno.readTextFile(timerPath);
assertStringIncludes(readService, 'ExecStart=/bin/true');
assertStringIncludes(readTimer, 'OnCalendar=daily');
});
Deno.test('resolveUnitTargetPath mit --output', () => {
const result = resolveUnitTargetPath({ output: '/tmp/units', user: false });
assertEquals(result, '/tmp/units');
});
Deno.test('resolveUnitTargetPath mit --user ohne output', () => {
Deno.env.set('HOME', '/home/maxp');
const result = resolveUnitTargetPath({ output: undefined, user: true });
assertEquals(result, '/home/maxp/.config/systemd/user');
});
Deno.test('resolveUnitTargetPath ohne output und ohne user', () => {
const result = resolveUnitTargetPath({ output: undefined, user: false });
assertEquals(result, '/etc/systemd/system');
});

View File

@@ -0,0 +1,19 @@
import { assertEquals } from 'https://deno.land/std@0.224.0/assert/mod.ts';
import { deriveNameFromExec } from '../mod.ts';
Deno.test('deriveNameFromExec - entfernt Pfad, Endung und Sonderzeichen', () => {
const tests: Array<[string, string]> = [
['/usr/local/bin/backup.sh', 'backup'],
['/usr/bin/python3 /home/user/myscript.py', 'python3'],
['./my-job.ts', 'my-job'],
['node ./tools/start.js', 'node'],
['/bin/custom-script.rb', 'custom-script'],
[' /usr/bin/something-strange!.bin ', 'something-strange'],
['weird:name?.sh', 'weird-name'],
['', 'job'],
];
for (const [input, expected] of tests) {
assertEquals(deriveNameFromExec(input), expected);
}
});

30
src/utils/fs.ts Normal file
View File

@@ -0,0 +1,30 @@
import { ensureDir } from 'https://deno.land/std@0.224.0/fs/mod.ts';
import { join } from 'https://deno.land/std@0.224.0/path/mod.ts';
import { TimerOptions } from '../types/mod.ts';
export async function writeUnitFiles(
name: string,
serviceContent: string,
timerContent: string,
options: TimerOptions,
): Promise<{ servicePath: string; timerPath: string }> {
const basePath = resolveUnitTargetPath(options);
await ensureDir(basePath);
const servicePath = join(basePath, `${name}.service`);
const timerPath = join(basePath, `${name}.timer`);
await Deno.writeTextFile(servicePath, serviceContent);
await Deno.writeTextFile(timerPath, timerContent);
return { servicePath, timerPath };
}
export function resolveUnitTargetPath(
options: Pick<TimerOptions, 'output' | 'user'>,
): string {
if (options.output) return options.output;
if (options.user) return `${Deno.env.get('HOME')}/.config/systemd/user`;
return '/etc/systemd/system';
}

12
src/utils/misc.ts Normal file
View File

@@ -0,0 +1,12 @@
export function deriveNameFromExec(exec: string): string {
const parts = exec.trim().split(' ');
const base = parts[0].split('/').pop() || 'job';
// remove the file extension
const withoutExt = base.replace(/\.(sh|py|ts|js|pl|rb|exe|bin)$/, '');
// replace illegal chars, then trim leading/trailing hyphens
return withoutExt
.replaceAll(/[^a-zA-Z0-9_-]/g, '-')
.replace(/^-+|-+$/g, '');
}

3
src/utils/mod.ts Normal file
View File

@@ -0,0 +1,3 @@
export { resolveUnitTargetPath, writeUnitFiles } from './fs.ts';
export { deriveNameFromExec } from './misc.ts';
export { getVersion } from './version.ts';

12
src/utils/version.ts Normal file
View File

@@ -0,0 +1,12 @@
export async function getVersion(): Promise<string> {
try {
const versionUrl = new URL('../../VERSION', import.meta.url);
const version = await Deno.readTextFile(versionUrl);
return version.trim();
} catch (err) {
if (err instanceof Deno.errors.NotFound) {
return 'dev';
}
return 'unknown';
}
}