@task0/cli 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -0
- package/dist/main.js +426 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,6 +62,55 @@ task0 task triage <id> # decompose an IDEA into ISSUE files
|
|
|
62
62
|
task0 run <id> # drive the full triage → plan → refine → exec loop
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
## Run as a service (autostart on login / boot)
|
|
66
|
+
|
|
67
|
+
`task0 daemon register` installs an OS-level autostart service in addition to
|
|
68
|
+
recording the daemon identity. After registering you can sign out, reboot, and
|
|
69
|
+
the daemon reconnects on its own.
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
# user-level (default) — starts at user login, no sudo needed
|
|
73
|
+
task0 daemon register --server https://central.example.com:4318
|
|
74
|
+
|
|
75
|
+
# system-level — starts at boot, runs without an active login
|
|
76
|
+
sudo -E task0 daemon register --system --server https://central.example.com:4318
|
|
77
|
+
|
|
78
|
+
# pause / resume without forgetting the identity
|
|
79
|
+
task0 daemon stop
|
|
80
|
+
task0 daemon start
|
|
81
|
+
|
|
82
|
+
# disable everything: stop the service, remove the unit, clear the identity
|
|
83
|
+
task0 daemon logout
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Per platform:
|
|
87
|
+
|
|
88
|
+
| Platform | Scope | File written |
|
|
89
|
+
|----------|----------|--------------|
|
|
90
|
+
| macOS | user | `~/Library/LaunchAgents/cc.cy0.task0.plist` (launchd) |
|
|
91
|
+
| macOS | system | `/Library/LaunchDaemons/cc.cy0.task0.plist` (launchd, needs sudo) |
|
|
92
|
+
| Linux | user | `~/.config/systemd/user/cc.cy0.task0.service` (systemd) |
|
|
93
|
+
| Linux | system | `/etc/systemd/system/cc.cy0.task0.service` (systemd, needs sudo) |
|
|
94
|
+
|
|
95
|
+
Logs land in `~/.task0/logs/daemon.{out,err}.log`. To watch them live:
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
tail -f ~/.task0/logs/daemon.out.log
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Linux user-scope note: systemd kills the service when you log out. To keep it
|
|
102
|
+
running across logouts, enable lingering once: `sudo loginctl enable-linger $USER`.
|
|
103
|
+
|
|
104
|
+
Useful flags on `register`:
|
|
105
|
+
|
|
106
|
+
- `--no-install` — register identity only; do not write a service unit
|
|
107
|
+
- `--no-start` — write the unit but do not start it immediately
|
|
108
|
+
- `--force` — re-register over an existing identity
|
|
109
|
+
|
|
110
|
+
For debugging or supervised setups you can also invoke the WebSocket loop
|
|
111
|
+
directly in the foreground with `task0 daemon run` (this is the same command
|
|
112
|
+
that the service unit invokes internally).
|
|
113
|
+
|
|
65
114
|
## Environment variables
|
|
66
115
|
|
|
67
116
|
| Variable | Purpose |
|
package/dist/main.js
CHANGED
|
@@ -901,6 +901,9 @@ var init_task_state2 = __esm({
|
|
|
901
901
|
});
|
|
902
902
|
|
|
903
903
|
// src/main.ts
|
|
904
|
+
import { readFileSync } from "fs";
|
|
905
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
906
|
+
import path24 from "path";
|
|
904
907
|
import { Command as Command22 } from "commander";
|
|
905
908
|
|
|
906
909
|
// src/commands/source.ts
|
|
@@ -1141,11 +1144,11 @@ async function request(method, pathname, body) {
|
|
|
1141
1144
|
return parsed;
|
|
1142
1145
|
}
|
|
1143
1146
|
var api = {
|
|
1144
|
-
get: (
|
|
1145
|
-
post: (
|
|
1146
|
-
put: (
|
|
1147
|
-
patch: (
|
|
1148
|
-
del: (
|
|
1147
|
+
get: (path25) => request("GET", path25),
|
|
1148
|
+
post: (path25, body) => request("POST", path25, body ?? {}),
|
|
1149
|
+
put: (path25, body) => request("PUT", path25, body ?? {}),
|
|
1150
|
+
patch: (path25, body) => request("PATCH", path25, body ?? {}),
|
|
1151
|
+
del: (path25) => request("DELETE", path25)
|
|
1149
1152
|
};
|
|
1150
1153
|
|
|
1151
1154
|
// src/commands/task/triage.ts
|
|
@@ -3809,7 +3812,7 @@ async function streamOutput(ref, runtimeId) {
|
|
|
3809
3812
|
}
|
|
3810
3813
|
|
|
3811
3814
|
// src/commands/daemon.ts
|
|
3812
|
-
import
|
|
3815
|
+
import os6 from "os";
|
|
3813
3816
|
import { Command as Command20 } from "commander";
|
|
3814
3817
|
import chalk20 from "chalk";
|
|
3815
3818
|
import WebSocket from "ws";
|
|
@@ -3887,6 +3890,308 @@ var rpcHandlers = {
|
|
|
3887
3890
|
}
|
|
3888
3891
|
};
|
|
3889
3892
|
|
|
3893
|
+
// src/core/daemon-service/launchd.ts
|
|
3894
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
3895
|
+
import fs23 from "fs";
|
|
3896
|
+
import path22 from "path";
|
|
3897
|
+
|
|
3898
|
+
// src/core/daemon-service/binary.ts
|
|
3899
|
+
import fs22 from "fs";
|
|
3900
|
+
import { fileURLToPath } from "url";
|
|
3901
|
+
function resolveTask0Invocation() {
|
|
3902
|
+
const node = process.execPath;
|
|
3903
|
+
const argv1 = process.argv[1] ?? fileURLToPath(import.meta.url);
|
|
3904
|
+
const main2 = fs22.realpathSync(argv1);
|
|
3905
|
+
if (!isInstalledBuild(main2) && process.env.TASK0_ALLOW_DEV_SERVICE !== "1") {
|
|
3906
|
+
throw new Error(
|
|
3907
|
+
`Refusing to install autostart service pointing at ${main2}.
|
|
3908
|
+
That looks like a development source, not an installed @task0/cli build.
|
|
3909
|
+
Install the CLI globally (\`npm i -g @task0/cli\`) and retry, or set
|
|
3910
|
+
TASK0_ALLOW_DEV_SERVICE=1 to override.`
|
|
3911
|
+
);
|
|
3912
|
+
}
|
|
3913
|
+
return { node, main: main2, args: ["daemon", "run"] };
|
|
3914
|
+
}
|
|
3915
|
+
function isInstalledBuild(p) {
|
|
3916
|
+
return /[/\\]dist[/\\]main\.js$/.test(p);
|
|
3917
|
+
}
|
|
3918
|
+
|
|
3919
|
+
// src/core/daemon-service/paths.ts
|
|
3920
|
+
import os5 from "os";
|
|
3921
|
+
import path21 from "path";
|
|
3922
|
+
|
|
3923
|
+
// src/core/daemon-service/types.ts
|
|
3924
|
+
var SERVICE_LABEL = "cc.cy0.task0";
|
|
3925
|
+
|
|
3926
|
+
// src/core/daemon-service/paths.ts
|
|
3927
|
+
function unitPath(scope) {
|
|
3928
|
+
const platform = process.platform;
|
|
3929
|
+
if (platform === "darwin") {
|
|
3930
|
+
return scope === "user" ? path21.join(os5.homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`) : path21.join("/", "Library", "LaunchDaemons", `${SERVICE_LABEL}.plist`);
|
|
3931
|
+
}
|
|
3932
|
+
if (platform === "linux") {
|
|
3933
|
+
return scope === "user" ? path21.join(os5.homedir(), ".config", "systemd", "user", `${SERVICE_LABEL}.service`) : path21.join("/", "etc", "systemd", "system", `${SERVICE_LABEL}.service`);
|
|
3934
|
+
}
|
|
3935
|
+
throw new Error(`Unsupported platform for service install: ${platform}`);
|
|
3936
|
+
}
|
|
3937
|
+
function logDir() {
|
|
3938
|
+
return path21.join(os5.homedir(), ".task0", "logs");
|
|
3939
|
+
}
|
|
3940
|
+
function logPaths() {
|
|
3941
|
+
const dir = logDir();
|
|
3942
|
+
return {
|
|
3943
|
+
out: path21.join(dir, "daemon.out.log"),
|
|
3944
|
+
err: path21.join(dir, "daemon.err.log")
|
|
3945
|
+
};
|
|
3946
|
+
}
|
|
3947
|
+
|
|
3948
|
+
// src/core/daemon-service/launchd.ts
|
|
3949
|
+
function escapeXml(s) {
|
|
3950
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3951
|
+
}
|
|
3952
|
+
function renderPlist(opts) {
|
|
3953
|
+
const programArgs = [opts.node, opts.main, ...opts.args].map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
|
|
3954
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
3955
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3956
|
+
<plist version="1.0">
|
|
3957
|
+
<dict>
|
|
3958
|
+
<key>Label</key>
|
|
3959
|
+
<string>${SERVICE_LABEL}</string>
|
|
3960
|
+
<key>ProgramArguments</key>
|
|
3961
|
+
<array>
|
|
3962
|
+
${programArgs}
|
|
3963
|
+
</array>
|
|
3964
|
+
<key>RunAtLoad</key>
|
|
3965
|
+
<true/>
|
|
3966
|
+
<key>KeepAlive</key>
|
|
3967
|
+
<dict>
|
|
3968
|
+
<key>SuccessfulExit</key>
|
|
3969
|
+
<false/>
|
|
3970
|
+
</dict>
|
|
3971
|
+
<key>ThrottleInterval</key>
|
|
3972
|
+
<integer>5</integer>
|
|
3973
|
+
<key>StandardOutPath</key>
|
|
3974
|
+
<string>${escapeXml(opts.out)}</string>
|
|
3975
|
+
<key>StandardErrorPath</key>
|
|
3976
|
+
<string>${escapeXml(opts.err)}</string>
|
|
3977
|
+
<key>WorkingDirectory</key>
|
|
3978
|
+
<string>${escapeXml(opts.home)}</string>
|
|
3979
|
+
<key>EnvironmentVariables</key>
|
|
3980
|
+
<dict>
|
|
3981
|
+
<key>PATH</key>
|
|
3982
|
+
<string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
|
|
3983
|
+
</dict>
|
|
3984
|
+
</dict>
|
|
3985
|
+
</plist>
|
|
3986
|
+
`;
|
|
3987
|
+
}
|
|
3988
|
+
function domainTarget(scope) {
|
|
3989
|
+
if (scope === "system") return "system";
|
|
3990
|
+
const uid = process.getuid?.() ?? 0;
|
|
3991
|
+
return `gui/${uid}`;
|
|
3992
|
+
}
|
|
3993
|
+
function serviceTarget(scope) {
|
|
3994
|
+
return `${domainTarget(scope)}/${SERVICE_LABEL}`;
|
|
3995
|
+
}
|
|
3996
|
+
function run2(cmd, args) {
|
|
3997
|
+
const res = spawnSync5(cmd, args, { encoding: "utf-8" });
|
|
3998
|
+
return { code: res.status ?? -1, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
|
|
3999
|
+
}
|
|
4000
|
+
function createLaunchdManager(scope) {
|
|
4001
|
+
const file = unitPath(scope);
|
|
4002
|
+
const logs = logPaths();
|
|
4003
|
+
async function install() {
|
|
4004
|
+
const inv = resolveTask0Invocation();
|
|
4005
|
+
fs23.mkdirSync(logDir(), { recursive: true });
|
|
4006
|
+
fs23.mkdirSync(path22.dirname(file), { recursive: true });
|
|
4007
|
+
const body = renderPlist({
|
|
4008
|
+
node: inv.node,
|
|
4009
|
+
main: inv.main,
|
|
4010
|
+
args: inv.args,
|
|
4011
|
+
home: process.env.HOME ?? "/",
|
|
4012
|
+
out: logs.out,
|
|
4013
|
+
err: logs.err
|
|
4014
|
+
});
|
|
4015
|
+
fs23.writeFileSync(file, body, { mode: 420 });
|
|
4016
|
+
const bootstrap = run2("launchctl", ["bootstrap", domainTarget(scope), file]);
|
|
4017
|
+
if (bootstrap.code !== 0) {
|
|
4018
|
+
const already = /already loaded|service already bootstrapped/i.test(bootstrap.stderr);
|
|
4019
|
+
if (already) {
|
|
4020
|
+
run2("launchctl", ["bootout", serviceTarget(scope)]);
|
|
4021
|
+
const retry = run2("launchctl", ["bootstrap", domainTarget(scope), file]);
|
|
4022
|
+
if (retry.code !== 0) {
|
|
4023
|
+
const legacy = run2("launchctl", ["load", "-w", file]);
|
|
4024
|
+
if (legacy.code !== 0) {
|
|
4025
|
+
throw new Error(
|
|
4026
|
+
`launchctl bootstrap/load failed: ${retry.stderr || legacy.stderr || bootstrap.stderr}`
|
|
4027
|
+
);
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
} else {
|
|
4031
|
+
const legacy = run2("launchctl", ["load", "-w", file]);
|
|
4032
|
+
if (legacy.code !== 0) {
|
|
4033
|
+
throw new Error(`launchctl bootstrap/load failed: ${bootstrap.stderr || legacy.stderr}`);
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
return { unitPath: file };
|
|
4038
|
+
}
|
|
4039
|
+
async function uninstall() {
|
|
4040
|
+
run2("launchctl", ["bootout", serviceTarget(scope)]);
|
|
4041
|
+
if (fs23.existsSync(file)) {
|
|
4042
|
+
run2("launchctl", ["unload", file]);
|
|
4043
|
+
fs23.unlinkSync(file);
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
async function start() {
|
|
4047
|
+
const res = run2("launchctl", ["kickstart", "-k", serviceTarget(scope)]);
|
|
4048
|
+
if (res.code !== 0) {
|
|
4049
|
+
const load = run2("launchctl", ["load", "-w", file]);
|
|
4050
|
+
if (load.code !== 0) {
|
|
4051
|
+
throw new Error(`launchctl start failed: ${res.stderr || load.stderr}`);
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
async function stop() {
|
|
4056
|
+
const res = run2("launchctl", ["kill", "SIGTERM", serviceTarget(scope)]);
|
|
4057
|
+
if (res.code !== 0) {
|
|
4058
|
+
run2("launchctl", ["unload", file]);
|
|
4059
|
+
}
|
|
4060
|
+
}
|
|
4061
|
+
async function status() {
|
|
4062
|
+
if (!fs23.existsSync(file)) return "absent";
|
|
4063
|
+
const printed = run2("launchctl", ["print", serviceTarget(scope)]);
|
|
4064
|
+
if (printed.code !== 0) return "installed";
|
|
4065
|
+
const out = printed.stdout;
|
|
4066
|
+
if (/\bstate\s*=\s*running/.test(out)) return "running";
|
|
4067
|
+
if (/\bstate\s*=\s*spawning/.test(out)) return "installed";
|
|
4068
|
+
if (/last exit code\s*=\s*[1-9]/i.test(out)) return "errored";
|
|
4069
|
+
return "stopped";
|
|
4070
|
+
}
|
|
4071
|
+
return {
|
|
4072
|
+
scope,
|
|
4073
|
+
install,
|
|
4074
|
+
uninstall,
|
|
4075
|
+
start,
|
|
4076
|
+
stop,
|
|
4077
|
+
status,
|
|
4078
|
+
unitPath: () => file,
|
|
4079
|
+
logPaths: () => logs
|
|
4080
|
+
};
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
// src/core/daemon-service/systemd.ts
|
|
4084
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
4085
|
+
import fs24 from "fs";
|
|
4086
|
+
import path23 from "path";
|
|
4087
|
+
function shellEscape(s) {
|
|
4088
|
+
if (!/[\s"\\$]/.test(s)) return s;
|
|
4089
|
+
return `"${s.replace(/[\\"]/g, (m) => `\\${m}`)}"`;
|
|
4090
|
+
}
|
|
4091
|
+
function renderUnit(opts) {
|
|
4092
|
+
const execStart = [opts.node, opts.main, ...opts.args].map(shellEscape).join(" ");
|
|
4093
|
+
const wantedBy = opts.scope === "user" ? "default.target" : "multi-user.target";
|
|
4094
|
+
return `[Unit]
|
|
4095
|
+
Description=task0 daemon \u2014 central-server bridge
|
|
4096
|
+
After=network-online.target
|
|
4097
|
+
Wants=network-online.target
|
|
4098
|
+
|
|
4099
|
+
[Service]
|
|
4100
|
+
Type=simple
|
|
4101
|
+
ExecStart=${execStart}
|
|
4102
|
+
Restart=on-failure
|
|
4103
|
+
RestartSec=5s
|
|
4104
|
+
StandardOutput=append:${opts.out}
|
|
4105
|
+
StandardError=append:${opts.err}
|
|
4106
|
+
Environment=NODE_ENV=production
|
|
4107
|
+
|
|
4108
|
+
[Install]
|
|
4109
|
+
WantedBy=${wantedBy}
|
|
4110
|
+
`;
|
|
4111
|
+
}
|
|
4112
|
+
function scopeFlag(scope) {
|
|
4113
|
+
return scope === "user" ? ["--user"] : [];
|
|
4114
|
+
}
|
|
4115
|
+
function run3(cmd, args) {
|
|
4116
|
+
const res = spawnSync6(cmd, args, { encoding: "utf-8" });
|
|
4117
|
+
return { code: res.status ?? -1, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
|
|
4118
|
+
}
|
|
4119
|
+
function createSystemdManager(scope) {
|
|
4120
|
+
const file = unitPath(scope);
|
|
4121
|
+
const logs = logPaths();
|
|
4122
|
+
const unitName = `${SERVICE_LABEL}.service`;
|
|
4123
|
+
async function install() {
|
|
4124
|
+
const inv = resolveTask0Invocation();
|
|
4125
|
+
fs24.mkdirSync(logDir(), { recursive: true });
|
|
4126
|
+
fs24.mkdirSync(path23.dirname(file), { recursive: true });
|
|
4127
|
+
const body = renderUnit({
|
|
4128
|
+
node: inv.node,
|
|
4129
|
+
main: inv.main,
|
|
4130
|
+
args: inv.args,
|
|
4131
|
+
out: logs.out,
|
|
4132
|
+
err: logs.err,
|
|
4133
|
+
scope
|
|
4134
|
+
});
|
|
4135
|
+
fs24.writeFileSync(file, body, { mode: 420 });
|
|
4136
|
+
const reload = run3("systemctl", [...scopeFlag(scope), "daemon-reload"]);
|
|
4137
|
+
if (reload.code !== 0) {
|
|
4138
|
+
throw new Error(`systemctl daemon-reload failed: ${reload.stderr}`);
|
|
4139
|
+
}
|
|
4140
|
+
return { unitPath: file };
|
|
4141
|
+
}
|
|
4142
|
+
async function uninstall() {
|
|
4143
|
+
run3("systemctl", [...scopeFlag(scope), "disable", "--now", unitName]);
|
|
4144
|
+
if (fs24.existsSync(file)) {
|
|
4145
|
+
fs24.unlinkSync(file);
|
|
4146
|
+
}
|
|
4147
|
+
run3("systemctl", [...scopeFlag(scope), "daemon-reload"]);
|
|
4148
|
+
}
|
|
4149
|
+
async function start() {
|
|
4150
|
+
const res = run3("systemctl", [...scopeFlag(scope), "enable", "--now", unitName]);
|
|
4151
|
+
if (res.code !== 0) {
|
|
4152
|
+
throw new Error(`systemctl enable --now failed: ${res.stderr}`);
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
4155
|
+
async function stop() {
|
|
4156
|
+
const res = run3("systemctl", [...scopeFlag(scope), "stop", unitName]);
|
|
4157
|
+
if (res.code !== 0) {
|
|
4158
|
+
throw new Error(`systemctl stop failed: ${res.stderr}`);
|
|
4159
|
+
}
|
|
4160
|
+
}
|
|
4161
|
+
async function status() {
|
|
4162
|
+
if (!fs24.existsSync(file)) return "absent";
|
|
4163
|
+
const res = run3("systemctl", [...scopeFlag(scope), "is-active", unitName]);
|
|
4164
|
+
const out = res.stdout.trim();
|
|
4165
|
+
if (out === "active") return "running";
|
|
4166
|
+
if (out === "inactive") return "stopped";
|
|
4167
|
+
if (out === "failed") return "errored";
|
|
4168
|
+
if (out === "activating" || out === "deactivating") return "installed";
|
|
4169
|
+
return "installed";
|
|
4170
|
+
}
|
|
4171
|
+
return {
|
|
4172
|
+
scope,
|
|
4173
|
+
install,
|
|
4174
|
+
uninstall,
|
|
4175
|
+
start,
|
|
4176
|
+
stop,
|
|
4177
|
+
status,
|
|
4178
|
+
unitPath: () => file,
|
|
4179
|
+
logPaths: () => logs
|
|
4180
|
+
};
|
|
4181
|
+
}
|
|
4182
|
+
|
|
4183
|
+
// src/core/daemon-service/index.ts
|
|
4184
|
+
function isPlatformSupported() {
|
|
4185
|
+
return process.platform === "darwin" || process.platform === "linux";
|
|
4186
|
+
}
|
|
4187
|
+
function getServiceManager(scope) {
|
|
4188
|
+
if (process.platform === "darwin") return createLaunchdManager(scope);
|
|
4189
|
+
if (process.platform === "linux") return createSystemdManager(scope);
|
|
4190
|
+
throw new Error(
|
|
4191
|
+
`Autostart service installation is not supported on ${process.platform}. Run \`task0 daemon run\` in a process supervisor of your choice.`
|
|
4192
|
+
);
|
|
4193
|
+
}
|
|
4194
|
+
|
|
3890
4195
|
// src/commands/daemon.ts
|
|
3891
4196
|
var DAEMON_VERSION = "0.1.0";
|
|
3892
4197
|
async function dispatchRpc(ws, id, method, params) {
|
|
@@ -3936,15 +4241,31 @@ async function jsonGet(url) {
|
|
|
3936
4241
|
}
|
|
3937
4242
|
return res.json();
|
|
3938
4243
|
}
|
|
4244
|
+
function requireRootIfSystem(scope, rerunHint) {
|
|
4245
|
+
if (scope !== "system") return;
|
|
4246
|
+
const uid = process.getuid?.();
|
|
4247
|
+
if (uid === 0) return;
|
|
4248
|
+
console.error(chalk20.red("--system requires root."));
|
|
4249
|
+
console.error("Re-run with sudo, preserving env:");
|
|
4250
|
+
console.error(chalk20.cyan(` sudo -E ${rerunHint}`));
|
|
4251
|
+
process.exit(1);
|
|
4252
|
+
}
|
|
4253
|
+
function rerunArgv() {
|
|
4254
|
+
return ["task0", ...process.argv.slice(2)].join(" ");
|
|
4255
|
+
}
|
|
3939
4256
|
var daemonCmd = new Command20("daemon").description("Manage this host as a task0 daemon registered with a central server");
|
|
3940
|
-
daemonCmd.command("register").description("Register this host with a central server
|
|
4257
|
+
daemonCmd.command("register").description("Register this host with a central server, install the autostart service, and start it").requiredOption("-s, --server <url>", "Central server URL (e.g. https://central.example.com:4318)").option("-n, --name <name>", "Display name for this daemon (defaults to hostname)").option("--force", "Overwrite existing identity if present").option("--system", "Install at the system layer (LaunchDaemons / /etc/systemd/system, requires sudo)").option("--no-install", "Skip installing the autostart service unit").option("--no-start", "Install the service unit but do not start it now").action(async (opts) => {
|
|
4258
|
+
const scope = opts.system ? "system" : "user";
|
|
4259
|
+
if (opts.install) {
|
|
4260
|
+
requireRootIfSystem(scope, rerunArgv());
|
|
4261
|
+
}
|
|
3941
4262
|
const existing = readDaemonIdentity();
|
|
3942
4263
|
if (existing && !opts.force) {
|
|
3943
4264
|
fail6(`Already registered as ${existing.daemon_id}. Pass --force to re-register.`);
|
|
3944
4265
|
}
|
|
3945
4266
|
const base = opts.server.replace(/\/$/, "");
|
|
3946
4267
|
const body = {
|
|
3947
|
-
hostname:
|
|
4268
|
+
hostname: os6.hostname(),
|
|
3948
4269
|
platform: process.platform,
|
|
3949
4270
|
name: opts.name
|
|
3950
4271
|
};
|
|
@@ -3974,8 +4295,67 @@ daemonCmd.command("register").description("Register this host with a central ser
|
|
|
3974
4295
|
writeDaemonIdentity(identity);
|
|
3975
4296
|
console.log(chalk20.green(`Registered as ${data.daemon.object_id} (${data.daemon.name})`));
|
|
3976
4297
|
console.log(`Identity saved to ${daemonConfigPath()}`);
|
|
4298
|
+
if (!opts.install) {
|
|
4299
|
+
console.log(chalk20.dim("Skipping service install (--no-install)."));
|
|
4300
|
+
console.log(chalk20.dim(`Run \`task0 daemon run\` to start the WebSocket loop in foreground.`));
|
|
4301
|
+
return;
|
|
4302
|
+
}
|
|
4303
|
+
if (!isPlatformSupported()) {
|
|
4304
|
+
console.log(
|
|
4305
|
+
chalk20.yellow(
|
|
4306
|
+
`Autostart service is not supported on ${process.platform}. Run \`task0 daemon run\` under a supervisor of your choice.`
|
|
4307
|
+
)
|
|
4308
|
+
);
|
|
4309
|
+
return;
|
|
4310
|
+
}
|
|
4311
|
+
const svc = getServiceManager(scope);
|
|
4312
|
+
try {
|
|
4313
|
+
const { unitPath: unitPath2 } = await svc.install({ identity });
|
|
4314
|
+
const logs = svc.logPaths();
|
|
4315
|
+
console.log(chalk20.green(`Installed service unit at ${unitPath2}`));
|
|
4316
|
+
console.log(chalk20.dim(`Logs: ${logs.out}`));
|
|
4317
|
+
console.log(chalk20.dim(` ${logs.err}`));
|
|
4318
|
+
if (opts.start) {
|
|
4319
|
+
await svc.start();
|
|
4320
|
+
console.log(chalk20.green("Service started."));
|
|
4321
|
+
} else {
|
|
4322
|
+
console.log(chalk20.dim("Skipping start (--no-start). Run `task0 daemon start` when ready."));
|
|
4323
|
+
}
|
|
4324
|
+
} catch (error2) {
|
|
4325
|
+
console.error(chalk20.red(`Service install failed: ${error2 instanceof Error ? error2.message : String(error2)}`));
|
|
4326
|
+
console.error(chalk20.dim("Identity is saved; rerun `task0 daemon register --force` once the issue is resolved."));
|
|
4327
|
+
process.exit(1);
|
|
4328
|
+
}
|
|
4329
|
+
});
|
|
4330
|
+
daemonCmd.command("start").description("Start the installed autostart service via launchctl / systemctl").option("--system", "Target the system-layer service").action(async (opts) => {
|
|
4331
|
+
const scope = opts.system ? "system" : "user";
|
|
4332
|
+
requireRootIfSystem(scope, rerunArgv());
|
|
4333
|
+
if (!isPlatformSupported()) {
|
|
4334
|
+
fail6(`Service control is not supported on ${process.platform}.`);
|
|
4335
|
+
}
|
|
4336
|
+
const svc = getServiceManager(scope);
|
|
4337
|
+
try {
|
|
4338
|
+
await svc.start();
|
|
4339
|
+
console.log(chalk20.green("Service started."));
|
|
4340
|
+
} catch (error2) {
|
|
4341
|
+
fail6(error2 instanceof Error ? error2.message : String(error2));
|
|
4342
|
+
}
|
|
4343
|
+
});
|
|
4344
|
+
daemonCmd.command("stop").description("Stop the autostart service via launchctl / systemctl").option("--system", "Target the system-layer service").action(async (opts) => {
|
|
4345
|
+
const scope = opts.system ? "system" : "user";
|
|
4346
|
+
requireRootIfSystem(scope, rerunArgv());
|
|
4347
|
+
if (!isPlatformSupported()) {
|
|
4348
|
+
fail6(`Service control is not supported on ${process.platform}.`);
|
|
4349
|
+
}
|
|
4350
|
+
const svc = getServiceManager(scope);
|
|
4351
|
+
try {
|
|
4352
|
+
await svc.stop();
|
|
4353
|
+
console.log(chalk20.green("Service stopped."));
|
|
4354
|
+
} catch (error2) {
|
|
4355
|
+
fail6(error2 instanceof Error ? error2.message : String(error2));
|
|
4356
|
+
}
|
|
3977
4357
|
});
|
|
3978
|
-
daemonCmd.command("
|
|
4358
|
+
daemonCmd.command("run").description("Run the daemon WebSocket loop in foreground (invoked by the service unit; useful for debugging)").action(async () => {
|
|
3979
4359
|
const identity = loadRequiredIdentity();
|
|
3980
4360
|
const wsUrl = identity.server_url.replace(/^http/, "ws").replace(/\/$/, "") + "/ws/daemon";
|
|
3981
4361
|
console.log(chalk20.green(`Starting daemon ${identity.daemon_id} \u2192 ${wsUrl}`));
|
|
@@ -4066,17 +4446,42 @@ daemonCmd.command("list").description("List daemons registered on the configured
|
|
|
4066
4446
|
);
|
|
4067
4447
|
}
|
|
4068
4448
|
});
|
|
4069
|
-
daemonCmd.command("show [daemonId]").description("Show local daemon identity (no arg) or remote daemon by id").action(async (daemonId) => {
|
|
4449
|
+
daemonCmd.command("show [daemonId]").description("Show local daemon identity (no arg) or remote daemon by id").option("--system", "Inspect the system-layer service status (default: user)").action(async (daemonId, opts) => {
|
|
4070
4450
|
if (!daemonId) {
|
|
4071
4451
|
const identity = loadRequiredIdentity();
|
|
4072
4452
|
console.log(JSON.stringify({ ...identity, token: "***" }, null, 2));
|
|
4453
|
+
if (isPlatformSupported()) {
|
|
4454
|
+
const scope = opts.system ? "system" : "user";
|
|
4455
|
+
try {
|
|
4456
|
+
const svc = getServiceManager(scope);
|
|
4457
|
+
const state2 = await svc.status();
|
|
4458
|
+
console.log(chalk20.dim(`service (${scope}): ${state2} \u2192 ${svc.unitPath()}`));
|
|
4459
|
+
} catch (error2) {
|
|
4460
|
+
console.log(chalk20.dim(`service status unavailable: ${error2 instanceof Error ? error2.message : String(error2)}`));
|
|
4461
|
+
}
|
|
4462
|
+
}
|
|
4073
4463
|
return;
|
|
4074
4464
|
}
|
|
4075
4465
|
const base = serverBase(readDaemonIdentity());
|
|
4076
4466
|
const data = await jsonGet(`${base}/api/daemons/${encodeURIComponent(daemonId)}`);
|
|
4077
4467
|
console.log(JSON.stringify(data.daemon, null, 2));
|
|
4078
4468
|
});
|
|
4079
|
-
daemonCmd.command("logout").description("
|
|
4469
|
+
daemonCmd.command("logout").description("Stop and uninstall the autostart service, then forget the locally stored daemon identity").option("--system", "Target the system-layer service").option("--keep-service", "Leave the installed service unit in place; only clear the identity file").action(async (opts) => {
|
|
4470
|
+
const scope = opts.system ? "system" : "user";
|
|
4471
|
+
if (!opts.keepService) {
|
|
4472
|
+
requireRootIfSystem(scope, rerunArgv());
|
|
4473
|
+
if (isPlatformSupported()) {
|
|
4474
|
+
const svc = getServiceManager(scope);
|
|
4475
|
+
try {
|
|
4476
|
+
await svc.uninstall();
|
|
4477
|
+
console.log(chalk20.dim(`Service unit removed (${scope}).`));
|
|
4478
|
+
} catch (error2) {
|
|
4479
|
+
console.error(
|
|
4480
|
+
chalk20.yellow(`Service uninstall warning: ${error2 instanceof Error ? error2.message : String(error2)}`)
|
|
4481
|
+
);
|
|
4482
|
+
}
|
|
4483
|
+
}
|
|
4484
|
+
}
|
|
4080
4485
|
if (clearDaemonIdentity()) {
|
|
4081
4486
|
console.log(chalk20.green("Daemon identity cleared."));
|
|
4082
4487
|
} else {
|
|
@@ -4345,7 +4750,16 @@ function captureTopLevel(err, options) {
|
|
|
4345
4750
|
}
|
|
4346
4751
|
|
|
4347
4752
|
// src/main.ts
|
|
4348
|
-
var TASK0_VERSION =
|
|
4753
|
+
var TASK0_VERSION = readVersion();
|
|
4754
|
+
function readVersion() {
|
|
4755
|
+
try {
|
|
4756
|
+
const here = path24.dirname(fileURLToPath2(import.meta.url));
|
|
4757
|
+
const pkg = JSON.parse(readFileSync(path24.resolve(here, "..", "package.json"), "utf-8"));
|
|
4758
|
+
return pkg.version ?? "unknown";
|
|
4759
|
+
} catch {
|
|
4760
|
+
return "unknown";
|
|
4761
|
+
}
|
|
4762
|
+
}
|
|
4349
4763
|
var program = new Command22().name("task0").description("Task-centric control layer for agent workflow").version(TASK0_VERSION);
|
|
4350
4764
|
program.addCommand(source);
|
|
4351
4765
|
program.addCommand(project);
|