mirror of
https://fastgit.cc/https://github.com/anomalyco/opencode
synced 2026-04-30 22:00:53 +08:00
sync
This commit is contained in:
@@ -1,50 +0,0 @@
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { ServiceManager } from "@/service"
|
||||
import { UI } from "../ui"
|
||||
import type { Argv } from "yargs"
|
||||
import { cmd } from "./cmd"
|
||||
|
||||
function exitWithError(error: unknown): never {
|
||||
UI.error(error instanceof Error ? error.message : String(error))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const InstallCommand = cmd({
|
||||
command: "install",
|
||||
describe: "install opencode as a background service",
|
||||
builder: (yargs: Argv) =>
|
||||
yargs
|
||||
.option("password", {
|
||||
type: "string",
|
||||
describe: "basic auth password (defaults to OPENCODE_PASSWORD, then OPENCODE_SERVER_PASSWORD, else random)",
|
||||
})
|
||||
.option("hostname", {
|
||||
type: "string",
|
||||
describe: "hostname to listen on",
|
||||
}),
|
||||
handler: async (args: { password?: string; hostname?: string }) => {
|
||||
const result = await AppRuntime.runPromise(ServiceManager.Service.use((svc) => svc.install(args))).catch(exitWithError)
|
||||
|
||||
console.log(`Installed OpenCode service on ${result.platform}`)
|
||||
console.log(`Target: ${result.target}`)
|
||||
console.log(`Hostname: ${result.hostname}`)
|
||||
console.log(`Password: ${result.password}`)
|
||||
},
|
||||
})
|
||||
|
||||
const PasswordCommand = cmd({
|
||||
command: "password",
|
||||
describe: "print the installed service password",
|
||||
handler: async () => {
|
||||
const password = await AppRuntime.runPromise(ServiceManager.Service.use((svc) => svc.password())).catch(exitWithError)
|
||||
|
||||
console.log(password)
|
||||
},
|
||||
})
|
||||
|
||||
export const ServiceCommand = cmd({
|
||||
command: "service",
|
||||
describe: "manage the background opencode service",
|
||||
builder: (yargs: Argv) => yargs.command(InstallCommand).command(PasswordCommand).demandCommand(),
|
||||
handler: () => {},
|
||||
})
|
||||
@@ -1,86 +0,0 @@
|
||||
export * as ServiceManager from "."
|
||||
|
||||
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Context, Effect, Layer } from "effect"
|
||||
import * as ServiceLinux from "./linux"
|
||||
import * as ServiceMacos from "./macos"
|
||||
import * as ServicePlatform from "./platform"
|
||||
import {
|
||||
chmodFile,
|
||||
CONFIG_FILE,
|
||||
generatePassword,
|
||||
type InstallInput,
|
||||
type InstallResult,
|
||||
readStoredConfig,
|
||||
resolveServeCommand,
|
||||
ServiceError,
|
||||
writeJsonFile,
|
||||
} from "./shared"
|
||||
import * as ServiceUnsupported from "./unsupported"
|
||||
import * as ServiceWindows from "./windows"
|
||||
|
||||
export interface Interface {
|
||||
readonly install: (input: InstallInput) => Effect.Effect<InstallResult, ServiceError>
|
||||
readonly password: () => Effect.Effect<string, ServiceError>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ServiceManager") {}
|
||||
|
||||
const platformLayer =
|
||||
process.platform === "linux"
|
||||
? ServiceLinux.layer
|
||||
: process.platform === "darwin"
|
||||
? ServiceMacos.layer
|
||||
: process.platform === "win32"
|
||||
? ServiceWindows.layer
|
||||
: ServiceUnsupported.layer
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const platform = yield* ServicePlatform.Service
|
||||
|
||||
const install = Effect.fn("ServiceManager.install")(function* (input: InstallInput) {
|
||||
const stored = yield* readStoredConfig(fs)
|
||||
const password =
|
||||
input.password ?? process.env.OPENCODE_PASSWORD ?? process.env.OPENCODE_SERVER_PASSWORD ?? stored?.password ?? generatePassword()
|
||||
const hostname = input.hostname ?? stored?.hostname ?? "127.0.0.1"
|
||||
|
||||
yield* writeJsonFile(fs, CONFIG_FILE, { password, hostname }, 0o600)
|
||||
yield* chmodFile(fs, CONFIG_FILE, 0o600).pipe(Effect.catch(() => Effect.void))
|
||||
|
||||
const result = yield* platform.install({
|
||||
password,
|
||||
hostname,
|
||||
command: yield* resolveServeCommand(fs, hostname),
|
||||
})
|
||||
|
||||
return {
|
||||
password,
|
||||
hostname,
|
||||
platform: process.platform,
|
||||
target: result.target,
|
||||
}
|
||||
})
|
||||
|
||||
const password = Effect.fn("ServiceManager.password")(function* () {
|
||||
const stored = yield* readStoredConfig(fs)
|
||||
if (stored?.password) return stored.password
|
||||
if (process.env.OPENCODE_PASSWORD) return process.env.OPENCODE_PASSWORD
|
||||
if (process.env.OPENCODE_SERVER_PASSWORD) return process.env.OPENCODE_SERVER_PASSWORD
|
||||
return yield* new ServiceError({
|
||||
message: "OpenCode service password is not configured. Run `opencode service install` first.",
|
||||
})
|
||||
})
|
||||
|
||||
return Service.of({ install, password })
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(
|
||||
Layer.provideMerge(platformLayer),
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
||||
)
|
||||
@@ -1,83 +0,0 @@
|
||||
import { Effect, Layer } from "effect"
|
||||
import { ChildProcessSpawner } from "effect/unstable/process"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import * as ServicePlatform from "./platform"
|
||||
import {
|
||||
buildUnixLauncher,
|
||||
chmodFile,
|
||||
quoteSystemd,
|
||||
UNIX_LAUNCHER,
|
||||
writeTextFile,
|
||||
ServiceError,
|
||||
} from "./shared"
|
||||
import path from "path"
|
||||
import { Global } from "@/global"
|
||||
import { ChildProcess } from "effect/unstable/process"
|
||||
import { Stream } from "effect"
|
||||
|
||||
const SYSTEMD_SERVICE_NAME = "opencode.service"
|
||||
const systemdUnitFile = path.join(path.dirname(Global.Path.config), "systemd", "user", SYSTEMD_SERVICE_NAME)
|
||||
|
||||
function buildLinuxUnit() {
|
||||
return [
|
||||
"[Unit]",
|
||||
"Description=OpenCode background server",
|
||||
"",
|
||||
"[Service]",
|
||||
"Type=simple",
|
||||
`ExecStart=${quoteSystemd(UNIX_LAUNCHER)}`,
|
||||
"WorkingDirectory=%h",
|
||||
"Restart=on-failure",
|
||||
"RestartSec=5",
|
||||
"",
|
||||
"[Install]",
|
||||
"WantedBy=default.target",
|
||||
"",
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
export const layer: Layer.Layer<
|
||||
ServicePlatform.Service,
|
||||
never,
|
||||
ChildProcessSpawner.ChildProcessSpawner | AppFileSystem.Service
|
||||
> = Layer.effect(
|
||||
ServicePlatform.Service,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
||||
|
||||
const run = Effect.fnUntraced(
|
||||
function* (command: string[]) {
|
||||
const handle = yield* spawner.spawn(
|
||||
ChildProcess.make(command[0], command.slice(1), { extendEnv: true, stdin: "ignore" }),
|
||||
)
|
||||
const [stdout, stderr] = yield* Effect.all(
|
||||
[Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))],
|
||||
{ concurrency: 2 },
|
||||
)
|
||||
const code = yield* handle.exitCode
|
||||
if (code === 0) return { stdout, stderr }
|
||||
return yield* new ServiceError({
|
||||
message: stderr.trim() || stdout.trim() || `Command failed: ${command.join(" ")}`,
|
||||
})
|
||||
},
|
||||
Effect.scoped,
|
||||
Effect.catch((cause) => {
|
||||
if (cause instanceof ServiceError) return Effect.fail(cause)
|
||||
return Effect.fail(new ServiceError({ message: "Failed to execute service manager command", cause }))
|
||||
}),
|
||||
)
|
||||
|
||||
const install = Effect.fn("ServiceLinux.install")(function* (input) {
|
||||
yield* writeTextFile(fs, UNIX_LAUNCHER, buildUnixLauncher(input.command, input.password), 0o700)
|
||||
yield* chmodFile(fs, UNIX_LAUNCHER, 0o700)
|
||||
yield* writeTextFile(fs, systemdUnitFile, buildLinuxUnit())
|
||||
yield* run(["systemctl", "--user", "daemon-reload"])
|
||||
yield* run(["systemctl", "--user", "enable", "--now", SYSTEMD_SERVICE_NAME])
|
||||
yield* run(["systemctl", "--user", "restart", SYSTEMD_SERVICE_NAME])
|
||||
return { target: systemdUnitFile }
|
||||
})
|
||||
|
||||
return ServicePlatform.Service.of({ install })
|
||||
}),
|
||||
)
|
||||
@@ -1,99 +0,0 @@
|
||||
import { Effect, Layer } from "effect"
|
||||
import { ChildProcessSpawner } from "effect/unstable/process"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import * as ServicePlatform from "./platform"
|
||||
import {
|
||||
buildUnixLauncher,
|
||||
chmodFile,
|
||||
escapeXml,
|
||||
stderrLogFile,
|
||||
ServiceError,
|
||||
stdoutLogFile,
|
||||
UNIX_LAUNCHER,
|
||||
writeTextFile,
|
||||
} from "./shared"
|
||||
import { Global } from "@/global"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import { ChildProcess } from "effect/unstable/process"
|
||||
import { Stream } from "effect"
|
||||
|
||||
const MAC_LABEL = "ai.opencode.service"
|
||||
const macLaunchAgentFile = path.join(Global.Path.home, "Library", "LaunchAgents", `${MAC_LABEL}.plist`)
|
||||
|
||||
function buildLaunchAgent() {
|
||||
return [
|
||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
||||
'<plist version="1.0">',
|
||||
"<dict>",
|
||||
" <key>Label</key>",
|
||||
` <string>${escapeXml(MAC_LABEL)}</string>`,
|
||||
" <key>ProgramArguments</key>",
|
||||
" <array>",
|
||||
` <string>${escapeXml(UNIX_LAUNCHER)}</string>`,
|
||||
" </array>",
|
||||
" <key>KeepAlive</key>",
|
||||
" <true/>",
|
||||
" <key>RunAtLoad</key>",
|
||||
" <true/>",
|
||||
" <key>WorkingDirectory</key>",
|
||||
` <string>${escapeXml(Global.Path.home)}</string>`,
|
||||
" <key>StandardOutPath</key>",
|
||||
` <string>${escapeXml(stdoutLogFile)}</string>`,
|
||||
" <key>StandardErrorPath</key>",
|
||||
` <string>${escapeXml(stderrLogFile)}</string>`,
|
||||
"</dict>",
|
||||
"</plist>",
|
||||
"",
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
export const layer: Layer.Layer<
|
||||
ServicePlatform.Service,
|
||||
never,
|
||||
ChildProcessSpawner.ChildProcessSpawner | AppFileSystem.Service
|
||||
> = Layer.effect(
|
||||
ServicePlatform.Service,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
||||
|
||||
const run = Effect.fnUntraced(
|
||||
function* (command: string[]) {
|
||||
const handle = yield* spawner.spawn(
|
||||
ChildProcess.make(command[0], command.slice(1), { extendEnv: true, stdin: "ignore" }),
|
||||
)
|
||||
const [stdout, stderr] = yield* Effect.all(
|
||||
[Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))],
|
||||
{ concurrency: 2 },
|
||||
)
|
||||
const code = yield* handle.exitCode
|
||||
if (code === 0) return { stdout, stderr }
|
||||
return yield* new ServiceError({
|
||||
message: stderr.trim() || stdout.trim() || `Command failed: ${command.join(" ")}`,
|
||||
})
|
||||
},
|
||||
Effect.scoped,
|
||||
Effect.catch((cause) => {
|
||||
if (cause instanceof ServiceError) return Effect.fail(cause)
|
||||
return Effect.fail(new ServiceError({ message: "Failed to execute service manager command", cause }))
|
||||
}),
|
||||
)
|
||||
|
||||
const install = Effect.fn("ServiceMacos.install")(function* (input) {
|
||||
const uid = typeof process.getuid === "function" ? process.getuid() : os.userInfo().uid
|
||||
const domain = `gui/${uid}`
|
||||
|
||||
yield* writeTextFile(fs, UNIX_LAUNCHER, buildUnixLauncher(input.command, input.password), 0o700)
|
||||
yield* chmodFile(fs, UNIX_LAUNCHER, 0o700)
|
||||
yield* writeTextFile(fs, macLaunchAgentFile, buildLaunchAgent())
|
||||
yield* run(["launchctl", "bootout", domain, macLaunchAgentFile]).pipe(Effect.catch(() => Effect.void))
|
||||
yield* run(["launchctl", "bootstrap", domain, macLaunchAgentFile])
|
||||
yield* run(["launchctl", "kickstart", "-k", `${domain}/${MAC_LABEL}`])
|
||||
return { target: macLaunchAgentFile }
|
||||
})
|
||||
|
||||
return ServicePlatform.Service.of({ install })
|
||||
}),
|
||||
)
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Context, Effect } from "effect"
|
||||
import type { PlatformInstallInput, PlatformInstallResult } from "./shared"
|
||||
import { ServiceError } from "./shared"
|
||||
|
||||
export interface Interface {
|
||||
readonly install: (input: PlatformInstallInput) => Effect.Effect<PlatformInstallResult, ServiceError>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/ServicePlatform") {}
|
||||
@@ -1,153 +0,0 @@
|
||||
import { Global } from "@/global"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Effect, Option, Schema } from "effect"
|
||||
import path from "path"
|
||||
|
||||
export const SERVICE_DIR = path.join(Global.Path.data, "service")
|
||||
export const CONFIG_FILE = path.join(SERVICE_DIR, "config.json")
|
||||
export const UNIX_LAUNCHER = path.join(SERVICE_DIR, "run.sh")
|
||||
export const WINDOWS_LAUNCHER = path.join(SERVICE_DIR, "run.ps1")
|
||||
export const stdoutLogFile = path.join(Global.Path.log, "service.stdout.log")
|
||||
export const stderrLogFile = path.join(Global.Path.log, "service.stderr.log")
|
||||
|
||||
export const StoredConfig = Schema.Struct({
|
||||
password: Schema.String,
|
||||
hostname: Schema.String,
|
||||
})
|
||||
|
||||
export type StoredConfig = Schema.Schema.Type<typeof StoredConfig>
|
||||
|
||||
export interface InstallInput {
|
||||
readonly password?: string
|
||||
readonly hostname?: string
|
||||
}
|
||||
|
||||
export interface InstallResult {
|
||||
readonly password: string
|
||||
readonly hostname: string
|
||||
readonly platform: NodeJS.Platform
|
||||
readonly target: string
|
||||
}
|
||||
|
||||
export interface PlatformInstallInput {
|
||||
readonly password: string
|
||||
readonly hostname: string
|
||||
readonly command: string[]
|
||||
}
|
||||
|
||||
export interface PlatformInstallResult {
|
||||
readonly target: string
|
||||
}
|
||||
|
||||
export class ServiceError extends Schema.TaggedErrorClass<ServiceError>()("ServiceError", {
|
||||
message: Schema.String,
|
||||
cause: Schema.optional(Schema.Defect),
|
||||
}) {}
|
||||
|
||||
export const fail = (message: string) => (cause: unknown) => new ServiceError({ message, cause })
|
||||
|
||||
export function generatePassword() {
|
||||
const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789-_"
|
||||
return Array.from(crypto.getRandomValues(new Uint8Array(24)), (byte) => alphabet[byte % alphabet.length]).join("")
|
||||
}
|
||||
|
||||
export function quoteShell(value: string) {
|
||||
return `'${value.replaceAll("'", `'"'"'`)}'`
|
||||
}
|
||||
|
||||
export function quotePowerShell(value: string) {
|
||||
return `'${value.replaceAll("'", "''")}'`
|
||||
}
|
||||
|
||||
export function quoteSystemd(value: string) {
|
||||
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`
|
||||
}
|
||||
|
||||
export function quoteWindowsArg(value: string) {
|
||||
if (!value) return '""'
|
||||
if (!/[\s"]/u.test(value)) return value
|
||||
let result = '"'
|
||||
let slashes = 0
|
||||
for (const char of value) {
|
||||
if (char === "\\") {
|
||||
slashes += 1
|
||||
continue
|
||||
}
|
||||
if (char === '"') {
|
||||
result += "\\".repeat(slashes * 2 + 1) + char
|
||||
slashes = 0
|
||||
continue
|
||||
}
|
||||
result += "\\".repeat(slashes) + char
|
||||
slashes = 0
|
||||
}
|
||||
return result + "\\".repeat(slashes * 2) + '"'
|
||||
}
|
||||
|
||||
export function escapeXml(value: string) {
|
||||
return value
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'")
|
||||
}
|
||||
|
||||
function usesScriptRuntime() {
|
||||
const basename = path.basename(process.execPath).toLowerCase()
|
||||
return basename === "node" || basename === "node.exe" || basename === "bun" || basename === "bun.exe"
|
||||
}
|
||||
|
||||
export function resolveServeCommand(fs: AppFileSystem.Interface, hostname: string) {
|
||||
return Effect.gen(function* () {
|
||||
if (usesScriptRuntime() && process.argv[1] && (yield* fs.existsSafe(process.argv[1]).pipe(Effect.orElseSucceed(() => false)))) {
|
||||
return [process.execPath, ...process.execArgv, process.argv[1], "serve", "--hostname", hostname]
|
||||
}
|
||||
return [process.execPath, "serve", "--hostname", hostname]
|
||||
})
|
||||
}
|
||||
|
||||
export function buildUnixLauncher(command: string[], password: string) {
|
||||
return [
|
||||
"#!/bin/sh",
|
||||
"set -eu",
|
||||
`export OPENCODE_SERVER_PASSWORD=${quoteShell(password)}`,
|
||||
`exec ${command.map(quoteShell).join(" ")}`,
|
||||
"",
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
export function buildWindowsLauncher(command: string[], password: string) {
|
||||
const args = command.slice(1).map(quotePowerShell).join(", ")
|
||||
return [
|
||||
"$ErrorActionPreference = 'Stop'",
|
||||
`[Environment]::SetEnvironmentVariable('OPENCODE_SERVER_PASSWORD', ${quotePowerShell(password)}, 'Process')`,
|
||||
`$arguments = @(${args})`,
|
||||
`& ${quotePowerShell(command[0])} @arguments`,
|
||||
"exit $LASTEXITCODE",
|
||||
"",
|
||||
].join("\r\n")
|
||||
}
|
||||
|
||||
export function readStoredConfig(fs: AppFileSystem.Interface) {
|
||||
const decodeStored = Schema.decodeUnknownOption(StoredConfig)
|
||||
return Effect.gen(function* () {
|
||||
const exists = yield* fs.existsSafe(CONFIG_FILE).pipe(Effect.mapError(fail(`Failed to access ${CONFIG_FILE}`)))
|
||||
if (!exists) return undefined
|
||||
const raw = yield* fs.readJson(CONFIG_FILE).pipe(Effect.mapError(fail(`Failed to read ${CONFIG_FILE}`)))
|
||||
return Option.getOrUndefined(decodeStored(raw))
|
||||
})
|
||||
}
|
||||
|
||||
export function writeTextFile(fs: AppFileSystem.Interface, file: string, content: string, mode?: number) {
|
||||
return fs.writeWithDirs(file, content, mode).pipe(Effect.mapError(fail(`Failed to write ${file}`)))
|
||||
}
|
||||
|
||||
export function writeJsonFile(fs: AppFileSystem.Interface, file: string, content: unknown, mode?: number) {
|
||||
return fs.writeWithDirs(file, JSON.stringify(content, null, 2), mode).pipe(Effect.mapError(fail(`Failed to write ${file}`)))
|
||||
}
|
||||
|
||||
export function chmodFile(fs: AppFileSystem.Interface, file: string, mode: number) {
|
||||
if (process.platform === "win32") return Effect.void
|
||||
return fs.chmod(file, mode).pipe(Effect.mapError(fail(`Failed to update permissions for ${file}`)))
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Effect, Layer } from "effect"
|
||||
import * as ServicePlatform from "./platform"
|
||||
import { ServiceError } from "./shared"
|
||||
|
||||
export const layer = Layer.succeed(
|
||||
ServicePlatform.Service,
|
||||
ServicePlatform.Service.of({
|
||||
install: () => Effect.fail(new ServiceError({ message: `Unsupported platform: ${process.platform}` })),
|
||||
}),
|
||||
)
|
||||
@@ -1,79 +0,0 @@
|
||||
import { Effect, Layer } from "effect"
|
||||
import { ChildProcessSpawner } from "effect/unstable/process"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import * as ServicePlatform from "./platform"
|
||||
import { buildWindowsLauncher, quoteWindowsArg, ServiceError, WINDOWS_LAUNCHER, writeTextFile } from "./shared"
|
||||
import { ChildProcess } from "effect/unstable/process"
|
||||
import { Stream } from "effect"
|
||||
|
||||
const WINDOWS_TASK_NAME = "OpenCode Service"
|
||||
|
||||
function buildWindowsTaskCommand() {
|
||||
return [
|
||||
"powershell.exe",
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-WindowStyle",
|
||||
"Hidden",
|
||||
"-File",
|
||||
WINDOWS_LAUNCHER,
|
||||
]
|
||||
.map(quoteWindowsArg)
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
export const layer: Layer.Layer<
|
||||
ServicePlatform.Service,
|
||||
never,
|
||||
ChildProcessSpawner.ChildProcessSpawner | AppFileSystem.Service
|
||||
> = Layer.effect(
|
||||
ServicePlatform.Service,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* AppFileSystem.Service
|
||||
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
||||
|
||||
const run = Effect.fnUntraced(
|
||||
function* (command: string[]) {
|
||||
const handle = yield* spawner.spawn(
|
||||
ChildProcess.make(command[0], command.slice(1), { extendEnv: true, stdin: "ignore" }),
|
||||
)
|
||||
const [stdout, stderr] = yield* Effect.all(
|
||||
[Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))],
|
||||
{ concurrency: 2 },
|
||||
)
|
||||
const code = yield* handle.exitCode
|
||||
if (code === 0) return { stdout, stderr }
|
||||
return yield* new ServiceError({
|
||||
message: stderr.trim() || stdout.trim() || `Command failed: ${command.join(" ")}`,
|
||||
})
|
||||
},
|
||||
Effect.scoped,
|
||||
Effect.catch((cause) => {
|
||||
if (cause instanceof ServiceError) return Effect.fail(cause)
|
||||
return Effect.fail(new ServiceError({ message: "Failed to execute service manager command", cause }))
|
||||
}),
|
||||
)
|
||||
|
||||
const install = Effect.fn("ServiceWindows.install")(function* (input) {
|
||||
yield* writeTextFile(fs, WINDOWS_LAUNCHER, buildWindowsLauncher(input.command, input.password))
|
||||
yield* run(["schtasks", "/end", "/tn", WINDOWS_TASK_NAME]).pipe(Effect.catch(() => Effect.void))
|
||||
yield* run([
|
||||
"schtasks",
|
||||
"/create",
|
||||
"/tn",
|
||||
WINDOWS_TASK_NAME,
|
||||
"/sc",
|
||||
"onlogon",
|
||||
"/tr",
|
||||
buildWindowsTaskCommand(),
|
||||
"/f",
|
||||
])
|
||||
yield* run(["schtasks", "/run", "/tn", WINDOWS_TASK_NAME]).pipe(Effect.catch(() => Effect.void))
|
||||
return { target: WINDOWS_TASK_NAME }
|
||||
})
|
||||
|
||||
return ServicePlatform.Service.of({ install })
|
||||
}),
|
||||
)
|
||||
Reference in New Issue
Block a user