@indigoai-us/hq-cli 5.167.0 → 5.169.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.
- package/CHANGELOG.md +30 -0
- package/dist/command-catalog.generated.d.ts +88 -1
- package/dist/command-catalog.generated.js +112 -1
- package/dist/commands/daemon.d.ts +2 -0
- package/dist/commands/daemon.js +79 -22
- package/dist/commands/lanes.js +146 -31
- package/dist/commands/mesh.js +13 -19
- package/dist/commands/outpost-pruner.d.ts +3 -0
- package/dist/commands/outpost-pruner.js +125 -0
- package/dist/lib/daemon/catalog.d.ts +11 -1
- package/dist/lib/daemon/catalog.js +31 -6
- package/dist/lib/daemon/install.d.ts +7 -0
- package/dist/lib/daemon/install.js +18 -2
- package/dist/lib/daemon/legacy.js +6 -1
- package/dist/lib/daemon/machine-type.d.ts +4 -1
- package/dist/lib/daemon/machine-type.js +29 -9
- package/dist/lib/daemon/migrate.js +2 -2
- package/dist/lib/daemon/remote-control.d.ts +32 -9
- package/dist/lib/daemon/remote-control.js +55 -11
- package/dist/lib/daemon/run.d.ts +6 -0
- package/dist/lib/daemon/run.js +43 -2
- package/dist/lib/daemon/tools.d.ts +52 -0
- package/dist/lib/daemon/tools.js +95 -0
- package/dist/lib/daemon/updater.d.ts +23 -15
- package/dist/lib/daemon/updater.js +97 -60
- package/dist/lib/lanes/mesh-events.d.ts +4 -0
- package/dist/lib/lanes/mesh-events.js +72 -4
- package/dist/lib/lanes/outpost-pruner-install.d.ts +37 -0
- package/dist/lib/lanes/outpost-pruner-install.js +226 -0
- package/dist/lib/lanes/outpost-worktree-pruner-flag.d.ts +19 -0
- package/dist/lib/lanes/outpost-worktree-pruner-flag.js +76 -0
- package/dist/lib/lanes/outpost-worktree-pruner.d.ts +60 -0
- package/dist/lib/lanes/outpost-worktree-pruner.js +1135 -0
- package/dist/lib/mesh/live/session-events-client.js +3 -2
- package/dist/lib/mesh/project-board.d.ts +7 -0
- package/dist/lib/mesh/project-board.js +29 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.169.0] — 2026-09-24
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `hq lanes create` and `hq lanes story <add|start|block|done>` keep story work
|
|
10
|
+
local when an existing Work Mesh Board rejects a missing story. The
|
|
11
|
+
`story_warning` identifies the project and story and suggests:
|
|
12
|
+
`hq mesh project ensure <projectId> --company <slug> --stories-file prd.json`.
|
|
13
|
+
|
|
14
|
+
## [5.168.0] — 2026-09-24
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- `hq mesh project set --stories-file` keeps live Board story status and
|
|
19
|
+
|
|
20
|
+
- `hq mesh` ignores nonpositive, malformed, and expired `Retry-After` hints.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- `hq daemon` keeps the hq CLI, Claude, Codex and Grok up to date on every machine type, desktops included, every six hours. It updates the login user's own copy of each tool through that tool's npm package or its own `update` command, and leaves a copy the user cannot write (for example one root installed in /usr) alone. On outposts the daemon also runs Codex remote control (the box's Codex runner) next to Claude remote control, keeps the session name the box's runner used, and restarts either one onto a newly installed Claude or Codex once the session has been quiet for 10 minutes, or after 2 hours at the latest. `hq daemon install` never sets the daemon up to run as root: a box runs it as the login user from `/etc/hq/machine-type`, the outpost runner unit, `--user` or `sudo`, and an outpost without the marker file is recognised from its runner unit. `hq daemon migrate --apply` also takes over the outpost runner watchdogs and the Codex update timer.
|
|
25
|
+
- Added outpost worktree cleanup commands for dry runs and pruning clean, aged
|
|
26
|
+
worktrees. Lane, session, and process references keep their worktrees. Pressure
|
|
27
|
+
and daily timers use the `outpost.worktree-pruner` hq-flags key, which starts off.
|
|
28
|
+
Live-lane `worker.out` files and matching Claude session transcripts are read in
|
|
29
|
+
bounded chunks. Transcript lookup reads only the file matching the lane's
|
|
30
|
+
provider session ID under `.claude*/projects/*`. An unresolved lane keeps
|
|
31
|
+
worktrees whose root, linked Git index, or linked Git `HEAD` changed at or after
|
|
32
|
+
its `created_at`; a missing or invalid start time keeps every candidate. A
|
|
33
|
+
never-spawned queued lane without a provider session does not hold a worktree.
|
|
34
|
+
|
|
5
35
|
## [5.167.0] — 2026-09-24
|
|
6
36
|
|
|
7
37
|
### Fixed
|
|
@@ -5731,6 +5731,9 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
5731
5731
|
}, {
|
|
5732
5732
|
readonly flags: "--create";
|
|
5733
5733
|
readonly description: "Create the Board view when the GET is a 404";
|
|
5734
|
+
}, {
|
|
5735
|
+
readonly flags: "--reset-status";
|
|
5736
|
+
readonly description: "Overwrite live story status from the PRD (prints a one-line summary of changes)";
|
|
5734
5737
|
}, {
|
|
5735
5738
|
readonly flags: "--json";
|
|
5736
5739
|
readonly description: "Print machine-readable JSON";
|
|
@@ -6775,6 +6778,72 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
6775
6778
|
readonly arguments: readonly [];
|
|
6776
6779
|
readonly options: readonly [];
|
|
6777
6780
|
readonly subcommands: readonly [{
|
|
6781
|
+
readonly name: "outpost-pruner";
|
|
6782
|
+
readonly description: "Inspect, install, and run safe outpost worktree pruning";
|
|
6783
|
+
readonly aliases: readonly [];
|
|
6784
|
+
readonly hidden: false;
|
|
6785
|
+
readonly usage: "[options] [command]";
|
|
6786
|
+
readonly arguments: readonly [];
|
|
6787
|
+
readonly options: readonly [];
|
|
6788
|
+
readonly subcommands: readonly [{
|
|
6789
|
+
readonly name: "run";
|
|
6790
|
+
readonly description: "Run the guarded outpost worktree and user-cache cleanup";
|
|
6791
|
+
readonly aliases: readonly [];
|
|
6792
|
+
readonly hidden: false;
|
|
6793
|
+
readonly usage: "[options]";
|
|
6794
|
+
readonly arguments: readonly [];
|
|
6795
|
+
readonly options: readonly [{
|
|
6796
|
+
readonly flags: "--trigger <kind>";
|
|
6797
|
+
readonly description: "pressure | daily | manual";
|
|
6798
|
+
}, {
|
|
6799
|
+
readonly flags: "--min-age-days <days>";
|
|
6800
|
+
readonly description: "Minimum age before cleanup (1–365 days)";
|
|
6801
|
+
}, {
|
|
6802
|
+
readonly flags: "--dry-run";
|
|
6803
|
+
readonly description: "List eligible worktrees without deleting files";
|
|
6804
|
+
}, {
|
|
6805
|
+
readonly flags: "--json";
|
|
6806
|
+
readonly description: "JSON on stdout (always)";
|
|
6807
|
+
}];
|
|
6808
|
+
readonly subcommands: readonly [];
|
|
6809
|
+
}, {
|
|
6810
|
+
readonly name: "install";
|
|
6811
|
+
readonly description: "Write the pressure and daily systemd user timers";
|
|
6812
|
+
readonly aliases: readonly [];
|
|
6813
|
+
readonly hidden: false;
|
|
6814
|
+
readonly usage: "[options]";
|
|
6815
|
+
readonly arguments: readonly [];
|
|
6816
|
+
readonly options: readonly [{
|
|
6817
|
+
readonly flags: "--systemd-root <path>";
|
|
6818
|
+
readonly description: "Systemd user unit directory";
|
|
6819
|
+
}, {
|
|
6820
|
+
readonly flags: "--enable";
|
|
6821
|
+
readonly description: "Enable both timers after writing the units";
|
|
6822
|
+
}, {
|
|
6823
|
+
readonly flags: "--json";
|
|
6824
|
+
readonly description: "JSON on stdout (always)";
|
|
6825
|
+
}];
|
|
6826
|
+
readonly subcommands: readonly [];
|
|
6827
|
+
}, {
|
|
6828
|
+
readonly name: "uninstall";
|
|
6829
|
+
readonly description: "Disable and remove the pressure and daily systemd user timers";
|
|
6830
|
+
readonly aliases: readonly [];
|
|
6831
|
+
readonly hidden: false;
|
|
6832
|
+
readonly usage: "[options]";
|
|
6833
|
+
readonly arguments: readonly [];
|
|
6834
|
+
readonly options: readonly [{
|
|
6835
|
+
readonly flags: "--systemd-root <path>";
|
|
6836
|
+
readonly description: "Systemd user unit directory";
|
|
6837
|
+
}, {
|
|
6838
|
+
readonly flags: "--disable";
|
|
6839
|
+
readonly description: "Disable both timers before removing their units";
|
|
6840
|
+
}, {
|
|
6841
|
+
readonly flags: "--json";
|
|
6842
|
+
readonly description: "JSON on stdout (always)";
|
|
6843
|
+
}];
|
|
6844
|
+
readonly subcommands: readonly [];
|
|
6845
|
+
}];
|
|
6846
|
+
}, {
|
|
6778
6847
|
readonly name: "create";
|
|
6779
6848
|
readonly description: "Register a lane, run admission, and return immediately";
|
|
6780
6849
|
readonly aliases: readonly [];
|
|
@@ -7660,6 +7729,9 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
7660
7729
|
}, {
|
|
7661
7730
|
readonly flags: "--hq-root <path>";
|
|
7662
7731
|
readonly description: "HQ folder the services work against";
|
|
7732
|
+
}, {
|
|
7733
|
+
readonly flags: "--user <login>";
|
|
7734
|
+
readonly description: "on a box: the login user the daemon runs as (never root)";
|
|
7663
7735
|
}];
|
|
7664
7736
|
readonly subcommands: readonly [];
|
|
7665
7737
|
}, {
|
|
@@ -7800,7 +7872,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
7800
7872
|
readonly options: readonly [];
|
|
7801
7873
|
readonly subcommands: readonly [{
|
|
7802
7874
|
readonly name: "self-update";
|
|
7803
|
-
readonly description: "Update the hq CLI
|
|
7875
|
+
readonly description: "Update the hq CLI, Claude, Codex and Grok, then restart the daemon onto a new CLI";
|
|
7804
7876
|
readonly aliases: readonly [];
|
|
7805
7877
|
readonly hidden: false;
|
|
7806
7878
|
readonly usage: "[options]";
|
|
@@ -7825,6 +7897,21 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
7825
7897
|
readonly description: "claude --permission-mode";
|
|
7826
7898
|
}];
|
|
7827
7899
|
readonly subcommands: readonly [];
|
|
7900
|
+
}, {
|
|
7901
|
+
readonly name: "codex-remote-control";
|
|
7902
|
+
readonly description: "Keep the box's Codex remote-control runner going in a tmux session, restarting it after a Codex update";
|
|
7903
|
+
readonly aliases: readonly [];
|
|
7904
|
+
readonly hidden: false;
|
|
7905
|
+
readonly usage: "[options]";
|
|
7906
|
+
readonly arguments: readonly [];
|
|
7907
|
+
readonly options: readonly [{
|
|
7908
|
+
readonly flags: "--script <path>";
|
|
7909
|
+
readonly description: "the box's Codex runner script";
|
|
7910
|
+
}, {
|
|
7911
|
+
readonly flags: "--codex <path>";
|
|
7912
|
+
readonly description: "codex binary, watched for updates";
|
|
7913
|
+
}];
|
|
7914
|
+
readonly subcommands: readonly [];
|
|
7828
7915
|
}];
|
|
7829
7916
|
}];
|
|
7830
7917
|
}, {
|
|
@@ -7396,6 +7396,10 @@ export const COMMAND_CATALOG = [
|
|
|
7396
7396
|
"flags": "--create",
|
|
7397
7397
|
"description": "Create the Board view when the GET is a 404"
|
|
7398
7398
|
},
|
|
7399
|
+
{
|
|
7400
|
+
"flags": "--reset-status",
|
|
7401
|
+
"description": "Overwrite live story status from the PRD (prints a one-line summary of changes)"
|
|
7402
|
+
},
|
|
7399
7403
|
{
|
|
7400
7404
|
"flags": "--json",
|
|
7401
7405
|
"description": "Print machine-readable JSON"
|
|
@@ -8754,6 +8758,90 @@ export const COMMAND_CATALOG = [
|
|
|
8754
8758
|
"arguments": [],
|
|
8755
8759
|
"options": [],
|
|
8756
8760
|
"subcommands": [
|
|
8761
|
+
{
|
|
8762
|
+
"name": "outpost-pruner",
|
|
8763
|
+
"description": "Inspect, install, and run safe outpost worktree pruning",
|
|
8764
|
+
"aliases": [],
|
|
8765
|
+
"hidden": false,
|
|
8766
|
+
"usage": "[options] [command]",
|
|
8767
|
+
"arguments": [],
|
|
8768
|
+
"options": [],
|
|
8769
|
+
"subcommands": [
|
|
8770
|
+
{
|
|
8771
|
+
"name": "run",
|
|
8772
|
+
"description": "Run the guarded outpost worktree and user-cache cleanup",
|
|
8773
|
+
"aliases": [],
|
|
8774
|
+
"hidden": false,
|
|
8775
|
+
"usage": "[options]",
|
|
8776
|
+
"arguments": [],
|
|
8777
|
+
"options": [
|
|
8778
|
+
{
|
|
8779
|
+
"flags": "--trigger <kind>",
|
|
8780
|
+
"description": "pressure | daily | manual"
|
|
8781
|
+
},
|
|
8782
|
+
{
|
|
8783
|
+
"flags": "--min-age-days <days>",
|
|
8784
|
+
"description": "Minimum age before cleanup (1–365 days)"
|
|
8785
|
+
},
|
|
8786
|
+
{
|
|
8787
|
+
"flags": "--dry-run",
|
|
8788
|
+
"description": "List eligible worktrees without deleting files"
|
|
8789
|
+
},
|
|
8790
|
+
{
|
|
8791
|
+
"flags": "--json",
|
|
8792
|
+
"description": "JSON on stdout (always)"
|
|
8793
|
+
}
|
|
8794
|
+
],
|
|
8795
|
+
"subcommands": []
|
|
8796
|
+
},
|
|
8797
|
+
{
|
|
8798
|
+
"name": "install",
|
|
8799
|
+
"description": "Write the pressure and daily systemd user timers",
|
|
8800
|
+
"aliases": [],
|
|
8801
|
+
"hidden": false,
|
|
8802
|
+
"usage": "[options]",
|
|
8803
|
+
"arguments": [],
|
|
8804
|
+
"options": [
|
|
8805
|
+
{
|
|
8806
|
+
"flags": "--systemd-root <path>",
|
|
8807
|
+
"description": "Systemd user unit directory"
|
|
8808
|
+
},
|
|
8809
|
+
{
|
|
8810
|
+
"flags": "--enable",
|
|
8811
|
+
"description": "Enable both timers after writing the units"
|
|
8812
|
+
},
|
|
8813
|
+
{
|
|
8814
|
+
"flags": "--json",
|
|
8815
|
+
"description": "JSON on stdout (always)"
|
|
8816
|
+
}
|
|
8817
|
+
],
|
|
8818
|
+
"subcommands": []
|
|
8819
|
+
},
|
|
8820
|
+
{
|
|
8821
|
+
"name": "uninstall",
|
|
8822
|
+
"description": "Disable and remove the pressure and daily systemd user timers",
|
|
8823
|
+
"aliases": [],
|
|
8824
|
+
"hidden": false,
|
|
8825
|
+
"usage": "[options]",
|
|
8826
|
+
"arguments": [],
|
|
8827
|
+
"options": [
|
|
8828
|
+
{
|
|
8829
|
+
"flags": "--systemd-root <path>",
|
|
8830
|
+
"description": "Systemd user unit directory"
|
|
8831
|
+
},
|
|
8832
|
+
{
|
|
8833
|
+
"flags": "--disable",
|
|
8834
|
+
"description": "Disable both timers before removing their units"
|
|
8835
|
+
},
|
|
8836
|
+
{
|
|
8837
|
+
"flags": "--json",
|
|
8838
|
+
"description": "JSON on stdout (always)"
|
|
8839
|
+
}
|
|
8840
|
+
],
|
|
8841
|
+
"subcommands": []
|
|
8842
|
+
}
|
|
8843
|
+
]
|
|
8844
|
+
},
|
|
8757
8845
|
{
|
|
8758
8846
|
"name": "create",
|
|
8759
8847
|
"description": "Register a lane, run admission, and return immediately",
|
|
@@ -9904,6 +9992,10 @@ export const COMMAND_CATALOG = [
|
|
|
9904
9992
|
{
|
|
9905
9993
|
"flags": "--hq-root <path>",
|
|
9906
9994
|
"description": "HQ folder the services work against"
|
|
9995
|
+
},
|
|
9996
|
+
{
|
|
9997
|
+
"flags": "--user <login>",
|
|
9998
|
+
"description": "on a box: the login user the daemon runs as (never root)"
|
|
9907
9999
|
}
|
|
9908
10000
|
],
|
|
9909
10001
|
"subcommands": []
|
|
@@ -10080,7 +10172,7 @@ export const COMMAND_CATALOG = [
|
|
|
10080
10172
|
"subcommands": [
|
|
10081
10173
|
{
|
|
10082
10174
|
"name": "self-update",
|
|
10083
|
-
"description": "Update the hq CLI
|
|
10175
|
+
"description": "Update the hq CLI, Claude, Codex and Grok, then restart the daemon onto a new CLI",
|
|
10084
10176
|
"aliases": [],
|
|
10085
10177
|
"hidden": false,
|
|
10086
10178
|
"usage": "[options]",
|
|
@@ -10110,6 +10202,25 @@ export const COMMAND_CATALOG = [
|
|
|
10110
10202
|
}
|
|
10111
10203
|
],
|
|
10112
10204
|
"subcommands": []
|
|
10205
|
+
},
|
|
10206
|
+
{
|
|
10207
|
+
"name": "codex-remote-control",
|
|
10208
|
+
"description": "Keep the box's Codex remote-control runner going in a tmux session, restarting it after a Codex update",
|
|
10209
|
+
"aliases": [],
|
|
10210
|
+
"hidden": false,
|
|
10211
|
+
"usage": "[options]",
|
|
10212
|
+
"arguments": [],
|
|
10213
|
+
"options": [
|
|
10214
|
+
{
|
|
10215
|
+
"flags": "--script <path>",
|
|
10216
|
+
"description": "the box's Codex runner script"
|
|
10217
|
+
},
|
|
10218
|
+
{
|
|
10219
|
+
"flags": "--codex <path>",
|
|
10220
|
+
"description": "codex binary, watched for updates"
|
|
10221
|
+
}
|
|
10222
|
+
],
|
|
10223
|
+
"subcommands": []
|
|
10113
10224
|
}
|
|
10114
10225
|
]
|
|
10115
10226
|
}
|
|
@@ -29,6 +29,8 @@ export interface DaemonCommandDeps {
|
|
|
29
29
|
isRoot: boolean;
|
|
30
30
|
fileExists: (p: string) => boolean;
|
|
31
31
|
readFile: (p: string) => string;
|
|
32
|
+
/** Writes a root-owned file such as /etc/hq/machine-type, creating its folder. */
|
|
33
|
+
writeFile: (p: string, text: string) => void;
|
|
32
34
|
legacyUnits: () => LegacyUnit[];
|
|
33
35
|
resolveHqRoot: (flag: string | undefined) => string | undefined;
|
|
34
36
|
/** Home folder of the login user a box's system unit runs as. */
|
package/dist/commands/daemon.js
CHANGED
|
@@ -20,10 +20,10 @@ import { DEFAULT_SERVICES, SERVICE_DESCRIPTIONS, enabledServices } from "../lib/
|
|
|
20
20
|
import { applyServiceToggle, readDaemonConfig, writeDaemonConfig } from "../lib/daemon/config.js";
|
|
21
21
|
import { daemonUnitInstalled } from "../lib/daemon/handoff.js";
|
|
22
22
|
import { adoptLegacyUnits, readAdoptedUnits, restoreLegacyUnits } from "../lib/daemon/migrate.js";
|
|
23
|
-
import { SYSTEM_UNIT_DIR, installDaemonUnit, installScopeFor, uninstallDaemonUnit, } from "../lib/daemon/install.js";
|
|
23
|
+
import { ROOT_REFUSAL, SYSTEM_UNIT_DIR, daemonRootRefusal, installDaemonUnit, installScopeFor, uninstallDaemonUnit, } from "../lib/daemon/install.js";
|
|
24
24
|
import { describeLegacyUnit, findLegacyUnits } from "../lib/daemon/legacy.js";
|
|
25
25
|
import { isDaemonProcess, runningDaemonPid } from "../lib/daemon/lock.js";
|
|
26
|
-
import { detectMachineType, homeFromPasswd } from "../lib/daemon/machine-type.js";
|
|
26
|
+
import { MACHINE_TYPE_MARKER_PATH, detectMachineType, homeFromPasswd, } from "../lib/daemon/machine-type.js";
|
|
27
27
|
import { daemonPaths } from "../lib/daemon/paths.js";
|
|
28
28
|
import { writeControlRequest } from "../lib/daemon/requests.js";
|
|
29
29
|
import { resolveDefaultHqRoot } from "../utils/cognito-session.js";
|
|
@@ -67,6 +67,11 @@ async function loadDefaults(over) {
|
|
|
67
67
|
isRoot: over.isRoot ?? (typeof process.getuid === "function" && process.getuid() === 0),
|
|
68
68
|
fileExists: over.fileExists ?? fs.existsSync,
|
|
69
69
|
readFile: over.readFile ?? ((p) => fs.readFileSync(p, "utf8")),
|
|
70
|
+
writeFile: over.writeFile ??
|
|
71
|
+
((p, text) => {
|
|
72
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
73
|
+
fs.writeFileSync(p, text, { mode: 0o644 });
|
|
74
|
+
}),
|
|
70
75
|
legacyUnits: over.legacyUnits ??
|
|
71
76
|
(() => findLegacyUnits({
|
|
72
77
|
platform,
|
|
@@ -142,17 +147,22 @@ function loginHome(user, run) {
|
|
|
142
147
|
* Where the daemon's files are for this machine. On boxes the system unit runs
|
|
143
148
|
* as the login user, so its files live in that user's home, not root's.
|
|
144
149
|
*/
|
|
145
|
-
function hostPaths(d, flag) {
|
|
150
|
+
function hostPaths(d, flag, userFlag) {
|
|
146
151
|
const own = daemonPaths(d.home, d.env);
|
|
147
152
|
const detected = detect(d, flag, readDaemonConfig(own.configPath));
|
|
148
153
|
if (installScopeFor(detected.type) !== "system")
|
|
149
154
|
return { paths: own, detected };
|
|
150
|
-
|
|
155
|
+
// An explicit --user, then the marker or the outpost runner's User=, then
|
|
156
|
+
// the account that ran sudo.
|
|
157
|
+
const sudoUser = d.env.SUDO_USER?.trim();
|
|
158
|
+
const runAsUser = userFlag ?? detected.runAsUser ?? (sudoUser && sudoUser !== "root" ? sudoUser : undefined);
|
|
159
|
+
if (runAsUser === "root")
|
|
160
|
+
throw new DaemonCommandError(`${ROOT_REFUSAL} Pass the box's login user with --user <name>.`);
|
|
151
161
|
if (!runAsUser) {
|
|
152
|
-
throw new DaemonCommandError("A box install needs the login user the daemon runs as
|
|
162
|
+
throw new DaemonCommandError("A box install needs the login user the daemon runs as: pass --user <name>, or provisioning writes it to /etc/hq/machine-type (user=<name>).");
|
|
153
163
|
}
|
|
154
164
|
const paths = daemonPaths(d.userHome(runAsUser), d.env);
|
|
155
|
-
if (flag !== undefined)
|
|
165
|
+
if (flag !== undefined || userFlag !== undefined)
|
|
156
166
|
return { paths, detected, runAsUser };
|
|
157
167
|
// Root normally has no saved config; the login user's saved type is the real one.
|
|
158
168
|
const saved = readDaemonConfig(paths.configPath);
|
|
@@ -177,8 +187,12 @@ function handOver(d, host) {
|
|
|
177
187
|
d.run("chown", ["-R", `${host.runAsUser}:`, host.paths.root]);
|
|
178
188
|
}
|
|
179
189
|
async function runInstall(d, opts) {
|
|
180
|
-
const { paths, detected, runAsUser } = hostPaths(d, opts.machineType);
|
|
190
|
+
const { paths, detected, runAsUser } = hostPaths(d, opts.machineType, opts.user);
|
|
181
191
|
const scope = installScopeFor(detected.type);
|
|
192
|
+
// A per-user unit installed by root would run every service as root.
|
|
193
|
+
const refusal = scope === "user" ? daemonRootRefusal(d.isRoot, detected.type) : null;
|
|
194
|
+
if (refusal)
|
|
195
|
+
throw new DaemonCommandError(refusal);
|
|
182
196
|
const config = {
|
|
183
197
|
...readDaemonConfig(paths.configPath),
|
|
184
198
|
machineType: detected.type,
|
|
@@ -198,6 +212,13 @@ async function runInstall(d, opts) {
|
|
|
198
212
|
for (const u of migratable) {
|
|
199
213
|
d.out(`Note: ${describeLegacyUnit(u)} still runs ${u.unitId}. The daemon leaves it alone until \`hq daemon migrate\` takes it over.`);
|
|
200
214
|
}
|
|
215
|
+
// A box worked out from its runner unit, --user or sudo gets the marker file
|
|
216
|
+
// provisioning would have written: `migrate --apply` moves the runner unit
|
|
217
|
+
// aside, and later root commands (uninstall) must still find the box's type
|
|
218
|
+
// and login user.
|
|
219
|
+
if (scope === "system" && runAsUser && d.isRoot && !d.fileExists(MACHINE_TYPE_MARKER_PATH)) {
|
|
220
|
+
d.writeFile(MACHINE_TYPE_MARKER_PATH, `type=${detected.type}\nuser=${runAsUser}\n`);
|
|
221
|
+
}
|
|
201
222
|
const runAsHome = runAsUser ? d.userHome(runAsUser) : undefined;
|
|
202
223
|
const result = installDaemonUnit(hostFor(d, paths, scope === "user"), { scope, runAsUser, runAsHome }, { run: d.run });
|
|
203
224
|
handOver(d, { paths, detected, runAsUser });
|
|
@@ -397,32 +418,41 @@ async function runMigrate(d, opts) {
|
|
|
397
418
|
d.out("Nothing was changed. `hq daemon migrate --apply` takes them over.");
|
|
398
419
|
}
|
|
399
420
|
async function runServiceSelfUpdate(d) {
|
|
400
|
-
const [{ daemonSelfUpdate }, {
|
|
421
|
+
const [{ daemonSelfUpdate }, { hostToolUpdater, runDaemonUpdater, updaterEnv }, { TOOL_NAMES }, { cliSelfUpdatable }] = await Promise.all([
|
|
401
422
|
import("../utils/self-update.js"),
|
|
402
423
|
import("../lib/daemon/updater.js"),
|
|
424
|
+
import("../lib/daemon/tools.js"),
|
|
403
425
|
import("../lib/daemon/run.js"),
|
|
404
426
|
]);
|
|
405
|
-
// Same guard as the plan: never replace a dev checkout or a project's copy with a global install.
|
|
406
|
-
if (!cliSelfUpdatable()) {
|
|
407
|
-
d.out("This hq CLI is not a global install; nothing to update here.");
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
427
|
const { paths } = currentHost(d);
|
|
428
|
+
const env = updaterEnv(d.env);
|
|
411
429
|
const result = await runDaemonUpdater({
|
|
412
|
-
|
|
413
|
-
|
|
430
|
+
// Same guard as every self-update: never replace a dev checkout or a project's copy with a global install.
|
|
431
|
+
updateCli: cliSelfUpdatable() ? () => daemonSelfUpdate({ env }) : null,
|
|
432
|
+
tools: TOOL_NAMES.map((name) => hostToolUpdater(name, env, d.home)),
|
|
414
433
|
requestDaemonRestart: () => writeControlRequest(paths.requestsDir, { action: "restart-daemon" }),
|
|
415
434
|
log: d.out,
|
|
416
435
|
});
|
|
417
436
|
if (!result.ok)
|
|
418
437
|
process.exitCode = 1;
|
|
419
438
|
}
|
|
420
|
-
|
|
439
|
+
/** Size, mtime and real path of a binary: changes whenever a new version is installed over it. */
|
|
440
|
+
function binaryFingerprint(bin) {
|
|
441
|
+
try {
|
|
442
|
+
const real = fs.realpathSync(bin);
|
|
443
|
+
const st = fs.statSync(real);
|
|
444
|
+
return `${real}:${st.size}:${st.mtimeMs}`;
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
async function runTmuxService(d, opts) {
|
|
421
451
|
const { runRemoteControlHost } = await import("../lib/daemon/remote-control.js");
|
|
422
452
|
const { findOnPath } = await import("../lib/daemon/run.js");
|
|
423
453
|
const hqRoot = d.env.HQ_ROOT?.trim();
|
|
424
454
|
if (!hqRoot)
|
|
425
|
-
throw new DaemonCommandError(
|
|
455
|
+
throw new DaemonCommandError(`${opts.label} needs HQ_ROOT (the daemon sets it from the saved HQ folder)`);
|
|
426
456
|
const tmux = findOnPath("tmux", d.env);
|
|
427
457
|
if (!tmux)
|
|
428
458
|
throw new DaemonCommandError("tmux is not installed");
|
|
@@ -433,10 +463,12 @@ async function runServiceRemoteControl(d, opts) {
|
|
|
433
463
|
try {
|
|
434
464
|
process.exitCode = await runRemoteControlHost({
|
|
435
465
|
tmux,
|
|
436
|
-
|
|
437
|
-
hqRoot,
|
|
438
|
-
|
|
439
|
-
|
|
466
|
+
session: opts.session,
|
|
467
|
+
cwd: hqRoot,
|
|
468
|
+
argv: opts.argv,
|
|
469
|
+
label: opts.label,
|
|
470
|
+
binaryFingerprint: () => binaryFingerprint(opts.watch),
|
|
471
|
+
now: () => Date.now(),
|
|
440
472
|
run: d.run,
|
|
441
473
|
pollMs: 5_000,
|
|
442
474
|
signal: controller.signal,
|
|
@@ -455,6 +487,24 @@ async function runServiceRemoteControl(d, opts) {
|
|
|
455
487
|
process.off("SIGINT", stop);
|
|
456
488
|
}
|
|
457
489
|
}
|
|
490
|
+
async function runServiceRemoteControl(d, opts) {
|
|
491
|
+
const { TMUX_SESSION, remoteControlArgs } = await import("../lib/daemon/remote-control.js");
|
|
492
|
+
await runTmuxService(d, {
|
|
493
|
+
session: TMUX_SESSION,
|
|
494
|
+
argv: [opts.claude, ...remoteControlArgs({ name: opts.name, permissionMode: opts.permissionMode })],
|
|
495
|
+
label: "claude remote-control",
|
|
496
|
+
watch: opts.claude,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
async function runServiceCodexRemoteControl(d, opts) {
|
|
500
|
+
const { CODEX_TMUX_SESSION } = await import("../lib/daemon/remote-control.js");
|
|
501
|
+
await runTmuxService(d, {
|
|
502
|
+
session: CODEX_TMUX_SESSION,
|
|
503
|
+
argv: ["bash", opts.script],
|
|
504
|
+
label: "codex remote-control",
|
|
505
|
+
watch: opts.codex,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
458
508
|
export function registerDaemonCommand(program, over = {}) {
|
|
459
509
|
let resolved = null;
|
|
460
510
|
const deps = () => (resolved ??= loadDefaults(over));
|
|
@@ -477,6 +527,7 @@ export function registerDaemonCommand(program, over = {}) {
|
|
|
477
527
|
.description("Detect this machine's type, install the daemon, and start its default services")
|
|
478
528
|
.option("--machine-type <type>", "desktop | self-hosted | outpost | agent-box | agent-kit (skips detection)")
|
|
479
529
|
.option("--hq-root <path>", "HQ folder the services work against")
|
|
530
|
+
.option("--user <login>", "on a box: the login user the daemon runs as (never root)")
|
|
480
531
|
.action(wrap((d, opts) => runInstall(d, opts)));
|
|
481
532
|
daemon
|
|
482
533
|
.command("uninstall")
|
|
@@ -540,7 +591,7 @@ export function registerDaemonCommand(program, over = {}) {
|
|
|
540
591
|
.description("Entry points the daemon runs for services that have no command of their own");
|
|
541
592
|
service
|
|
542
593
|
.command("self-update")
|
|
543
|
-
.description("Update the hq CLI
|
|
594
|
+
.description("Update the hq CLI, Claude, Codex and Grok, then restart the daemon onto a new CLI")
|
|
544
595
|
.action(wrap((d) => runServiceSelfUpdate(d)));
|
|
545
596
|
service
|
|
546
597
|
.command("remote-control")
|
|
@@ -549,6 +600,12 @@ export function registerDaemonCommand(program, over = {}) {
|
|
|
549
600
|
.requiredOption("--name <name>", "remote-control session name")
|
|
550
601
|
.option("--permission-mode <mode>", "claude --permission-mode")
|
|
551
602
|
.action(wrap((d, opts) => runServiceRemoteControl(d, opts)));
|
|
603
|
+
service
|
|
604
|
+
.command("codex-remote-control")
|
|
605
|
+
.description("Keep the box's Codex remote-control runner going in a tmux session, restarting it after a Codex update")
|
|
606
|
+
.requiredOption("--script <path>", "the box's Codex runner script")
|
|
607
|
+
.requiredOption("--codex <path>", "codex binary, watched for updates")
|
|
608
|
+
.action(wrap((d, opts) => runServiceCodexRemoteControl(d, opts)));
|
|
552
609
|
return daemon;
|
|
553
610
|
}
|
|
554
611
|
//# sourceMappingURL=daemon.js.map
|