@phnx-labs/agents-cli 1.20.8 → 1.20.10
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 +11 -0
- package/README.md +1 -1
- package/dist/commands/daemon.js +6 -6
- package/dist/commands/import.js +3 -6
- package/dist/commands/inspect.d.ts +2 -0
- package/dist/commands/inspect.js +75 -28
- package/dist/commands/models.js +2 -1
- package/dist/commands/plugins.js +3 -2
- package/dist/commands/refresh-rules.js +4 -4
- package/dist/commands/routines.js +8 -7
- package/dist/commands/sessions.js +17 -2
- package/dist/commands/subagents.js +2 -1
- package/dist/commands/usage.js +11 -3
- package/dist/index.js +69 -47
- package/dist/lib/agents.d.ts +18 -1
- package/dist/lib/agents.js +89 -23
- package/dist/lib/browser/chrome.d.ts +4 -3
- package/dist/lib/browser/chrome.js +87 -12
- package/dist/lib/browser/ipc.js +59 -13
- package/dist/lib/daemon.js +20 -8
- package/dist/lib/fs-walk.d.ts +7 -1
- package/dist/lib/fs-walk.js +45 -11
- package/dist/lib/git.js +5 -2
- package/dist/lib/log-follow.d.ts +7 -0
- package/dist/lib/log-follow.js +65 -0
- package/dist/lib/platform/index.d.ts +1 -0
- package/dist/lib/platform/index.js +1 -0
- package/dist/lib/platform/ipc.d.ts +11 -0
- package/dist/lib/platform/ipc.js +21 -0
- package/dist/lib/platform/paths.d.ts +7 -0
- package/dist/lib/platform/paths.js +9 -0
- package/dist/lib/platform/process.d.ts +9 -1
- package/dist/lib/platform/process.js +27 -0
- package/dist/lib/plugins.js +5 -3
- package/dist/lib/self-update.d.ts +86 -0
- package/dist/lib/self-update.js +178 -0
- package/dist/lib/versions.js +3 -3
- package/package.json +1 -1
- package/scripts/postinstall.js +29 -19
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform `tail -f`.
|
|
3
|
+
*
|
|
4
|
+
* Replaces spawning the POSIX `tail` binary (absent on Windows) with a poll-based
|
|
5
|
+
* follower that behaves identically on every platform and needs no external
|
|
6
|
+
* dependency. Reads bytes appended since the last position every `intervalMs`;
|
|
7
|
+
* resets to 0 if the file shrinks (truncation / log rotation). The active timer
|
|
8
|
+
* keeps the event loop alive, so callers just register a SIGINT handler that
|
|
9
|
+
* calls the returned stop().
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
export function followFile(filePath, onChunk, opts = {}) {
|
|
13
|
+
const intervalMs = opts.intervalMs ?? 500;
|
|
14
|
+
let pos = 0;
|
|
15
|
+
if (opts.fromEnd) {
|
|
16
|
+
try {
|
|
17
|
+
pos = fs.statSync(filePath).size;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
pos = 0;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
try {
|
|
25
|
+
const initial = fs.readFileSync(filePath);
|
|
26
|
+
if (initial.length > 0)
|
|
27
|
+
onChunk(initial.toString('utf-8'));
|
|
28
|
+
pos = initial.length;
|
|
29
|
+
}
|
|
30
|
+
catch { /* file may not exist yet — start at 0 and wait for it to appear */ }
|
|
31
|
+
}
|
|
32
|
+
const poll = () => {
|
|
33
|
+
let size;
|
|
34
|
+
try {
|
|
35
|
+
size = fs.statSync(filePath).size;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return; /* gone / not yet created */
|
|
39
|
+
}
|
|
40
|
+
if (size < pos)
|
|
41
|
+
pos = 0; // truncated or rotated — re-read from the top
|
|
42
|
+
if (size <= pos)
|
|
43
|
+
return;
|
|
44
|
+
let fd;
|
|
45
|
+
try {
|
|
46
|
+
fd = fs.openSync(filePath, 'r');
|
|
47
|
+
const buf = Buffer.alloc(size - pos);
|
|
48
|
+
const bytes = fs.readSync(fd, buf, 0, buf.length, pos);
|
|
49
|
+
pos += bytes;
|
|
50
|
+
if (bytes > 0)
|
|
51
|
+
onChunk(buf.subarray(0, bytes).toString('utf-8'));
|
|
52
|
+
}
|
|
53
|
+
catch { /* transient read error — retry next tick */ }
|
|
54
|
+
finally {
|
|
55
|
+
if (fd !== undefined) {
|
|
56
|
+
try {
|
|
57
|
+
fs.closeSync(fd);
|
|
58
|
+
}
|
|
59
|
+
catch { /* noop */ }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const timer = setInterval(poll, intervalMs);
|
|
64
|
+
return () => clearInterval(timer);
|
|
65
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the address a local daemon listens on / clients connect to.
|
|
3
|
+
*
|
|
4
|
+
* POSIX: the AF_UNIX socket file path itself.
|
|
5
|
+
* Windows: a named pipe (`\\.\pipe\agents-<hash>`). Filesystem socket files
|
|
6
|
+
* aren't supported there, and named pipes are NOT filesystem objects — derive a
|
|
7
|
+
* stable name from a hash of the socket path so client and server agree without
|
|
8
|
+
* touching disk, and never probe the result with fs.existsSync (it always reports
|
|
9
|
+
* false). Both forms are accepted by net.createServer / net.createConnection.
|
|
10
|
+
*/
|
|
11
|
+
export declare function ipcEndpoint(socketPath: string, platform?: NodeJS.Platform): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local-daemon IPC endpoint, platform-aware.
|
|
3
|
+
*/
|
|
4
|
+
import * as crypto from 'crypto';
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the address a local daemon listens on / clients connect to.
|
|
7
|
+
*
|
|
8
|
+
* POSIX: the AF_UNIX socket file path itself.
|
|
9
|
+
* Windows: a named pipe (`\\.\pipe\agents-<hash>`). Filesystem socket files
|
|
10
|
+
* aren't supported there, and named pipes are NOT filesystem objects — derive a
|
|
11
|
+
* stable name from a hash of the socket path so client and server agree without
|
|
12
|
+
* touching disk, and never probe the result with fs.existsSync (it always reports
|
|
13
|
+
* false). Both forms are accepted by net.createServer / net.createConnection.
|
|
14
|
+
*/
|
|
15
|
+
export function ipcEndpoint(socketPath, platform = process.platform) {
|
|
16
|
+
if (platform === 'win32') {
|
|
17
|
+
const hash = crypto.createHash('sha1').update(socketPath).digest('hex').slice(0, 16);
|
|
18
|
+
return `\\\\.\\pipe\\agents-${hash}`;
|
|
19
|
+
}
|
|
20
|
+
return socketPath;
|
|
21
|
+
}
|
|
@@ -20,3 +20,10 @@ export declare function toComparablePath(p: string, platform?: NodeJS.Platform):
|
|
|
20
20
|
* correctly on all three platforms.
|
|
21
21
|
*/
|
|
22
22
|
export declare function homeDir(): string;
|
|
23
|
+
/**
|
|
24
|
+
* Is this a Windows absolute path — a drive-letter root (`C:\`, `C:/`) or a UNC
|
|
25
|
+
* share (`\\server\share`)? Used by local-source parsing to recognize a native
|
|
26
|
+
* Windows path that the POSIX `/`, `./`, `../` prefixes miss. Caller decides
|
|
27
|
+
* whether to apply it (typically gated on win32).
|
|
28
|
+
*/
|
|
29
|
+
export declare function isWindowsAbsolutePath(p: string): boolean;
|
|
@@ -47,3 +47,12 @@ export function toComparablePath(p, platform = process.platform) {
|
|
|
47
47
|
export function homeDir() {
|
|
48
48
|
return os.homedir();
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Is this a Windows absolute path — a drive-letter root (`C:\`, `C:/`) or a UNC
|
|
52
|
+
* share (`\\server\share`)? Used by local-source parsing to recognize a native
|
|
53
|
+
* Windows path that the POSIX `/`, `./`, `../` prefixes miss. Caller decides
|
|
54
|
+
* whether to apply it (typically gated on win32).
|
|
55
|
+
*/
|
|
56
|
+
export function isWindowsAbsolutePath(p) {
|
|
57
|
+
return WIN_DRIVE_RE.test(p) || p.startsWith('\\\\');
|
|
58
|
+
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Forcefully terminate a process AND its descendant tree.
|
|
3
|
+
*
|
|
4
|
+
* Windows: `taskkill /F /T /PID` — the only reliable way to take down the whole
|
|
5
|
+
* tree (a bare TerminateProcess leaves children orphaned, which is exactly the
|
|
6
|
+
* "stop reported success but the tree is still alive" bug). POSIX: SIGKILL to the
|
|
7
|
+
* pid (matching the existing hard-kill behavior; callers that own a process group
|
|
8
|
+
* can pass the negative pid). Best-effort — never throws; an already-exited
|
|
9
|
+
* process counts as success.
|
|
3
10
|
*/
|
|
11
|
+
export declare function killTree(pid: number): void;
|
|
4
12
|
/**
|
|
5
13
|
* Is a process with this PID currently alive?
|
|
6
14
|
*
|
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Process liveness / control, platform-aware.
|
|
3
3
|
*/
|
|
4
|
+
import { execFileSync } from 'child_process';
|
|
5
|
+
/**
|
|
6
|
+
* Forcefully terminate a process AND its descendant tree.
|
|
7
|
+
*
|
|
8
|
+
* Windows: `taskkill /F /T /PID` — the only reliable way to take down the whole
|
|
9
|
+
* tree (a bare TerminateProcess leaves children orphaned, which is exactly the
|
|
10
|
+
* "stop reported success but the tree is still alive" bug). POSIX: SIGKILL to the
|
|
11
|
+
* pid (matching the existing hard-kill behavior; callers that own a process group
|
|
12
|
+
* can pass the negative pid). Best-effort — never throws; an already-exited
|
|
13
|
+
* process counts as success.
|
|
14
|
+
*/
|
|
15
|
+
export function killTree(pid) {
|
|
16
|
+
if (!pid || pid <= 0)
|
|
17
|
+
return;
|
|
18
|
+
if (process.platform === 'win32') {
|
|
19
|
+
try {
|
|
20
|
+
execFileSync('taskkill', ['/F', '/T', '/PID', String(pid)], { stdio: 'ignore' });
|
|
21
|
+
}
|
|
22
|
+
catch { /* already gone, or no such pid */ }
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
try {
|
|
26
|
+
process.kill(pid, 'SIGKILL');
|
|
27
|
+
}
|
|
28
|
+
catch { /* already gone */ }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
4
31
|
/**
|
|
5
32
|
* Is a process with this PID currently alive?
|
|
6
33
|
*
|
package/dist/lib/plugins.js
CHANGED
|
@@ -12,6 +12,7 @@ import * as fs from 'fs';
|
|
|
12
12
|
import * as path from 'path';
|
|
13
13
|
import { execFileSync } from 'child_process';
|
|
14
14
|
import { getPluginsDir, getTrashPluginsDir, getExtraPluginsDir, getProjectPluginsDir } from './state.js';
|
|
15
|
+
import { IS_WINDOWS, isWindowsAbsolutePath, homeDir } from './platform/index.js';
|
|
15
16
|
import { listInstalledVersions, getVersionHomePath } from './versions.js';
|
|
16
17
|
import { AGENTS, agentConfigDirName } from './agents.js';
|
|
17
18
|
import { capableAgents, isCapable } from './capabilities.js';
|
|
@@ -1007,9 +1008,10 @@ export function parseInstallSpec(spec) {
|
|
|
1007
1008
|
export async function installPlugin(spec) {
|
|
1008
1009
|
const { name: specName, source } = parseInstallSpec(spec);
|
|
1009
1010
|
// Resolve local path (handle ~)
|
|
1010
|
-
const isLocalPath = source.startsWith('/') || source.startsWith('./') || source.startsWith('../') || source.startsWith('~')
|
|
1011
|
+
const isLocalPath = source.startsWith('/') || source.startsWith('./') || source.startsWith('../') || source.startsWith('~')
|
|
1012
|
+
|| (IS_WINDOWS && isWindowsAbsolutePath(source));
|
|
1011
1013
|
const resolvedSource = isLocalPath
|
|
1012
|
-
? source.replace(/^~/,
|
|
1014
|
+
? source.replace(/^~/, homeDir())
|
|
1013
1015
|
: source;
|
|
1014
1016
|
const pluginsDir = getPluginsDir();
|
|
1015
1017
|
fs.mkdirSync(pluginsDir, { recursive: true });
|
|
@@ -1085,7 +1087,7 @@ export async function updatePlugin(name) {
|
|
|
1085
1087
|
execFileSync('git', ['-C', plugin.root, 'pull', '--ff-only'], { stdio: 'pipe' });
|
|
1086
1088
|
}
|
|
1087
1089
|
else {
|
|
1088
|
-
const resolvedSource = sourceInfo.source.replace(/^~/,
|
|
1090
|
+
const resolvedSource = sourceInfo.source.replace(/^~/, homeDir());
|
|
1089
1091
|
if (!fs.existsSync(resolvedSource)) {
|
|
1090
1092
|
return { success: false, error: `Source path no longer exists: ${resolvedSource}` };
|
|
1091
1093
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-update install plumbing.
|
|
3
|
+
*
|
|
4
|
+
* The hard requirement: an upgrade must replace the copy that is currently
|
|
5
|
+
* running. A bare `npm install -g` writes into the global prefix of whatever
|
|
6
|
+
* `npm` PATH happens to resolve — on machines with more than one node
|
|
7
|
+
* installation (nvm + Homebrew + vendored runtimes) that prefix can belong to
|
|
8
|
+
* a different node than the one this copy lives under. The install then
|
|
9
|
+
* "succeeds" while the running copy stays stale and re-prompts forever.
|
|
10
|
+
*
|
|
11
|
+
* So every step here is anchored to the running package root on disk, never
|
|
12
|
+
* to PATH resolution.
|
|
13
|
+
*/
|
|
14
|
+
export declare const NPM_PACKAGE_NAME = "@phnx-labs/agents-cli";
|
|
15
|
+
export interface UpdateCheckCache {
|
|
16
|
+
lastCheck: number;
|
|
17
|
+
latestVersion: string;
|
|
18
|
+
dismissed?: string;
|
|
19
|
+
}
|
|
20
|
+
/** Read the cached update-check state from disk. Returns null if the file is missing or corrupt. */
|
|
21
|
+
export declare function readUpdateCache(file: string): UpdateCheckCache | null;
|
|
22
|
+
/**
|
|
23
|
+
* Persist the latest known version and current timestamp. Preserves an
|
|
24
|
+
* existing `dismissed` marker — the background refresh must not erase a
|
|
25
|
+
* user's "Skip this version" choice, or they get re-prompted for the exact
|
|
26
|
+
* version they dismissed.
|
|
27
|
+
*/
|
|
28
|
+
export declare function saveUpdateCheck(file: string, latestVersion: string): void;
|
|
29
|
+
/** Record that the user chose to skip `version`; suppresses prompts until a newer version appears. */
|
|
30
|
+
export declare function dismissUpdateVersion(file: string, version: string): void;
|
|
31
|
+
/** Whether the cached state warrants an upgrade prompt for a copy running `currentVersion`. */
|
|
32
|
+
export declare function shouldPromptUpgrade(cache: UpdateCheckCache | null, currentVersion: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Derive the npm global prefix that owns the install at `packageRoot`.
|
|
35
|
+
*
|
|
36
|
+
* npm's global layout for a scoped package:
|
|
37
|
+
* POSIX: <prefix>/lib/node_modules/@phnx-labs/agents-cli
|
|
38
|
+
* Windows: <prefix>/node_modules/@phnx-labs/agents-cli
|
|
39
|
+
*
|
|
40
|
+
* Throws when `packageRoot` is not inside a node_modules tree (e.g. running
|
|
41
|
+
* from a source checkout) — there is no prefix to install into, and guessing
|
|
42
|
+
* one is exactly the bug this module exists to prevent.
|
|
43
|
+
*/
|
|
44
|
+
export declare function deriveGlobalPrefix(packageRoot: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Install `spec` into an explicit global prefix. `--prefix` pins the
|
|
47
|
+
* destination no matter which npm binary PATH resolves. `--ignore-scripts`
|
|
48
|
+
* skips lifecycle scripts; the caller refreshes alias shims afterwards via
|
|
49
|
+
* refreshAliasShims().
|
|
50
|
+
*/
|
|
51
|
+
export declare function installPackageIntoPrefix(spec: string, prefix: string): Promise<void>;
|
|
52
|
+
/** Read the version field of the package.json at `packageRoot`, fresh from disk. */
|
|
53
|
+
export declare function readInstalledVersion(packageRoot: string): string;
|
|
54
|
+
/**
|
|
55
|
+
* Assert that the install at `packageRoot` now carries `expectedVersion`.
|
|
56
|
+
* npm exiting 0 only proves it wrote *somewhere*; this proves it wrote *here*.
|
|
57
|
+
*/
|
|
58
|
+
export declare function verifyInstalledVersion(packageRoot: string, expectedVersion: string): void;
|
|
59
|
+
/**
|
|
60
|
+
* Re-run the freshly installed copy's postinstall in shims-only mode so the
|
|
61
|
+
* bare-command aliases (secrets, sessions, ...) pick up the new entrypoint
|
|
62
|
+
* and any aliases added in the new version. Best-effort: a failure here
|
|
63
|
+
* leaves the previous shims in place, which still point at the (now
|
|
64
|
+
* upgraded) package root.
|
|
65
|
+
*/
|
|
66
|
+
export declare function refreshAliasShims(packageRoot: string): void;
|
|
67
|
+
export interface AgentsCliInstall {
|
|
68
|
+
/** The PATH entry (`<dir>/agents`) that resolves to this install. */
|
|
69
|
+
binPath: string;
|
|
70
|
+
/** Package root containing package.json and dist/. */
|
|
71
|
+
packageRoot: string;
|
|
72
|
+
version: string;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Scan PATH for `agents` entrypoints and resolve each to the agents-cli
|
|
76
|
+
* package root it executes. More than one distinct root means upgrades,
|
|
77
|
+
* shims, and the command the user types can act on different copies — the
|
|
78
|
+
* divergence behind silently-failing self-updates.
|
|
79
|
+
*
|
|
80
|
+
* npm bin entries are symlinks that resolve to `<packageRoot>/dist/index.js`
|
|
81
|
+
* (the dev install's `~/.local/bin/agents` chains through the dev prefix to
|
|
82
|
+
* the same shape). Anything that doesn't resolve to a dist/index.js inside a
|
|
83
|
+
* package named @phnx-labs/agents-cli is some other tool and is skipped.
|
|
84
|
+
* POSIX-only: Windows npm bins are .cmd wrappers, not symlinks.
|
|
85
|
+
*/
|
|
86
|
+
export declare function findAgentsCliInstalls(pathEnv: string): AgentsCliInstall[];
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-update install plumbing.
|
|
3
|
+
*
|
|
4
|
+
* The hard requirement: an upgrade must replace the copy that is currently
|
|
5
|
+
* running. A bare `npm install -g` writes into the global prefix of whatever
|
|
6
|
+
* `npm` PATH happens to resolve — on machines with more than one node
|
|
7
|
+
* installation (nvm + Homebrew + vendored runtimes) that prefix can belong to
|
|
8
|
+
* a different node than the one this copy lives under. The install then
|
|
9
|
+
* "succeeds" while the running copy stays stale and re-prompts forever.
|
|
10
|
+
*
|
|
11
|
+
* So every step here is anchored to the running package root on disk, never
|
|
12
|
+
* to PATH resolution.
|
|
13
|
+
*/
|
|
14
|
+
import * as fs from 'fs';
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
import { spawnSync } from 'child_process';
|
|
17
|
+
import { compareVersions } from './versions.js';
|
|
18
|
+
export const NPM_PACKAGE_NAME = '@phnx-labs/agents-cli';
|
|
19
|
+
/** Read the cached update-check state from disk. Returns null if the file is missing or corrupt. */
|
|
20
|
+
export function readUpdateCache(file) {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
/* cache file missing or corrupt */
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Persist the latest known version and current timestamp. Preserves an
|
|
31
|
+
* existing `dismissed` marker — the background refresh must not erase a
|
|
32
|
+
* user's "Skip this version" choice, or they get re-prompted for the exact
|
|
33
|
+
* version they dismissed.
|
|
34
|
+
*/
|
|
35
|
+
export function saveUpdateCheck(file, latestVersion) {
|
|
36
|
+
try {
|
|
37
|
+
const dir = path.dirname(file);
|
|
38
|
+
if (!fs.existsSync(dir))
|
|
39
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
40
|
+
const dismissed = readUpdateCache(file)?.dismissed;
|
|
41
|
+
fs.writeFileSync(file, JSON.stringify({ lastCheck: Date.now(), latestVersion, ...(dismissed ? { dismissed } : {}) }));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
/* best-effort cache update */
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Record that the user chose to skip `version`; suppresses prompts until a newer version appears. */
|
|
48
|
+
export function dismissUpdateVersion(file, version) {
|
|
49
|
+
try {
|
|
50
|
+
const dir = path.dirname(file);
|
|
51
|
+
if (!fs.existsSync(dir))
|
|
52
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
53
|
+
const existing = readUpdateCache(file);
|
|
54
|
+
fs.writeFileSync(file, JSON.stringify({
|
|
55
|
+
lastCheck: existing?.lastCheck ?? Date.now(),
|
|
56
|
+
latestVersion: version,
|
|
57
|
+
dismissed: version,
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
/* best-effort */
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Whether the cached state warrants an upgrade prompt for a copy running `currentVersion`. */
|
|
65
|
+
export function shouldPromptUpgrade(cache, currentVersion) {
|
|
66
|
+
if (!cache?.latestVersion)
|
|
67
|
+
return false;
|
|
68
|
+
return (cache.latestVersion !== currentVersion &&
|
|
69
|
+
compareVersions(cache.latestVersion, currentVersion) > 0 &&
|
|
70
|
+
cache.latestVersion !== cache.dismissed);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Derive the npm global prefix that owns the install at `packageRoot`.
|
|
74
|
+
*
|
|
75
|
+
* npm's global layout for a scoped package:
|
|
76
|
+
* POSIX: <prefix>/lib/node_modules/@phnx-labs/agents-cli
|
|
77
|
+
* Windows: <prefix>/node_modules/@phnx-labs/agents-cli
|
|
78
|
+
*
|
|
79
|
+
* Throws when `packageRoot` is not inside a node_modules tree (e.g. running
|
|
80
|
+
* from a source checkout) — there is no prefix to install into, and guessing
|
|
81
|
+
* one is exactly the bug this module exists to prevent.
|
|
82
|
+
*/
|
|
83
|
+
export function deriveGlobalPrefix(packageRoot) {
|
|
84
|
+
const resolved = path.resolve(packageRoot);
|
|
85
|
+
// Two levels up from the package root: the scope dir, then node_modules.
|
|
86
|
+
const nodeModulesDir = path.dirname(path.dirname(resolved));
|
|
87
|
+
if (path.basename(nodeModulesDir) !== 'node_modules') {
|
|
88
|
+
throw new Error(`${resolved} is not an npm-managed install; reinstall with: npm install -g ${NPM_PACKAGE_NAME}`);
|
|
89
|
+
}
|
|
90
|
+
const parent = path.dirname(nodeModulesDir);
|
|
91
|
+
return path.basename(parent) === 'lib' ? path.dirname(parent) : parent;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Install `spec` into an explicit global prefix. `--prefix` pins the
|
|
95
|
+
* destination no matter which npm binary PATH resolves. `--ignore-scripts`
|
|
96
|
+
* skips lifecycle scripts; the caller refreshes alias shims afterwards via
|
|
97
|
+
* refreshAliasShims().
|
|
98
|
+
*/
|
|
99
|
+
export async function installPackageIntoPrefix(spec, prefix) {
|
|
100
|
+
const { execFile } = await import('child_process');
|
|
101
|
+
const { promisify } = await import('util');
|
|
102
|
+
const execFileAsync = promisify(execFile);
|
|
103
|
+
await execFileAsync('npm', ['install', '-g', '--prefix', prefix, spec, '--ignore-scripts']);
|
|
104
|
+
}
|
|
105
|
+
/** Read the version field of the package.json at `packageRoot`, fresh from disk. */
|
|
106
|
+
export function readInstalledVersion(packageRoot) {
|
|
107
|
+
return JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf-8')).version;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Assert that the install at `packageRoot` now carries `expectedVersion`.
|
|
111
|
+
* npm exiting 0 only proves it wrote *somewhere*; this proves it wrote *here*.
|
|
112
|
+
*/
|
|
113
|
+
export function verifyInstalledVersion(packageRoot, expectedVersion) {
|
|
114
|
+
const actual = readInstalledVersion(packageRoot);
|
|
115
|
+
if (actual !== expectedVersion) {
|
|
116
|
+
throw new Error(`npm reported success but ${packageRoot} is still ${actual} (expected ${expectedVersion}). ` +
|
|
117
|
+
`Run manually: npm install -g --prefix ${deriveGlobalPrefix(packageRoot)} ${NPM_PACKAGE_NAME}@${expectedVersion}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Re-run the freshly installed copy's postinstall in shims-only mode so the
|
|
122
|
+
* bare-command aliases (secrets, sessions, ...) pick up the new entrypoint
|
|
123
|
+
* and any aliases added in the new version. Best-effort: a failure here
|
|
124
|
+
* leaves the previous shims in place, which still point at the (now
|
|
125
|
+
* upgraded) package root.
|
|
126
|
+
*/
|
|
127
|
+
export function refreshAliasShims(packageRoot) {
|
|
128
|
+
spawnSync(process.execPath, [path.join(packageRoot, 'scripts', 'postinstall.js')], {
|
|
129
|
+
env: { ...process.env, AGENTS_POSTINSTALL_SHIMS_ONLY: '1' },
|
|
130
|
+
stdio: 'ignore',
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Scan PATH for `agents` entrypoints and resolve each to the agents-cli
|
|
135
|
+
* package root it executes. More than one distinct root means upgrades,
|
|
136
|
+
* shims, and the command the user types can act on different copies — the
|
|
137
|
+
* divergence behind silently-failing self-updates.
|
|
138
|
+
*
|
|
139
|
+
* npm bin entries are symlinks that resolve to `<packageRoot>/dist/index.js`
|
|
140
|
+
* (the dev install's `~/.local/bin/agents` chains through the dev prefix to
|
|
141
|
+
* the same shape). Anything that doesn't resolve to a dist/index.js inside a
|
|
142
|
+
* package named @phnx-labs/agents-cli is some other tool and is skipped.
|
|
143
|
+
* POSIX-only: Windows npm bins are .cmd wrappers, not symlinks.
|
|
144
|
+
*/
|
|
145
|
+
export function findAgentsCliInstalls(pathEnv) {
|
|
146
|
+
if (process.platform === 'win32')
|
|
147
|
+
return [];
|
|
148
|
+
const installs = [];
|
|
149
|
+
const seenRoots = new Set();
|
|
150
|
+
for (const dir of pathEnv.split(path.delimiter).filter(Boolean)) {
|
|
151
|
+
const candidate = path.join(dir, 'agents');
|
|
152
|
+
let real;
|
|
153
|
+
try {
|
|
154
|
+
real = fs.realpathSync(candidate);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
continue; // missing or dangling symlink
|
|
158
|
+
}
|
|
159
|
+
if (path.basename(real) !== 'index.js' || path.basename(path.dirname(real)) !== 'dist') {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const packageRoot = path.dirname(path.dirname(real));
|
|
163
|
+
if (seenRoots.has(packageRoot))
|
|
164
|
+
continue;
|
|
165
|
+
let pkg;
|
|
166
|
+
try {
|
|
167
|
+
pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf-8'));
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (pkg.name !== NPM_PACKAGE_NAME || typeof pkg.version !== 'string')
|
|
173
|
+
continue;
|
|
174
|
+
seenRoots.add(packageRoot);
|
|
175
|
+
installs.push({ binPath: candidate, packageRoot, version: pkg.version });
|
|
176
|
+
}
|
|
177
|
+
return installs;
|
|
178
|
+
}
|
package/dist/lib/versions.js
CHANGED
|
@@ -777,9 +777,9 @@ export function parseAgentSpec(spec) {
|
|
|
777
777
|
if (parts.length > 2) {
|
|
778
778
|
return null;
|
|
779
779
|
}
|
|
780
|
-
const agentName = parts[0].toLowerCase();
|
|
781
780
|
const version = parts[1] || 'latest';
|
|
782
|
-
|
|
781
|
+
const agent = resolveAgentName(parts[0]);
|
|
782
|
+
if (!agent) {
|
|
783
783
|
return null;
|
|
784
784
|
}
|
|
785
785
|
// Reject any version string that could escape an exec context or a
|
|
@@ -788,7 +788,7 @@ export function parseAgentSpec(spec) {
|
|
|
788
788
|
return null;
|
|
789
789
|
}
|
|
790
790
|
return {
|
|
791
|
-
agent
|
|
791
|
+
agent,
|
|
792
792
|
version,
|
|
793
793
|
};
|
|
794
794
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.10",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
package/scripts/postinstall.js
CHANGED
|
@@ -32,6 +32,35 @@ function shellQuote(value) {
|
|
|
32
32
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// Shorthands that delegate to the installed agents-cli entrypoint.
|
|
36
|
+
const ALIASES = ['sessions', 'secrets', 'browser', 'pty', 'teams'];
|
|
37
|
+
|
|
38
|
+
function writeAliasShims() {
|
|
39
|
+
const written = [];
|
|
40
|
+
for (const name of ALIASES) {
|
|
41
|
+
const target = path.join(SHIMS_DIR, name);
|
|
42
|
+
const script = `#!/bin/sh\nAGENTS_BIN=${shellQuote(AGENTS_BIN)}\nif [ -z "$AGENTS_BIN" ] || [ ! -x "$AGENTS_BIN" ]; then\n echo "agents: agents-cli entrypoint missing or not executable: $AGENTS_BIN" >&2\n exit 127\nfi\nexec "$AGENTS_BIN" ${name} "$@"\n`;
|
|
43
|
+
fs.writeFileSync(target, script, { mode: 0o755 });
|
|
44
|
+
// Windows can't run the POSIX shim; drop a `.cmd` companion that invokes the
|
|
45
|
+
// entrypoint via node so the bare shorthand works in a Windows shell.
|
|
46
|
+
if (process.platform === 'win32') {
|
|
47
|
+
fs.writeFileSync(target + '.cmd', `@echo off\r\nnode "${AGENTS_BIN}" ${name} %*\r\n`);
|
|
48
|
+
}
|
|
49
|
+
written.push(name);
|
|
50
|
+
}
|
|
51
|
+
return written;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Self-updater entry: the upgrade installs with --ignore-scripts (skipping
|
|
55
|
+
// this script as an npm lifecycle hook), then re-invokes it with this env var
|
|
56
|
+
// so the alias shims are refreshed from the newly installed copy. Shims only —
|
|
57
|
+
// no prompts, no rc-file edits, no output.
|
|
58
|
+
if (process.env.AGENTS_POSTINSTALL_SHIMS_ONLY === '1') {
|
|
59
|
+
fs.mkdirSync(SHIMS_DIR, { recursive: true });
|
|
60
|
+
writeAliasShims();
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
|
|
35
64
|
// For local installs, create directories and show a message
|
|
36
65
|
const isGlobalInstall = process.env.npm_config_global || process.argv.includes('-g');
|
|
37
66
|
if (!isGlobalInstall) {
|
|
@@ -86,25 +115,6 @@ const exportLine = shellName === 'fish'
|
|
|
86
115
|
? `fish_add_path ${SHIMS_DIR}`
|
|
87
116
|
: `export PATH="${SHIMS_DIR}:$PATH"`;
|
|
88
117
|
|
|
89
|
-
// Shorthands that delegate to the installed agents-cli entrypoint.
|
|
90
|
-
const ALIASES = ['sessions', 'secrets', 'browser', 'pty', 'teams'];
|
|
91
|
-
|
|
92
|
-
function writeAliasShims() {
|
|
93
|
-
const written = [];
|
|
94
|
-
for (const name of ALIASES) {
|
|
95
|
-
const target = path.join(SHIMS_DIR, name);
|
|
96
|
-
const script = `#!/bin/sh\nAGENTS_BIN=${shellQuote(AGENTS_BIN)}\nif [ -z "$AGENTS_BIN" ] || [ ! -x "$AGENTS_BIN" ]; then\n echo "agents: agents-cli entrypoint missing or not executable: $AGENTS_BIN" >&2\n exit 127\nfi\nexec "$AGENTS_BIN" ${name} "$@"\n`;
|
|
97
|
-
fs.writeFileSync(target, script, { mode: 0o755 });
|
|
98
|
-
// Windows can't run the POSIX shim; drop a `.cmd` companion that invokes the
|
|
99
|
-
// entrypoint via node so the bare shorthand works in a Windows shell.
|
|
100
|
-
if (process.platform === 'win32') {
|
|
101
|
-
fs.writeFileSync(target + '.cmd', `@echo off\r\nnode "${AGENTS_BIN}" ${name} %*\r\n`);
|
|
102
|
-
}
|
|
103
|
-
written.push(name);
|
|
104
|
-
}
|
|
105
|
-
return written;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
118
|
function getVersion() {
|
|
109
119
|
const pkgPath = new URL('../package.json', import.meta.url).pathname;
|
|
110
120
|
try {
|