feat: add gateway service commands
This commit is contained in:
parent
fec7f37271
commit
a1e7d321bc
11
README.md
11
README.md
@ -138,6 +138,17 @@ Run `clawdbot doctor` to surface risky/misconfigured DM policies.
|
||||
- [Nix mode](https://docs.clawd.bot/nix) for declarative config; [Docker](https://docs.clawd.bot/docker)-based installs.
|
||||
- [Doctor](https://docs.clawd.bot/doctor) migrations, [logging](https://docs.clawd.bot/logging).
|
||||
|
||||
#### Gateway service (optional)
|
||||
|
||||
Install a supervised service (launchd/systemd/Scheduled Task):
|
||||
|
||||
```bash
|
||||
clawdbot gateway install
|
||||
clawdbot gateway service-status
|
||||
```
|
||||
|
||||
Aliases: `clawdbot service ...` or `clawdbot daemon ...`.
|
||||
|
||||
## How it works (short)
|
||||
|
||||
```
|
||||
|
||||
@ -69,6 +69,11 @@ clawdbot login
|
||||
clawdbot gateway --port 18789
|
||||
```
|
||||
|
||||
Optional (supervised, always-on):
|
||||
```bash
|
||||
clawdbot gateway install
|
||||
```
|
||||
|
||||
3) Put a minimal config in `~/.clawdbot/clawdbot.json`:
|
||||
|
||||
```json5
|
||||
|
||||
@ -504,6 +504,11 @@ The gateway runs under a supervisor that auto-restarts it. You need to stop the
|
||||
- To inspect launchd state: `launchctl print gui/$UID | grep clawdbot`
|
||||
|
||||
**macOS (CLI launchd service, if installed)**
|
||||
If you haven’t installed a supervisor yet, install one first:
|
||||
```bash
|
||||
clawdbot gateway install
|
||||
# Or: clawdbot service install
|
||||
```
|
||||
|
||||
```bash
|
||||
clawdbot gateway stop
|
||||
|
||||
@ -219,8 +219,13 @@ sudo systemctl enable --now clawdbot-gateway.service
|
||||
- `clawdbot gateway send --to <num> --message "hi" [--media-url ...]` — send via Gateway (idempotent).
|
||||
- `clawdbot gateway agent --message "hi" [--to ...]` — run an agent turn (waits for final by default).
|
||||
- `clawdbot gateway call <method> --params '{"k":"v"}'` — raw method invoker for debugging.
|
||||
- `clawdbot gateway install|uninstall` — install/remove the supervised gateway service (launchd/systemd/schtasks).
|
||||
- `clawdbot gateway service-status` — report supervised gateway service status.
|
||||
- `clawdbot gateway stop|restart` — stop/restart the supervised gateway service (launchd/systemd/schtasks).
|
||||
- Gateway helper subcommands assume a running gateway on `--url`; they no longer auto-spawn one.
|
||||
- Service aliases:
|
||||
- `clawdbot service ...` — service controls (install/uninstall/status/stop/restart)
|
||||
- `clawdbot daemon ...` — alias for `clawdbot service ...`
|
||||
|
||||
## Migration guidance
|
||||
- Retire uses of `clawdbot gateway` and the legacy TCP control port.
|
||||
|
||||
@ -105,6 +105,20 @@ pnpm clawdbot health
|
||||
- Sessions: `~/.clawdbot/agents/<agentId>/sessions/`
|
||||
- Logs: `/tmp/clawdbot/`
|
||||
|
||||
## Gateway service (optional)
|
||||
|
||||
If you want the Gateway supervised outside the macOS app (CLI-only, headless, or remote setups). The macOS app already manages the Gateway via launchd.
|
||||
|
||||
```bash
|
||||
clawdbot gateway install
|
||||
clawdbot gateway service-status
|
||||
clawdbot gateway uninstall
|
||||
```
|
||||
|
||||
Aliases:
|
||||
- `clawdbot service ...`
|
||||
- `clawdbot daemon ...`
|
||||
|
||||
## Updating (without wrecking your setup)
|
||||
|
||||
- Keep `~/clawd` and `~/.clawdbot/` as “your stuff”; don’t put personal prompts/config into the `clawdbot` repo.
|
||||
|
||||
@ -167,6 +167,12 @@ clawdbot gateway stop
|
||||
# Or: launchctl bootout gui/$UID/com.clawdbot.gateway
|
||||
```
|
||||
|
||||
If no supervisor is installed yet:
|
||||
```bash
|
||||
clawdbot gateway install
|
||||
clawdbot gateway service-status
|
||||
```
|
||||
|
||||
**Fix 2: Check embedded gateway**
|
||||
Ensure the gateway relay was properly bundled. Run [`./scripts/package-mac-app.sh`](https://github.com/clawdbot/clawdbot/blob/main/scripts/package-mac-app.sh) and ensure `bun` is installed.
|
||||
|
||||
|
||||
@ -74,13 +74,14 @@ It does **not** install or change anything on the remote host.
|
||||
- DM security: default is pairing (unknown DMs get a pairing code). Approve via `clawdbot pairing approve --provider <provider> <code>`.
|
||||
|
||||
6) **Daemon install**
|
||||
- macOS: LaunchAgent
|
||||
- Requires a logged-in user session; for headless, use a custom LaunchDaemon (not shipped).
|
||||
- Linux: systemd user unit
|
||||
- Wizard attempts to enable lingering via `loginctl enable-linger <user>` so the Gateway stays up after logout.
|
||||
- May prompt for sudo (writes `/var/lib/systemd/linger`); it tries without sudo first.
|
||||
- Windows: Scheduled Task
|
||||
- Runs on user logon; headless/system services are not configured by default.
|
||||
- macOS: LaunchAgent
|
||||
- Requires a logged-in user session; for headless, use a custom LaunchDaemon (not shipped).
|
||||
- Linux: systemd user unit
|
||||
- Wizard attempts to enable lingering via `loginctl enable-linger <user>` so the Gateway stays up after logout.
|
||||
- May prompt for sudo (writes `/var/lib/systemd/linger`); it tries without sudo first.
|
||||
- Windows: Scheduled Task
|
||||
- Runs on user logon; headless/system services are not configured by default.
|
||||
- Manual alternative: `clawdbot gateway install` (or `clawdbot service install`).
|
||||
|
||||
7) **Health check**
|
||||
- Starts the Gateway (if needed) and runs `clawdbot health`.
|
||||
|
||||
@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import * as sessions from "../../config/sessions.js";
|
||||
import type { SessionEntry } from "../../config/sessions.js";
|
||||
import * as sessions from "../../config/sessions.js";
|
||||
import type { TemplateContext } from "../templating.js";
|
||||
|
||||
@ -13,9 +13,16 @@ const forceFreePortAndWait = vi.fn(async () => ({
|
||||
waitedMs: 0,
|
||||
escalatedToSigkill: false,
|
||||
}));
|
||||
const resolveGatewayProgramArguments = vi.fn(async () => ({
|
||||
programArguments: ["node", "cli", "gateway-daemon", "--port", "18789"],
|
||||
workingDirectory: "/tmp",
|
||||
}));
|
||||
const serviceInstall = vi.fn().mockResolvedValue(undefined);
|
||||
const serviceStop = vi.fn().mockResolvedValue(undefined);
|
||||
const serviceUninstall = vi.fn().mockResolvedValue(undefined);
|
||||
const serviceRestart = vi.fn().mockResolvedValue(undefined);
|
||||
const serviceIsLoaded = vi.fn().mockResolvedValue(true);
|
||||
const serviceReadCommand = vi.fn().mockResolvedValue(null);
|
||||
|
||||
const runtimeLogs: string[] = [];
|
||||
const runtimeErrors: string[] = [];
|
||||
@ -77,17 +84,22 @@ vi.mock("./ports.js", () => ({
|
||||
forceFreePortAndWait: (port: number) => forceFreePortAndWait(port),
|
||||
}));
|
||||
|
||||
vi.mock("../daemon/program-args.js", () => ({
|
||||
resolveGatewayProgramArguments: (params: unknown) =>
|
||||
resolveGatewayProgramArguments(params),
|
||||
}));
|
||||
|
||||
vi.mock("../daemon/service.js", () => ({
|
||||
resolveGatewayService: () => ({
|
||||
label: "LaunchAgent",
|
||||
loadedText: "loaded",
|
||||
notLoadedText: "not loaded",
|
||||
install: vi.fn(),
|
||||
uninstall: vi.fn(),
|
||||
install: serviceInstall,
|
||||
uninstall: serviceUninstall,
|
||||
stop: serviceStop,
|
||||
restart: serviceRestart,
|
||||
isLoaded: serviceIsLoaded,
|
||||
readCommand: vi.fn(),
|
||||
readCommand: serviceReadCommand,
|
||||
}),
|
||||
}));
|
||||
|
||||
@ -264,6 +276,66 @@ describe("gateway-cli coverage", () => {
|
||||
expect(serviceRestart).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("supports gateway install/uninstall for service setup", async () => {
|
||||
runtimeLogs.length = 0;
|
||||
runtimeErrors.length = 0;
|
||||
serviceInstall.mockClear();
|
||||
serviceUninstall.mockClear();
|
||||
serviceIsLoaded.mockResolvedValue(false);
|
||||
|
||||
const { registerGatewayCli } = await import("./gateway-cli.js");
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
registerGatewayCli(program);
|
||||
|
||||
await program.parseAsync(["gateway", "install"], { from: "user" });
|
||||
await program.parseAsync(["gateway", "uninstall"], { from: "user" });
|
||||
|
||||
expect(serviceInstall).toHaveBeenCalledTimes(1);
|
||||
expect(serviceUninstall).toHaveBeenCalledTimes(1);
|
||||
expect(resolveGatewayProgramArguments).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("supports service alias commands", async () => {
|
||||
runtimeLogs.length = 0;
|
||||
runtimeErrors.length = 0;
|
||||
serviceStop.mockClear();
|
||||
serviceRestart.mockClear();
|
||||
serviceIsLoaded.mockResolvedValue(true);
|
||||
|
||||
const { registerGatewayCli } = await import("./gateway-cli.js");
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
registerGatewayCli(program);
|
||||
|
||||
await program.parseAsync(["service", "stop"], { from: "user" });
|
||||
await program.parseAsync(["daemon", "restart"], { from: "user" });
|
||||
|
||||
expect(serviceStop).toHaveBeenCalledTimes(1);
|
||||
expect(serviceRestart).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("prints gateway service status details when available", async () => {
|
||||
runtimeLogs.length = 0;
|
||||
runtimeErrors.length = 0;
|
||||
serviceReadCommand.mockResolvedValueOnce({
|
||||
programArguments: ["node", "cli", "gateway-daemon", "--port", "18789"],
|
||||
workingDirectory: "/tmp",
|
||||
});
|
||||
serviceIsLoaded.mockResolvedValue(true);
|
||||
|
||||
const { registerGatewayCli } = await import("./gateway-cli.js");
|
||||
const program = new Command();
|
||||
program.exitOverride();
|
||||
registerGatewayCli(program);
|
||||
|
||||
await program.parseAsync(["gateway", "service-status"], { from: "user" });
|
||||
|
||||
expect(runtimeLogs.join("\n")).toContain("Gateway service loaded.");
|
||||
expect(runtimeLogs.join("\n")).toContain("Command:");
|
||||
expect(runtimeLogs.join("\n")).toContain("Working directory:");
|
||||
});
|
||||
|
||||
it("prints stop hints on GatewayLockError when service is loaded", async () => {
|
||||
runtimeLogs.length = 0;
|
||||
runtimeErrors.length = 0;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import type { Command } from "commander";
|
||||
import {
|
||||
@ -11,6 +12,7 @@ import {
|
||||
GATEWAY_SYSTEMD_SERVICE_NAME,
|
||||
GATEWAY_WINDOWS_TASK_NAME,
|
||||
} from "../daemon/constants.js";
|
||||
import { resolveGatewayProgramArguments } from "../daemon/program-args.js";
|
||||
import { resolveGatewayService } from "../daemon/service.js";
|
||||
import { callGateway, randomIdempotencyKey } from "../gateway/call.js";
|
||||
import { startGatewayServer } from "../gateway/server.js";
|
||||
@ -36,6 +38,12 @@ type GatewayRpcOpts = {
|
||||
const gatewayLog = createSubsystemLogger("gateway");
|
||||
|
||||
type GatewayRunSignalAction = "stop" | "restart";
|
||||
type GatewayServiceInstallOpts = {
|
||||
port?: unknown;
|
||||
token?: unknown;
|
||||
password?: unknown;
|
||||
reinstall?: boolean;
|
||||
};
|
||||
|
||||
function parsePort(raw: unknown): number | null {
|
||||
if (raw === undefined || raw === null) return null;
|
||||
@ -106,6 +114,14 @@ function renderGatewayServiceStartHints(): string[] {
|
||||
}
|
||||
}
|
||||
|
||||
function renderGatewayServiceInstallHints(): string[] {
|
||||
return [
|
||||
"Install with: clawdbot gateway install",
|
||||
"Or: clawdbot onboard --install-daemon",
|
||||
"Or: clawdbot configure",
|
||||
];
|
||||
}
|
||||
|
||||
async function maybeExplainGatewayServiceStop() {
|
||||
const service = resolveGatewayService();
|
||||
let loaded: boolean | null = null;
|
||||
@ -125,6 +141,151 @@ async function maybeExplainGatewayServiceStop() {
|
||||
}
|
||||
}
|
||||
|
||||
async function stopGatewayService() {
|
||||
const service = resolveGatewayService();
|
||||
let loaded = false;
|
||||
try {
|
||||
loaded = await service.isLoaded({ env: process.env });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
if (!loaded) {
|
||||
defaultRuntime.log(`Gateway service ${service.notLoadedText}.`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await service.stop({ stdout: process.stdout });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway stop failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function restartGatewayService() {
|
||||
const service = resolveGatewayService();
|
||||
let loaded = false;
|
||||
try {
|
||||
loaded = await service.isLoaded({ env: process.env });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
if (!loaded) {
|
||||
defaultRuntime.log(`Gateway service ${service.notLoadedText}.`);
|
||||
for (const hint of renderGatewayServiceStartHints()) {
|
||||
defaultRuntime.log(`Start with: ${hint}`);
|
||||
}
|
||||
for (const hint of renderGatewayServiceInstallHints()) {
|
||||
defaultRuntime.log(hint);
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await service.restart({ stdout: process.stdout });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway restart failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function installGatewayService(opts: GatewayServiceInstallOpts) {
|
||||
const cfg = loadConfig();
|
||||
const portOverride = parsePort(opts.port);
|
||||
if (opts.port !== undefined && portOverride === null) {
|
||||
defaultRuntime.error("Invalid port");
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
const port = portOverride ?? resolveGatewayPort(cfg);
|
||||
if (!Number.isFinite(port) || port <= 0) {
|
||||
defaultRuntime.error("Invalid port");
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const service = resolveGatewayService();
|
||||
let loaded = false;
|
||||
try {
|
||||
loaded = await service.isLoaded({ env: process.env });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
if (loaded && !opts.reinstall) {
|
||||
defaultRuntime.log(`Gateway service already ${service.loadedText}.`);
|
||||
defaultRuntime.log("Use: clawdbot gateway uninstall (or --reinstall)");
|
||||
return;
|
||||
}
|
||||
if (loaded && opts.reinstall) {
|
||||
await service.uninstall({ env: process.env, stdout: process.stdout });
|
||||
}
|
||||
|
||||
const devMode =
|
||||
process.argv[1]?.includes(`${path.sep}src${path.sep}`) &&
|
||||
process.argv[1]?.endsWith(".ts");
|
||||
const { programArguments, workingDirectory } =
|
||||
await resolveGatewayProgramArguments({ port, dev: devMode });
|
||||
const environment: Record<string, string | undefined> = {
|
||||
PATH: process.env.PATH,
|
||||
CLAWDBOT_GATEWAY_TOKEN: opts.token ? String(opts.token) : undefined,
|
||||
CLAWDBOT_GATEWAY_PASSWORD: opts.password ? String(opts.password) : undefined,
|
||||
CLAWDBOT_LAUNCHD_LABEL:
|
||||
process.platform === "darwin" ? GATEWAY_LAUNCH_AGENT_LABEL : undefined,
|
||||
};
|
||||
try {
|
||||
await service.install({
|
||||
env: process.env,
|
||||
stdout: process.stdout,
|
||||
programArguments,
|
||||
workingDirectory,
|
||||
environment,
|
||||
});
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway install failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function uninstallGatewayService() {
|
||||
const service = resolveGatewayService();
|
||||
try {
|
||||
await service.uninstall({ env: process.env, stdout: process.stdout });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway uninstall failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function statusGatewayService() {
|
||||
const service = resolveGatewayService();
|
||||
let loaded = false;
|
||||
try {
|
||||
loaded = await service.isLoaded({ env: process.env });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
defaultRuntime.log(
|
||||
`Gateway service ${loaded ? service.loadedText : service.notLoadedText}.`,
|
||||
);
|
||||
try {
|
||||
const command = await service.readCommand(process.env);
|
||||
if (command?.programArguments?.length) {
|
||||
defaultRuntime.log(`Command: ${command.programArguments.join(" ")}`);
|
||||
}
|
||||
if (command?.workingDirectory) {
|
||||
defaultRuntime.log(`Working directory: ${command.workingDirectory}`);
|
||||
}
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway service status failed: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function runGatewayLoop(params: {
|
||||
start: () => Promise<Awaited<ReturnType<typeof startGatewayServer>>>;
|
||||
runtime: typeof defaultRuntime;
|
||||
@ -229,6 +390,51 @@ const callGatewayCli = async (
|
||||
mode: "cli",
|
||||
});
|
||||
|
||||
function registerGatewayServiceCli(
|
||||
command: Command,
|
||||
opts: { statusName?: string } = {},
|
||||
) {
|
||||
const statusName = opts.statusName ?? "status";
|
||||
command
|
||||
.command("install")
|
||||
.description("Install the Gateway service (launchd/systemd/schtasks)")
|
||||
.option("--port <port>", "Gateway port for the service")
|
||||
.option("--token <token>", "Gateway token (optional)")
|
||||
.option("--password <password>", "Gateway password (optional)")
|
||||
.option("--reinstall", "Uninstall + reinstall if already present", false)
|
||||
.action(async (options) => {
|
||||
await installGatewayService(options);
|
||||
});
|
||||
|
||||
command
|
||||
.command("uninstall")
|
||||
.description("Remove the Gateway service (launchd/systemd/schtasks)")
|
||||
.action(async () => {
|
||||
await uninstallGatewayService();
|
||||
});
|
||||
|
||||
command
|
||||
.command(statusName)
|
||||
.description("Show Gateway service status")
|
||||
.action(async () => {
|
||||
await statusGatewayService();
|
||||
});
|
||||
|
||||
command
|
||||
.command("stop")
|
||||
.description("Stop the Gateway service (launchd/systemd/schtasks)")
|
||||
.action(async () => {
|
||||
await stopGatewayService();
|
||||
});
|
||||
|
||||
command
|
||||
.command("restart")
|
||||
.description("Restart the Gateway service (launchd/systemd/schtasks)")
|
||||
.action(async () => {
|
||||
await restartGatewayService();
|
||||
});
|
||||
}
|
||||
|
||||
export function registerGatewayCli(program: Command) {
|
||||
program
|
||||
.command("gateway-daemon")
|
||||
@ -733,58 +939,17 @@ export function registerGatewayCli(program: Command) {
|
||||
}),
|
||||
);
|
||||
|
||||
gateway
|
||||
.command("stop")
|
||||
.description("Stop the Gateway service (launchd/systemd/schtasks)")
|
||||
.action(async () => {
|
||||
const service = resolveGatewayService();
|
||||
let loaded = false;
|
||||
try {
|
||||
loaded = await service.isLoaded({ env: process.env });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
if (!loaded) {
|
||||
defaultRuntime.log(`Gateway service ${service.notLoadedText}.`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await service.stop({ stdout: process.stdout });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway stop failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
}
|
||||
});
|
||||
registerGatewayServiceCli(gateway, { statusName: "service-status" });
|
||||
|
||||
gateway
|
||||
.command("restart")
|
||||
.description("Restart the Gateway service (launchd/systemd/schtasks)")
|
||||
.action(async () => {
|
||||
const service = resolveGatewayService();
|
||||
let loaded = false;
|
||||
try {
|
||||
loaded = await service.isLoaded({ env: process.env });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway service check failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
if (!loaded) {
|
||||
defaultRuntime.log(`Gateway service ${service.notLoadedText}.`);
|
||||
for (const hint of renderGatewayServiceStartHints()) {
|
||||
defaultRuntime.log(`Start with: ${hint}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await service.restart({ stdout: process.stdout });
|
||||
} catch (err) {
|
||||
defaultRuntime.error(`Gateway restart failed: ${String(err)}`);
|
||||
defaultRuntime.exit(1);
|
||||
}
|
||||
});
|
||||
const service = program
|
||||
.command("service")
|
||||
.description("Gateway service controls (launchd/systemd/schtasks)");
|
||||
registerGatewayServiceCli(service);
|
||||
|
||||
const daemon = program
|
||||
.command("daemon")
|
||||
.description("Alias for gateway service controls");
|
||||
registerGatewayServiceCli(daemon);
|
||||
|
||||
// Build default deps (keeps parity with other commands; future-proofing).
|
||||
void createDefaultDeps();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user