@task0/cli 0.1.0 → 0.2.0

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.
Files changed (4) hide show
  1. package/README.md +49 -0
  2. package/dist/main.js +413 -11
  3. package/package.json +1 -1
  4. package/LICENSE +0 -21
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
@@ -1141,11 +1141,11 @@ async function request(method, pathname, body) {
1141
1141
  return parsed;
1142
1142
  }
1143
1143
  var api = {
1144
- get: (path21) => request("GET", path21),
1145
- post: (path21, body) => request("POST", path21, body ?? {}),
1146
- put: (path21, body) => request("PUT", path21, body ?? {}),
1147
- patch: (path21, body) => request("PATCH", path21, body ?? {}),
1148
- del: (path21) => request("DELETE", path21)
1144
+ get: (path24) => request("GET", path24),
1145
+ post: (path24, body) => request("POST", path24, body ?? {}),
1146
+ put: (path24, body) => request("PUT", path24, body ?? {}),
1147
+ patch: (path24, body) => request("PATCH", path24, body ?? {}),
1148
+ del: (path24) => request("DELETE", path24)
1149
1149
  };
1150
1150
 
1151
1151
  // src/commands/task/triage.ts
@@ -3809,7 +3809,7 @@ async function streamOutput(ref, runtimeId) {
3809
3809
  }
3810
3810
 
3811
3811
  // src/commands/daemon.ts
3812
- import os5 from "os";
3812
+ import os6 from "os";
3813
3813
  import { Command as Command20 } from "commander";
3814
3814
  import chalk20 from "chalk";
3815
3815
  import WebSocket from "ws";
@@ -3887,6 +3887,308 @@ var rpcHandlers = {
3887
3887
  }
3888
3888
  };
3889
3889
 
3890
+ // src/core/daemon-service/launchd.ts
3891
+ import { spawnSync as spawnSync5 } from "child_process";
3892
+ import fs23 from "fs";
3893
+ import path22 from "path";
3894
+
3895
+ // src/core/daemon-service/binary.ts
3896
+ import fs22 from "fs";
3897
+ import { fileURLToPath } from "url";
3898
+ function resolveTask0Invocation() {
3899
+ const node = process.execPath;
3900
+ const argv1 = process.argv[1] ?? fileURLToPath(import.meta.url);
3901
+ const main2 = fs22.realpathSync(argv1);
3902
+ if (!isInstalledBuild(main2) && process.env.TASK0_ALLOW_DEV_SERVICE !== "1") {
3903
+ throw new Error(
3904
+ `Refusing to install autostart service pointing at ${main2}.
3905
+ That looks like a development source, not an installed @task0/cli build.
3906
+ Install the CLI globally (\`npm i -g @task0/cli\`) and retry, or set
3907
+ TASK0_ALLOW_DEV_SERVICE=1 to override.`
3908
+ );
3909
+ }
3910
+ return { node, main: main2, args: ["daemon", "run"] };
3911
+ }
3912
+ function isInstalledBuild(p) {
3913
+ return /[/\\]dist[/\\]main\.js$/.test(p);
3914
+ }
3915
+
3916
+ // src/core/daemon-service/paths.ts
3917
+ import os5 from "os";
3918
+ import path21 from "path";
3919
+
3920
+ // src/core/daemon-service/types.ts
3921
+ var SERVICE_LABEL = "cc.cy0.task0";
3922
+
3923
+ // src/core/daemon-service/paths.ts
3924
+ function unitPath(scope) {
3925
+ const platform = process.platform;
3926
+ if (platform === "darwin") {
3927
+ return scope === "user" ? path21.join(os5.homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`) : path21.join("/", "Library", "LaunchDaemons", `${SERVICE_LABEL}.plist`);
3928
+ }
3929
+ if (platform === "linux") {
3930
+ return scope === "user" ? path21.join(os5.homedir(), ".config", "systemd", "user", `${SERVICE_LABEL}.service`) : path21.join("/", "etc", "systemd", "system", `${SERVICE_LABEL}.service`);
3931
+ }
3932
+ throw new Error(`Unsupported platform for service install: ${platform}`);
3933
+ }
3934
+ function logDir() {
3935
+ return path21.join(os5.homedir(), ".task0", "logs");
3936
+ }
3937
+ function logPaths() {
3938
+ const dir = logDir();
3939
+ return {
3940
+ out: path21.join(dir, "daemon.out.log"),
3941
+ err: path21.join(dir, "daemon.err.log")
3942
+ };
3943
+ }
3944
+
3945
+ // src/core/daemon-service/launchd.ts
3946
+ function escapeXml(s) {
3947
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3948
+ }
3949
+ function renderPlist(opts) {
3950
+ const programArgs = [opts.node, opts.main, ...opts.args].map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
3951
+ return `<?xml version="1.0" encoding="UTF-8"?>
3952
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3953
+ <plist version="1.0">
3954
+ <dict>
3955
+ <key>Label</key>
3956
+ <string>${SERVICE_LABEL}</string>
3957
+ <key>ProgramArguments</key>
3958
+ <array>
3959
+ ${programArgs}
3960
+ </array>
3961
+ <key>RunAtLoad</key>
3962
+ <true/>
3963
+ <key>KeepAlive</key>
3964
+ <dict>
3965
+ <key>SuccessfulExit</key>
3966
+ <false/>
3967
+ </dict>
3968
+ <key>ThrottleInterval</key>
3969
+ <integer>5</integer>
3970
+ <key>StandardOutPath</key>
3971
+ <string>${escapeXml(opts.out)}</string>
3972
+ <key>StandardErrorPath</key>
3973
+ <string>${escapeXml(opts.err)}</string>
3974
+ <key>WorkingDirectory</key>
3975
+ <string>${escapeXml(opts.home)}</string>
3976
+ <key>EnvironmentVariables</key>
3977
+ <dict>
3978
+ <key>PATH</key>
3979
+ <string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
3980
+ </dict>
3981
+ </dict>
3982
+ </plist>
3983
+ `;
3984
+ }
3985
+ function domainTarget(scope) {
3986
+ if (scope === "system") return "system";
3987
+ const uid = process.getuid?.() ?? 0;
3988
+ return `gui/${uid}`;
3989
+ }
3990
+ function serviceTarget(scope) {
3991
+ return `${domainTarget(scope)}/${SERVICE_LABEL}`;
3992
+ }
3993
+ function run2(cmd, args) {
3994
+ const res = spawnSync5(cmd, args, { encoding: "utf-8" });
3995
+ return { code: res.status ?? -1, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
3996
+ }
3997
+ function createLaunchdManager(scope) {
3998
+ const file = unitPath(scope);
3999
+ const logs = logPaths();
4000
+ async function install() {
4001
+ const inv = resolveTask0Invocation();
4002
+ fs23.mkdirSync(logDir(), { recursive: true });
4003
+ fs23.mkdirSync(path22.dirname(file), { recursive: true });
4004
+ const body = renderPlist({
4005
+ node: inv.node,
4006
+ main: inv.main,
4007
+ args: inv.args,
4008
+ home: process.env.HOME ?? "/",
4009
+ out: logs.out,
4010
+ err: logs.err
4011
+ });
4012
+ fs23.writeFileSync(file, body, { mode: 420 });
4013
+ const bootstrap = run2("launchctl", ["bootstrap", domainTarget(scope), file]);
4014
+ if (bootstrap.code !== 0) {
4015
+ const already = /already loaded|service already bootstrapped/i.test(bootstrap.stderr);
4016
+ if (already) {
4017
+ run2("launchctl", ["bootout", serviceTarget(scope)]);
4018
+ const retry = run2("launchctl", ["bootstrap", domainTarget(scope), file]);
4019
+ if (retry.code !== 0) {
4020
+ const legacy = run2("launchctl", ["load", "-w", file]);
4021
+ if (legacy.code !== 0) {
4022
+ throw new Error(
4023
+ `launchctl bootstrap/load failed: ${retry.stderr || legacy.stderr || bootstrap.stderr}`
4024
+ );
4025
+ }
4026
+ }
4027
+ } else {
4028
+ const legacy = run2("launchctl", ["load", "-w", file]);
4029
+ if (legacy.code !== 0) {
4030
+ throw new Error(`launchctl bootstrap/load failed: ${bootstrap.stderr || legacy.stderr}`);
4031
+ }
4032
+ }
4033
+ }
4034
+ return { unitPath: file };
4035
+ }
4036
+ async function uninstall() {
4037
+ run2("launchctl", ["bootout", serviceTarget(scope)]);
4038
+ if (fs23.existsSync(file)) {
4039
+ run2("launchctl", ["unload", file]);
4040
+ fs23.unlinkSync(file);
4041
+ }
4042
+ }
4043
+ async function start() {
4044
+ const res = run2("launchctl", ["kickstart", "-k", serviceTarget(scope)]);
4045
+ if (res.code !== 0) {
4046
+ const load = run2("launchctl", ["load", "-w", file]);
4047
+ if (load.code !== 0) {
4048
+ throw new Error(`launchctl start failed: ${res.stderr || load.stderr}`);
4049
+ }
4050
+ }
4051
+ }
4052
+ async function stop() {
4053
+ const res = run2("launchctl", ["kill", "SIGTERM", serviceTarget(scope)]);
4054
+ if (res.code !== 0) {
4055
+ run2("launchctl", ["unload", file]);
4056
+ }
4057
+ }
4058
+ async function status() {
4059
+ if (!fs23.existsSync(file)) return "absent";
4060
+ const printed = run2("launchctl", ["print", serviceTarget(scope)]);
4061
+ if (printed.code !== 0) return "installed";
4062
+ const out = printed.stdout;
4063
+ if (/\bstate\s*=\s*running/.test(out)) return "running";
4064
+ if (/\bstate\s*=\s*spawning/.test(out)) return "installed";
4065
+ if (/last exit code\s*=\s*[1-9]/i.test(out)) return "errored";
4066
+ return "stopped";
4067
+ }
4068
+ return {
4069
+ scope,
4070
+ install,
4071
+ uninstall,
4072
+ start,
4073
+ stop,
4074
+ status,
4075
+ unitPath: () => file,
4076
+ logPaths: () => logs
4077
+ };
4078
+ }
4079
+
4080
+ // src/core/daemon-service/systemd.ts
4081
+ import { spawnSync as spawnSync6 } from "child_process";
4082
+ import fs24 from "fs";
4083
+ import path23 from "path";
4084
+ function shellEscape(s) {
4085
+ if (!/[\s"\\$]/.test(s)) return s;
4086
+ return `"${s.replace(/[\\"]/g, (m) => `\\${m}`)}"`;
4087
+ }
4088
+ function renderUnit(opts) {
4089
+ const execStart = [opts.node, opts.main, ...opts.args].map(shellEscape).join(" ");
4090
+ const wantedBy = opts.scope === "user" ? "default.target" : "multi-user.target";
4091
+ return `[Unit]
4092
+ Description=task0 daemon \u2014 central-server bridge
4093
+ After=network-online.target
4094
+ Wants=network-online.target
4095
+
4096
+ [Service]
4097
+ Type=simple
4098
+ ExecStart=${execStart}
4099
+ Restart=on-failure
4100
+ RestartSec=5s
4101
+ StandardOutput=append:${opts.out}
4102
+ StandardError=append:${opts.err}
4103
+ Environment=NODE_ENV=production
4104
+
4105
+ [Install]
4106
+ WantedBy=${wantedBy}
4107
+ `;
4108
+ }
4109
+ function scopeFlag(scope) {
4110
+ return scope === "user" ? ["--user"] : [];
4111
+ }
4112
+ function run3(cmd, args) {
4113
+ const res = spawnSync6(cmd, args, { encoding: "utf-8" });
4114
+ return { code: res.status ?? -1, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
4115
+ }
4116
+ function createSystemdManager(scope) {
4117
+ const file = unitPath(scope);
4118
+ const logs = logPaths();
4119
+ const unitName = `${SERVICE_LABEL}.service`;
4120
+ async function install() {
4121
+ const inv = resolveTask0Invocation();
4122
+ fs24.mkdirSync(logDir(), { recursive: true });
4123
+ fs24.mkdirSync(path23.dirname(file), { recursive: true });
4124
+ const body = renderUnit({
4125
+ node: inv.node,
4126
+ main: inv.main,
4127
+ args: inv.args,
4128
+ out: logs.out,
4129
+ err: logs.err,
4130
+ scope
4131
+ });
4132
+ fs24.writeFileSync(file, body, { mode: 420 });
4133
+ const reload = run3("systemctl", [...scopeFlag(scope), "daemon-reload"]);
4134
+ if (reload.code !== 0) {
4135
+ throw new Error(`systemctl daemon-reload failed: ${reload.stderr}`);
4136
+ }
4137
+ return { unitPath: file };
4138
+ }
4139
+ async function uninstall() {
4140
+ run3("systemctl", [...scopeFlag(scope), "disable", "--now", unitName]);
4141
+ if (fs24.existsSync(file)) {
4142
+ fs24.unlinkSync(file);
4143
+ }
4144
+ run3("systemctl", [...scopeFlag(scope), "daemon-reload"]);
4145
+ }
4146
+ async function start() {
4147
+ const res = run3("systemctl", [...scopeFlag(scope), "enable", "--now", unitName]);
4148
+ if (res.code !== 0) {
4149
+ throw new Error(`systemctl enable --now failed: ${res.stderr}`);
4150
+ }
4151
+ }
4152
+ async function stop() {
4153
+ const res = run3("systemctl", [...scopeFlag(scope), "stop", unitName]);
4154
+ if (res.code !== 0) {
4155
+ throw new Error(`systemctl stop failed: ${res.stderr}`);
4156
+ }
4157
+ }
4158
+ async function status() {
4159
+ if (!fs24.existsSync(file)) return "absent";
4160
+ const res = run3("systemctl", [...scopeFlag(scope), "is-active", unitName]);
4161
+ const out = res.stdout.trim();
4162
+ if (out === "active") return "running";
4163
+ if (out === "inactive") return "stopped";
4164
+ if (out === "failed") return "errored";
4165
+ if (out === "activating" || out === "deactivating") return "installed";
4166
+ return "installed";
4167
+ }
4168
+ return {
4169
+ scope,
4170
+ install,
4171
+ uninstall,
4172
+ start,
4173
+ stop,
4174
+ status,
4175
+ unitPath: () => file,
4176
+ logPaths: () => logs
4177
+ };
4178
+ }
4179
+
4180
+ // src/core/daemon-service/index.ts
4181
+ function isPlatformSupported() {
4182
+ return process.platform === "darwin" || process.platform === "linux";
4183
+ }
4184
+ function getServiceManager(scope) {
4185
+ if (process.platform === "darwin") return createLaunchdManager(scope);
4186
+ if (process.platform === "linux") return createSystemdManager(scope);
4187
+ throw new Error(
4188
+ `Autostart service installation is not supported on ${process.platform}. Run \`task0 daemon run\` in a process supervisor of your choice.`
4189
+ );
4190
+ }
4191
+
3890
4192
  // src/commands/daemon.ts
3891
4193
  var DAEMON_VERSION = "0.1.0";
3892
4194
  async function dispatchRpc(ws, id, method, params) {
@@ -3936,15 +4238,31 @@ async function jsonGet(url) {
3936
4238
  }
3937
4239
  return res.json();
3938
4240
  }
4241
+ function requireRootIfSystem(scope, rerunHint) {
4242
+ if (scope !== "system") return;
4243
+ const uid = process.getuid?.();
4244
+ if (uid === 0) return;
4245
+ console.error(chalk20.red("--system requires root."));
4246
+ console.error("Re-run with sudo, preserving env:");
4247
+ console.error(chalk20.cyan(` sudo -E ${rerunHint}`));
4248
+ process.exit(1);
4249
+ }
4250
+ function rerunArgv() {
4251
+ return ["task0", ...process.argv.slice(2)].join(" ");
4252
+ }
3939
4253
  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 and save the identity locally").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").action(async (opts) => {
4254
+ 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) => {
4255
+ const scope = opts.system ? "system" : "user";
4256
+ if (opts.install) {
4257
+ requireRootIfSystem(scope, rerunArgv());
4258
+ }
3941
4259
  const existing = readDaemonIdentity();
3942
4260
  if (existing && !opts.force) {
3943
4261
  fail6(`Already registered as ${existing.daemon_id}. Pass --force to re-register.`);
3944
4262
  }
3945
4263
  const base = opts.server.replace(/\/$/, "");
3946
4264
  const body = {
3947
- hostname: os5.hostname(),
4265
+ hostname: os6.hostname(),
3948
4266
  platform: process.platform,
3949
4267
  name: opts.name
3950
4268
  };
@@ -3974,8 +4292,67 @@ daemonCmd.command("register").description("Register this host with a central ser
3974
4292
  writeDaemonIdentity(identity);
3975
4293
  console.log(chalk20.green(`Registered as ${data.daemon.object_id} (${data.daemon.name})`));
3976
4294
  console.log(`Identity saved to ${daemonConfigPath()}`);
4295
+ if (!opts.install) {
4296
+ console.log(chalk20.dim("Skipping service install (--no-install)."));
4297
+ console.log(chalk20.dim(`Run \`task0 daemon run\` to start the WebSocket loop in foreground.`));
4298
+ return;
4299
+ }
4300
+ if (!isPlatformSupported()) {
4301
+ console.log(
4302
+ chalk20.yellow(
4303
+ `Autostart service is not supported on ${process.platform}. Run \`task0 daemon run\` under a supervisor of your choice.`
4304
+ )
4305
+ );
4306
+ return;
4307
+ }
4308
+ const svc = getServiceManager(scope);
4309
+ try {
4310
+ const { unitPath: unitPath2 } = await svc.install({ identity });
4311
+ const logs = svc.logPaths();
4312
+ console.log(chalk20.green(`Installed service unit at ${unitPath2}`));
4313
+ console.log(chalk20.dim(`Logs: ${logs.out}`));
4314
+ console.log(chalk20.dim(` ${logs.err}`));
4315
+ if (opts.start) {
4316
+ await svc.start();
4317
+ console.log(chalk20.green("Service started."));
4318
+ } else {
4319
+ console.log(chalk20.dim("Skipping start (--no-start). Run `task0 daemon start` when ready."));
4320
+ }
4321
+ } catch (error2) {
4322
+ console.error(chalk20.red(`Service install failed: ${error2 instanceof Error ? error2.message : String(error2)}`));
4323
+ console.error(chalk20.dim("Identity is saved; rerun `task0 daemon register --force` once the issue is resolved."));
4324
+ process.exit(1);
4325
+ }
4326
+ });
4327
+ daemonCmd.command("start").description("Start the installed autostart service via launchctl / systemctl").option("--system", "Target the system-layer service").action(async (opts) => {
4328
+ const scope = opts.system ? "system" : "user";
4329
+ requireRootIfSystem(scope, rerunArgv());
4330
+ if (!isPlatformSupported()) {
4331
+ fail6(`Service control is not supported on ${process.platform}.`);
4332
+ }
4333
+ const svc = getServiceManager(scope);
4334
+ try {
4335
+ await svc.start();
4336
+ console.log(chalk20.green("Service started."));
4337
+ } catch (error2) {
4338
+ fail6(error2 instanceof Error ? error2.message : String(error2));
4339
+ }
4340
+ });
4341
+ daemonCmd.command("stop").description("Stop the autostart service via launchctl / systemctl").option("--system", "Target the system-layer service").action(async (opts) => {
4342
+ const scope = opts.system ? "system" : "user";
4343
+ requireRootIfSystem(scope, rerunArgv());
4344
+ if (!isPlatformSupported()) {
4345
+ fail6(`Service control is not supported on ${process.platform}.`);
4346
+ }
4347
+ const svc = getServiceManager(scope);
4348
+ try {
4349
+ await svc.stop();
4350
+ console.log(chalk20.green("Service stopped."));
4351
+ } catch (error2) {
4352
+ fail6(error2 instanceof Error ? error2.message : String(error2));
4353
+ }
3977
4354
  });
3978
- daemonCmd.command("start").description("Start the local daemon and connect to the registered server (long-running)").action(async () => {
4355
+ daemonCmd.command("run").description("Run the daemon WebSocket loop in foreground (invoked by the service unit; useful for debugging)").action(async () => {
3979
4356
  const identity = loadRequiredIdentity();
3980
4357
  const wsUrl = identity.server_url.replace(/^http/, "ws").replace(/\/$/, "") + "/ws/daemon";
3981
4358
  console.log(chalk20.green(`Starting daemon ${identity.daemon_id} \u2192 ${wsUrl}`));
@@ -4066,17 +4443,42 @@ daemonCmd.command("list").description("List daemons registered on the configured
4066
4443
  );
4067
4444
  }
4068
4445
  });
4069
- daemonCmd.command("show [daemonId]").description("Show local daemon identity (no arg) or remote daemon by id").action(async (daemonId) => {
4446
+ 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
4447
  if (!daemonId) {
4071
4448
  const identity = loadRequiredIdentity();
4072
4449
  console.log(JSON.stringify({ ...identity, token: "***" }, null, 2));
4450
+ if (isPlatformSupported()) {
4451
+ const scope = opts.system ? "system" : "user";
4452
+ try {
4453
+ const svc = getServiceManager(scope);
4454
+ const state2 = await svc.status();
4455
+ console.log(chalk20.dim(`service (${scope}): ${state2} \u2192 ${svc.unitPath()}`));
4456
+ } catch (error2) {
4457
+ console.log(chalk20.dim(`service status unavailable: ${error2 instanceof Error ? error2.message : String(error2)}`));
4458
+ }
4459
+ }
4073
4460
  return;
4074
4461
  }
4075
4462
  const base = serverBase(readDaemonIdentity());
4076
4463
  const data = await jsonGet(`${base}/api/daemons/${encodeURIComponent(daemonId)}`);
4077
4464
  console.log(JSON.stringify(data.daemon, null, 2));
4078
4465
  });
4079
- daemonCmd.command("logout").description("Forget the locally stored daemon identity").action(() => {
4466
+ 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) => {
4467
+ const scope = opts.system ? "system" : "user";
4468
+ if (!opts.keepService) {
4469
+ requireRootIfSystem(scope, rerunArgv());
4470
+ if (isPlatformSupported()) {
4471
+ const svc = getServiceManager(scope);
4472
+ try {
4473
+ await svc.uninstall();
4474
+ console.log(chalk20.dim(`Service unit removed (${scope}).`));
4475
+ } catch (error2) {
4476
+ console.error(
4477
+ chalk20.yellow(`Service uninstall warning: ${error2 instanceof Error ? error2.message : String(error2)}`)
4478
+ );
4479
+ }
4480
+ }
4481
+ }
4080
4482
  if (clearDaemonIdentity()) {
4081
4483
  console.log(chalk20.green("Daemon identity cleared."));
4082
4484
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@task0/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "task0 — task-centric agent workflow CLI",
5
5
  "homepage": "https://github.com/cy0-labs/task0#readme",
6
6
  "repository": {
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Ying Cai
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.