@f5-sales-demo/xcsh 19.83.0 → 19.84.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/xcsh",
|
|
4
|
-
"version": "19.
|
|
4
|
+
"version": "19.84.0",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/f5-sales-demo/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -56,13 +56,13 @@
|
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@agentclientprotocol/sdk": "0.16.1",
|
|
58
58
|
"@mozilla/readability": "^0.6",
|
|
59
|
-
"@f5-sales-demo/xcsh-stats": "19.
|
|
60
|
-
"@f5-sales-demo/pi-agent-core": "19.
|
|
61
|
-
"@f5-sales-demo/pi-ai": "19.
|
|
62
|
-
"@f5-sales-demo/pi-natives": "19.
|
|
63
|
-
"@f5-sales-demo/pi-resource-management": "19.
|
|
64
|
-
"@f5-sales-demo/pi-tui": "19.
|
|
65
|
-
"@f5-sales-demo/pi-utils": "19.
|
|
59
|
+
"@f5-sales-demo/xcsh-stats": "19.84.0",
|
|
60
|
+
"@f5-sales-demo/pi-agent-core": "19.84.0",
|
|
61
|
+
"@f5-sales-demo/pi-ai": "19.84.0",
|
|
62
|
+
"@f5-sales-demo/pi-natives": "19.84.0",
|
|
63
|
+
"@f5-sales-demo/pi-resource-management": "19.84.0",
|
|
64
|
+
"@f5-sales-demo/pi-tui": "19.84.0",
|
|
65
|
+
"@f5-sales-demo/pi-utils": "19.84.0",
|
|
66
66
|
"@sinclair/typebox": "^0.34",
|
|
67
67
|
"@xterm/headless": "^6.0",
|
|
68
68
|
"ajv": "^8.20",
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supersede / recycle a foreground `xcsh office serve`.
|
|
3
|
+
*
|
|
4
|
+
* `office serve` binds a FIXED :8444 listener, so after a `brew upgrade` the old
|
|
5
|
+
* serve squats the port on the stale binary and a fresh `office serve` dies with
|
|
6
|
+
* "port 8444 in use". These helpers let the new serve step the old one down on
|
|
7
|
+
* start (supersede), and give `office recycle` (run from brew post_install) a way
|
|
8
|
+
* to stop the stale serve so the next start is clean — the office analog of
|
|
9
|
+
* `chrome recycle`.
|
|
10
|
+
*
|
|
11
|
+
* Safety: we only ever signal a PID that (a) actually holds :8444 AND (b) looks
|
|
12
|
+
* like an `office serve` (its `ps` command line contains "office serve"). A
|
|
13
|
+
* foreign holder is reported, never killed — so PID reuse can't make us signal an
|
|
14
|
+
* unrelated process.
|
|
15
|
+
*/
|
|
16
|
+
import { OFFICE_PANE_PORT } from "./office-pane-server";
|
|
17
|
+
|
|
18
|
+
/** Injectable seams so the wait/signal logic is unit-testable without real procs. */
|
|
19
|
+
export interface ServeLifecycleDeps {
|
|
20
|
+
/** PID listening on `port`, or 0 if none/unknown (best-effort via lsof). */
|
|
21
|
+
pidListeningOn: (port: number) => number;
|
|
22
|
+
/** Whether `pid`'s command line is an `xcsh office serve`. */
|
|
23
|
+
isOfficeServe: (pid: number) => boolean;
|
|
24
|
+
/** Send a signal to `pid` (process.kill). */
|
|
25
|
+
signal: (pid: number, sig: NodeJS.Signals) => void;
|
|
26
|
+
/** Await `ms` (setTimeout in prod; immediate in tests). */
|
|
27
|
+
sleep: (ms: number) => Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SupersedeResult {
|
|
31
|
+
/** True when a stale serve was found and stepped down. */
|
|
32
|
+
superseded: boolean;
|
|
33
|
+
/** The stale PID we signalled (present only when superseded). */
|
|
34
|
+
pid?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const WAIT_MS = 3000;
|
|
38
|
+
const POLL_MS = 100;
|
|
39
|
+
|
|
40
|
+
/** PID listening on `port`, or 0 (best-effort; lsof unavailable → 0). */
|
|
41
|
+
function pidListeningOn(port: number): number {
|
|
42
|
+
try {
|
|
43
|
+
const out = Bun.spawnSync(["lsof", "-t", `-iTCP:${port}`, "-sTCP:LISTEN"])
|
|
44
|
+
.stdout.toString()
|
|
45
|
+
.trim()
|
|
46
|
+
.split("\n")[0];
|
|
47
|
+
const pid = Number(out);
|
|
48
|
+
return Number.isInteger(pid) && pid > 0 ? pid : 0;
|
|
49
|
+
} catch {
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Whether `pid`'s full command line is an `xcsh office serve` (ps, best-effort). */
|
|
55
|
+
function isOfficeServe(pid: number): boolean {
|
|
56
|
+
try {
|
|
57
|
+
const out = Bun.spawnSync(["ps", "-p", String(pid), "-o", "command="]).stdout.toString();
|
|
58
|
+
return /office\s+serve/.test(out);
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const defaultDeps: ServeLifecycleDeps = {
|
|
65
|
+
pidListeningOn,
|
|
66
|
+
isOfficeServe,
|
|
67
|
+
signal: (pid, sig) => process.kill(pid, sig),
|
|
68
|
+
sleep: ms => new Promise(resolve => setTimeout(resolve, ms)),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** SIGTERM `pid` and poll until `port` is released by it, or throw on timeout. */
|
|
72
|
+
async function signalAndWait(pid: number, port: number, deps: ServeLifecycleDeps): Promise<void> {
|
|
73
|
+
deps.signal(pid, "SIGTERM");
|
|
74
|
+
for (let waited = 0; waited < WAIT_MS; waited += POLL_MS) {
|
|
75
|
+
await deps.sleep(POLL_MS);
|
|
76
|
+
const now = deps.pidListeningOn(port);
|
|
77
|
+
if (now <= 0 || now !== pid) return; // freed (or a different serve took over)
|
|
78
|
+
}
|
|
79
|
+
throw new Error(`Timed out waiting for the stale office serve (PID ${pid}) to release port ${port}.`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Step down a stale `office serve` holding `port` so a fresh serve can bind.
|
|
84
|
+
* No-op when the port is free. Throws (without signalling) when the holder is not
|
|
85
|
+
* an office serve — the caller should surface that rather than kill a stranger.
|
|
86
|
+
*/
|
|
87
|
+
export async function supersedeStaleServe(
|
|
88
|
+
port = OFFICE_PANE_PORT,
|
|
89
|
+
deps: ServeLifecycleDeps = defaultDeps,
|
|
90
|
+
): Promise<SupersedeResult> {
|
|
91
|
+
const pid = deps.pidListeningOn(port);
|
|
92
|
+
if (pid <= 0 || pid === process.pid) return { superseded: false };
|
|
93
|
+
if (!deps.isOfficeServe(pid)) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Port ${port} is held by PID ${pid}, which isn't an xcsh office serve. Stop it manually, then retry.`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
await signalAndWait(pid, port, deps);
|
|
99
|
+
return { superseded: true, pid };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Stop a running `office serve` (if any) — used by `office recycle` and brew
|
|
104
|
+
* post_install so an upgrade clears the stale serve. Returns a human message;
|
|
105
|
+
* never throws for the ordinary cases (nothing running / foreign holder).
|
|
106
|
+
*/
|
|
107
|
+
export async function recycleOfficeServe(
|
|
108
|
+
port = OFFICE_PANE_PORT,
|
|
109
|
+
deps: ServeLifecycleDeps = defaultDeps,
|
|
110
|
+
): Promise<string> {
|
|
111
|
+
const pid = deps.pidListeningOn(port);
|
|
112
|
+
if (pid <= 0) return "No xcsh office serve is running.";
|
|
113
|
+
if (!deps.isOfficeServe(pid)) {
|
|
114
|
+
return `Port ${port} is held by PID ${pid}, which isn't an xcsh office serve — leaving it alone.`;
|
|
115
|
+
}
|
|
116
|
+
await signalAndWait(pid, port, deps);
|
|
117
|
+
return `Stopped the running office serve (PID ${pid}). Start it again with \`xcsh office serve\`.`;
|
|
118
|
+
}
|
package/src/cli/office-cli.ts
CHANGED
|
@@ -13,13 +13,15 @@ import { LOCALIP_HOST } from "../browser/bridge-cert";
|
|
|
13
13
|
import { type HeadlessChatBridge, startHeadlessChatBridge } from "../browser/headless-bridge";
|
|
14
14
|
import {
|
|
15
15
|
getOfficePaneDir,
|
|
16
|
+
OFFICE_PANE_PORT,
|
|
16
17
|
type OfficePaneServer,
|
|
17
18
|
readManifest,
|
|
18
19
|
startOfficePaneServer,
|
|
19
20
|
} from "../browser/office-pane-server";
|
|
21
|
+
import { recycleOfficeServe, supersedeStaleServe } from "../browser/office-serve-lifecycle";
|
|
20
22
|
|
|
21
23
|
/** The subcommands `xcsh office` accepts (also the Args `options` constraint). */
|
|
22
|
-
export const OFFICE_ACTIONS = ["serve", "manifest", "sideload"] as const;
|
|
24
|
+
export const OFFICE_ACTIONS = ["serve", "manifest", "sideload", "recycle"] as const;
|
|
23
25
|
export type OfficeAction = (typeof OFFICE_ACTIONS)[number];
|
|
24
26
|
|
|
25
27
|
/** Desktop Office apps a sideload can target. */
|
|
@@ -50,9 +52,10 @@ export async function writeManifest(outPath?: string): Promise<string> {
|
|
|
50
52
|
export interface OfficeServeDeps {
|
|
51
53
|
startOfficePaneServer: typeof startOfficePaneServer;
|
|
52
54
|
startHeadlessChatBridge: typeof startHeadlessChatBridge;
|
|
55
|
+
supersedeStaleServe: typeof supersedeStaleServe;
|
|
53
56
|
}
|
|
54
57
|
|
|
55
|
-
const defaultServeDeps: OfficeServeDeps = { startOfficePaneServer, startHeadlessChatBridge };
|
|
58
|
+
const defaultServeDeps: OfficeServeDeps = { startOfficePaneServer, startHeadlessChatBridge, supersedeStaleServe };
|
|
56
59
|
|
|
57
60
|
/** A running `office serve`: the pane file server, the (optional) chat bridge, and
|
|
58
61
|
* a teardown that disposes both. */
|
|
@@ -70,6 +73,14 @@ export interface OfficeServeHandle {
|
|
|
70
73
|
* Extracted from {@link runServe} so the start/teardown wiring is unit-testable.
|
|
71
74
|
*/
|
|
72
75
|
export async function startOfficeServe(deps: OfficeServeDeps = defaultServeDeps): Promise<OfficeServeHandle> {
|
|
76
|
+
// Step down a stale serve squatting :8444 (e.g. left over from before a
|
|
77
|
+
// `brew upgrade`) so this start binds cleanly instead of "port 8444 in use".
|
|
78
|
+
const superseded = await deps.supersedeStaleServe(OFFICE_PANE_PORT);
|
|
79
|
+
if (superseded.superseded) {
|
|
80
|
+
console.log(
|
|
81
|
+
`Superseded a previous office serve (PID ${superseded.pid}) that was holding port ${OFFICE_PANE_PORT}.`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
73
84
|
const server = await deps.startOfficePaneServer();
|
|
74
85
|
let chat: HeadlessChatBridge | null = null;
|
|
75
86
|
try {
|
|
@@ -170,5 +181,8 @@ export async function runOfficeCommand(args: OfficeCommandArgs): Promise<void> {
|
|
|
170
181
|
case "sideload":
|
|
171
182
|
await runSideload(args.app ?? "excel");
|
|
172
183
|
return;
|
|
184
|
+
case "recycle":
|
|
185
|
+
console.log(await recycleOfficeServe());
|
|
186
|
+
return;
|
|
173
187
|
}
|
|
174
188
|
}
|
package/src/commands/office.ts
CHANGED
|
@@ -9,7 +9,7 @@ export default class Office extends Command {
|
|
|
9
9
|
|
|
10
10
|
static args = {
|
|
11
11
|
action: Args.string({
|
|
12
|
-
description: "serve | manifest | sideload",
|
|
12
|
+
description: "serve | manifest | sideload | recycle",
|
|
13
13
|
required: false,
|
|
14
14
|
options: OFFICE_ACTIONS,
|
|
15
15
|
}),
|
|
@@ -31,6 +31,7 @@ export default class Office extends Command {
|
|
|
31
31
|
console.log(" serve Start the https://127-0-0-1.local-ip.sh:8444 task-pane listener");
|
|
32
32
|
console.log(" manifest Print (or -o write) the add-in manifest.json");
|
|
33
33
|
console.log(" sideload Sideload the add-in into a desktop Office app");
|
|
34
|
+
console.log(" recycle Stop a running serve (so the next `serve` starts clean after an upgrade)");
|
|
34
35
|
return;
|
|
35
36
|
}
|
|
36
37
|
await runOfficeCommand({
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "19.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "19.84.0",
|
|
21
|
+
"commit": "61adb144f98de4a4989e874a9c8b6641effc3b52",
|
|
22
|
+
"shortCommit": "61adb14",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v19.
|
|
25
|
-
"commitDate": "2026-07-
|
|
26
|
-
"buildDate": "2026-07-
|
|
24
|
+
"tag": "v19.84.0",
|
|
25
|
+
"commitDate": "2026-07-23T14:56:34Z",
|
|
26
|
+
"buildDate": "2026-07-23T15:21:30.427Z",
|
|
27
27
|
"dirty": true,
|
|
28
28
|
"prNumber": "",
|
|
29
29
|
"repoUrl": "https://github.com/f5-sales-demo/xcsh",
|
|
30
30
|
"repoSlug": "f5-sales-demo/xcsh",
|
|
31
|
-
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/
|
|
32
|
-
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.
|
|
31
|
+
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/61adb144f98de4a4989e874a9c8b6641effc3b52",
|
|
32
|
+
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.84.0"
|
|
33
33
|
};
|