@phnx-labs/agents-cli 1.20.7 → 1.20.9
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 +6 -0
- package/README.md +1 -1
- package/dist/commands/computer-actions.d.ts +19 -0
- package/dist/commands/computer-actions.js +159 -1
- package/dist/commands/computer.js +2 -2
- package/dist/commands/daemon.js +6 -6
- package/dist/commands/import.js +3 -6
- package/dist/commands/inspect.js +17 -8
- 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/setup.js +2 -2
- package/dist/commands/subagents.js +2 -1
- package/dist/commands/usage.js +11 -3
- package/dist/commands/versions.js +2 -2
- 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/computer-rpc.d.ts +2 -0
- package/dist/lib/computer-rpc.js +21 -1
- 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/refresh.js +2 -2
- package/dist/lib/self-update.d.ts +86 -0
- package/dist/lib/self-update.js +178 -0
- package/dist/lib/shims.d.ts +13 -8
- package/dist/lib/shims.js +46 -11
- package/dist/lib/versions.js +3 -3
- package/package.json +1 -1
- package/scripts/postinstall.js +36 -26
|
@@ -21,6 +21,8 @@ export declare function writeComputerPeers(allowedExecPaths: string[]): void;
|
|
|
21
21
|
export declare function resolveHelperExec(): string | null;
|
|
22
22
|
export declare function resolveHelperApp(): string | null;
|
|
23
23
|
export declare function openComputerClient(): ComputerClient;
|
|
24
|
+
export declare const RPC_TIMEOUT_MS = 30000;
|
|
25
|
+
export declare function resolveRpcTimeoutMs(env: string | undefined): number;
|
|
24
26
|
export declare function describeTransport(): {
|
|
25
27
|
kind: 'socket' | 'stdio' | 'none';
|
|
26
28
|
path: string | null;
|
package/dist/lib/computer-rpc.js
CHANGED
|
@@ -200,6 +200,15 @@ export function openComputerClient() {
|
|
|
200
200
|
}
|
|
201
201
|
return new StdioClient(helperExec);
|
|
202
202
|
}
|
|
203
|
+
// Per-call RPC timeout. Without it a hung daemon (deadlocked connection
|
|
204
|
+
// queue, stopped process) hangs the CLI forever — the waiter map never
|
|
205
|
+
// settles. 30s clears every daemon-side ceiling (wait caps at 30s,
|
|
206
|
+
// launch_app at 10s, screenshot at 5s). Overridable for slower flows.
|
|
207
|
+
export const RPC_TIMEOUT_MS = 30_000;
|
|
208
|
+
export function resolveRpcTimeoutMs(env) {
|
|
209
|
+
const n = Number(env);
|
|
210
|
+
return Number.isFinite(n) && n > 0 ? n : RPC_TIMEOUT_MS;
|
|
211
|
+
}
|
|
203
212
|
// Shared waiter map + line parser. Both transports plug their reader into
|
|
204
213
|
// `handleChunk` and their writer into `send`.
|
|
205
214
|
class BaseClient {
|
|
@@ -241,8 +250,19 @@ class BaseClient {
|
|
|
241
250
|
}
|
|
242
251
|
const id = this.nextId++;
|
|
243
252
|
const payload = JSON.stringify({ id, method, params: params ?? {} }) + '\n';
|
|
253
|
+
const timeoutMs = resolveRpcTimeoutMs(process.env.COMPUTER_HELPER_RPC_TIMEOUT_MS);
|
|
244
254
|
return new Promise((resolve) => {
|
|
245
|
-
|
|
255
|
+
const timer = setTimeout(() => {
|
|
256
|
+
if (this.waiters.delete(id)) {
|
|
257
|
+
resolve({ id, error: { code: 'rpc_timeout', message: `helper did not respond within ${timeoutMs}ms` } });
|
|
258
|
+
}
|
|
259
|
+
}, timeoutMs);
|
|
260
|
+
// Resolve as an error (never reject) so callers flow through unwrap()
|
|
261
|
+
// uniformly, matching failPending's contract.
|
|
262
|
+
this.waiters.set(id, (r) => {
|
|
263
|
+
clearTimeout(timer);
|
|
264
|
+
resolve(r);
|
|
265
|
+
});
|
|
246
266
|
this.send(payload);
|
|
247
267
|
});
|
|
248
268
|
}
|
package/dist/lib/daemon.js
CHANGED
|
@@ -11,7 +11,7 @@ import * as fs from 'fs';
|
|
|
11
11
|
import * as path from 'path';
|
|
12
12
|
import * as os from 'os';
|
|
13
13
|
import { getDaemonDir as getDaemonDirRoot } from './state.js';
|
|
14
|
-
import { isAlive } from './platform/index.js';
|
|
14
|
+
import { isAlive, killTree } from './platform/index.js';
|
|
15
15
|
import { listJobs as listAllJobs } from './routines.js';
|
|
16
16
|
import { JobScheduler } from './scheduler.js';
|
|
17
17
|
import { executeJobDetached, monitorRunningJobs } from './runner.js';
|
|
@@ -437,17 +437,23 @@ export function stopDaemon() {
|
|
|
437
437
|
}
|
|
438
438
|
const pid = readDaemonPid();
|
|
439
439
|
if (pid) {
|
|
440
|
-
|
|
441
|
-
|
|
440
|
+
if (process.platform === 'win32') {
|
|
441
|
+
// Windows has no graceful termination signal — terminate the daemon and
|
|
442
|
+
// its job/browser child tree in one shot (taskkill /T), so stop doesn't
|
|
443
|
+
// report success while children keep running.
|
|
444
|
+
killTree(pid);
|
|
442
445
|
}
|
|
443
|
-
|
|
444
|
-
setTimeout(() => {
|
|
446
|
+
else {
|
|
445
447
|
try {
|
|
446
|
-
process.kill(pid,
|
|
447
|
-
process.kill(pid, 'SIGKILL');
|
|
448
|
+
process.kill(pid, 'SIGTERM');
|
|
448
449
|
}
|
|
449
450
|
catch { /* process already exited */ }
|
|
450
|
-
|
|
451
|
+
// Escalate to a hard tree-kill if it ignored SIGTERM after the grace period.
|
|
452
|
+
setTimeout(() => {
|
|
453
|
+
if (isAlive(pid))
|
|
454
|
+
killTree(pid);
|
|
455
|
+
}, 5000);
|
|
456
|
+
}
|
|
451
457
|
}
|
|
452
458
|
removeDaemonPid();
|
|
453
459
|
return true;
|
|
@@ -479,6 +485,12 @@ export function signalDaemonReload() {
|
|
|
479
485
|
const pid = readDaemonPid();
|
|
480
486
|
if (!pid)
|
|
481
487
|
return false;
|
|
488
|
+
if (process.platform === 'win32') {
|
|
489
|
+
// Windows has no SIGHUP, so signal-based live reload isn't available. Sending
|
|
490
|
+
// it would throw; instead report "not reloaded" so callers tell the user to
|
|
491
|
+
// restart the daemon to pick up job changes.
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
482
494
|
try {
|
|
483
495
|
process.kill(pid, 'SIGHUP');
|
|
484
496
|
return true;
|
package/dist/lib/fs-walk.d.ts
CHANGED
|
@@ -1,2 +1,8 @@
|
|
|
1
|
-
/** Walk a directory recursively for files with a given extension. */
|
|
1
|
+
/** Walk a directory recursively for files with a given extension, newest first. */
|
|
2
2
|
export declare function walkForFiles(dir: string, ext: string, limit: number): string[];
|
|
3
|
+
/**
|
|
4
|
+
* Return the newest mtime (ms) among files with the given extension, or null
|
|
5
|
+
* when none match. Single pass tracking the max — no collection or sort.
|
|
6
|
+
* Hot-path helper for the `agents run` account-recency probe.
|
|
7
|
+
*/
|
|
8
|
+
export declare function latestFileMtimeMs(dir: string, ext: string): number | null;
|
package/dist/lib/fs-walk.js
CHANGED
|
@@ -1,35 +1,69 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Recursively visit files with a given extension, calling onFile with each
|
|
5
|
+
* match's path and mtime. Uses dirent types from readdir so only matching
|
|
6
|
+
* files (and symlinks, to preserve follow semantics) pay a stat call —
|
|
7
|
+
* directories are classified for free. On large session trees this roughly
|
|
8
|
+
* halves the syscall count versus stat-per-entry.
|
|
9
|
+
*/
|
|
10
|
+
function walkEntries(dir, ext, onFile) {
|
|
6
11
|
function walk(d, depth) {
|
|
7
12
|
if (depth > 5)
|
|
8
13
|
return;
|
|
9
14
|
let entries;
|
|
10
15
|
try {
|
|
11
|
-
entries = fs.readdirSync(d);
|
|
16
|
+
entries = fs.readdirSync(d, { withFileTypes: true });
|
|
12
17
|
}
|
|
13
18
|
catch {
|
|
14
19
|
return;
|
|
15
20
|
}
|
|
16
21
|
for (const entry of entries) {
|
|
17
|
-
const full = path.join(d, entry);
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
const full = path.join(d, entry.name);
|
|
23
|
+
let isDirectory = entry.isDirectory();
|
|
24
|
+
// Symlinks: dirent reports the link itself, but the previous stat-based
|
|
25
|
+
// walk followed links into directories and matched linked files. Stat
|
|
26
|
+
// (which follows) only for symlinks to keep that behavior.
|
|
27
|
+
if (entry.isSymbolicLink()) {
|
|
28
|
+
const stat = safeStatSync(full);
|
|
29
|
+
if (!stat)
|
|
30
|
+
continue;
|
|
31
|
+
isDirectory = stat.isDirectory();
|
|
32
|
+
}
|
|
33
|
+
if (isDirectory) {
|
|
22
34
|
walk(full, depth + 1);
|
|
23
35
|
}
|
|
24
|
-
else if (entry.endsWith(ext)) {
|
|
25
|
-
|
|
36
|
+
else if (entry.name.endsWith(ext)) {
|
|
37
|
+
const stat = safeStatSync(full);
|
|
38
|
+
if (stat)
|
|
39
|
+
onFile(full, stat.mtimeMs);
|
|
26
40
|
}
|
|
27
41
|
}
|
|
28
42
|
}
|
|
29
43
|
walk(dir, 0);
|
|
44
|
+
}
|
|
45
|
+
/** Walk a directory recursively for files with a given extension, newest first. */
|
|
46
|
+
export function walkForFiles(dir, ext, limit) {
|
|
47
|
+
const results = [];
|
|
48
|
+
walkEntries(dir, ext, (filePath, mtimeMs) => {
|
|
49
|
+
results.push({ path: filePath, mtime: mtimeMs });
|
|
50
|
+
});
|
|
30
51
|
results.sort((a, b) => b.mtime - a.mtime);
|
|
31
52
|
return results.slice(0, limit).map(r => r.path);
|
|
32
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Return the newest mtime (ms) among files with the given extension, or null
|
|
56
|
+
* when none match. Single pass tracking the max — no collection or sort.
|
|
57
|
+
* Hot-path helper for the `agents run` account-recency probe.
|
|
58
|
+
*/
|
|
59
|
+
export function latestFileMtimeMs(dir, ext) {
|
|
60
|
+
let latest = null;
|
|
61
|
+
walkEntries(dir, ext, (_filePath, mtimeMs) => {
|
|
62
|
+
if (latest === null || mtimeMs > latest)
|
|
63
|
+
latest = mtimeMs;
|
|
64
|
+
});
|
|
65
|
+
return latest;
|
|
66
|
+
}
|
|
33
67
|
function safeStatSync(p) {
|
|
34
68
|
try {
|
|
35
69
|
return fs.statSync(p);
|
package/dist/lib/git.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import simpleGit from 'simple-git';
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
11
|
+
import { IS_WINDOWS, isWindowsAbsolutePath } from './platform/index.js';
|
|
11
12
|
import { getPackageLocalPath } from './state.js';
|
|
12
13
|
import { DEFAULT_SYSTEM_REPO, systemRepoSlug } from './types.js';
|
|
13
14
|
/**
|
|
@@ -127,8 +128,10 @@ export function parseSource(source) {
|
|
|
127
128
|
ref,
|
|
128
129
|
};
|
|
129
130
|
}
|
|
130
|
-
// Local path (absolute or relative)
|
|
131
|
-
|
|
131
|
+
// Local path (absolute or relative). On Windows also recognize drive-letter
|
|
132
|
+
// (C:\…) and UNC (\\…) roots, which the POSIX prefixes miss.
|
|
133
|
+
if (cleanSource.startsWith('/') || cleanSource.startsWith('./') || cleanSource.startsWith('../')
|
|
134
|
+
|| (IS_WINDOWS && isWindowsAbsolutePath(cleanSource))) {
|
|
132
135
|
if (fs.existsSync(cleanSource)) {
|
|
133
136
|
return {
|
|
134
137
|
type: 'local',
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface FollowOptions {
|
|
2
|
+
/** Poll interval in milliseconds (default 500). */
|
|
3
|
+
intervalMs?: number;
|
|
4
|
+
/** Start at the current end of file (skip existing content). Default false. */
|
|
5
|
+
fromEnd?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function followFile(filePath: string, onChunk: (text: string) => void, opts?: FollowOptions): () => void;
|
|
@@ -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
|
}
|
package/dist/lib/refresh.js
CHANGED
|
@@ -212,8 +212,8 @@ export async function refresh(options = {}) {
|
|
|
212
212
|
if (!isShimsInPath()) {
|
|
213
213
|
const pathResult = addShimsToPath();
|
|
214
214
|
if (pathResult.success && !pathResult.alreadyPresent) {
|
|
215
|
-
console.log(chalk.green(`\nAdded shims to
|
|
216
|
-
console.log(chalk.gray(
|
|
215
|
+
console.log(chalk.green(`\nAdded shims to ${pathResult.location}`));
|
|
216
|
+
console.log(chalk.gray(pathResult.reloadHint));
|
|
217
217
|
}
|
|
218
218
|
else if (!pathResult.success) {
|
|
219
219
|
console.log(chalk.yellow('\nCould not auto-add shims to PATH:'));
|
|
@@ -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[];
|