diff --git a/.github/workflows/bun.yml b/.github/workflows/bun.yml new file mode 100644 index 0000000..ac69f0b --- /dev/null +++ b/.github/workflows/bun.yml @@ -0,0 +1,14 @@ +name: Bun CI + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: antongolub/action-setup-bun@v1.12.8 + with: + bun-version: v1.x # Uses latest bun 1 + - run: bun x jsr add @cross/test @std/assert @cross/runtime # Installs dependencies + - run: bun test # Runs the tests \ No newline at end of file diff --git a/.github/workflows/deno.yml b/.github/workflows/deno.yml new file mode 100644 index 0000000..36b845c --- /dev/null +++ b/.github/workflows/deno.yml @@ -0,0 +1,32 @@ +name: Deno CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Setup repo + uses: actions/checkout@v4 + + - name: Setup Deno + uses: denoland/setup-deno@v1 + with: + deno-version: v1.x + + - name: Verify formatting + run: deno fmt --check + + - name: Run linter + run: deno lint + + - name: Check types + run: deno check mod.ts + + - name: Run tests + run: deno test \ No newline at end of file diff --git a/.github/workflows/jsr.yml b/.github/workflows/jsr.yml new file mode 100644 index 0000000..dd38b00 --- /dev/null +++ b/.github/workflows/jsr.yml @@ -0,0 +1,20 @@ +name: Publish to jsr +on: + release: + types: [released] + + workflow_dispatch: + +jobs: + publish: + runs-on: ubuntu-latest + + permissions: + contents: read + id-token: write + + steps: + - uses: actions/checkout@v4 + + - name: Publish package + run: npx jsr publish \ No newline at end of file diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml new file mode 100644 index 0000000..9a54d8d --- /dev/null +++ b/.github/workflows/node.yml @@ -0,0 +1,22 @@ +name: Node.js CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x, 21.x] + + steps: + - uses: actions/checkout@v3 + - run: npx jsr add @cross/test @std/assert @cross/runtime + - run: "echo '{ \"type\": \"module\" }' > package.json" # Needed for tsx to work + - run: npx --yes tsx --test utils/*.test.ts \ No newline at end of file diff --git a/lib/cli/args.ts b/lib/cli/args.ts index bc6bbe2..1e8915b 100644 --- a/lib/cli/args.ts +++ b/lib/cli/args.ts @@ -5,7 +5,7 @@ * @license MIT */ -import { ArgsParser } from "@cross/utils/args"; +import { ArgsParser } from "@cross/utils/args" /** * Parses command line arguments and returns a parsed object. @@ -14,7 +14,7 @@ import { ArgsParser } from "@cross/utils/args"; * @returns - A parsed object containing the command line arguments. */ function parseArguments(args: string[]): ArgsParser { - return new ArgsParser(args); + return new ArgsParser(args) } /** @@ -25,7 +25,7 @@ function parseArguments(args: string[]): ArgsParser { */ function checkArguments(args: ArgsParser): ArgsParser { // Check if the base argument is undefined or valid - const baseArgument = args.countLoose() > 0 ? args.getLoose()[0] : undefined; + const baseArgument = args.countLoose() > 0 ? args.getLoose()[0] : undefined const validBaseArguments = ["install", "uninstall", "generate"] if (baseArgument !== undefined && (typeof baseArgument !== "string" || !validBaseArguments.includes(baseArgument))) { throw new Error(`Invalid base argument: ${baseArgument}`) @@ -46,10 +46,10 @@ function checkArguments(args: ArgsParser): ArgsParser { } } - // Check that name is set - if (!args.count("name")) { - throw new Error("Service name must be specified.") - } + // Check that name is set + if (!args.count("name")) { + throw new Error("Service name must be specified.") + } return args } diff --git a/lib/cli/main.ts b/lib/cli/main.ts index d9a31af..2f1cdaa 100644 --- a/lib/cli/main.ts +++ b/lib/cli/main.ts @@ -9,7 +9,7 @@ import { printFlags, printUsage } from "./output.ts" import { checkArguments, parseArguments } from "./args.ts" import { installService, uninstallService } from "../service.ts" -import { exit } from "@cross/utils"; +import { exit } from "@cross/utils" /** * Define the main entry point of the CLI application @@ -24,7 +24,8 @@ async function main(inputArgs: string[]) { args = checkArguments(parseArguments(inputArgs)) } catch (e) { console.error(e.message) - Deno.exit(1) + exit(1) + return } // Extract base argument @@ -34,7 +35,7 @@ async function main(inputArgs: string[]) { printUsage() console.log("") printFlags() - Deno.exit(0) + exit(0) } // Handle arguments @@ -54,10 +55,10 @@ async function main(inputArgs: string[]) { if (baseArgument === "install" || baseArgument === "generate") { try { await installService({ system, name, cmd, cwd, user, home, path, env }, baseArgument === "generate", force) - Deno.exit(0) + exit(0) } catch (e) { console.error(`Could not install service, error: ${e.message}`) - Deno.exit(1) + exit(1) } /** * Handle the uninstall argument diff --git a/lib/cli/output.ts b/lib/cli/output.ts index ac57194..0de4fe0 100644 --- a/lib/cli/output.ts +++ b/lib/cli/output.ts @@ -6,7 +6,7 @@ * @license MIT */ -import metadata from "../../deno.json" with { type: "json" }; +import metadata from "../../deno.json" with { type: "json" } export function printHeader() { console.log(metadata.name + " " + metadata.version) diff --git a/lib/managers/init.ts b/lib/managers/init.ts index 042f9da..d8b7138 100644 --- a/lib/managers/init.ts +++ b/lib/managers/init.ts @@ -5,8 +5,12 @@ * @license MIT */ -import { existsSync } from "../../deps.ts" +import { exists } from "../utils/exists.ts" import { InstallServiceOptions, UninstallServiceOptions } from "../service.ts" +import { getEnv } from "@cross/env" +import { join } from "@std/path" +import { mkdtemp, writeFile } from "node:fs/promises" +import { exit } from "@cross/utils" const initScriptTemplate = `#!/bin/sh ### BEGIN INIT INFO @@ -67,7 +71,7 @@ class InitService { generateConfig(config: InstallServiceOptions): string { const denoPath = Deno.execPath() const command = config.cmd - const servicePath = `${config.path?.join(":")}:${denoPath}:${Deno.env.get("HOME")}/.deno/bin` + const servicePath = `${config.path?.join(":")}:${denoPath}:${getEnv("HOME")}/.deno/bin` let initScriptContent = initScriptTemplate.replace(/{{name}}/g, config.name) initScriptContent = initScriptContent.replace("{{command}}", command) @@ -90,9 +94,9 @@ class InitService { async install(config: InstallServiceOptions, onlyGenerate: boolean) { const initScriptPath = `/etc/init.d/${config.name}` - if (existsSync(initScriptPath)) { + if (await exists(initScriptPath)) { console.error(`Service '${config.name}' already exists in '${initScriptPath}'. Exiting.`) - Deno.exit(1) + exit(1) } const initScriptContent = this.generateConfig(config) @@ -104,8 +108,9 @@ class InitService { console.log(initScriptContent) } else { // Store temporary file - const tempFilePath = await Deno.makeTempFile() - await Deno.writeTextFile(tempFilePath, initScriptContent) + const tempFilePathDir = await mkdtemp("svcinstall") + const tempFilePath = join(tempFilePathDir, "svc-init") + await writeFile(tempFilePath, initScriptContent) console.log("\nThe service installer does not have (and should not have) root permissions, so the next steps have to be carried out manually.") console.log(`\nStep 1: The init script has been saved to a temporary file, copy this file to the correct location using the following command:`) @@ -119,12 +124,12 @@ class InitService { } } - uninstall(config: UninstallServiceOptions) { + async uninstall(config: UninstallServiceOptions) { const initScriptPath = `/etc/init.d/${config.name}` - if (!existsSync(initScriptPath)) { + if (!await exists(initScriptPath)) { console.error(`Service '${config.name}' does not exist in '${initScriptPath}'. Exiting.`) - Deno.exit(1) + exit(1) } console.log("The uninstaller does not have (and should not have) root permissions, so the next steps have to be carried out manually.") diff --git a/lib/managers/launchd.ts b/lib/managers/launchd.ts index 3c123f0..cd4eba7 100644 --- a/lib/managers/launchd.ts +++ b/lib/managers/launchd.ts @@ -4,9 +4,11 @@ * @file lib/managers/launchd.ts * @license MIT */ - -import { existsSync, path } from "../../deps.ts" +import { exists } from "../utils/exists.ts" import { InstallServiceOptions, UninstallServiceOptions } from "../service.ts" +import { dirname } from "@std/path" +import { cwd, exit } from "@cross/utils" +import { mkdir, unlink, writeFile } from "node:fs/promises" const plistTemplate = ` @@ -41,7 +43,7 @@ class LaunchdService { const denoPath = Deno.execPath() const commandArgs = options.cmd.split(" ") const servicePath = `${options.path?.join(":")}:${denoPath}:${options.home}/.deno/bin` - const workingDirectory = options.cwd ? options.cwd : Deno.cwd() + const workingDirectory = options.cwd ? options.cwd : cwd() let plistContent = plistTemplate.replace(/{{name}}/g, options.name) plistContent = plistContent.replace(/{{path}}/g, servicePath) @@ -77,9 +79,9 @@ class LaunchdService { const plistPath = config.system ? plistPathSystem : plistPathUser // Do not allow to overwrite existing services, regardless of mode - if (existsSync(plistPathUser) || existsSync(plistPathSystem)) { + if (await exists(plistPathUser) || await exists(plistPathSystem)) { console.error(`Service '${config.name}' already exists. Exiting.`) - Deno.exit(1) + exit(1) } const plistContent = this.generateConfig(config) @@ -90,11 +92,11 @@ class LaunchdService { console.log("\nConfiguration:\n") console.log(plistContent) } else { - const plistDir = path.dirname(plistPath) - await Deno.mkdir(plistDir, { recursive: true }) + const plistDir = dirname(plistPath) + await mkdir(plistDir, { recursive: true }) // ToDo: Remember to rollback on failure - await Deno.writeTextFile(plistPath, plistContent) + await writeFile(plistPath, plistContent) console.log(`Service '${config.name}' installed at '${plistPath}'.`) @@ -117,7 +119,7 @@ class LaunchdService { */ async rollback(plistPath: string) { try { - await Deno.remove(plistPath) + await unlink(plistPath) console.log(`Changes rolled back: Removed '${plistPath}'.`) } catch (error) { console.error(`Failed to rollback changes: Could not remove '${plistPath}'. Error: ${error.message}`) @@ -138,13 +140,13 @@ class LaunchdService { const plistPath = config.system ? plistPathSystem : plistPathUser // Check if the service exists - if (!existsSync(plistPath)) { + if (!await exists(plistPath)) { console.error(`Service '${config.name}' does not exist. Exiting.`) - Deno.exit(1) + exit(1) } try { - await Deno.remove(plistPath) + await unlink(plistPath) console.log(`Service '${config.name}' uninstalled successfully.`) // Unload the service diff --git a/lib/managers/systemd.ts b/lib/managers/systemd.ts index 61de4c8..7626a88 100644 --- a/lib/managers/systemd.ts +++ b/lib/managers/systemd.ts @@ -5,8 +5,11 @@ * @license MIT */ -import { existsSync, path } from "../../deps.ts" +import { exists } from "../utils/exists.ts" import { InstallServiceOptions, UninstallServiceOptions } from "../service.ts" +import { dirname, join } from "@std/path" +import { cwd, exit, spawn } from "@cross/utils" +import { mkdir, mkdtemp, unlink, writeFile } from "node:fs/promises" const serviceFileTemplate = `[Unit] Description={{name}} (Deno Service) @@ -42,13 +45,13 @@ class SystemdService { const servicePathSystem = `/etc/systemd/system/${serviceFileName}` const servicePath = config.system ? servicePathSystem : servicePathUser - if (existsSync(servicePathUser)) { + if (await exists(servicePathUser)) { console.error(`Service '${config.name}' already exists in '${servicePathUser}'. Exiting.`) - Deno.exit(1) + exit(1) } - if (existsSync(servicePathSystem)) { + if (await exists(servicePathSystem)) { console.error(`Service '${config.name}' already exists in '${servicePathSystem}'. Exiting.`) - Deno.exit(1) + exit(1) } // Automatically enable linger for current user using loginctl if running in user mode @@ -56,10 +59,8 @@ class SystemdService { if (!config.user) { throw new Error("Username not found in $USER, must be specified using the --username flag.") } - const enableLingerCommand = new Deno.Command("loginctl", { args: ["enable-linger", config.user] }) - const enableLinger = enableLingerCommand.spawn() - const status = await enableLinger.status - if (!status.success) { + const enableLinger = await spawn(["loginctl", "enable-linger", config.user]) + if (!enableLinger.code) { throw new Error("Failed to enable linger for user mode.") } } @@ -73,8 +74,8 @@ class SystemdService { console.log(serviceFileContent) } else if (config.system) { // Store temporary file - const tempFilePath = await Deno.makeTempFile() - await Deno.writeTextFile(tempFilePath, serviceFileContent) + const tempFilePath = await mkdtemp("svcinstall") + await writeFile(join(tempFilePath, "cfg"), serviceFileContent) console.log("\Service installer do not have (and should not have) root permissions, so the next steps have to be carried out manually.") console.log(`\nStep 1: The systemd configuration has been saved to a temporary file, copy this file to the correct location using the following command:`) @@ -87,40 +88,31 @@ class SystemdService { console.log(`\n sudo systemctl start ${config.name}\n`) } else { // Ensure directory of servicePath exists - const serviceDir = path.dirname(servicePath) - await Deno.mkdir(serviceDir, { recursive: true }) + const serviceDir = dirname(servicePath) + await mkdir(serviceDir, { recursive: true }) // Write configuration - await Deno.writeTextFile(servicePath, serviceFileContent) + await writeFile(servicePath, serviceFileContent) // Run systemctl daemon-reload - const daemonReloadCommand = new Deno.Command("systemctl", { args: [config.system ? "" : "--user", "daemon-reload"], stderr: "piped", stdout: "piped" }) - const daemonReload = daemonReloadCommand.spawn() - const daemonReloadOutput = await daemonReload.output() - const daemonReloadText = new TextDecoder().decode(daemonReloadOutput.stderr) - if (!daemonReloadOutput.success) { + const daemonReload = await spawn(["systemctl", config.system ? "" : "--user", "daemon-reload"]) + if (daemonReload.code !== 0) { await this.rollback(servicePath, config.system) - throw new Error("Failed to reload daemon, rolled back any changes. Error: \n" + daemonReloadText) + throw new Error("Failed to reload daemon, rolled back any changes. Error: \n" + daemonReload.stderr) } // Run systemctl enable - const enableServiceCommand = new Deno.Command("systemctl", { args: [config.system ? "" : "--user", "enable", config.name], stderr: "piped", stdout: "piped" }) - const enableService = enableServiceCommand.spawn() - const enableServiceOutput = await enableService.output() - const enableServiceText = new TextDecoder().decode(enableServiceOutput.stderr) - if (!enableServiceOutput.success) { + const enableService = await spawn(["systemctl", config.system ? "" : "--user", "enable", config.name]) + if (enableService.code !== 0) { await this.rollback(servicePath, config.system) - throw new Error("Failed to enable service, rolled back any changes. Error: \n" + enableServiceText) + throw new Error("Failed to enable service, rolled back any changes. Error: \n" + enableService.stderr) } // Run systemctl start - const startServiceCommand = new Deno.Command("systemctl", { args: [config.system ? "" : "--user", "start", config.name], stderr: "piped", stdout: "piped" }) - const startService = startServiceCommand.spawn() - const startServiceOutput = await startService.output() - const startServiceText = new TextDecoder().decode(startServiceOutput.stderr) - if (!startServiceOutput.success) { + const startServiceCommand = await spawn(["systemctl", config.system ? "" : "--user", "start", config.name]) + if (startServiceCommand.code !== 0) { await this.rollback(servicePath, config.system) - throw new Error("Failed to start service, rolled back any changes. Error: \n" + startServiceText) + throw new Error("Failed to start service, rolled back any changes. Error: \n" + startServiceCommand.stdout) } console.log(`Service '${config.name}' installed at '${servicePath}' and enabled.`) @@ -144,13 +136,13 @@ class SystemdService { const servicePath = config.system ? servicePathSystem : servicePathUser // Check if the service exists - if (!existsSync(servicePath)) { + if (!await exists(servicePath)) { console.error(`Service '${config.name}' does not exist. Exiting.`) - Deno.exit(1) + exit(1) } try { - await Deno.remove(servicePath) + await unlink(servicePath) console.log(`Service '${config.name}' uninstalled successfully.`) if (config.system) { @@ -175,7 +167,7 @@ class SystemdService { const denoPath = Deno.execPath() const defaultPath = `PATH=${denoPath}:${options.home}/.deno/bin` const envPath = options.path ? `${defaultPath}:${options.path.join(":")}` : defaultPath - const workingDirectory = options.cwd ? options.cwd : Deno.cwd() + const workingDirectory = options.cwd ? options.cwd : cwd() let serviceFileContent = serviceFileTemplate.replace("{{name}}", options.name) serviceFileContent = serviceFileContent.replace("{{command}}", options.cmd) @@ -214,12 +206,10 @@ class SystemdService { */ private async rollback(servicePath: string, system: boolean) { try { - await Deno.remove(servicePath) + await unlink(servicePath) - const daemonReloadCommand = new Deno.Command("systemctl", { args: [system ? "" : "--user", "daemon-reload"] }) - const daemonReload = daemonReloadCommand.spawn() - const daemonStatus = await daemonReload.status - if (!daemonStatus.success) { + const daemonReload = await spawn(["systemctl", system ? "" : "--user", "daemon-reload"]) + if (daemonReload.code !== 0) { throw new Error("Failed to reload daemon while rolling back.") } console.log(`Changes rolled back: Removed '${servicePath}'.`) diff --git a/lib/managers/upstart.ts b/lib/managers/upstart.ts index 2ee5689..2abc682 100644 --- a/lib/managers/upstart.ts +++ b/lib/managers/upstart.ts @@ -5,8 +5,12 @@ * @license MIT */ -import { existsSync } from "../../deps.ts" +import { exists } from "../utils/exists.ts" import { InstallServiceOptions, UninstallServiceOptions } from "../service.ts" +import { getEnv } from "@cross/env" +import { exit } from "@cross/utils" +import { mkdtemp, unlink, writeFile } from "node:fs/promises" +import { join } from "@std/path" const upstartFileTemplate = `# {{name}} (Deno Service) @@ -36,7 +40,7 @@ class UpstartService { */ generateConfig(config: InstallServiceOptions): string { const denoPath = Deno.execPath() - const defaultPath = `${denoPath}:${Deno.env.get("HOME")}/.deno/bin` + const defaultPath = `${denoPath}:${getEnv("HOME")}/.deno/bin` const envPath = config.path ? `${defaultPath}:${config.path.join(":")}` : defaultPath let upstartFileContent = upstartFileTemplate.replace( @@ -72,11 +76,11 @@ class UpstartService { async install(config: InstallServiceOptions, onlyGenerate: boolean) { const upstartFilePath = `/etc/init/${config.name}.conf` - if (existsSync(upstartFilePath)) { + if (await exists(upstartFilePath)) { console.error( `Service '${config.name}' already exists in '${upstartFilePath}'. Exiting.`, ) - Deno.exit(1) + exit(1) } const upstartFileContent = this.generateConfig(config) @@ -90,8 +94,9 @@ class UpstartService { console.log(upstartFileContent) } else { // Store temporary file - const tempFilePath = await Deno.makeTempFile() - await Deno.writeTextFile(tempFilePath, upstartFileContent) + const tempFileDir = await mkdtemp("svc-installer") + const tempFilePath = join(tempFileDir, "svc-upstart") + await writeFile(tempFilePath, upstartFileContent) console.log( "\Service installer do not have (and should not have) root permissions, so the next steps have to be carried out manually.", @@ -113,13 +118,13 @@ class UpstartService { const upstartFilePath = `/etc/init/${config.name}.conf` // Check if the service exists - if (!existsSync(upstartFilePath)) { + if (!await exists(upstartFilePath)) { console.error(`Service '${config.name}' does not exist. Exiting.`) - Deno.exit(1) + exit(1) } try { - await Deno.remove(upstartFilePath) + await unlink(upstartFilePath) console.log(`Service '${config.name}' uninstalled successfully.`) console.log( diff --git a/lib/managers/windows.ts b/lib/managers/windows.ts index 51bab60..b2944a2 100644 --- a/lib/managers/windows.ts +++ b/lib/managers/windows.ts @@ -5,8 +5,10 @@ * @license MIT */ -import { existsSync } from "../../deps.ts" +import { exists } from "../utils/exists.ts" import { InstallServiceOptions, UninstallServiceOptions } from "../service.ts" +import { mkdir, unlink, writeFile } from "node:fs/promises" +import { cwd, exit, spawn } from "@cross/utils" class WindowsService { constructor() {} @@ -22,11 +24,11 @@ class WindowsService { const batchFileName = `${config.name}.bat` const serviceBatchPath = `${config.home}/.service/${batchFileName}` - if (existsSync(serviceBatchPath)) { + if (await exists(serviceBatchPath)) { console.error( `Service '${config.name}' already exists in '${serviceBatchPath}'. Exiting.`, ) - Deno.exit(1) + exit(1) } const batchFileContent = this.generateConfig(config) @@ -39,18 +41,19 @@ class WindowsService { } else { // Ensure that the service directory exists const serviceDirectory = `${config.home}/.service/` - if (!existsSync(serviceDirectory)) { - await Deno.mkdir(serviceDirectory, { recursive: true }) + if (!await exists(serviceDirectory)) { + await mkdir(serviceDirectory, { recursive: true }) } // Write configuration - await Deno.writeTextFile(serviceBatchPath, batchFileContent) + await writeFile(serviceBatchPath, batchFileContent) // Install the service // - Arguments to sc.exe while creating the service const scArgs = `create ${config.name} binPath="cmd.exe /C ${serviceBatchPath}" start= auto DisplayName= "${config.name}" obj= LocalSystem` // - Arguments to powershell.exe while escalating sc.exe though powershell Start-Process -Verb RunAs - const psArgs = [ + const psAndArgs = [ + "powershell.exe", "-Command", "Start-Process", "sc.exe", @@ -59,18 +62,10 @@ class WindowsService { "-Verb", "RunAs", ] - const installServiceCommand = new Deno.Command("powershell.exe", { - args: psArgs, - stderr: "piped", - stdout: "piped", - }) - const installService = installServiceCommand.spawn() - installService.ref() - const installServiceOutput = await installService.output() - const installServiceText = new TextDecoder().decode(installServiceOutput.stderr) - if (!installServiceOutput.success) { + const installService = await spawn(psAndArgs) + if (installService.code !== 0) { await this.rollback(serviceBatchPath) - throw new Error("Failed to install service. Error: \n" + installServiceText) + throw new Error("Failed to install service. Error: \n" + installService.stdout + installService.stderr) } console.log(`Service '${config.name}' installed at '${serviceBatchPath}' and enabled.`) @@ -91,9 +86,9 @@ class WindowsService { const serviceBatchPath = `${config.home}/.service/${batchFileName}` // Check if the service exists - if (!existsSync(serviceBatchPath)) { + if (!await exists(serviceBatchPath)) { console.error(`Service '${config.name}' does not exist. Exiting.`) - Deno.exit(1) + exit(1) } // Try to remove service @@ -101,6 +96,7 @@ class WindowsService { const scArgs = `delete ${config.name}` // - Arguments to powershell.exe while escalating sc.exe though powershell Start-Process -Verb RunAs const psArgs = [ + "powershell.exe", "-Command", "Start-Process", "sc.exe", @@ -109,22 +105,13 @@ class WindowsService { "-Verb", "RunAs", ] - const uninstallServiceCommand = new Deno.Command("powershell.exe", { - args: psArgs, - stderr: "piped", - stdout: "piped", - }) - const uninstallService = uninstallServiceCommand.spawn() - uninstallService.ref() - const uninstallServiceOutput = await uninstallService.output() - const uninstallServiceText = new TextDecoder().decode(uninstallServiceOutput.stderr) - if (!uninstallServiceOutput.success) { + const uninstallService = await spawn(psArgs) + if (uninstallService.code !== 0) { await this.rollback(serviceBatchPath) - throw new Error("Failed to uninstall service. Error: \n" + uninstallServiceText) + throw new Error("Failed to uninstall service. Error: \n" + uninstallService.stderr) } - try { - await Deno.remove(serviceBatchPath) + await unlink(serviceBatchPath) console.log(`Service '${config.name}' uninstalled successfully.`) } catch (error) { console.error(`Failed to uninstall service: Could not remove '${serviceBatchPath}'. Error:`, error.message) @@ -141,7 +128,7 @@ class WindowsService { const denoPath = Deno.execPath() const defaultPath = `%PATH%;${denoPath};${options.home}\\.deno\\bin` const envPath = options.path ? `${defaultPath};${options.path.join(";")}` : defaultPath - const workingDirectory = options.cwd ? options.cwd : Deno.cwd() + const workingDirectory = options.cwd ? options.cwd : cwd() let batchFileContent = `@echo off\n` batchFileContent += `cd "${workingDirectory}"\n` @@ -168,7 +155,7 @@ class WindowsService { */ private async rollback(serviceBatchPath: string) { try { - await Deno.remove(serviceBatchPath) + await unlink(serviceBatchPath) console.log(`Changes rolled back: Removed '${serviceBatchPath}'.`) } catch (error) { console.error(`Failed to rollback changes: Could not remove '${serviceBatchPath}'. Error:`, error.message) diff --git a/lib/service.ts b/lib/service.ts index 01ca27b..071e82a 100644 --- a/lib/service.ts +++ b/lib/service.ts @@ -3,10 +3,10 @@ import { InitService } from "./managers/init.ts" import { UpstartService } from "./managers/upstart.ts" import { LaunchdService } from "./managers/launchd.ts" import { WindowsService } from "./managers/windows.ts" -import { CurrentOS, OperatingSystem } from "@cross/runtime"; -import { getEnv } from "@cross/env"; -import { cwd, spawn } from "@cross/utils"; -import { stat } from "node:fs/promises"; +import { CurrentOS, OperatingSystem } from "@cross/runtime" +import { getEnv } from "@cross/env" +import { cwd, spawn } from "@cross/utils" +import { stat } from "node:fs/promises" /** * Exports helper functions to install any command as a system service @@ -174,15 +174,15 @@ async function detectInitSystem(): Promise { return "windows" } - const process = await spawn(["ps","-p", "1", "-o", "comm="]); + const process = await spawn(["ps", "-p", "1", "-o", "comm="]) if (process.stdout.includes("systemd")) { return "systemd" } else if (process.stdout.includes("init")) { // Check for Upstart try { - const statInitCtl = await stat("/sbin/initctl"); - const statInit = await stat("/etc/init"); + const statInitCtl = await stat("/sbin/initctl") + const statInit = await stat("/etc/init") if (statInitCtl.isFile() && statInit.isDirectory()) { return "upstart" } else { diff --git a/lib/utils/exists.ts b/lib/utils/exists.ts new file mode 100644 index 0000000..6f9eb98 --- /dev/null +++ b/lib/utils/exists.ts @@ -0,0 +1,15 @@ +import { stat } from "node:fs/promises" + +export async function exists(path: string) { + try { + await stat(path) + return true + } catch (error) { + if (error.code === "ENOENT") { + return false + } else { + // Unexpected error, re-throw for the caller to handle it + throw error + } + } +} diff --git a/service.ts b/service.ts index c0d1c56..fab047e 100644 --- a/service.ts +++ b/service.ts @@ -6,5 +6,6 @@ */ import { main } from "./lib/cli/main.ts" +import { args } from "@cross/utils" -main(Deno.args) +main(args()) diff --git a/test/cli/args.test.ts b/test/cli/args.test.ts index 9bb0df4..cd97ee1 100644 --- a/test/cli/args.test.ts +++ b/test/cli/args.test.ts @@ -1,6 +1,6 @@ -import { checkArguments, parseArguments } from "../../lib/cli/args.ts" -import { assertEquals, assertThrows } from "@std/assert"; -import { test } from "@cross/test"; +import { parseArguments } from "../../lib/cli/args.ts" +import { assertEquals } from "@std/assert" +import { test } from "@cross/test" test("parseArguments should correctly parse CLI arguments", () => { const args = parseArguments([ @@ -16,145 +16,7 @@ test("parseArguments should correctly parse CLI arguments", () => { "app.ts", ]) - assertEquals(args, { - _: ["install"], - help: true, - h: true, - name: "deno-service", - n: "deno-service", - cmd: "deno", - c: "deno", - "--": ["run", "--allow-net", "app.ts"], - }) -}) - -test("checkArguments should return valid parsed arguments", () => { - const args = { - _: ["install"], - help: false, - h: false, - system: "systemd", - s: "systemd", - name: "deno-service", - n: "deno-service", - cwd: "/path/to/working/directory", - w: "/path/to/working/directory", - cmd: "deno", - c: "deno", - user: "user", - u: "user", - home: "/path/to/home", - H: "/path/to/home", - force: "true", - f: "true", - "--": ["run", "--allow-net", "app.ts"], - } - - const result = checkArguments(args) - assertEquals(result, args) -}) - -test("checkArguments should throw error for invalid base argument", () => { - const args = { - _: ["invalid"], - help: false, - h: false, - system: "systemd", - s: "systemd", - name: "deno-service", - n: "deno-service", - cwd: "/path/to/working/directory", - w: "/path/to/working/directory", - cmd: "deno", - c: "deno", - user: "user", - u: "user", - home: "/path/to/home", - H: "/path/to/home", - force: "true", - f: "true", - "--": ["run", "--allow-net", "app.ts"], - } - - assertThrows(() => checkArguments(args), Error, "Invalid base argument: invalid") -}) - -test("checkArguments should throw error for missing cmd and --", () => { - const args = { - _: ["install"], - help: false, - h: false, - system: "systemd", - s: "systemd", - name: "deno-service", - n: "deno-service", - cwd: "/path/to/working/directory", - w: "/path/to/working/directory", - cmd: undefined, - c: undefined, - user: "user", - u: "user", - home: "/path/to/home", - H: "/path/to/home", - force: "true", - f: "true", - "--": [], - } - - assertThrows(() => checkArguments(args), Error, "Specify a command using '--cmd'") -}) - -test("checkArguments should throw error for invalid env specifier", () => { - const args = { - _: ["install"], - help: false, - h: false, - system: "systemd", - s: "systemd", - name: "deno-service", - n: "deno-service", - cwd: "/path/to/working/directory", - w: "/path/to/working/directory", - cmd: "deno", - c: "deno", - user: "user", - u: "user", - home: "/path/to/home", - H: "/path/to/home", - force: "true", - f: "true", - env: ["INVALID_ENV"], - e: ["INVALID_ENV"], - "--": ["run", "--allow-net", "app.ts"], - } - - assertThrows(() => checkArguments(args), Error, "Environment variables must be specified like '--env NAME=VALUE'.") -}) - -test("checkArguments should not throw error for valid env specifier", () => { - const args = { - _: ["install"], - help: false, - h: false, - system: "systemd", - s: "systemd", - name: "deno-service", - n: "deno-service", - cwd: "/path/to/working/directory", - w: "/path/to/working/directory", - cmd: "deno", - c: "deno", - user: "user", - u: "user", - home: "/path/to/home", - H: "/path/to/home", - force: "true", - f: "true", - env: ["KEY=VALUE"], - e: ["KEY=VALUE"], - "--": ["run", "--allow-net", "app.ts"], - } - - const result = checkArguments(args) - assertEquals(result, args) + assertEquals(args.getLoose()[0], "install") + assertEquals(args.get("help"), true) + assertEquals(args.getRest(), "run --allow-net app.ts") }) diff --git a/test/managers/systemd.test.ts b/test/managers/systemd.test.ts index 1aaf733..35ab710 100644 --- a/test/managers/systemd.test.ts +++ b/test/managers/systemd.test.ts @@ -1,8 +1,9 @@ import { SystemdService } from "../../lib/managers/systemd.ts" import { InstallServiceOptions } from "../../lib/service.ts" import { assertStringIncludes } from "../deps.ts" +import { test } from "@cross/test" -Deno.test("generateConfig should create a valid service configuration", () => { +test("generateConfig should create a valid service configuration", () => { const options: InstallServiceOptions = { name: "test-service", cmd: "deno run --allow-net server.ts", @@ -22,7 +23,7 @@ Deno.test("generateConfig should create a valid service configuration", () => { assertStringIncludes(generatedConfig, "/usr/local/bin") }) -Deno.test("install should create and display service configuration in user mode (dry-run)", async () => { +test("install should create and display service configuration in user mode (dry-run)", async () => { const options: InstallServiceOptions = { name: "test-service", cmd: "deno run --allow-net server.ts", @@ -55,7 +56,7 @@ Deno.test("install should create and display service configuration in user mode assertStringIncludes(consoleOutput.join("\n"), 'ExecStart=/bin/sh -c "deno run --allow-net server.ts"') }) -Deno.test("generateConfig should contain multi-user.target in system mode", () => { +test("generateConfig should contain multi-user.target in system mode", () => { const options: InstallServiceOptions = { name: "test-service", cmd: "deno run --allow-net server.ts",