@tt-a1i/hive 1.4.3 → 1.4.4
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 +22 -0
- package/README.en.md +5 -4
- package/README.md +1 -1
- package/dist/src/cli/hive-update.d.ts +29 -0
- package/dist/src/cli/hive-update.js +54 -15
- package/dist/src/cli/hive.d.ts +32 -0
- package/dist/src/cli/hive.js +72 -17
- package/dist/src/cli/team.js +17 -5
- package/dist/src/server/agent-command-resolver.d.ts +10 -1
- package/dist/src/server/agent-command-resolver.js +48 -4
- package/dist/src/server/agent-launch-resolver.js +9 -3
- package/dist/src/server/agent-manager-support.d.ts +28 -0
- package/dist/src/server/agent-manager-support.js +58 -4
- package/dist/src/server/agent-run-bootstrap.d.ts +17 -1
- package/dist/src/server/agent-run-bootstrap.js +30 -2
- package/dist/src/server/agent-startup-instructions.js +1 -1
- package/dist/src/server/app.d.ts +1 -0
- package/dist/src/server/app.js +12 -2
- package/dist/src/server/fs-browse.d.ts +14 -1
- package/dist/src/server/fs-browse.js +48 -5
- package/dist/src/server/fs-pick-folder.js +54 -11
- package/dist/src/server/hive-team-guidance.js +5 -4
- package/dist/src/server/open-target-commands.js +30 -4
- package/dist/src/server/post-start-input-writer.js +6 -3
- package/dist/src/server/routes-team.js +10 -1
- package/dist/src/server/runtime-store.d.ts +3 -1
- package/dist/src/server/session-capture-claude.d.ts +23 -0
- package/dist/src/server/session-capture-claude.js +24 -1
- package/dist/src/server/session-capture-opencode.d.ts +18 -0
- package/dist/src/server/session-capture-opencode.js +27 -2
- package/dist/src/server/startup-command-parser.d.ts +15 -0
- package/dist/src/server/startup-command-parser.js +33 -2
- package/dist/src/server/tasks-file-watcher.d.ts +26 -0
- package/dist/src/server/tasks-file-watcher.js +29 -3
- package/dist/src/server/team-operations.d.ts +5 -1
- package/dist/src/server/team-operations.js +44 -3
- package/dist/src/server/terminal-input-profile.js +2 -8
- package/dist/src/server/terminal-ws-server.js +26 -8
- package/package.json +2 -2
- package/web/dist/assets/{AddWorkerDialog-DmkDOdp6.js → AddWorkerDialog-DeZhTQLi.js} +2 -2
- package/web/dist/assets/AddWorkspaceDialog-DDpXNEKf.js +1 -0
- package/web/dist/assets/{FirstRunWizard-SAd1wsH4.js → FirstRunWizard-B5wLcat5.js} +1 -1
- package/web/dist/assets/{MarketplaceDrawer-B_8aG2uT.js → MarketplaceDrawer-BC0eBOEW.js} +1 -1
- package/web/dist/assets/{WorkerModal-CQmjiPme.js → WorkerModal-BwMHq-Bi.js} +1 -1
- package/web/dist/assets/WorkspaceTaskDrawer-CxvT4nqs.js +1 -0
- package/web/dist/assets/{WorkspaceTerminalPanels-BReWh1YL.js → WorkspaceTerminalPanels-CvibsPSd.js} +1 -1
- package/web/dist/assets/index-Ddb7bDN5.js +75 -0
- package/web/dist/assets/path-join-S7qkXQtP.js +1 -0
- package/web/dist/index.html +1 -1
- package/web/dist/sw.js +1 -1
- package/web/dist/assets/AddWorkspaceDialog-BsVnH3Xe.js +0 -1
- package/web/dist/assets/WorkspaceTaskDrawer-B0DmCWcV.js +0 -1
- package/web/dist/assets/chevron-right-CtLjVEl7.js +0 -1
- package/web/dist/assets/index-Cn8X3get.js +0 -76
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
All notable user-facing changes will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 1.4.4 - 2026-05-29
|
|
6
|
+
|
|
7
|
+
Windows portability and team protocol hardening.
|
|
8
|
+
|
|
9
|
+
- Fixes Windows `.cmd` / `.bat` launch handling for built-in and custom startup
|
|
10
|
+
commands, including quoted paths from nvm4w and `Program Files`.
|
|
11
|
+
- Improves Windows runtime shutdown by tearing down WebSocket connections before
|
|
12
|
+
closing the HTTP server and killing worker process trees with `taskkill /T /F`
|
|
13
|
+
before falling back to PTY termination.
|
|
14
|
+
- Makes `hive update`, open-in-editor commands, folder picking, filesystem
|
|
15
|
+
browsing, and port-in-use recovery friendlier on Windows.
|
|
16
|
+
- Preserves CRLF line endings in `.hive/tasks.md` mutations and makes the tasks
|
|
17
|
+
watcher more tolerant of atomic-save editors.
|
|
18
|
+
- Resolves OpenCode session data under `%LOCALAPPDATA%` on Windows and aligns
|
|
19
|
+
Claude session path encoding with Claude Code's project directory format.
|
|
20
|
+
- Hardens `team send` against stale worker names by returning a 409 with the
|
|
21
|
+
current roster and updates orchestrator guidance to refresh the member list
|
|
22
|
+
before dispatching.
|
|
23
|
+
- Expands Windows-focused unit and integration coverage across startup command
|
|
24
|
+
parsing, CLI shims, stdin protocol help, terminal profiles, filesystem
|
|
25
|
+
browsing, process cleanup, and path rendering.
|
|
26
|
+
|
|
5
27
|
## 1.4.3 - 2026-05-28
|
|
6
28
|
|
|
7
29
|
Update hardening for multi-Node installs.
|
package/README.en.md
CHANGED
|
@@ -107,10 +107,11 @@ a separate app from `hive --port 3000`. To uninstall, visit `chrome://apps`,
|
|
|
107
107
|
right-click the Hive tile, and choose **Remove from Chrome…**.
|
|
108
108
|
|
|
109
109
|
Hive asks the browser to confirm before closing the tab or PWA window so an
|
|
110
|
-
accidental Cmd-W
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
accidental close shortcut (Cmd-W on macOS, Ctrl-W on Windows/Linux) doesn't
|
|
111
|
+
drop your session. Modern browsers gate that prompt on prior page interaction
|
|
112
|
+
— if you open the PWA and immediately press the close shortcut without
|
|
113
|
+
clicking or typing anywhere first, it still closes cleanly. That's a browser
|
|
114
|
+
policy, not a Hive bug.
|
|
114
115
|
|
|
115
116
|
First-run flow:
|
|
116
117
|
|
package/README.md
CHANGED
|
@@ -74,7 +74,7 @@ hive update
|
|
|
74
74
|
|
|
75
75
|
PWA 只是 UI 壳,Hive 后端仍需要在终端里跑着。如果启动 PWA 时后端没起,会看到 “Hive 后端未启动” 页面,等你跑起 `hive` 后会自动刷新。PWA 的 install scope 按 origin(含端口)划分,所以 `hive --port 4011` 跟 `hive --port 3000` 在浏览器看来是两个独立应用。卸载方法:浏览器地址栏访问 `chrome://apps`,右键 Hive 图标,选 **从 Chrome 中移除…**。
|
|
76
76
|
|
|
77
|
-
关闭 PWA 窗口或 tab 时 Hive
|
|
77
|
+
关闭 PWA 窗口或 tab 时 Hive 会主动请求浏览器弹原生确认对话框,避免关闭快捷键(macOS 上是 Cmd+W、Windows / Linux 上是 Ctrl+W)误关丢失会话。但现代浏览器要求你跟页面"交互过"(点击 / 滚动 / 输入)才会真的弹这个对话框——刚打开 PWA 立刻按关闭快捷键仍会直接关闭,这是浏览器策略,不是 Hive 的 bug。
|
|
78
78
|
|
|
79
79
|
首次使用流程:
|
|
80
80
|
|
|
@@ -4,6 +4,35 @@ export interface RunUpdateResult {
|
|
|
4
4
|
spawnError?: Error;
|
|
5
5
|
}
|
|
6
6
|
export type RunUpdate = (command: string, args: readonly string[]) => Promise<RunUpdateResult>;
|
|
7
|
+
/**
|
|
8
|
+
* Build the spawn options for the upgrade child. The non-obvious part is
|
|
9
|
+
* Windows: npm ships as `npm.cmd` (a batch shim), and Node 22+ refuses to
|
|
10
|
+
* spawn `.cmd` / `.bat` files without `shell: true` after CVE-2024-27980.
|
|
11
|
+
* Detect by file extension rather than by `process.platform` so the same
|
|
12
|
+
* code path works for both real Windows runs and our cross-platform unit
|
|
13
|
+
* tests (which inject `platform: 'win32'` so that `getNpmCommand` returns
|
|
14
|
+
* `npm.cmd`).
|
|
15
|
+
*
|
|
16
|
+
* Known limitation: with `shell: true` the args are stringified through
|
|
17
|
+
* cmd.exe without quoting, so an install prefix containing spaces (e.g.
|
|
18
|
+
* `C:\Program Files\nodejs`) will be tokenized incorrectly. The common
|
|
19
|
+
* Windows prefix `%APPDATA%\npm` does not have this problem; fixing the
|
|
20
|
+
* spaces case requires a verbatim `cmd.exe /d /s /c call npm.cmd …`
|
|
21
|
+
* wrapper similar to `agent-command-resolver` and is tracked separately.
|
|
22
|
+
*/
|
|
23
|
+
export declare const buildSpawnOptionsForCommand: (command: string) => {
|
|
24
|
+
shell: boolean;
|
|
25
|
+
stdio: "inherit";
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Signals the upgrade child should receive when the parent runtime is
|
|
29
|
+
* interrupted. Beyond the POSIX-only SIGTERM/SIGINT, SIGHUP is what
|
|
30
|
+
* libuv synthesises from Windows CTRL_CLOSE_EVENT (window X close),
|
|
31
|
+
* and SIGBREAK comes from Windows Ctrl+Break. Without forwarding
|
|
32
|
+
* those two the npm child outlives the runtime on the most common
|
|
33
|
+
* Windows exit paths.
|
|
34
|
+
*/
|
|
35
|
+
export declare const FORWARDED_UPDATE_SIGNALS: readonly NodeJS.Signals[];
|
|
7
36
|
export declare const defaultRunUpdate: RunUpdate;
|
|
8
37
|
export declare const resolveHiveUpdateInstallArgs: (moduleUrl?: string) => string[];
|
|
9
38
|
interface RunHiveUpdateOptions {
|
|
@@ -19,27 +19,66 @@ export const HIVE_UPDATE_USAGE = [
|
|
|
19
19
|
'Options:',
|
|
20
20
|
' -h, --help Print this help.',
|
|
21
21
|
].join('\n');
|
|
22
|
+
/**
|
|
23
|
+
* Build the spawn options for the upgrade child. The non-obvious part is
|
|
24
|
+
* Windows: npm ships as `npm.cmd` (a batch shim), and Node 22+ refuses to
|
|
25
|
+
* spawn `.cmd` / `.bat` files without `shell: true` after CVE-2024-27980.
|
|
26
|
+
* Detect by file extension rather than by `process.platform` so the same
|
|
27
|
+
* code path works for both real Windows runs and our cross-platform unit
|
|
28
|
+
* tests (which inject `platform: 'win32'` so that `getNpmCommand` returns
|
|
29
|
+
* `npm.cmd`).
|
|
30
|
+
*
|
|
31
|
+
* Known limitation: with `shell: true` the args are stringified through
|
|
32
|
+
* cmd.exe without quoting, so an install prefix containing spaces (e.g.
|
|
33
|
+
* `C:\Program Files\nodejs`) will be tokenized incorrectly. The common
|
|
34
|
+
* Windows prefix `%APPDATA%\npm` does not have this problem; fixing the
|
|
35
|
+
* spaces case requires a verbatim `cmd.exe /d /s /c call npm.cmd …`
|
|
36
|
+
* wrapper similar to `agent-command-resolver` and is tracked separately.
|
|
37
|
+
*/
|
|
38
|
+
export const buildSpawnOptionsForCommand = (command) => ({
|
|
39
|
+
shell: /\.(cmd|bat)$/i.test(command),
|
|
40
|
+
stdio: 'inherit',
|
|
41
|
+
});
|
|
42
|
+
/**
|
|
43
|
+
* Signals the upgrade child should receive when the parent runtime is
|
|
44
|
+
* interrupted. Beyond the POSIX-only SIGTERM/SIGINT, SIGHUP is what
|
|
45
|
+
* libuv synthesises from Windows CTRL_CLOSE_EVENT (window X close),
|
|
46
|
+
* and SIGBREAK comes from Windows Ctrl+Break. Without forwarding
|
|
47
|
+
* those two the npm child outlives the runtime on the most common
|
|
48
|
+
* Windows exit paths.
|
|
49
|
+
*/
|
|
50
|
+
export const FORWARDED_UPDATE_SIGNALS = [
|
|
51
|
+
'SIGINT',
|
|
52
|
+
'SIGTERM',
|
|
53
|
+
'SIGHUP',
|
|
54
|
+
'SIGBREAK',
|
|
55
|
+
];
|
|
22
56
|
export const defaultRunUpdate = (command, args) => new Promise((resolve) => {
|
|
23
|
-
const child = spawn(command, [...args],
|
|
57
|
+
const child = spawn(command, [...args], buildSpawnOptionsForCommand(command));
|
|
24
58
|
let resolved = false;
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
59
|
+
// Handlers are registered with `once` so they don't accumulate
|
|
60
|
+
// across invocations and explicitly removed at finalize().
|
|
61
|
+
const handlers = new Map();
|
|
62
|
+
for (const signal of FORWARDED_UPDATE_SIGNALS) {
|
|
63
|
+
const handler = () => {
|
|
64
|
+
try {
|
|
65
|
+
child.kill(signal);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// child.kill on Windows throws if the signal name isn't
|
|
69
|
+
// implemented; we forward what we can and ignore the rest.
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
handlers.set(signal, handler);
|
|
73
|
+
process.once(signal, handler);
|
|
74
|
+
}
|
|
37
75
|
const finalize = (result) => {
|
|
38
76
|
if (resolved)
|
|
39
77
|
return;
|
|
40
78
|
resolved = true;
|
|
41
|
-
|
|
42
|
-
|
|
79
|
+
for (const [signal, handler] of handlers) {
|
|
80
|
+
process.off(signal, handler);
|
|
81
|
+
}
|
|
43
82
|
resolve(result);
|
|
44
83
|
};
|
|
45
84
|
child.on('error', (error) => {
|
package/dist/src/cli/hive.d.ts
CHANGED
|
@@ -9,7 +9,39 @@ interface RunHiveCommandResult {
|
|
|
9
9
|
type RunHiveCommandOptions = {
|
|
10
10
|
versionService?: VersionService;
|
|
11
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* Signals that should drive a graceful shutdown. The interesting ones:
|
|
14
|
+
*
|
|
15
|
+
* SIGINT — Ctrl+C in the runtime terminal (all platforms).
|
|
16
|
+
* SIGTERM — `kill <pid>` on POSIX. Never delivered on Windows.
|
|
17
|
+
* SIGHUP — POSIX: parent shell exits. On Windows libuv synthesises
|
|
18
|
+
* SIGHUP from `CTRL_CLOSE_EVENT`, i.e. the user clicking
|
|
19
|
+
* the X on the runtime's cmd / Terminal window. Without
|
|
20
|
+
* this listener that close path skips the graceful path
|
|
21
|
+
* entirely on Windows.
|
|
22
|
+
* SIGBREAK — Windows: Ctrl+Break. Less common than Ctrl+C but kit
|
|
23
|
+
* scripts and CI hosts still send it.
|
|
24
|
+
*
|
|
25
|
+
* Stale agent_runs from a non-graceful exit are reconciled at next
|
|
26
|
+
* startup via `agentRunStore.markUnfinishedRunsStale()`
|
|
27
|
+
* (runtime-store-helpers.ts), so a dropped signal does not leave the
|
|
28
|
+
* database in an inconsistent state — only the PTY children miss the
|
|
29
|
+
* forwarded SIGTERM. On Windows there's no graceful equivalent for
|
|
30
|
+
* those children anyway (pty.kill is TerminateProcess), so this
|
|
31
|
+
* registration is mostly about giving SQLite a chance to checkpoint.
|
|
32
|
+
*/
|
|
33
|
+
export declare const SHUTDOWN_SIGNALS: readonly ["SIGINT", "SIGTERM", "SIGHUP", "SIGBREAK"];
|
|
12
34
|
export declare const HIVE_USAGE: string;
|
|
13
35
|
export declare const handleHiveInfoCommand: (argv: string[]) => boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Recovery hint formatter for the "port already in use" error. Platform-aware
|
|
38
|
+
* because the lsof / xargs / kill pipeline is POSIX-only; on Windows a user
|
|
39
|
+
* pasting that command into cmd or PowerShell gets nothing useful. The
|
|
40
|
+
* Windows path swaps in `netstat -ano | findstr` + `taskkill /F /PID` which
|
|
41
|
+
* is the documented Microsoft workflow for the same problem.
|
|
42
|
+
*
|
|
43
|
+
* Exported for unit testing.
|
|
44
|
+
*/
|
|
45
|
+
export declare const formatPortInUseMessage: (port: number, platform?: NodeJS.Platform) => string;
|
|
14
46
|
export declare const runHiveCommand: (argv: string[], options?: RunHiveCommandOptions) => Promise<RunHiveCommandResult>;
|
|
15
47
|
export type { RunHiveCommandResult };
|
package/dist/src/cli/hive.js
CHANGED
|
@@ -10,6 +10,28 @@ import { readPackageVersion } from '../server/package-version.js';
|
|
|
10
10
|
import { createRuntimeStore } from '../server/runtime-store.js';
|
|
11
11
|
import { createVersionService } from '../server/version-service.js';
|
|
12
12
|
import { runHiveUpdateCommand } from './hive-update.js';
|
|
13
|
+
/**
|
|
14
|
+
* Signals that should drive a graceful shutdown. The interesting ones:
|
|
15
|
+
*
|
|
16
|
+
* SIGINT — Ctrl+C in the runtime terminal (all platforms).
|
|
17
|
+
* SIGTERM — `kill <pid>` on POSIX. Never delivered on Windows.
|
|
18
|
+
* SIGHUP — POSIX: parent shell exits. On Windows libuv synthesises
|
|
19
|
+
* SIGHUP from `CTRL_CLOSE_EVENT`, i.e. the user clicking
|
|
20
|
+
* the X on the runtime's cmd / Terminal window. Without
|
|
21
|
+
* this listener that close path skips the graceful path
|
|
22
|
+
* entirely on Windows.
|
|
23
|
+
* SIGBREAK — Windows: Ctrl+Break. Less common than Ctrl+C but kit
|
|
24
|
+
* scripts and CI hosts still send it.
|
|
25
|
+
*
|
|
26
|
+
* Stale agent_runs from a non-graceful exit are reconciled at next
|
|
27
|
+
* startup via `agentRunStore.markUnfinishedRunsStale()`
|
|
28
|
+
* (runtime-store-helpers.ts), so a dropped signal does not leave the
|
|
29
|
+
* database in an inconsistent state — only the PTY children miss the
|
|
30
|
+
* forwarded SIGTERM. On Windows there's no graceful equivalent for
|
|
31
|
+
* those children anyway (pty.kill is TerminateProcess), so this
|
|
32
|
+
* registration is mostly about giving SQLite a chance to checkpoint.
|
|
33
|
+
*/
|
|
34
|
+
export const SHUTDOWN_SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGBREAK'];
|
|
13
35
|
export const HIVE_USAGE = [
|
|
14
36
|
'Usage:',
|
|
15
37
|
' hive [--port <port>]',
|
|
@@ -66,19 +88,39 @@ const maybePrintUpdateHint = async (versionService) => {
|
|
|
66
88
|
console.log(`Hive update available: ${info.current_version} -> ${info.latest_version}. Run: ${info.install_hint}`);
|
|
67
89
|
};
|
|
68
90
|
const isListenError = (error) => error instanceof Error && typeof error.code === 'string';
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Recovery hint formatter for the "port already in use" error. Platform-aware
|
|
93
|
+
* because the lsof / xargs / kill pipeline is POSIX-only; on Windows a user
|
|
94
|
+
* pasting that command into cmd or PowerShell gets nothing useful. The
|
|
95
|
+
* Windows path swaps in `netstat -ano | findstr` + `taskkill /F /PID` which
|
|
96
|
+
* is the documented Microsoft workflow for the same problem.
|
|
97
|
+
*
|
|
98
|
+
* Exported for unit testing.
|
|
99
|
+
*/
|
|
100
|
+
export const formatPortInUseMessage = (port, platform = process.platform) => {
|
|
101
|
+
const stopHint = platform === 'win32'
|
|
102
|
+
? [
|
|
103
|
+
' - Stop the process using that port:',
|
|
104
|
+
` netstat -ano | findstr ":${port}"`,
|
|
105
|
+
' taskkill /PID <pid> /F',
|
|
106
|
+
]
|
|
107
|
+
: [
|
|
108
|
+
' - Stop the process using that port:',
|
|
109
|
+
` lsof -tiTCP:${port} -sTCP:LISTEN | xargs kill`,
|
|
110
|
+
];
|
|
111
|
+
return [
|
|
112
|
+
`Hive could not start because port ${port} is already in use.`,
|
|
113
|
+
'',
|
|
114
|
+
'Another Hive instance may already be running:',
|
|
115
|
+
` http://127.0.0.1:${port}`,
|
|
116
|
+
'',
|
|
117
|
+
'Options:',
|
|
118
|
+
' - Open the existing Hive window.',
|
|
119
|
+
...stopHint,
|
|
120
|
+
' - Start Hive on another port:',
|
|
121
|
+
` hive --port ${port + 1}`,
|
|
122
|
+
].join('\n');
|
|
123
|
+
};
|
|
82
124
|
const formatListenError = (error, requestedPort) => {
|
|
83
125
|
if (isListenError(error) && error.code === 'EADDRINUSE') {
|
|
84
126
|
return new Error(formatPortInUseMessage(error.port ?? requestedPort));
|
|
@@ -119,8 +161,20 @@ export const runHiveCommand = async (argv, options = {}) => {
|
|
|
119
161
|
return closePromise;
|
|
120
162
|
}
|
|
121
163
|
closePromise = (async () => {
|
|
122
|
-
|
|
123
|
-
|
|
164
|
+
for (const signal of SHUTDOWN_SIGNALS) {
|
|
165
|
+
process.off(signal, gracefulShutdown);
|
|
166
|
+
}
|
|
167
|
+
// Tear down the WebSocket layer FIRST. `app.server.close()` waits
|
|
168
|
+
// on every existing socket, including upgraded WebSocket clients
|
|
169
|
+
// that never go idle on their own; `server.closeAllConnections()`
|
|
170
|
+
// alone does NOT terminate already-upgraded WS — only sockets
|
|
171
|
+
// still in the HTTP request/response state machine. The Windows
|
|
172
|
+
// symptom of skipping this step is Ctrl+C in the runtime cmd
|
|
173
|
+
// window hanging the process as long as any browser tab is
|
|
174
|
+
// connected to /ws/terminal/<runId> or /ws/tasks/<workspaceId>.
|
|
175
|
+
app.closeWebSockets();
|
|
176
|
+
// Then force-close any remaining plain-HTTP keep-alive sockets.
|
|
177
|
+
app.server.closeAllConnections();
|
|
124
178
|
await new Promise((resolve, reject) => {
|
|
125
179
|
app.server.close((error) => {
|
|
126
180
|
if (error) {
|
|
@@ -144,8 +198,9 @@ export const runHiveCommand = async (argv, options = {}) => {
|
|
|
144
198
|
process.exit(1);
|
|
145
199
|
});
|
|
146
200
|
};
|
|
147
|
-
|
|
148
|
-
|
|
201
|
+
for (const signal of SHUTDOWN_SIGNALS) {
|
|
202
|
+
process.once(signal, gracefulShutdown);
|
|
203
|
+
}
|
|
149
204
|
console.log(`Hive running at http://127.0.0.1:${address.port}`);
|
|
150
205
|
void maybePrintUpdateHint(versionService).catch(() => { });
|
|
151
206
|
return {
|
package/dist/src/cli/team.js
CHANGED
|
@@ -17,10 +17,13 @@ const TEAM_USAGE = [
|
|
|
17
17
|
' team status --stdin [--artifact <path>]',
|
|
18
18
|
'',
|
|
19
19
|
'Flags can appear in any order. Use --stdin to pipe long bodies and avoid shell-escaping issues.',
|
|
20
|
-
|
|
21
|
-
" team report --stdin --dispatch <id> <<'EOF'",
|
|
22
|
-
'
|
|
23
|
-
'
|
|
20
|
+
'The body comes from stdin — use whatever your shell supports:',
|
|
21
|
+
" POSIX: team report --stdin --dispatch <id> <<'EOF'",
|
|
22
|
+
' ... long report ...',
|
|
23
|
+
' EOF',
|
|
24
|
+
' Windows cmd: type body.txt | team report --stdin --dispatch <id>',
|
|
25
|
+
' PowerShell: Get-Content body.txt | team report --stdin --dispatch <id>',
|
|
26
|
+
' Portable: team report --stdin --dispatch <id> < body.txt',
|
|
24
27
|
'',
|
|
25
28
|
'For role rules, workflow, and recovery instructions, see .hive/PROTOCOL.md',
|
|
26
29
|
].join('\n');
|
|
@@ -226,7 +229,16 @@ export const runTeamCommand = async (argv) => {
|
|
|
226
229
|
to: workerName,
|
|
227
230
|
text: task,
|
|
228
231
|
});
|
|
229
|
-
|
|
232
|
+
const payload = (await response.json());
|
|
233
|
+
/* When the dispatch happened to also auto-wake a stopped worker
|
|
234
|
+
(PTY had no active run), make the silent restart visible. Stderr
|
|
235
|
+
is the right channel because the JSON on stdout is the
|
|
236
|
+
machine-readable payload; the human-readable narration goes
|
|
237
|
+
beside it so it doesn't corrupt parsers. */
|
|
238
|
+
if (payload.restarted_worker === true) {
|
|
239
|
+
console.error(`Hive woke up worker "${workerName}" before dispatching.`);
|
|
240
|
+
}
|
|
241
|
+
console.log(JSON.stringify(payload));
|
|
230
242
|
return;
|
|
231
243
|
}
|
|
232
244
|
if (command === 'cancel') {
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
interface ResolvedSpawnCommand {
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* `args` is a `string[]` for plain executables (node-pty's serializer is
|
|
4
|
+
* fine for them) and a verbatim `string` for Windows `.cmd`/`.bat` shim
|
|
5
|
+
* launches. The verbatim form bypasses node-pty's `argsToCommandLine`,
|
|
6
|
+
* because that function backslash-escapes any embedded `"` and cmd.exe
|
|
7
|
+
* does NOT recognize `\"` as an escape — it treats `\` as literal, which
|
|
8
|
+
* leaves cmd looking up a program name containing literal quote chars.
|
|
9
|
+
* See `node-pty/src/windowsPtyAgent.ts` `argsToCommandLine` for the rule.
|
|
10
|
+
*/
|
|
11
|
+
args: string | string[];
|
|
3
12
|
command: string;
|
|
4
13
|
}
|
|
5
14
|
export declare const resolveCommandPath: (command: string, cwd: string, env: NodeJS.ProcessEnv, platform?: NodeJS.Platform) => string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { accessSync, constants } from 'node:fs';
|
|
2
|
-
import { delimiter, extname, isAbsolute, join } from 'node:path';
|
|
2
|
+
import { basename, delimiter, extname, isAbsolute, join } from 'node:path';
|
|
3
3
|
const hasPathSeparator = (command) => command.includes('/') || command.includes('\\');
|
|
4
4
|
const canExecute = (path, platform = process.platform) => {
|
|
5
5
|
try {
|
|
@@ -54,16 +54,60 @@ const isWindowsBatchFile = (command) => {
|
|
|
54
54
|
const extension = extname(command).toLowerCase();
|
|
55
55
|
return extension === '.cmd' || extension === '.bat';
|
|
56
56
|
};
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
/**
|
|
58
|
+
* cmd.exe-style escape: doubles inner `"` (cmd's only literal-quote idiom)
|
|
59
|
+
* and only wraps in `"..."` when the token contains whitespace or a cmd
|
|
60
|
+
* metachar. Plain alnum/path tokens stay unquoted, which is the form
|
|
61
|
+
* cmd.exe parses most predictably.
|
|
62
|
+
*
|
|
63
|
+
* Crucially, we do NOT use the `\"` form that the previous implementation
|
|
64
|
+
* used and that node-pty's `argsToCommandLine` produces: cmd doesn't honor
|
|
65
|
+
* backslash-quote escapes in its own command-line parsing.
|
|
66
|
+
*/
|
|
67
|
+
const escapeCmdToken = (value) => {
|
|
68
|
+
if (value.length === 0)
|
|
69
|
+
return '""';
|
|
70
|
+
const doubled = value.replace(/"/g, '""');
|
|
71
|
+
return /[\s"&<>|^()]/.test(value) ? `"${doubled}"` : doubled;
|
|
72
|
+
};
|
|
73
|
+
const buildWindowsBatchCommandLine = (command, args) => {
|
|
74
|
+
const tokens = [command, ...args].map(escapeCmdToken).join(' ');
|
|
75
|
+
// `call` is cmd's built-in batch invocation; it handles quoted .cmd / .bat
|
|
76
|
+
// paths reliably (this is the same pattern Node.js's child_process uses
|
|
77
|
+
// internally on Windows since the CVE-2024-27980 fix).
|
|
78
|
+
return `/d /s /c call ${tokens}`;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Recognize the exact shape that `createStartupCommandLaunch` produces on
|
|
82
|
+
* Windows: `cmd.exe` with args `['/d', '/s', '/c', '<raw user command>']`.
|
|
83
|
+
* Pinned to length 4 so this branch only fires for that single contract;
|
|
84
|
+
* any other cmd.exe invocation (e.g. someone explicitly composing custom
|
|
85
|
+
* shell args via the launch config) keeps the default node-pty path.
|
|
86
|
+
*
|
|
87
|
+
* We need this repackaging because the user's raw command often contains `"`
|
|
88
|
+
* (Windows users habitually wrap paths) and node-pty's `argsToCommandLine`
|
|
89
|
+
* backslash-escapes those — cmd.exe then sees `\"...\"` and looks up a
|
|
90
|
+
* program whose name starts with `\`.
|
|
91
|
+
*/
|
|
92
|
+
const isCmdExeShellLaunch = (resolvedCommand, args) => basename(resolvedCommand).toLowerCase() === 'cmd.exe' &&
|
|
93
|
+
args.length === 4 &&
|
|
94
|
+
args[0] === '/d' &&
|
|
95
|
+
args[1] === '/s' &&
|
|
96
|
+
args[2] === '/c';
|
|
59
97
|
export const resolveSpawnCommand = (command, cwd, env, args = [], platform = process.platform) => {
|
|
60
98
|
const resolvedCommand = resolveCommandPath(command, cwd, env, platform);
|
|
61
99
|
if (platform === 'win32' && isWindowsBatchFile(resolvedCommand)) {
|
|
62
100
|
return {
|
|
63
|
-
args:
|
|
101
|
+
args: buildWindowsBatchCommandLine(resolvedCommand, args),
|
|
64
102
|
command: getEnvValue(env, 'ComSpec', platform) ?? 'cmd.exe',
|
|
65
103
|
};
|
|
66
104
|
}
|
|
105
|
+
if (platform === 'win32' && isCmdExeShellLaunch(resolvedCommand, args)) {
|
|
106
|
+
return {
|
|
107
|
+
args: args.join(' '),
|
|
108
|
+
command: resolvedCommand,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
67
111
|
return { args, command: resolvedCommand };
|
|
68
112
|
};
|
|
69
113
|
export const assertCommandIsExecutable = (command, cwd, env) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createStartupCommandLaunch, getStartupCommandExecutable, } from './startup-command-parser.js';
|
|
1
|
+
import { createStartupCommandLaunch, getStartupCommandExecutable, normalizeExecutableToken, } from './startup-command-parser.js';
|
|
2
2
|
export const resolveCommandPresetLaunchConfig = (settings, commandPresetId) => {
|
|
3
3
|
const preset = settings.getCommandPreset(commandPresetId);
|
|
4
4
|
if (!preset)
|
|
@@ -12,8 +12,14 @@ export const resolveCommandPresetLaunchConfig = (settings, commandPresetId) => {
|
|
|
12
12
|
const findPresetForStartupCommand = (settings, startupCommand, commandPresetId) => {
|
|
13
13
|
if (commandPresetId)
|
|
14
14
|
return settings.getCommandPreset(commandPresetId);
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
// Reduce the raw token (which may be a bare command, an absolute path,
|
|
16
|
+
// or a Windows path with spaces and a .cmd suffix) to the canonical
|
|
17
|
+
// brand id before looking up the preset. Without this normalization
|
|
18
|
+
// step `getCommandPreset` only matched bare command names — Windows
|
|
19
|
+
// users typing the full nvm4w path lost CLI brand identification,
|
|
20
|
+
// session capture, and post-start input strategy in one swoop.
|
|
21
|
+
const brandId = normalizeExecutableToken(getStartupCommandExecutable(startupCommand));
|
|
22
|
+
return brandId ? settings.getCommandPreset(brandId) : undefined;
|
|
17
23
|
};
|
|
18
24
|
export const resolveStartupCommandLaunchConfig = (settings, startupCommand, commandPresetId = null) => {
|
|
19
25
|
const trimmedStartupCommand = startupCommand.trim();
|
|
@@ -2,6 +2,34 @@ import type { IPty } from 'node-pty';
|
|
|
2
2
|
import type { AgentRunRecord, AgentRunSnapshot } from './agent-manager.js';
|
|
3
3
|
import type { PtyOutputBus } from './pty-output-bus.js';
|
|
4
4
|
export declare const MAX_RUN_OUTPUT_LENGTH = 1000000;
|
|
5
|
+
type ExecRunner = (cmd: string, args: readonly string[]) => void;
|
|
6
|
+
/**
|
|
7
|
+
* Windows analogue of POSIX `process.kill(-pgid, SIGKILL)`. node-pty on
|
|
8
|
+
* Windows hands `pty.kill()` to TerminateProcess against the PTY's main
|
|
9
|
+
* process only — children that the worker spawned (npm install, build
|
|
10
|
+
* scripts, custom tooling) become orphans and keep writing to the
|
|
11
|
+
* filesystem after the worker card flips to stopped.
|
|
12
|
+
*
|
|
13
|
+
* `taskkill /pid <pid> /t /f` walks the process tree (`/t`) and forces
|
|
14
|
+
* termination (`/f`), matching what task manager would do.
|
|
15
|
+
*
|
|
16
|
+
* IMPORTANT — call this BEFORE any other termination of the parent
|
|
17
|
+
* process. taskkill /T builds the tree by querying the parent for its
|
|
18
|
+
* descendants; if the parent is already gone (e.g. pty.kill() ran
|
|
19
|
+
* first) the enumeration returns empty and the children become
|
|
20
|
+
* orphans. /F also terminates the parent itself, so a parent-kill
|
|
21
|
+
* after this call is only useful as a fallback when taskkill itself
|
|
22
|
+
* failed (taskkill missing from PATH, restricted PowerShell, etc.).
|
|
23
|
+
*
|
|
24
|
+
* Best-effort: non-zero exits (process already gone, taskkill missing
|
|
25
|
+
* from PATH, access denied) are swallowed and surface as a `false`
|
|
26
|
+
* return. The caller is expected to fall back to pty.kill().
|
|
27
|
+
*
|
|
28
|
+
* Exported for unit testing — the `runner` parameter lets tests assert
|
|
29
|
+
* the exact argv without mocking node:child_process.
|
|
30
|
+
*/
|
|
31
|
+
export declare const taskkillProcessTree: (pid: number, platform?: NodeJS.Platform, runner?: ExecRunner) => boolean;
|
|
5
32
|
export declare const toAgentRunSnapshot: (run: AgentRunRecord) => AgentRunSnapshot;
|
|
6
33
|
export declare const finishAgentRun: (run: AgentRunRecord, exitCode: number | null, ptyOutputBus: PtyOutputBus) => void;
|
|
7
34
|
export declare const attachAgentPty: (run: AgentRunRecord, pty: IPty, ptyOutputBus: PtyOutputBus) => void;
|
|
35
|
+
export {};
|
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
export const MAX_RUN_OUTPUT_LENGTH = 1_000_000;
|
|
3
3
|
const FORCE_KILL_DELAY_MS = 750;
|
|
4
|
+
const defaultExecRunner = (cmd, args) => {
|
|
5
|
+
execFileSync(cmd, [...args], { stdio: 'ignore', windowsHide: true });
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Windows analogue of POSIX `process.kill(-pgid, SIGKILL)`. node-pty on
|
|
9
|
+
* Windows hands `pty.kill()` to TerminateProcess against the PTY's main
|
|
10
|
+
* process only — children that the worker spawned (npm install, build
|
|
11
|
+
* scripts, custom tooling) become orphans and keep writing to the
|
|
12
|
+
* filesystem after the worker card flips to stopped.
|
|
13
|
+
*
|
|
14
|
+
* `taskkill /pid <pid> /t /f` walks the process tree (`/t`) and forces
|
|
15
|
+
* termination (`/f`), matching what task manager would do.
|
|
16
|
+
*
|
|
17
|
+
* IMPORTANT — call this BEFORE any other termination of the parent
|
|
18
|
+
* process. taskkill /T builds the tree by querying the parent for its
|
|
19
|
+
* descendants; if the parent is already gone (e.g. pty.kill() ran
|
|
20
|
+
* first) the enumeration returns empty and the children become
|
|
21
|
+
* orphans. /F also terminates the parent itself, so a parent-kill
|
|
22
|
+
* after this call is only useful as a fallback when taskkill itself
|
|
23
|
+
* failed (taskkill missing from PATH, restricted PowerShell, etc.).
|
|
24
|
+
*
|
|
25
|
+
* Best-effort: non-zero exits (process already gone, taskkill missing
|
|
26
|
+
* from PATH, access denied) are swallowed and surface as a `false`
|
|
27
|
+
* return. The caller is expected to fall back to pty.kill().
|
|
28
|
+
*
|
|
29
|
+
* Exported for unit testing — the `runner` parameter lets tests assert
|
|
30
|
+
* the exact argv without mocking node:child_process.
|
|
31
|
+
*/
|
|
32
|
+
export const taskkillProcessTree = (pid, platform = process.platform, runner = defaultExecRunner) => {
|
|
33
|
+
if (platform !== 'win32' || pid <= 0)
|
|
34
|
+
return false;
|
|
35
|
+
try {
|
|
36
|
+
runner('taskkill', ['/pid', String(pid), '/t', '/f']);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
4
43
|
export const toAgentRunSnapshot = (run) => ({
|
|
5
44
|
runId: run.runId,
|
|
6
45
|
agentId: run.agentId,
|
|
@@ -62,8 +101,18 @@ export const attachAgentPty = (run, pty, ptyOutputBus) => {
|
|
|
62
101
|
};
|
|
63
102
|
const killPty = (signal) => {
|
|
64
103
|
try {
|
|
65
|
-
if (process.platform === 'win32')
|
|
66
|
-
|
|
104
|
+
if (process.platform === 'win32') {
|
|
105
|
+
// taskkill /pid <pid> /t /f walks the parent's process tree
|
|
106
|
+
// BEFORE terminating it — so we have to run it while the parent
|
|
107
|
+
// is still alive. Calling pty.kill() first (the previous
|
|
108
|
+
// ordering) detaches the children: taskkill /T then fails with
|
|
109
|
+
// "process not found" and the npm-installs / build scripts
|
|
110
|
+
// become orphans. taskkill /f also terminates the parent, so
|
|
111
|
+
// pty.kill() is the fallback for the rare case where taskkill
|
|
112
|
+
// is missing from PATH or refused (e.g. restricted PowerShell).
|
|
113
|
+
if (!taskkillProcessTree(pty.pid))
|
|
114
|
+
pty.kill();
|
|
115
|
+
}
|
|
67
116
|
else
|
|
68
117
|
pty.kill(signal);
|
|
69
118
|
}
|
|
@@ -88,8 +137,13 @@ export const attachAgentPty = (run, pty, ptyOutputBus) => {
|
|
|
88
137
|
forceKillTimer = setTimeout(() => {
|
|
89
138
|
forceKillTimer = undefined;
|
|
90
139
|
try {
|
|
91
|
-
if (process.platform === 'win32')
|
|
92
|
-
|
|
140
|
+
if (process.platform === 'win32') {
|
|
141
|
+
// Same ordering as killPty(): tree-kill before terminating the
|
|
142
|
+
// parent, so taskkill /T can still enumerate the process tree.
|
|
143
|
+
// pty.kill() is the fallback for taskkill-missing hosts.
|
|
144
|
+
if (!taskkillProcessTree(pty.pid))
|
|
145
|
+
pty.kill();
|
|
146
|
+
}
|
|
93
147
|
else
|
|
94
148
|
pty.kill('SIGKILL');
|
|
95
149
|
}
|
|
@@ -3,6 +3,23 @@ import type { AgentLaunchConfigInput } from './agent-run-store.js';
|
|
|
3
3
|
import type { AgentSessionStorePort } from './agent-runtime-ports.js';
|
|
4
4
|
import type { CommandPresetRecord } from './command-preset-store.js';
|
|
5
5
|
import { type SessionCaptureSnapshot } from './session-capture.js';
|
|
6
|
+
/**
|
|
7
|
+
* Builds a `{ <PATH-key>: <new-value> }` object for the spawn env override.
|
|
8
|
+
* Critical on Windows: the OS env block reports PATH under its native casing
|
|
9
|
+
* (typically `Path`). Writing to a literal `PATH` key would, after spread
|
|
10
|
+
* with `process.env`, leave two entries — `Path` carrying the original value
|
|
11
|
+
* and `PATH` carrying our prepend. CreateProcess then sees both and the
|
|
12
|
+
* effective lookup order is undefined; in practice the child PTY often falls
|
|
13
|
+
* back to the original `Path` and never sees `HIVE_BIN_DIR`, breaking every
|
|
14
|
+
* `team` shim resolution.
|
|
15
|
+
*
|
|
16
|
+
* We detect the existing key (case-insensitive on Windows) and overwrite IT,
|
|
17
|
+
* so the merge produces exactly one PATH entry.
|
|
18
|
+
*
|
|
19
|
+
* Exported for unit testing — `buildAgentRunBootstrap` is the only in-tree
|
|
20
|
+
* caller.
|
|
21
|
+
*/
|
|
22
|
+
export declare const buildSpawnPathEnvEntry: (parentEnv: NodeJS.ProcessEnv, hiveBinDir: string, platform: NodeJS.Platform) => NodeJS.ProcessEnv;
|
|
6
23
|
export declare const buildAgentRunBootstrap: (workspace: WorkspaceSummary, agentId: string, config: AgentLaunchConfigInput, sessionStore: AgentSessionStorePort, getCommandPreset: (id: string) => CommandPresetRecord | undefined, agent?: AgentSummary) => {
|
|
7
24
|
sessionCaptureSnapshot: {
|
|
8
25
|
discriminator?: {
|
|
@@ -50,7 +67,6 @@ export declare const buildAgentRunBootstrap: (workspace: WorkspaceSummary, agent
|
|
|
50
67
|
HIVE_PROJECT_ID: string;
|
|
51
68
|
HIVE_AGENT_ID: string;
|
|
52
69
|
HIVE_AGENT_TOKEN: string;
|
|
53
|
-
PATH: string;
|
|
54
70
|
};
|
|
55
71
|
};
|
|
56
72
|
export declare const startAgentRunCapture: ({ agentId, sessionCaptureSnapshot, sessionStore, startConfig, workspace, }: {
|