@phnx-labs/agents-cli 1.20.27 → 1.20.28
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 +3 -0
- package/dist/commands/doctor.js +57 -4
- package/dist/commands/exec.d.ts +1 -1
- package/dist/commands/exec.js +177 -6
- package/dist/commands/hosts.d.ts +11 -0
- package/dist/commands/hosts.js +229 -0
- package/dist/commands/repo.d.ts +29 -0
- package/dist/commands/repo.js +174 -38
- package/dist/commands/secrets.d.ts +2 -7
- package/dist/commands/secrets.js +15 -23
- package/dist/commands/sessions.d.ts +2 -0
- package/dist/commands/sessions.js +7 -24
- package/dist/commands/sync.d.ts +2 -0
- package/dist/commands/sync.js +22 -5
- package/dist/commands/view.js +27 -11
- package/dist/index.js +2 -1
- package/dist/lib/agents.d.ts +1 -0
- package/dist/lib/agents.js +44 -4
- package/dist/lib/browser/drivers/ssh.d.ts +47 -2
- package/dist/lib/browser/drivers/ssh.js +113 -24
- package/dist/lib/browser/profiles.js +28 -1
- package/dist/lib/browser/runtime-state.js +28 -8
- package/dist/lib/browser/types.d.ts +10 -1
- package/dist/lib/cli-resources.js +10 -1
- package/dist/lib/doctor-diff.d.ts +12 -0
- package/dist/lib/doctor-diff.js +89 -2
- package/dist/lib/exec.d.ts +27 -0
- package/dist/lib/exec.js +62 -19
- package/dist/lib/hooks.d.ts +17 -0
- package/dist/lib/hooks.js +127 -3
- package/dist/lib/hosts/dispatch.d.ts +26 -0
- package/dist/lib/hosts/dispatch.js +71 -0
- package/dist/lib/hosts/progress.d.ts +21 -0
- package/dist/lib/hosts/progress.js +49 -0
- package/dist/lib/hosts/providers/local.d.ts +17 -0
- package/dist/lib/hosts/providers/local.js +81 -0
- package/dist/lib/hosts/ready.d.ts +37 -0
- package/dist/lib/hosts/ready.js +88 -0
- package/dist/lib/hosts/registry.d.ts +22 -0
- package/dist/lib/hosts/registry.js +65 -0
- package/dist/lib/hosts/ssh-config.d.ts +37 -0
- package/dist/lib/hosts/ssh-config.js +157 -0
- package/dist/lib/hosts/tasks.d.ts +32 -0
- package/dist/lib/hosts/tasks.js +58 -0
- package/dist/lib/hosts/types.d.ts +51 -0
- package/dist/lib/hosts/types.js +21 -0
- package/dist/lib/loop.d.ts +9 -0
- package/dist/lib/loop.js +13 -1
- package/dist/lib/mcp.js +12 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.js +9 -5
- package/dist/lib/platform/exec.d.ts +10 -0
- package/dist/lib/platform/exec.js +17 -0
- package/dist/lib/platform/index.d.ts +1 -0
- package/dist/lib/platform/index.js +1 -0
- package/dist/lib/platform/links.d.ts +15 -0
- package/dist/lib/platform/links.js +42 -0
- package/dist/lib/platform/paths.d.ts +18 -0
- package/dist/lib/platform/paths.js +22 -0
- package/dist/lib/platform/posixpath.d.ts +28 -0
- package/dist/lib/platform/posixpath.js +153 -0
- package/dist/lib/plugins.d.ts +10 -0
- package/dist/lib/plugins.js +1 -1
- package/dist/lib/project-launch.js +6 -3
- package/dist/lib/sandbox.js +5 -2
- package/dist/lib/self-update.js +7 -2
- package/dist/lib/session/db.d.ts +23 -0
- package/dist/lib/session/db.js +76 -1
- package/dist/lib/session/discover.d.ts +26 -0
- package/dist/lib/session/discover.js +75 -4
- package/dist/lib/session/relative-time.d.ts +7 -0
- package/dist/lib/session/relative-time.js +28 -0
- package/dist/lib/session/remote.d.ts +31 -3
- package/dist/lib/session/remote.js +121 -14
- package/dist/lib/ssh-exec.d.ts +45 -0
- package/dist/lib/ssh-exec.js +61 -0
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/types.d.ts +21 -0
- package/dist/lib/versions.d.ts +6 -2
- package/dist/lib/versions.js +8 -4
- package/package.json +1 -1
- package/scripts/postinstall.js +62 -0
package/dist/lib/migrate.js
CHANGED
|
@@ -9,6 +9,7 @@ import * as path from 'path';
|
|
|
9
9
|
import * as os from 'os';
|
|
10
10
|
import * as yaml from 'yaml';
|
|
11
11
|
import { AGENTS, agentConfigDirName, findInPath } from './agents.js';
|
|
12
|
+
import { createLink } from './platform/index.js';
|
|
12
13
|
const HOME = process.env.HOME ?? os.homedir();
|
|
13
14
|
const USER_DIR = path.join(HOME, '.agents');
|
|
14
15
|
/** Canonical system-repo location (post-fold). */
|
|
@@ -57,7 +58,7 @@ export function foldLegacySystemRepo() {
|
|
|
57
58
|
try {
|
|
58
59
|
fs.renameSync(LEGACY_SYSTEM_DIR, SYSTEM_DIR);
|
|
59
60
|
try {
|
|
60
|
-
|
|
61
|
+
createLink(SYSTEM_DIR, LEGACY_SYSTEM_DIR);
|
|
61
62
|
}
|
|
62
63
|
catch { /* best-effort */ }
|
|
63
64
|
console.error('Folded ~/.agents-system/ into ~/.agents/.system/ (left back-compat symlink)');
|
|
@@ -71,7 +72,7 @@ export function foldLegacySystemRepo() {
|
|
|
71
72
|
copyDirSkipExisting(LEGACY_SYSTEM_DIR, SYSTEM_DIR);
|
|
72
73
|
fs.rmSync(LEGACY_SYSTEM_DIR, { recursive: true, force: true });
|
|
73
74
|
try {
|
|
74
|
-
|
|
75
|
+
createLink(SYSTEM_DIR, LEGACY_SYSTEM_DIR);
|
|
75
76
|
}
|
|
76
77
|
catch { /* best-effort */ }
|
|
77
78
|
console.error('Merged ~/.agents-system/ into ~/.agents/.system/ (left back-compat symlink)');
|
|
@@ -698,14 +699,14 @@ function repairAgentConfigSymlinks() {
|
|
|
698
699
|
continue; // already correct
|
|
699
700
|
try {
|
|
700
701
|
fs.unlinkSync(symlinkPath);
|
|
701
|
-
|
|
702
|
+
createLink(userTarget, symlinkPath);
|
|
702
703
|
repaired++;
|
|
703
704
|
}
|
|
704
705
|
catch { /* best-effort */ }
|
|
705
706
|
}
|
|
706
707
|
else if (!stat) {
|
|
707
708
|
try {
|
|
708
|
-
|
|
709
|
+
createLink(userTarget, symlinkPath);
|
|
709
710
|
repaired++;
|
|
710
711
|
}
|
|
711
712
|
catch { /* best-effort */ }
|
|
@@ -798,8 +799,11 @@ export function repairSelfReferentialBinShims(versionsRoot = path.join(HISTORY_D
|
|
|
798
799
|
const realBinary = findInPath(cli);
|
|
799
800
|
try {
|
|
800
801
|
fs.unlinkSync(binLink);
|
|
802
|
+
// createLink: a real symlink where the OS allows it (POSIX, and Windows
|
|
803
|
+
// with the symlink privilege), copy-fallback otherwise — so the repair
|
|
804
|
+
// lands a working binary on Windows runners without the symlink right.
|
|
801
805
|
if (realBinary)
|
|
802
|
-
|
|
806
|
+
createLink(realBinary, binLink);
|
|
803
807
|
repaired++;
|
|
804
808
|
}
|
|
805
809
|
catch { /* best-effort */ }
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
/** PATH-search command for the platform: `where` on Windows, else `which`. */
|
|
2
2
|
export declare function whichCommand(platform?: NodeJS.Platform): string;
|
|
3
|
+
/**
|
|
4
|
+
* Does spawning `binary` require `shell: true` on this platform?
|
|
5
|
+
*
|
|
6
|
+
* On Windows a `.cmd`/`.bat` wrapper (npm.cmd, bun.cmd, the agent shims) cannot
|
|
7
|
+
* be exec'd directly — `spawn`/`execFile` look for a literal executable and miss
|
|
8
|
+
* the PATHEXT/cmd-interpreter step, surfacing as `ENOENT`/`EINVAL`. A bare
|
|
9
|
+
* command name (not an absolute path) needs the same PATHEXT resolution. Both
|
|
10
|
+
* cases require the shell. Always false off Windows, where direct exec is right.
|
|
11
|
+
*/
|
|
12
|
+
export declare function needsWindowsShell(binary: string, platform?: NodeJS.Platform): boolean;
|
|
3
13
|
/**
|
|
4
14
|
* Resolve an executable name to its absolute path via the OS PATH search, or
|
|
5
15
|
* `null` if not found. On Windows `where` can return several lines (one per
|
|
@@ -2,10 +2,27 @@
|
|
|
2
2
|
* Executable resolution, platform-aware.
|
|
3
3
|
*/
|
|
4
4
|
import { execFileSync } from 'child_process';
|
|
5
|
+
import * as path from 'path';
|
|
5
6
|
/** PATH-search command for the platform: `where` on Windows, else `which`. */
|
|
6
7
|
export function whichCommand(platform = process.platform) {
|
|
7
8
|
return platform === 'win32' ? 'where' : 'which';
|
|
8
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Does spawning `binary` require `shell: true` on this platform?
|
|
12
|
+
*
|
|
13
|
+
* On Windows a `.cmd`/`.bat` wrapper (npm.cmd, bun.cmd, the agent shims) cannot
|
|
14
|
+
* be exec'd directly — `spawn`/`execFile` look for a literal executable and miss
|
|
15
|
+
* the PATHEXT/cmd-interpreter step, surfacing as `ENOENT`/`EINVAL`. A bare
|
|
16
|
+
* command name (not an absolute path) needs the same PATHEXT resolution. Both
|
|
17
|
+
* cases require the shell. Always false off Windows, where direct exec is right.
|
|
18
|
+
*/
|
|
19
|
+
export function needsWindowsShell(binary, platform = process.platform) {
|
|
20
|
+
if (platform !== 'win32')
|
|
21
|
+
return false;
|
|
22
|
+
// path.win32.isAbsolute, not path.isAbsolute: the latter uses the HOST's rules,
|
|
23
|
+
// so a Windows path would read as relative when this runs on a Linux CI host.
|
|
24
|
+
return !path.win32.isAbsolute(binary) || /\.(cmd|bat)$/i.test(binary);
|
|
25
|
+
}
|
|
9
26
|
/**
|
|
10
27
|
* Resolve an executable name to its absolute path via the OS PATH search, or
|
|
11
28
|
* `null` if not found. On Windows `where` can return several lines (one per
|
|
@@ -17,6 +17,7 @@ export declare const IS_MACOS: boolean;
|
|
|
17
17
|
export declare const IS_LINUX: boolean;
|
|
18
18
|
export * from './paths.js';
|
|
19
19
|
export * from './exec.js';
|
|
20
|
+
export * from './links.js';
|
|
20
21
|
export * from './process.js';
|
|
21
22
|
export * from './ipc.js';
|
|
22
23
|
export * from './winpath.js';
|
|
@@ -17,6 +17,7 @@ export const IS_MACOS = process.platform === 'darwin';
|
|
|
17
17
|
export const IS_LINUX = process.platform === 'linux';
|
|
18
18
|
export * from './paths.js';
|
|
19
19
|
export * from './exec.js';
|
|
20
|
+
export * from './links.js';
|
|
20
21
|
export * from './process.js';
|
|
21
22
|
export * from './ipc.js';
|
|
22
23
|
export * from './winpath.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a link at `dst` pointing to `src`, portable across platforms.
|
|
3
|
+
*
|
|
4
|
+
* - **Directory** target: junction on Windows (no Developer Mode needed), plain
|
|
5
|
+
* symlink on POSIX.
|
|
6
|
+
* - **File** target: symlink, falling back to `copyFileSync` when Windows
|
|
7
|
+
* refuses the symlink (`EPERM`/`ENOSYS` — no Developer Mode). The copy is a
|
|
8
|
+
* point-in-time snapshot, not a live link; acceptable for the immutable
|
|
9
|
+
* targets we link (binaries, config files).
|
|
10
|
+
*
|
|
11
|
+
* `dst` must not already exist. Callers that replace atomically should link to a
|
|
12
|
+
* temp name and `rename` over the destination, exactly as before — the copy
|
|
13
|
+
* fallback is non-atomic, so the temp+rename stays the caller's responsibility.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createLink(src: string, dst: string): void;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem linking, platform-aware.
|
|
3
|
+
*
|
|
4
|
+
* POSIX symlinks have no portable equivalent on Windows: directory symlinks and
|
|
5
|
+
* file symlinks both require either Administrator or Developer Mode, while
|
|
6
|
+
* *junctions* (directories only) need no elevation. `createLink` picks the form
|
|
7
|
+
* that works without privilege — junction for directories — and falls back to a
|
|
8
|
+
* copy for file links when the OS refuses the symlink.
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
/**
|
|
12
|
+
* Create a link at `dst` pointing to `src`, portable across platforms.
|
|
13
|
+
*
|
|
14
|
+
* - **Directory** target: junction on Windows (no Developer Mode needed), plain
|
|
15
|
+
* symlink on POSIX.
|
|
16
|
+
* - **File** target: symlink, falling back to `copyFileSync` when Windows
|
|
17
|
+
* refuses the symlink (`EPERM`/`ENOSYS` — no Developer Mode). The copy is a
|
|
18
|
+
* point-in-time snapshot, not a live link; acceptable for the immutable
|
|
19
|
+
* targets we link (binaries, config files).
|
|
20
|
+
*
|
|
21
|
+
* `dst` must not already exist. Callers that replace atomically should link to a
|
|
22
|
+
* temp name and `rename` over the destination, exactly as before — the copy
|
|
23
|
+
* fallback is non-atomic, so the temp+rename stays the caller's responsibility.
|
|
24
|
+
*/
|
|
25
|
+
export function createLink(src, dst) {
|
|
26
|
+
const win = process.platform === 'win32';
|
|
27
|
+
const isDir = fs.statSync(src).isDirectory();
|
|
28
|
+
// Type is ignored on POSIX; on Windows, junction (dir) avoids the elevation a
|
|
29
|
+
// dir symlink would need, and 'file' is the explicit file-symlink form.
|
|
30
|
+
const type = win ? (isDir ? 'junction' : 'file') : undefined;
|
|
31
|
+
try {
|
|
32
|
+
fs.symlinkSync(src, dst, type);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
const code = err.code;
|
|
36
|
+
if (win && !isDir && (code === 'EPERM' || code === 'ENOSYS')) {
|
|
37
|
+
fs.copyFileSync(src, dst);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -27,3 +27,21 @@ export declare function homeDir(): string;
|
|
|
27
27
|
* whether to apply it (typically gated on win32).
|
|
28
28
|
*/
|
|
29
29
|
export declare function isWindowsAbsolutePath(p: string): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Fold backslashes to forward slashes. Use when a path is going into a string
|
|
32
|
+
* that must read the same on every OS — a doc-comparable display path, a regex
|
|
33
|
+
* subject, a forward-slash-keyed lookup. Pure string transform; on POSIX input
|
|
34
|
+
* (no backslashes) it returns the value unchanged.
|
|
35
|
+
*/
|
|
36
|
+
export declare function toPosix(p: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Derive a filesystem-safe key/slug from an absolute path. Drops the Windows
|
|
39
|
+
* drive colon (`:` is illegal in NTFS filenames / is the ADS separator) and
|
|
40
|
+
* folds path separators and spaces to `_`. For a POSIX path this produces the
|
|
41
|
+
* exact historical slug (`/a/b c` -> `_a_b_c`), so existing on-disk keys are
|
|
42
|
+
* unchanged; on Windows `C:\a\b` -> `C_a_b` instead of an unusable name.
|
|
43
|
+
*
|
|
44
|
+
* Shell mirror (keep byte-identical in any bash shim that recomputes this key):
|
|
45
|
+
* printf '%s' "$P" | tr -d ':' | tr '\\/ ' '_'
|
|
46
|
+
*/
|
|
47
|
+
export declare function toPortableKey(p: string): string;
|
|
@@ -56,3 +56,25 @@ export function homeDir() {
|
|
|
56
56
|
export function isWindowsAbsolutePath(p) {
|
|
57
57
|
return WIN_DRIVE_RE.test(p) || p.startsWith('\\\\');
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Fold backslashes to forward slashes. Use when a path is going into a string
|
|
61
|
+
* that must read the same on every OS — a doc-comparable display path, a regex
|
|
62
|
+
* subject, a forward-slash-keyed lookup. Pure string transform; on POSIX input
|
|
63
|
+
* (no backslashes) it returns the value unchanged.
|
|
64
|
+
*/
|
|
65
|
+
export function toPosix(p) {
|
|
66
|
+
return p.replace(/\\/g, '/');
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Derive a filesystem-safe key/slug from an absolute path. Drops the Windows
|
|
70
|
+
* drive colon (`:` is illegal in NTFS filenames / is the ADS separator) and
|
|
71
|
+
* folds path separators and spaces to `_`. For a POSIX path this produces the
|
|
72
|
+
* exact historical slug (`/a/b c` -> `_a_b_c`), so existing on-disk keys are
|
|
73
|
+
* unchanged; on Windows `C:\a\b` -> `C_a_b` instead of an unusable name.
|
|
74
|
+
*
|
|
75
|
+
* Shell mirror (keep byte-identical in any bash shim that recomputes this key):
|
|
76
|
+
* printf '%s' "$P" | tr -d ':' | tr '\\/ ' '_'
|
|
77
|
+
*/
|
|
78
|
+
export function toPortableKey(p) {
|
|
79
|
+
return p.replace(/^([a-zA-Z]):/, '$1').replace(/[\\/ ]/g, '_');
|
|
80
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** The XDG user-bin dir, `~/.local/bin`. */
|
|
2
|
+
export declare function localBinDir(home?: string): string;
|
|
3
|
+
export interface SymlinkResult {
|
|
4
|
+
/** A usable symlink to `target` exists at the path after this call. */
|
|
5
|
+
ok: boolean;
|
|
6
|
+
/** True only when this call created the symlink (false = already correct, or left untouched). */
|
|
7
|
+
created: boolean;
|
|
8
|
+
/** Why an existing entry was left untouched (set only when ok is false). */
|
|
9
|
+
skippedReason?: string;
|
|
10
|
+
path: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Ensure `<dir>/<name>` is a symlink to `target`, NEVER clobbering a
|
|
14
|
+
* pre-existing real file or a symlink that already points elsewhere — most
|
|
15
|
+
* importantly a developer's `scripts/install.sh` dev build at
|
|
16
|
+
* `~/.local/bin/agents`. Only an absent path, or a symlink already pointing at
|
|
17
|
+
* `target`, is created/treated as ok. Idempotent.
|
|
18
|
+
*/
|
|
19
|
+
export declare function ensureLocalBinSymlink(name: string, target: string, dir?: string): SymlinkResult;
|
|
20
|
+
/**
|
|
21
|
+
* Does a fresh `bash -lc` resolve `cmd` on its PATH? This is the real question
|
|
22
|
+
* for the consumers we heal. Best-effort: any probe failure returns false so
|
|
23
|
+
* the caller heals rather than wrongly assuming success. `command -v` is a
|
|
24
|
+
* POSIX builtin.
|
|
25
|
+
*/
|
|
26
|
+
export declare function loginShellResolves(cmd: string): boolean;
|
|
27
|
+
/** Is `dir` on a fresh `bash -lc` PATH? Best-effort; a probe failure returns false. */
|
|
28
|
+
export declare function dirOnLoginPath(dir: string): boolean;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POSIX login-PATH primitives for making the `agents` command resolvable.
|
|
3
|
+
*
|
|
4
|
+
* The Unix analog of winpath.ts. `agents`/`ag` land on PATH only via npm's
|
|
5
|
+
* bin symlink into the npm global-bin dir; under nvm (and other per-user node
|
|
6
|
+
* prefixes) that dir is absent from a *non-interactive login* shell's PATH, so
|
|
7
|
+
* `bash -lc 'agents …'` fails with command-not-found. That breaks every
|
|
8
|
+
* consumer that drives a login shell on the box: `agents secrets export --host`
|
|
9
|
+
* (which runs `bash -lc 'agents secrets import …'` on the remote) and the
|
|
10
|
+
* routines daemon (`src/lib/daemon.ts`, which falls back to bare `agents`).
|
|
11
|
+
*
|
|
12
|
+
* The fix mirrors the Windows postinstall branch (which registers npm's
|
|
13
|
+
* global-bin dir on the user PATH): symlink the entrypoint into ~/.local/bin —
|
|
14
|
+
* the XDG user-bin dir that mainstream distros auto-add to the login PATH when
|
|
15
|
+
* it exists (Debian/Ubuntu via ~/.profile, Fedora via /etc/profile.d) — and,
|
|
16
|
+
* where it is not yet on PATH, the caller adds it.
|
|
17
|
+
*
|
|
18
|
+
* Leaf module — imports only child_process / fs / os / path so it is cheap to
|
|
19
|
+
* load from the npm lifecycle script without pulling the rest of the CLI.
|
|
20
|
+
*/
|
|
21
|
+
import { spawnSync } from 'child_process';
|
|
22
|
+
import * as fs from 'fs';
|
|
23
|
+
import * as os from 'os';
|
|
24
|
+
import * as path from 'path';
|
|
25
|
+
/** The XDG user-bin dir, `~/.local/bin`. */
|
|
26
|
+
export function localBinDir(home = os.homedir()) {
|
|
27
|
+
return path.join(home, '.local', 'bin');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Ensure `<dir>/<name>` is a symlink to `target`, NEVER clobbering a
|
|
31
|
+
* pre-existing real file or a symlink that already points elsewhere — most
|
|
32
|
+
* importantly a developer's `scripts/install.sh` dev build at
|
|
33
|
+
* `~/.local/bin/agents`. Only an absent path, or a symlink already pointing at
|
|
34
|
+
* `target`, is created/treated as ok. Idempotent.
|
|
35
|
+
*/
|
|
36
|
+
export function ensureLocalBinSymlink(name, target, dir = localBinDir()) {
|
|
37
|
+
const linkPath = path.join(dir, name);
|
|
38
|
+
const want = path.resolve(target);
|
|
39
|
+
let current = null;
|
|
40
|
+
try {
|
|
41
|
+
current = fs.readlinkSync(linkPath);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
const code = err.code;
|
|
45
|
+
if (code === 'EINVAL') {
|
|
46
|
+
// The path exists but is not a symlink — a real file/dir. Never clobber.
|
|
47
|
+
return { ok: false, created: false, skippedReason: 'a non-symlink file already exists here', path: linkPath };
|
|
48
|
+
}
|
|
49
|
+
if (code !== 'ENOENT') {
|
|
50
|
+
return { ok: false, created: false, skippedReason: err.message, path: linkPath };
|
|
51
|
+
}
|
|
52
|
+
// ENOENT — nothing there; fall through to create.
|
|
53
|
+
}
|
|
54
|
+
if (current !== null) {
|
|
55
|
+
const resolved = path.isAbsolute(current) ? current : path.resolve(dir, current);
|
|
56
|
+
if (resolved === want)
|
|
57
|
+
return { ok: true, created: false, path: linkPath };
|
|
58
|
+
// Points somewhere else (e.g. a dev build) — the existing link wins.
|
|
59
|
+
return { ok: false, created: false, skippedReason: `symlink already points to ${current}`, path: linkPath };
|
|
60
|
+
}
|
|
61
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
62
|
+
fs.symlinkSync(target, linkPath);
|
|
63
|
+
return { ok: true, created: true, path: linkPath };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Absolute path to `bash`. We probe bash specifically — NOT the user's $SHELL —
|
|
67
|
+
* because the consumers we are healing run `bash -lc` regardless of login shell:
|
|
68
|
+
* `secrets export --host` builds `bash -lc 'agents secrets import …'` for the
|
|
69
|
+
* remote, and the routines daemon resolves `agents` the same way. On a box whose
|
|
70
|
+
* login shell is zsh, zsh's login PATH may lack ~/.local/bin while bash's
|
|
71
|
+
* includes it (Debian/Ubuntu ~/.profile) — so probing $SHELL would give the
|
|
72
|
+
* wrong answer for what the consumer actually sees. Resolve from the *current*
|
|
73
|
+
* PATH (the install process has one) and fall back to /bin/bash.
|
|
74
|
+
*/
|
|
75
|
+
function bashPath() {
|
|
76
|
+
const found = (process.env.PATH || '')
|
|
77
|
+
.split(path.delimiter)
|
|
78
|
+
.map((d) => (d ? path.join(d, 'bash') : ''))
|
|
79
|
+
.find((p) => {
|
|
80
|
+
if (!p)
|
|
81
|
+
return false;
|
|
82
|
+
try {
|
|
83
|
+
fs.accessSync(p, fs.constants.X_OK);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
return found || '/bin/bash';
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Environment for the probe shell that reproduces a *fresh* `bash -lc` — the
|
|
94
|
+
* PATH that `ssh host 'bash -lc …'` and the routines daemon actually get — and
|
|
95
|
+
* NOT the nvm/npm-augmented PATH of the install process.
|
|
96
|
+
*
|
|
97
|
+
* This is the crux: `npm i -g` (and a dev `bun run`) execute under nvm, so the
|
|
98
|
+
* caller's PATH already contains nvm's bin. A spawned shell inherits that PATH,
|
|
99
|
+
* so it would resolve `agents` and the heal would skip — during the very nvm
|
|
100
|
+
* install it exists to fix. We strip PATH (and nvm's hint vars) so the login
|
|
101
|
+
* profile rebuilds PATH from scratch, exactly as an incoming SSH command shell
|
|
102
|
+
* does. Login bash still sources /etc/profile + ~/.profile (which adds
|
|
103
|
+
* ~/.local/bin) but bails out of ~/.bashrc's nvm block early because it is
|
|
104
|
+
* non-interactive — which is precisely why bare `agents` is missing there.
|
|
105
|
+
*/
|
|
106
|
+
function loginProbeEnv() {
|
|
107
|
+
const env = { ...process.env };
|
|
108
|
+
delete env.PATH;
|
|
109
|
+
delete env.NVM_BIN;
|
|
110
|
+
delete env.NVM_INC;
|
|
111
|
+
for (const k of Object.keys(env))
|
|
112
|
+
if (k.startsWith('npm_'))
|
|
113
|
+
delete env[k];
|
|
114
|
+
return env;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Does a fresh `bash -lc` resolve `cmd` on its PATH? This is the real question
|
|
118
|
+
* for the consumers we heal. Best-effort: any probe failure returns false so
|
|
119
|
+
* the caller heals rather than wrongly assuming success. `command -v` is a
|
|
120
|
+
* POSIX builtin.
|
|
121
|
+
*/
|
|
122
|
+
export function loginShellResolves(cmd) {
|
|
123
|
+
try {
|
|
124
|
+
const res = spawnSync(bashPath(), ['-lc', `command -v ${cmd}`], {
|
|
125
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
126
|
+
encoding: 'utf-8',
|
|
127
|
+
timeout: 5000,
|
|
128
|
+
env: loginProbeEnv(),
|
|
129
|
+
});
|
|
130
|
+
return res.status === 0 && !!res.stdout && res.stdout.trim().length > 0;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Is `dir` on a fresh `bash -lc` PATH? Best-effort; a probe failure returns false. */
|
|
137
|
+
export function dirOnLoginPath(dir) {
|
|
138
|
+
try {
|
|
139
|
+
const res = spawnSync(bashPath(), ['-lc', 'printf %s "$PATH"'], {
|
|
140
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
141
|
+
encoding: 'utf-8',
|
|
142
|
+
timeout: 5000,
|
|
143
|
+
env: loginProbeEnv(),
|
|
144
|
+
});
|
|
145
|
+
if (res.status !== 0 || !res.stdout)
|
|
146
|
+
return false;
|
|
147
|
+
const want = path.resolve(dir);
|
|
148
|
+
return res.stdout.split(':').some((p) => p && path.resolve(p) === want);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
package/dist/lib/plugins.d.ts
CHANGED
|
@@ -123,6 +123,16 @@ export declare function saveUserConfig(pluginName: string, config: Record<string
|
|
|
123
123
|
* Returns names of missing dependencies (warning only — not a hard error).
|
|
124
124
|
*/
|
|
125
125
|
export declare function checkPluginDependencies(manifest: PluginManifest): string[];
|
|
126
|
+
/**
|
|
127
|
+
* Reconstruct a MarketplaceSpec from a marketplace name. The inverse of
|
|
128
|
+
* marketplaceNameFor(): "agents-cli" → user, "agents-project" → project,
|
|
129
|
+
* "agents-system" → system, "agents-<alias>" → extra. The per-version
|
|
130
|
+
* marketplace operations only key off the name (never spec.root), but we
|
|
131
|
+
* resolve the real source root anyway so the spec is honest for any caller that
|
|
132
|
+
* inspects it (e.g. descriptionFor, which would otherwise label the system
|
|
133
|
+
* marketplace as an extra repo named "system").
|
|
134
|
+
*/
|
|
135
|
+
export declare function marketplaceSpecForName(name: string | undefined, cwd?: string): MarketplaceSpec;
|
|
126
136
|
/**
|
|
127
137
|
* Sync a plugin to a specific agent version's home directory.
|
|
128
138
|
*
|
package/dist/lib/plugins.js
CHANGED
|
@@ -397,7 +397,7 @@ export function checkPluginDependencies(manifest) {
|
|
|
397
397
|
* inspects it (e.g. descriptionFor, which would otherwise label the system
|
|
398
398
|
* marketplace as an extra repo named "system").
|
|
399
399
|
*/
|
|
400
|
-
function marketplaceSpecForName(name, cwd = process.cwd()) {
|
|
400
|
+
export function marketplaceSpecForName(name, cwd = process.cwd()) {
|
|
401
401
|
if (!name || name === MARKETPLACE_NAME)
|
|
402
402
|
return { kind: 'user' };
|
|
403
403
|
if (name === SYSTEM_MARKETPLACE_NAME) {
|
|
@@ -47,6 +47,7 @@ import * as path from 'path';
|
|
|
47
47
|
import { supports } from './capabilities.js';
|
|
48
48
|
import { getEnabledExtraRepos, getExtraPluginsDir, getPluginsDir, getProjectAgentsDir, getProjectPluginsDir, getSystemPluginsDir, } from './state.js';
|
|
49
49
|
import { getVersionHomePath } from './versions.js';
|
|
50
|
+
import { toPortableKey } from './platform/index.js';
|
|
50
51
|
import { transformSubagentForClaude } from './subagents.js';
|
|
51
52
|
import { compileRulesForProject } from './rules/compile.js';
|
|
52
53
|
import { discoverPluginsInDir, hasPluginExecSurfaces, inspectPluginCapabilities } from './plugins.js';
|
|
@@ -88,15 +89,17 @@ export function runLaunchSync(opts) {
|
|
|
88
89
|
}
|
|
89
90
|
/**
|
|
90
91
|
* Path of the shim's skip-fast sentinel for this (agent, version, cwd) tuple.
|
|
91
|
-
* Must match the SHIM-SIDE format in src/lib/shims.ts (PROJECT_SLUG derivation)
|
|
92
|
-
*
|
|
92
|
+
* Must match the SHIM-SIDE format in src/lib/shims.ts (PROJECT_SLUG derivation),
|
|
93
|
+
* which is the canonical `toPortableKey` mapping: drop the Windows drive colon
|
|
94
|
+
* and fold `\`, `/`, and ` ` → `_`. On POSIX this is byte-identical to the old
|
|
95
|
+
* `/` and ` ` → `_` slug; on Windows it yields a legal filename (no `C:\`).
|
|
93
96
|
*
|
|
94
97
|
* Cache leak note: this dir accumulates one zero-byte file per
|
|
95
98
|
* (agent, version, project) tuple ever launched. Disk impact is negligible
|
|
96
99
|
* (inodes only). A periodic GC belongs in `agents prune` — follow-up.
|
|
97
100
|
*/
|
|
98
101
|
function launchSentinelPath(agent, version, cwd) {
|
|
99
|
-
const slug = cwd
|
|
102
|
+
const slug = toPortableKey(cwd);
|
|
100
103
|
// Prefer $HOME (respects test overrides + matches bash's $HOME expansion in
|
|
101
104
|
// the shim), fall back to os.homedir() so the lookup never resolves to '/'
|
|
102
105
|
// if HOME is somehow unset.
|
package/dist/lib/sandbox.js
CHANGED
|
@@ -11,6 +11,7 @@ import * as path from 'path';
|
|
|
11
11
|
import * as os from 'os';
|
|
12
12
|
import { setGeminiAutoUpdateDisabled, updateGeminiSettings } from './gemini-settings.js';
|
|
13
13
|
import { getRoutinesDir, getUserAgentsDir } from './state.js';
|
|
14
|
+
import { createLink } from './platform/index.js';
|
|
14
15
|
function resolveRealHome() {
|
|
15
16
|
const home = os.homedir();
|
|
16
17
|
try {
|
|
@@ -126,9 +127,11 @@ export function symlinkAllowedDirs(overlayHome, dirs) {
|
|
|
126
127
|
fs.mkdirSync(parentDir, { recursive: true });
|
|
127
128
|
if (!fs.existsSync(symlinkTarget)) {
|
|
128
129
|
try {
|
|
129
|
-
|
|
130
|
+
// createLink uses a junction for directories on Windows (no elevation),
|
|
131
|
+
// a plain symlink on POSIX — allowed dirs are directories.
|
|
132
|
+
createLink(realPath, symlinkTarget);
|
|
130
133
|
}
|
|
131
|
-
catch { /*
|
|
134
|
+
catch { /* link already exists or refused */ }
|
|
132
135
|
}
|
|
133
136
|
}
|
|
134
137
|
}
|
package/dist/lib/self-update.js
CHANGED
|
@@ -16,6 +16,7 @@ import * as os from 'os';
|
|
|
16
16
|
import * as path from 'path';
|
|
17
17
|
import { spawnSync } from 'child_process';
|
|
18
18
|
import { compareVersions } from './versions.js';
|
|
19
|
+
import { needsWindowsShell } from './platform/index.js';
|
|
19
20
|
export const NPM_PACKAGE_NAME = '@phnx-labs/agents-cli';
|
|
20
21
|
/**
|
|
21
22
|
* The directory bun installs global packages into:
|
|
@@ -145,7 +146,10 @@ export async function installPackageIntoPrefix(spec, prefix) {
|
|
|
145
146
|
const { execFile } = await import('child_process');
|
|
146
147
|
const { promisify } = await import('util');
|
|
147
148
|
const execFileAsync = promisify(execFile);
|
|
148
|
-
|
|
149
|
+
// On Windows `npm` is `npm.cmd`; execFile cannot run it without a shell (ENOENT).
|
|
150
|
+
await execFileAsync('npm', ['install', '-g', '--prefix', prefix, spec, '--ignore-scripts'], {
|
|
151
|
+
shell: needsWindowsShell('npm'),
|
|
152
|
+
});
|
|
149
153
|
}
|
|
150
154
|
/**
|
|
151
155
|
* Install `spec` into bun's global store with `bun add -g`. bun writes to
|
|
@@ -159,7 +163,8 @@ export async function installPackageWithBun(spec) {
|
|
|
159
163
|
const { execFile } = await import('child_process');
|
|
160
164
|
const { promisify } = await import('util');
|
|
161
165
|
const execFileAsync = promisify(execFile);
|
|
162
|
-
|
|
166
|
+
// On Windows `bun` resolves to `bun.exe`/`bun.cmd`; force shell for the .cmd case.
|
|
167
|
+
await execFileAsync('bun', ['add', '-g', spec], { shell: needsWindowsShell('bun') });
|
|
163
168
|
}
|
|
164
169
|
/** Read the version field of the package.json at `packageRoot`, fresh from disk. */
|
|
165
170
|
export function readInstalledVersion(packageRoot) {
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -45,6 +45,10 @@ export interface QueryOptions {
|
|
|
45
45
|
/** Match any session whose cwd equals this or is a descendant of it. */
|
|
46
46
|
cwdPrefix?: string;
|
|
47
47
|
project?: string;
|
|
48
|
+
/** Match the full session id or short id, case-insensitively (exact). */
|
|
49
|
+
idExact?: string;
|
|
50
|
+
/** Match sessions whose id or short id begins with this (case-insensitive prefix). */
|
|
51
|
+
idPrefix?: string;
|
|
48
52
|
sinceMs?: number;
|
|
49
53
|
untilMs?: number;
|
|
50
54
|
limit?: number;
|
|
@@ -118,6 +122,16 @@ export declare function upsertSessionsBatch(entries: Array<{
|
|
|
118
122
|
* Leaves FTS5 content/topic/project untouched — cheap to call every run.
|
|
119
123
|
*/
|
|
120
124
|
export declare function syncLabels(labelMap: Map<string, string | null>): number;
|
|
125
|
+
/**
|
|
126
|
+
* Sync topics (session titles) for a set of sessions, keyed by id. For agents
|
|
127
|
+
* whose human-readable title lives in a side index that updates independently
|
|
128
|
+
* of the transcript (Codex `session_index.jsonl`), the per-file scan can't see
|
|
129
|
+
* a title that lands later. This applies those titles by id, updating both
|
|
130
|
+
* `sessions.topic` and the FTS5 topic column. Only ever sets a non-empty title
|
|
131
|
+
* and only when it differs from the stored value — cheap to call every run.
|
|
132
|
+
* Returns the number of rows updated.
|
|
133
|
+
*/
|
|
134
|
+
export declare function syncTopics(topicMap: Map<string, string>): number;
|
|
121
135
|
/** Query sessions from the database, applying filters and ordering by timestamp descending. */
|
|
122
136
|
export declare function querySessions(options?: QueryOptions): SessionMeta[];
|
|
123
137
|
/** Count sessions matching the given filter options. */
|
|
@@ -160,6 +174,15 @@ export declare function getAllFilePaths(): Set<string>;
|
|
|
160
174
|
export declare function getSessionsByFilePaths(paths: string[]): Map<string, SessionMeta>;
|
|
161
175
|
/** Look up a single session by its unique ID. */
|
|
162
176
|
export declare function getSessionById(id: string): SessionMeta | null;
|
|
177
|
+
/**
|
|
178
|
+
* Resolve a full-or-partial session id against the index, exact-first then
|
|
179
|
+
* prefix — the DB-backed equivalent of resolveSessionById() that runs over the
|
|
180
|
+
* SQLite table instead of a pre-loaded array. Matches both the full id and the
|
|
181
|
+
* short id. An exact hit short-circuits so a complete id never also drags in its
|
|
182
|
+
* prefix siblings. `scope` narrows by agent / version / project (cwd) so an
|
|
183
|
+
* ambiguous prefix disambiguates against the caller's context.
|
|
184
|
+
*/
|
|
185
|
+
export declare function findSessionsById(idQuery: string, scope?: Pick<QueryOptions, 'agent' | 'version' | 'cwd' | 'project'>): SessionMeta[];
|
|
163
186
|
/** A single full-text search result with ranking score. */
|
|
164
187
|
export interface FtsHit {
|
|
165
188
|
sessionId: string;
|