@tt-a1i/hive 1.1.5 → 1.3.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 +101 -0
- package/README.en.md +33 -0
- package/README.md +16 -0
- package/dist/src/cli/hive-update.d.ts +15 -0
- package/dist/src/cli/hive-update.js +81 -0
- package/dist/src/cli/hive.js +21 -5
- package/dist/src/server/agent-run-store.d.ts +1 -1
- package/dist/src/server/app.d.ts +3 -1
- package/dist/src/server/app.js +14 -1
- package/dist/src/server/open-target-commands.d.ts +53 -0
- package/dist/src/server/open-target-commands.js +176 -0
- package/dist/src/server/package-version.d.ts +15 -0
- package/dist/src/server/package-version.js +15 -0
- package/dist/src/server/route-types.d.ts +6 -0
- package/dist/src/server/routes-open-workspace.d.ts +2 -0
- package/dist/src/server/routes-open-workspace.js +47 -0
- package/dist/src/server/routes.js +2 -0
- package/dist/src/server/version-service.js +4 -1
- package/dist/src/server/workspace-shell-runtime.js +34 -8
- package/dist/src/shared/open-targets.d.ts +20 -0
- package/dist/src/shared/open-targets.js +36 -0
- package/package.json +3 -3
- package/web/dist/assets/finder-C4Jmsb0B.png +0 -0
- package/web/dist/assets/ghostty-D-Js4rdm.png +0 -0
- package/web/dist/assets/index-CSEt-Qiy.js +66 -0
- package/web/dist/assets/index-RsXXnrVz.css +1 -0
- package/web/dist/assets/zed-C5BQT8X3.png +0 -0
- package/web/dist/icons/apple-touch-icon-180.png +0 -0
- package/web/dist/icons/icon-192.png +0 -0
- package/web/dist/icons/icon-32.png +0 -0
- package/web/dist/icons/icon-512-maskable.png +0 -0
- package/web/dist/icons/icon-512.png +0 -0
- package/web/dist/index.html +11 -3
- package/web/dist/manifest.webmanifest +60 -0
- package/web/dist/screenshots/wide-overview.png +0 -0
- package/web/dist/sw.js +99 -0
- package/web/dist/assets/index-BHCSrZ_0.js +0 -66
- package/web/dist/assets/index-BUjVAMN8.css +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,107 @@
|
|
|
2
2
|
|
|
3
3
|
All notable user-facing changes will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 1.3.0 - 2026-05-20
|
|
6
|
+
|
|
7
|
+
Installable Hive: turns the web shell into a real PWA so Chrome / Edge can
|
|
8
|
+
launch it from a dock icon, in its own window, without a visible browser
|
|
9
|
+
chrome.
|
|
10
|
+
|
|
11
|
+
- Adds a web app manifest with icons (192, 512, maskable 512, apple-touch 180),
|
|
12
|
+
a wide screenshot, and shortcuts for "Add Workspace" and "Try Demo" so
|
|
13
|
+
right-clicking the dock icon jumps straight to those flows.
|
|
14
|
+
- Installation is driven entirely by the browser's omnibox install icon
|
|
15
|
+
(Chrome / Edge / Brave); Hive deliberately does not add a redundant topbar
|
|
16
|
+
button.
|
|
17
|
+
- Ships a service worker (`/sw.js`) that caches the SPA shell + hashed asset
|
|
18
|
+
chunks + static icons / sounds / cli-icons, but never intercepts `/api/*`,
|
|
19
|
+
`/ws/*`, or non-GET requests — auth cookies and WebSockets keep their
|
|
20
|
+
native paths. Each release writes to its own cache bucket and older buckets
|
|
21
|
+
are kept so tabs still controlled by the previous SW can resolve their
|
|
22
|
+
lazy-imported chunks.
|
|
23
|
+
- Surfaces shell updates as a bottom-right toast (`Web UI updated — Reload to
|
|
24
|
+
activate`) instead of forcing a refresh. The Reload button stays disabled
|
|
25
|
+
while any terminal run is still working so updates never interrupt an
|
|
26
|
+
in-flight agent.
|
|
27
|
+
- Routes service-worker auto-reloads through the same silent reload helper used
|
|
28
|
+
elsewhere in the app, so browser updates do not trip the close-confirmation
|
|
29
|
+
guard.
|
|
30
|
+
- Replaces the workspace area with a dedicated `Hive runtime is not running`
|
|
31
|
+
page when the initial bootstrap fails. The page pings `/api/version` every
|
|
32
|
+
three seconds and reloads automatically once the daemon comes back; a manual
|
|
33
|
+
Retry button is offered alongside.
|
|
34
|
+
- Hardens the server: `/sw.js` is served with `Cache-Control: no-store` and the
|
|
35
|
+
manifest with `Cache-Control: max-age=0, must-revalidate`, so SW updates
|
|
36
|
+
propagate the next time the browser checks instead of waiting on a stale
|
|
37
|
+
HTTP cache.
|
|
38
|
+
- Notes for first-time installers: the SW activates after the first reload
|
|
39
|
+
following install. On separate ports (`hive --port 4011` vs `--port 3000`)
|
|
40
|
+
Chrome treats Hive as two distinct PWAs because the install scope is keyed
|
|
41
|
+
by origin. To fully remove a PWA install, use
|
|
42
|
+
`chrome://apps` → right-click the Hive tile → Remove.
|
|
43
|
+
- Always asks the browser to confirm before closing the tab or PWA window so
|
|
44
|
+
Cmd-W on an installed app never closes silently. Modern browsers gate the
|
|
45
|
+
prompt on prior page interaction — opening the window and immediately
|
|
46
|
+
pressing Cmd-W still closes cleanly by browser policy.
|
|
47
|
+
- Open Workspace dropdown now uses each app's brand color for its icon
|
|
48
|
+
instead of the previous monochrome white treatment, and visually separates
|
|
49
|
+
VS Code from VS Code Insiders so users can tell their installed targets
|
|
50
|
+
apart at a glance.
|
|
51
|
+
- Workspace avatars in the sidebar stay the same size when the user drags
|
|
52
|
+
the sidebar wider. Previously the wide layout used a 22px avatar while the
|
|
53
|
+
collapsed layout used 32px, so expanding the sidebar made the avatars
|
|
54
|
+
smaller; both modes now render at 32px.
|
|
55
|
+
- Drops IntelliJ IDEA, Windsurf, and iTerm2 from the Open Workspace dropdown.
|
|
56
|
+
IntelliJ users typically launch from JetBrains Toolbox rather than a folder
|
|
57
|
+
picker; Windsurf overlaps with the existing Cursor / VS Code entries;
|
|
58
|
+
iTerm2 overlaps with the built-in macOS Terminal entry. macOS now exposes
|
|
59
|
+
seven targets (VS Code, VS Code Insiders, Cursor, Finder, Terminal,
|
|
60
|
+
Ghostty, Zed); Windows / Linux expose five (VS Code, VS Code Insiders,
|
|
61
|
+
Cursor, File Explorer / File Manager, Zed). A stored preference for any
|
|
62
|
+
removed target silently falls back to the platform default at load time.
|
|
63
|
+
- Swaps the Zed, Ghostty, and Finder dropdown icons for the apps' official
|
|
64
|
+
brand marks (Finder uses the macOS app icon, Ghostty 96×96 / Zed 64×64
|
|
65
|
+
raster) so each entry reads as the real application rather than an
|
|
66
|
+
abstract glyph. Ghostty's mark renders inside a generous safe-zone so its
|
|
67
|
+
display size is bumped 20% via CSS scale to balance the row visually.
|
|
68
|
+
- Replaces the Worker detail modal and Workspace shell dialog with a docked,
|
|
69
|
+
resizable, VSCode-style terminal panel inside the right column (under the
|
|
70
|
+
team members pane). Worker tabs and shell tabs share the strip; clicking a
|
|
71
|
+
member card opens that worker as a tab; the panel hides when no tabs are
|
|
72
|
+
open. Closing a worker tab keeps the underlying PTY running — worker
|
|
73
|
+
lifecycle is owned by the card hover cluster. Tab list, active tab, and
|
|
74
|
+
panel height all persist (height globally, tabs + active per-workspace).
|
|
75
|
+
Cmd-W (Ctrl-W on Windows / Linux) closes the active tab; a "+" button in
|
|
76
|
+
the tab strip starts a new shell. Start failures and shell-start failures
|
|
77
|
+
now surface as toasts instead of inline modal/dialog banners.
|
|
78
|
+
- Moves "Save as template" into the role-instructions toolbar in the Add Member
|
|
79
|
+
flow, keeping template actions closer to the prompt editor instead of adding
|
|
80
|
+
another standalone control in the dialog body.
|
|
81
|
+
|
|
82
|
+
## 1.2.0 - 2026-05-18
|
|
83
|
+
|
|
84
|
+
Opens the active workspace in your editor, terminal, or file manager from
|
|
85
|
+
Hive's topbar.
|
|
86
|
+
|
|
87
|
+
- Adds an "Open" split button to the topbar that launches the active workspace
|
|
88
|
+
in a chosen application. Ten targets on macOS (VS Code, VS Code Insiders,
|
|
89
|
+
Cursor, Windsurf, Finder, Terminal, iTerm2, Ghostty, IntelliJ IDEA, Zed) and
|
|
90
|
+
six on Windows / Linux (VS Code, VS Code Insiders, Cursor, Windsurf, File
|
|
91
|
+
Explorer / File Manager, Zed).
|
|
92
|
+
- Persists the preferred target per browser via `localStorage` so the next
|
|
93
|
+
click jumps to the same app. Stale preferences for apps that aren't valid on
|
|
94
|
+
the current platform fall back to the OS file manager instead of erroring.
|
|
95
|
+
- Surfaces failures as localized toast notifications. Distinguishes
|
|
96
|
+
"app not installed", "launcher not on PATH", and other failure modes so a
|
|
97
|
+
missing Cursor install reads differently from a misconfigured `code` CLI.
|
|
98
|
+
- Backend launches each command via `execFile` with an argv array — no shell
|
|
99
|
+
is involved, so workspace paths containing spaces, Unicode, or quotes pass
|
|
100
|
+
through verbatim. Paths containing newlines or NUL bytes are rejected before
|
|
101
|
+
dispatch.
|
|
102
|
+
- Special-cases Windows `explorer.exe`, which returns exit code 1 even on
|
|
103
|
+
success: spawn-errors are still surfaced, but a non-zero exit no longer
|
|
104
|
+
shows a spurious toast.
|
|
105
|
+
|
|
5
106
|
## 1.1.5 - 2026-05-18
|
|
6
107
|
|
|
7
108
|
Custom startup command and close-guard fixes.
|
package/README.en.md
CHANGED
|
@@ -71,6 +71,39 @@ hive
|
|
|
71
71
|
Open the printed local URL, usually `http://127.0.0.1:3000/`. Use
|
|
72
72
|
`hive --port 4010` when you need a specific local port.
|
|
73
73
|
|
|
74
|
+
To upgrade in place:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
hive update
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`hive update` runs `npm install -g @tt-a1i/hive@latest` in place. Restart any
|
|
81
|
+
in-flight Hive process to pick up the new version. If you installed Hive with
|
|
82
|
+
pnpm or yarn, upgrade through the same package manager — otherwise the new
|
|
83
|
+
npm copy will shadow your existing install.
|
|
84
|
+
|
|
85
|
+
Install Hive as an app (optional):
|
|
86
|
+
|
|
87
|
+
Open `http://127.0.0.1:3000/` in Chrome, Edge, or Brave and click the install
|
|
88
|
+
icon at the right edge of the browser's omnibox. The PWA launches in its own
|
|
89
|
+
dock-anchored window without browser chrome and shows **Add Workspace** /
|
|
90
|
+
**Try Demo** shortcuts from the dock right-click menu. Firefox and Safari
|
|
91
|
+
currently don't implement the install-prompt protocol, so the omnibox icon
|
|
92
|
+
only appears in Chromium-based browsers.
|
|
93
|
+
|
|
94
|
+
The Hive daemon must still be running for the PWA to do anything; if the
|
|
95
|
+
runtime isn't reachable when you launch the app, you'll see a "Hive runtime
|
|
96
|
+
is not running" page that auto-reloads once `hive` is back on `127.0.0.1`.
|
|
97
|
+
The PWA install scope is keyed by origin, so `hive --port 4011` installs as
|
|
98
|
+
a separate app from `hive --port 3000`. To uninstall, visit `chrome://apps`,
|
|
99
|
+
right-click the Hive tile, and choose **Remove from Chrome…**.
|
|
100
|
+
|
|
101
|
+
Hive asks the browser to confirm before closing the tab or PWA window so an
|
|
102
|
+
accidental Cmd-W doesn't drop your session. Modern browsers gate that prompt
|
|
103
|
+
on prior page interaction — if you open the PWA and immediately press Cmd-W
|
|
104
|
+
without clicking or typing anywhere first, it still closes cleanly. That's a
|
|
105
|
+
browser policy, not a Hive bug.
|
|
106
|
+
|
|
74
107
|
First-run flow:
|
|
75
108
|
|
|
76
109
|
1. Create a workspace from a project folder.
|
package/README.md
CHANGED
|
@@ -52,6 +52,22 @@ hive
|
|
|
52
52
|
|
|
53
53
|
打开终端打印出来的本机地址,通常是 `http://127.0.0.1:3000/`。如果你想指定端口,可以用 `hive --port 4010`。
|
|
54
54
|
|
|
55
|
+
升级到最新版本:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
hive update
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`hive update` 会在原位运行 `npm install -g @tt-a1i/hive@latest`,完事后重启 Hive 就能用上新版。如果当初是用 pnpm / yarn 装的 Hive,请用同一个包管理器升级,避免装出第二份。
|
|
62
|
+
|
|
63
|
+
把 Hive 装为应用(可选):
|
|
64
|
+
|
|
65
|
+
在 Chrome / Edge / Brave 里打开 `http://127.0.0.1:3000/`,点浏览器地址栏右侧的安装图标即可。装好后 Hive 会以独立窗口启动、有自己的 dock 图标,且 dock 右键菜单上会显示 **添加 Workspace** / **试用演示** 两个快捷入口。Firefox 和 Safari 暂未实现 PWA install-prompt 协议,浏览器地址栏的安装图标只在 Chromium 系浏览器里出现。
|
|
66
|
+
|
|
67
|
+
PWA 只是 UI 壳,Hive 后端仍需要在终端里跑着。如果启动 PWA 时后端没起,会看到 “Hive 后端未启动” 页面,等你跑起 `hive` 后会自动刷新。PWA 的 install scope 按 origin(含端口)划分,所以 `hive --port 4011` 跟 `hive --port 3000` 在浏览器看来是两个独立应用。卸载方法:浏览器地址栏访问 `chrome://apps`,右键 Hive 图标,选 **从 Chrome 中移除…**。
|
|
68
|
+
|
|
69
|
+
关闭 PWA 窗口或 tab 时 Hive 会主动请求浏览器弹原生确认对话框,避免 Cmd+W 误关丢失会话。但现代浏览器要求你跟页面"交互过"(点击 / 滚动 / 输入)才会真的弹这个对话框——刚打开 PWA 立刻按 Cmd+W 仍会直接关闭,这是浏览器策略,不是 Hive 的 bug。
|
|
70
|
+
|
|
55
71
|
首次使用流程:
|
|
56
72
|
|
|
57
73
|
1. 选择一个项目目录作为 workspace。
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const HIVE_UPDATE_USAGE: string;
|
|
2
|
+
export interface RunUpdateResult {
|
|
3
|
+
exitCode: number;
|
|
4
|
+
spawnError?: Error;
|
|
5
|
+
}
|
|
6
|
+
export type RunUpdate = (command: string, args: readonly string[]) => Promise<RunUpdateResult>;
|
|
7
|
+
export declare const defaultRunUpdate: RunUpdate;
|
|
8
|
+
interface RunHiveUpdateOptions {
|
|
9
|
+
/** Inject a fake spawn for tests. */
|
|
10
|
+
runUpdate?: RunUpdate;
|
|
11
|
+
/** Override platform detection for tests. */
|
|
12
|
+
platform?: NodeJS.Platform;
|
|
13
|
+
}
|
|
14
|
+
export declare const runHiveUpdateCommand: (argv: string[], options?: RunHiveUpdateOptions) => Promise<number>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { getNpmCommand, INSTALL_COMMAND_ARGS, INSTALL_COMMAND_DISPLAY, } from '../server/package-version.js';
|
|
3
|
+
export const HIVE_UPDATE_USAGE = [
|
|
4
|
+
'Usage:',
|
|
5
|
+
' hive update',
|
|
6
|
+
'',
|
|
7
|
+
`Runs \`${INSTALL_COMMAND_DISPLAY}\` to upgrade Hive in place.`,
|
|
8
|
+
'Restart any running Hive process afterwards to pick up the new version.',
|
|
9
|
+
'',
|
|
10
|
+
'Note: only npm-installed Hive can be upgraded this way. If you installed',
|
|
11
|
+
'Hive via pnpm or yarn, upgrade through the same package manager instead;',
|
|
12
|
+
'otherwise the npm copy will shadow your existing install.',
|
|
13
|
+
'',
|
|
14
|
+
'Options:',
|
|
15
|
+
' -h, --help Print this help.',
|
|
16
|
+
].join('\n');
|
|
17
|
+
export const defaultRunUpdate = (command, args) => new Promise((resolve) => {
|
|
18
|
+
const child = spawn(command, [...args], { stdio: 'inherit' });
|
|
19
|
+
let resolved = false;
|
|
20
|
+
// Forward Ctrl+C / SIGTERM to the npm child so it can clean up rather
|
|
21
|
+
// than getting orphaned mid-install. The handlers are registered with
|
|
22
|
+
// `once` so they don't accumulate across invocations, and we also
|
|
23
|
+
// explicitly remove them when the child exits in case the user only
|
|
24
|
+
// sent one signal (Node would otherwise keep the handler alive).
|
|
25
|
+
const handleSignal = (signal) => () => {
|
|
26
|
+
child.kill(signal);
|
|
27
|
+
};
|
|
28
|
+
const handleSigint = handleSignal('SIGINT');
|
|
29
|
+
const handleSigterm = handleSignal('SIGTERM');
|
|
30
|
+
process.once('SIGINT', handleSigint);
|
|
31
|
+
process.once('SIGTERM', handleSigterm);
|
|
32
|
+
const finalize = (result) => {
|
|
33
|
+
if (resolved)
|
|
34
|
+
return;
|
|
35
|
+
resolved = true;
|
|
36
|
+
process.off('SIGINT', handleSigint);
|
|
37
|
+
process.off('SIGTERM', handleSigterm);
|
|
38
|
+
resolve(result);
|
|
39
|
+
};
|
|
40
|
+
child.on('error', (error) => {
|
|
41
|
+
finalize({ exitCode: 1, spawnError: error });
|
|
42
|
+
});
|
|
43
|
+
child.on('close', (code) => {
|
|
44
|
+
finalize({ exitCode: typeof code === 'number' ? code : 1 });
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
const printManualFallback = () => {
|
|
48
|
+
console.error(`You can run the upgrade manually: ${INSTALL_COMMAND_DISPLAY}`);
|
|
49
|
+
};
|
|
50
|
+
export const runHiveUpdateCommand = async (argv, options = {}) => {
|
|
51
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
52
|
+
console.log(HIVE_UPDATE_USAGE);
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
// Reject unknown flags rather than silently ignoring them — keeps behavior
|
|
56
|
+
// consistent with how `parsePort` validates `hive` itself.
|
|
57
|
+
const extra = argv.find((arg) => arg !== '--help' && arg !== '-h');
|
|
58
|
+
if (extra !== undefined) {
|
|
59
|
+
console.error(`Unknown argument: ${extra}`);
|
|
60
|
+
console.error(HIVE_UPDATE_USAGE);
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
const run = options.runUpdate ?? defaultRunUpdate;
|
|
64
|
+
const command = getNpmCommand(options.platform);
|
|
65
|
+
console.log(`Running: ${INSTALL_COMMAND_DISPLAY}`);
|
|
66
|
+
const result = await run(command, INSTALL_COMMAND_ARGS);
|
|
67
|
+
if (result.spawnError) {
|
|
68
|
+
console.error(`Failed to spawn npm: ${result.spawnError.message}`);
|
|
69
|
+
printManualFallback();
|
|
70
|
+
return 1;
|
|
71
|
+
}
|
|
72
|
+
if (result.exitCode === 0) {
|
|
73
|
+
console.log('Hive updated. Restart any running Hive process to pick up the new version.');
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
console.error(`npm install exited with code ${result.exitCode}.`);
|
|
77
|
+
// Permission failures (EACCES on root-owned /usr/bin/npm) and other
|
|
78
|
+
// non-spawn errors leave the user with copy-paste recovery either way.
|
|
79
|
+
printManualFallback();
|
|
80
|
+
return result.exitCode;
|
|
81
|
+
};
|
package/dist/src/cli/hive.js
CHANGED
|
@@ -9,14 +9,19 @@ import { createApp } from '../server/app.js';
|
|
|
9
9
|
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
|
+
import { runHiveUpdateCommand } from './hive-update.js';
|
|
12
13
|
export const HIVE_USAGE = [
|
|
13
14
|
'Usage:',
|
|
14
15
|
' hive [--port <port>]',
|
|
16
|
+
' hive update',
|
|
15
17
|
'',
|
|
16
18
|
'Options:',
|
|
17
19
|
' --port <port> Bind the local runtime to a specific port (default: 3000).',
|
|
18
20
|
' -h, --help Print this help.',
|
|
19
21
|
' -v, --version Print the installed Hive version.',
|
|
22
|
+
'',
|
|
23
|
+
'Commands:',
|
|
24
|
+
' update Upgrade Hive in place via `npm install -g`.',
|
|
20
25
|
].join('\n');
|
|
21
26
|
export const handleHiveInfoCommand = (argv) => {
|
|
22
27
|
if (argv.includes('--help') || argv.includes('-h')) {
|
|
@@ -128,10 +133,21 @@ const isMainModule = process.argv[1]
|
|
|
128
133
|
: false;
|
|
129
134
|
if (isMainModule) {
|
|
130
135
|
const argv = process.argv.slice(2);
|
|
131
|
-
if (
|
|
136
|
+
if (argv[0] === 'update') {
|
|
137
|
+
runHiveUpdateCommand(argv.slice(1))
|
|
138
|
+
.then((code) => process.exit(code))
|
|
139
|
+
.catch((error) => {
|
|
140
|
+
console.error(error);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
else if (handleHiveInfoCommand(argv)) {
|
|
132
145
|
process.exit(0);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
runHiveCommand(argv).catch((error) => {
|
|
149
|
+
console.error(error);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
137
153
|
}
|
|
@@ -27,7 +27,7 @@ export declare const createAgentRunStore: (db: Database) => {
|
|
|
27
27
|
runId: string;
|
|
28
28
|
agentId: string;
|
|
29
29
|
pid: number | null;
|
|
30
|
-
status: "
|
|
30
|
+
status: "error" | "starting" | "running" | "exited";
|
|
31
31
|
exitCode: number | null;
|
|
32
32
|
startedAt: number;
|
|
33
33
|
endedAt: number | null;
|
package/dist/src/server/app.d.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { type IncomingMessage, type ServerResponse } from 'node:http';
|
|
2
2
|
import { type PickFolderResponse } from './fs-pick-folder.js';
|
|
3
|
+
import type { OpenWorkspaceService } from './route-types.js';
|
|
3
4
|
import type { RuntimeStore } from './runtime-store.js';
|
|
4
5
|
import { type TasksFileService } from './tasks-file.js';
|
|
5
6
|
import { type VersionService } from './version-service.js';
|
|
6
7
|
interface CreateAppOptions {
|
|
7
8
|
store: RuntimeStore;
|
|
8
9
|
pickFolderService?: () => Promise<PickFolderResponse>;
|
|
10
|
+
openWorkspaceService?: OpenWorkspaceService;
|
|
9
11
|
tasksFileService?: TasksFileService;
|
|
10
12
|
versionService?: VersionService;
|
|
11
13
|
}
|
|
12
|
-
export declare const createApp: ({ store, pickFolderService, tasksFileService, versionService, }: CreateAppOptions) => {
|
|
14
|
+
export declare const createApp: ({ store, pickFolderService, openWorkspaceService, tasksFileService, versionService, }: CreateAppOptions) => {
|
|
13
15
|
server: import("http").Server<typeof IncomingMessage, typeof ServerResponse>;
|
|
14
16
|
store: RuntimeStore;
|
|
15
17
|
};
|
package/dist/src/server/app.js
CHANGED
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
import { pickFolder } from './fs-pick-folder.js';
|
|
7
7
|
import { HttpError } from './http-errors.js';
|
|
8
8
|
import { assertLocalRequest } from './local-request-guard.js';
|
|
9
|
+
import { openWorkspace } from './open-target-commands.js';
|
|
9
10
|
import { matchRoute } from './routes.js';
|
|
10
11
|
import { createTasksFileService } from './tasks-file.js';
|
|
11
12
|
import { createTerminalWebSocketServer } from './terminal-ws-server.js';
|
|
@@ -53,6 +54,14 @@ const CONTENT_TYPES = {
|
|
|
53
54
|
'.webp': 'image/webp',
|
|
54
55
|
'.woff2': 'font/woff2',
|
|
55
56
|
};
|
|
57
|
+
// PWA boot files must bypass HTTP caching: `sw.js` because the browser does its
|
|
58
|
+
// own byte-diff update check, and the manifest because Chrome consults it on
|
|
59
|
+
// every install/uninstall transition. Without these, SW updates can stall on a
|
|
60
|
+
// stale cached copy and the install prompt won't reflect a renamed app.
|
|
61
|
+
const PWA_BOOT_CACHE_CONTROL = {
|
|
62
|
+
'/manifest.webmanifest': 'max-age=0, must-revalidate',
|
|
63
|
+
'/sw.js': 'no-store',
|
|
64
|
+
};
|
|
56
65
|
const sendStatic = async (response, staticDir, pathname, request) => {
|
|
57
66
|
if (request.method !== 'GET' && request.method !== 'HEAD')
|
|
58
67
|
return false;
|
|
@@ -62,6 +71,9 @@ const sendStatic = async (response, staticDir, pathname, request) => {
|
|
|
62
71
|
try {
|
|
63
72
|
const content = await readFile(filePath);
|
|
64
73
|
response.setHeader('content-type', CONTENT_TYPES[extname(filePath)] ?? 'application/octet-stream');
|
|
74
|
+
const cacheControl = PWA_BOOT_CACHE_CONTROL[pathname];
|
|
75
|
+
if (cacheControl !== undefined)
|
|
76
|
+
response.setHeader('cache-control', cacheControl);
|
|
65
77
|
response.statusCode = 200;
|
|
66
78
|
response.end(request.method === 'HEAD' ? undefined : content);
|
|
67
79
|
return true;
|
|
@@ -75,7 +87,7 @@ const sendJson = (response, statusCode, body) => {
|
|
|
75
87
|
response.setHeader('content-type', 'application/json; charset=utf-8');
|
|
76
88
|
response.end(JSON.stringify(body));
|
|
77
89
|
};
|
|
78
|
-
export const createApp = ({ store, pickFolderService = pickFolder, tasksFileService = createTasksFileService(), versionService = createVersionService(), }) => {
|
|
90
|
+
export const createApp = ({ store, pickFolderService = pickFolder, openWorkspaceService = (input) => openWorkspace(input), tasksFileService = createTasksFileService(), versionService = createVersionService(), }) => {
|
|
79
91
|
const staticDir = process.env.HIVE_STATIC_DIR ?? getDefaultStaticDir();
|
|
80
92
|
const staticAvailablePromise = canServeStatic(staticDir);
|
|
81
93
|
const server = createServer(async (request, response) => {
|
|
@@ -91,6 +103,7 @@ export const createApp = ({ store, pickFolderService = pickFolder, tasksFileServ
|
|
|
91
103
|
store,
|
|
92
104
|
tasksFileService,
|
|
93
105
|
pickFolderService,
|
|
106
|
+
openWorkspaceService,
|
|
94
107
|
versionService,
|
|
95
108
|
params: match.params,
|
|
96
109
|
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type ExecFileOptions } from 'node:child_process';
|
|
2
|
+
import { type OpenTargetId, type OpenTargetPlatform, type OpenWorkspaceErrorCode } from '../shared/open-targets.js';
|
|
3
|
+
export type { OpenTargetId, OpenTargetPlatform, OpenWorkspaceErrorCode, } from '../shared/open-targets.js';
|
|
4
|
+
export { getEffectiveOpenTargetId, isOpenTargetId, isOpenTargetSupported, OPEN_TARGET_IDS_BY_PLATFORM, } from '../shared/open-targets.js';
|
|
5
|
+
export declare const resolveOpenTargetPlatform: (platform: NodeJS.Platform) => OpenTargetPlatform;
|
|
6
|
+
export interface OpenAttempt {
|
|
7
|
+
command: string;
|
|
8
|
+
args: string[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Returns the ordered list of commands to try. First success wins; remaining
|
|
12
|
+
* entries are fallbacks (e.g. IntelliJ IDEA → IntelliJ IDEA CE on older Macs).
|
|
13
|
+
* Empty list means the requested target is unsupported on this platform —
|
|
14
|
+
* callers should have already routed through `getEffectiveOpenTargetId` to
|
|
15
|
+
* fall back, so this should never happen in practice.
|
|
16
|
+
*/
|
|
17
|
+
export declare const buildOpenAttempts: (targetId: OpenTargetId, path: string, platform: OpenTargetPlatform) => OpenAttempt[];
|
|
18
|
+
export interface OpenCommandSuccess {
|
|
19
|
+
ok: true;
|
|
20
|
+
effectiveTargetId: OpenTargetId;
|
|
21
|
+
}
|
|
22
|
+
export interface OpenCommandFailure {
|
|
23
|
+
ok: false;
|
|
24
|
+
effectiveTargetId: OpenTargetId;
|
|
25
|
+
errorCode: OpenWorkspaceErrorCode;
|
|
26
|
+
stderr: string;
|
|
27
|
+
}
|
|
28
|
+
export type OpenCommandResult = OpenCommandSuccess | OpenCommandFailure;
|
|
29
|
+
interface SpawnResult {
|
|
30
|
+
stderr: string;
|
|
31
|
+
stdout: string;
|
|
32
|
+
status: number | null;
|
|
33
|
+
signal: string | null;
|
|
34
|
+
spawnError: NodeJS.ErrnoException | null;
|
|
35
|
+
}
|
|
36
|
+
export type RunOpenCommand = (command: string, args: string[], options: ExecFileOptions) => Promise<SpawnResult>;
|
|
37
|
+
export interface OpenWorkspaceInput {
|
|
38
|
+
path: string;
|
|
39
|
+
targetId: OpenTargetId;
|
|
40
|
+
}
|
|
41
|
+
export interface OpenWorkspaceOptions {
|
|
42
|
+
platform?: NodeJS.Platform;
|
|
43
|
+
runCommand?: RunOpenCommand;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Workspace paths originate from the OS folder picker or from manual paste;
|
|
47
|
+
* the picker output is sandbox-validated at create time, but a path stored
|
|
48
|
+
* before path-validation existed (or one pasted into a hypothetical migration
|
|
49
|
+
* future) could contain `\n` / `\0`. Reject those here so we never hand an
|
|
50
|
+
* ambiguous path to `xdg-open`, where shell wrappers split on newline.
|
|
51
|
+
*/
|
|
52
|
+
export declare const isOpenWorkspacePathSafe: (path: string) => boolean;
|
|
53
|
+
export declare const openWorkspace: (input: OpenWorkspaceInput, options?: OpenWorkspaceOptions) => Promise<OpenCommandResult>;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { getDefaultOpenTargetIdForPlatform, getEffectiveOpenTargetId, isOpenTargetId, } from '../shared/open-targets.js';
|
|
3
|
+
export { getEffectiveOpenTargetId, isOpenTargetId, isOpenTargetSupported, OPEN_TARGET_IDS_BY_PLATFORM, } from '../shared/open-targets.js';
|
|
4
|
+
export const resolveOpenTargetPlatform = (platform) => {
|
|
5
|
+
if (platform === 'darwin')
|
|
6
|
+
return 'mac';
|
|
7
|
+
if (platform === 'win32')
|
|
8
|
+
return 'windows';
|
|
9
|
+
if (platform === 'linux')
|
|
10
|
+
return 'linux';
|
|
11
|
+
return 'other';
|
|
12
|
+
};
|
|
13
|
+
const macAttempts = (targetId, path) => {
|
|
14
|
+
switch (targetId) {
|
|
15
|
+
case 'finder':
|
|
16
|
+
return [{ command: 'open', args: [path] }];
|
|
17
|
+
case 'vscode':
|
|
18
|
+
return [{ command: 'open', args: ['-a', 'Visual Studio Code', path] }];
|
|
19
|
+
case 'vscode-insiders':
|
|
20
|
+
return [{ command: 'open', args: ['-a', 'Visual Studio Code - Insiders', path] }];
|
|
21
|
+
case 'cursor':
|
|
22
|
+
return [{ command: 'open', args: ['-a', 'Cursor', path] }];
|
|
23
|
+
case 'terminal':
|
|
24
|
+
return [{ command: 'open', args: ['-a', 'Terminal', path] }];
|
|
25
|
+
case 'ghostty':
|
|
26
|
+
return [{ command: 'open', args: ['-a', 'Ghostty', path] }];
|
|
27
|
+
case 'zed':
|
|
28
|
+
return [{ command: 'open', args: ['-a', 'Zed', path] }];
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const linuxAttempts = (targetId, path) => {
|
|
32
|
+
switch (targetId) {
|
|
33
|
+
case 'finder':
|
|
34
|
+
return [{ command: 'xdg-open', args: [path] }];
|
|
35
|
+
case 'vscode':
|
|
36
|
+
return [{ command: 'code', args: [path] }];
|
|
37
|
+
case 'vscode-insiders':
|
|
38
|
+
return [{ command: 'code-insiders', args: [path] }];
|
|
39
|
+
case 'cursor':
|
|
40
|
+
return [{ command: 'cursor', args: [path] }];
|
|
41
|
+
case 'zed':
|
|
42
|
+
return [{ command: 'zed', args: [path] }];
|
|
43
|
+
default:
|
|
44
|
+
return [{ command: 'xdg-open', args: [path] }];
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const windowsAttempts = (targetId, path) => {
|
|
48
|
+
switch (targetId) {
|
|
49
|
+
case 'finder':
|
|
50
|
+
return [{ command: 'explorer', args: [path] }];
|
|
51
|
+
case 'vscode':
|
|
52
|
+
return [{ command: 'code', args: [path] }];
|
|
53
|
+
case 'vscode-insiders':
|
|
54
|
+
return [{ command: 'code-insiders', args: [path] }];
|
|
55
|
+
case 'cursor':
|
|
56
|
+
return [{ command: 'cursor', args: [path] }];
|
|
57
|
+
case 'zed':
|
|
58
|
+
return [{ command: 'zed', args: [path] }];
|
|
59
|
+
default:
|
|
60
|
+
return [{ command: 'explorer', args: [path] }];
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Returns the ordered list of commands to try. First success wins; remaining
|
|
65
|
+
* entries are fallbacks (e.g. IntelliJ IDEA → IntelliJ IDEA CE on older Macs).
|
|
66
|
+
* Empty list means the requested target is unsupported on this platform —
|
|
67
|
+
* callers should have already routed through `getEffectiveOpenTargetId` to
|
|
68
|
+
* fall back, so this should never happen in practice.
|
|
69
|
+
*/
|
|
70
|
+
export const buildOpenAttempts = (targetId, path, platform) => {
|
|
71
|
+
const effectiveTargetId = getEffectiveOpenTargetId(targetId, platform);
|
|
72
|
+
if (platform === 'mac')
|
|
73
|
+
return macAttempts(effectiveTargetId, path);
|
|
74
|
+
if (platform === 'linux')
|
|
75
|
+
return linuxAttempts(effectiveTargetId, path);
|
|
76
|
+
if (platform === 'windows')
|
|
77
|
+
return windowsAttempts(effectiveTargetId, path);
|
|
78
|
+
return [{ command: 'open', args: [path] }];
|
|
79
|
+
};
|
|
80
|
+
const defaultRunOpenCommand = (command, args, options) => new Promise((resolve) => {
|
|
81
|
+
const child = execFile(command, args, options, (error, stdout, stderr) => {
|
|
82
|
+
const errno = error;
|
|
83
|
+
resolve({
|
|
84
|
+
stderr: String(stderr ?? ''),
|
|
85
|
+
stdout: String(stdout ?? ''),
|
|
86
|
+
status: typeof errno?.code === 'number' ? errno.code : (child.exitCode ?? 0),
|
|
87
|
+
signal: typeof errno?.signal === 'string' ? errno.signal : null,
|
|
88
|
+
spawnError: errno && typeof errno.code === 'string' ? errno : null,
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
const APP_NOT_INSTALLED_PATTERNS = [
|
|
93
|
+
/unable to find application/i,
|
|
94
|
+
/can'?t find/i,
|
|
95
|
+
/not authorized to send keystrokes/i,
|
|
96
|
+
/application can'?t be found/i,
|
|
97
|
+
];
|
|
98
|
+
const classifyFailure = (result) => {
|
|
99
|
+
if (result.spawnError?.code === 'ENOENT')
|
|
100
|
+
return 'command-not-in-path';
|
|
101
|
+
const stderr = result.stderr.toLowerCase();
|
|
102
|
+
if (APP_NOT_INSTALLED_PATTERNS.some((re) => re.test(stderr)))
|
|
103
|
+
return 'app-not-installed';
|
|
104
|
+
return 'unknown';
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Workspace paths originate from the OS folder picker or from manual paste;
|
|
108
|
+
* the picker output is sandbox-validated at create time, but a path stored
|
|
109
|
+
* before path-validation existed (or one pasted into a hypothetical migration
|
|
110
|
+
* future) could contain `\n` / `\0`. Reject those here so we never hand an
|
|
111
|
+
* ambiguous path to `xdg-open`, where shell wrappers split on newline.
|
|
112
|
+
*/
|
|
113
|
+
export const isOpenWorkspacePathSafe = (path) => {
|
|
114
|
+
if (path.length === 0)
|
|
115
|
+
return false;
|
|
116
|
+
for (let i = 0; i < path.length; i++) {
|
|
117
|
+
const code = path.charCodeAt(i);
|
|
118
|
+
if (code === 0 || code === 10 || code === 13)
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
};
|
|
123
|
+
export const openWorkspace = async (input, options = {}) => {
|
|
124
|
+
const platform = resolveOpenTargetPlatform(options.platform ?? process.platform);
|
|
125
|
+
const run = options.runCommand ?? defaultRunOpenCommand;
|
|
126
|
+
if (!isOpenTargetId(input.targetId)) {
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
effectiveTargetId: getDefaultOpenTargetIdForPlatform(platform),
|
|
130
|
+
errorCode: 'invalid-target',
|
|
131
|
+
stderr: `Unknown open target: ${String(input.targetId)}`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (!isOpenWorkspacePathSafe(input.path)) {
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
effectiveTargetId: input.targetId,
|
|
138
|
+
errorCode: 'invalid-path',
|
|
139
|
+
stderr: 'Workspace path contains newline or null byte and was rejected.',
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const effectiveTargetId = getEffectiveOpenTargetId(input.targetId, platform);
|
|
143
|
+
const attempts = buildOpenAttempts(input.targetId, input.path, platform);
|
|
144
|
+
let lastFailure = null;
|
|
145
|
+
for (const attempt of attempts) {
|
|
146
|
+
const result = await run(attempt.command, attempt.args, {});
|
|
147
|
+
// Windows `explorer.exe` returns exit code 1 even on success — checking
|
|
148
|
+
// exit code here would surface a spurious error to the user on every
|
|
149
|
+
// File Explorer open. spawnError still catches the "explorer not on PATH"
|
|
150
|
+
// case, which is the only real failure mode worth surfacing.
|
|
151
|
+
if (attempt.command === 'explorer') {
|
|
152
|
+
if (result.spawnError?.code === 'ENOENT') {
|
|
153
|
+
lastFailure = result;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
return { ok: true, effectiveTargetId };
|
|
157
|
+
}
|
|
158
|
+
if (!result.spawnError && (result.status === 0 || result.status === null)) {
|
|
159
|
+
return { ok: true, effectiveTargetId };
|
|
160
|
+
}
|
|
161
|
+
lastFailure = result;
|
|
162
|
+
}
|
|
163
|
+
const fallback = lastFailure ?? {
|
|
164
|
+
stderr: 'No command attempts were made.',
|
|
165
|
+
stdout: '',
|
|
166
|
+
status: null,
|
|
167
|
+
signal: null,
|
|
168
|
+
spawnError: null,
|
|
169
|
+
};
|
|
170
|
+
return {
|
|
171
|
+
ok: false,
|
|
172
|
+
effectiveTargetId,
|
|
173
|
+
errorCode: classifyFailure(fallback),
|
|
174
|
+
stderr: fallback.stderr.trim() || fallback.stdout.trim() || 'Failed to open workspace.',
|
|
175
|
+
};
|
|
176
|
+
};
|
|
@@ -1,2 +1,17 @@
|
|
|
1
1
|
export declare const PACKAGE_NAME = "@tt-a1i/hive";
|
|
2
|
+
/**
|
|
3
|
+
* Canonical argv for the upgrade command. Sharing one source between the
|
|
4
|
+
* server's install hint (`version-service.ts`) and the CLI upgrade path
|
|
5
|
+
* (`hive-update.ts`) keeps the two from drifting if the package name ever
|
|
6
|
+
* moves.
|
|
7
|
+
*/
|
|
8
|
+
export declare const INSTALL_COMMAND_ARGS: readonly ["install", "-g", "@tt-a1i/hive@latest"];
|
|
9
|
+
export declare const INSTALL_COMMAND_DISPLAY: string;
|
|
10
|
+
/**
|
|
11
|
+
* Windows ships npm as `npm.cmd` (a batch shim); Node's `child_process.spawn`
|
|
12
|
+
* will not resolve `.cmd` without `shell: true` or an explicit suffix, so the
|
|
13
|
+
* default `'npm'` produces ENOENT on Windows. Use this helper any time you
|
|
14
|
+
* spawn npm directly.
|
|
15
|
+
*/
|
|
16
|
+
export declare const getNpmCommand: (platform?: NodeJS.Platform) => string;
|
|
2
17
|
export declare const readPackageVersion: () => string;
|