@f5-sales-demo/xcsh 21.30.1 → 21.32.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": "21.30.1",
4
+ "version": "21.32.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",
@@ -63,13 +63,13 @@
63
63
  },
64
64
  "dependencies": {
65
65
  "@agentclientprotocol/sdk": "1.4.0",
66
- "@f5-sales-demo/pi-agent-core": "21.30.1",
67
- "@f5-sales-demo/pi-ai": "21.30.1",
68
- "@f5-sales-demo/pi-natives": "21.30.1",
69
- "@f5-sales-demo/pi-resource-management": "21.30.1",
70
- "@f5-sales-demo/pi-tui": "21.30.1",
71
- "@f5-sales-demo/pi-utils": "21.30.1",
72
- "@f5-sales-demo/xcsh-stats": "21.30.1",
66
+ "@f5-sales-demo/pi-agent-core": "21.32.0",
67
+ "@f5-sales-demo/pi-ai": "21.32.0",
68
+ "@f5-sales-demo/pi-natives": "21.32.0",
69
+ "@f5-sales-demo/pi-resource-management": "21.32.0",
70
+ "@f5-sales-demo/pi-tui": "21.32.0",
71
+ "@f5-sales-demo/pi-utils": "21.32.0",
72
+ "@f5-sales-demo/xcsh-stats": "21.32.0",
73
73
  "@mozilla/readability": "^0.6",
74
74
  "@sinclair/typebox": "0.34.52",
75
75
  "@xterm/headless": "^6.0",
@@ -155,7 +155,7 @@ export function paneUnavailableMessage(distDir: string, source: PaneSource): str
155
155
  return (
156
156
  "The Office pane is not included in the npm package — it ships as a build-time asset of the " +
157
157
  "compiled binary, which is also what provides the sideload and the trusted certificate. " +
158
- "Install it with `brew install f5-sales-demo/tap/xcsh` and run `xcsh office` from there."
158
+ "Install it with `brew install --cask f5-sales-demo/tap/xcsh` and run `xcsh office` from there."
159
159
  );
160
160
  }
161
161
 
@@ -75,7 +75,7 @@ type UpdateTarget =
75
75
  * Detection order:
76
76
  * 1. bun — binary is inside bun's global bin directory
77
77
  * 2. npm — binary is a symlink whose resolution chain contains "node_modules"
78
- * 3. brew — binary path or realpath contains "Cellar" or "homebrew"
78
+ * 3. brew — binary path or realpath contains "Cellar", "Caskroom", or "homebrew"
79
79
  * 4. binary — fallback for standalone installs
80
80
  */
81
81
  function detectInstallMethod(binPath: string, bunBinDir: string | undefined): InstallMethod {
@@ -106,14 +106,18 @@ function detectInstallMethod(binPath: string, bunBinDir: string | undefined): In
106
106
  // lstat/readlink may fail; fall through
107
107
  }
108
108
 
109
- // 3. brew: path or realpath contains Cellar or homebrew
109
+ // 3. brew: path or realpath contains Cellar, Caskroom, or homebrew
110
110
  const lowerBinPath = binPath.toLowerCase();
111
- if (lowerBinPath.includes("/cellar/") || lowerBinPath.includes("/homebrew/")) {
111
+ if (
112
+ lowerBinPath.includes("/cellar/") ||
113
+ lowerBinPath.includes("/caskroom/") ||
114
+ lowerBinPath.includes("/homebrew/")
115
+ ) {
112
116
  return "brew";
113
117
  }
114
118
  try {
115
119
  const realPath = fs.realpathSync(binPath).toLowerCase();
116
- if (realPath.includes("/cellar/") || realPath.includes("/homebrew/")) {
120
+ if (realPath.includes("/cellar/") || realPath.includes("/caskroom/") || realPath.includes("/homebrew/")) {
117
121
  return "brew";
118
122
  }
119
123
  } catch {
@@ -322,7 +326,7 @@ async function updateViaNpm(expectedVersion: string): Promise<void> {
322
326
  function updateViaBrew(targetPath: string, expectedVersion: string): void {
323
327
  console.log(chalk.yellow(`\n${APP_NAME} at ${targetPath} was installed via Homebrew.`));
324
328
  console.log(chalk.yellow(`To update to ${expectedVersion}, run:`));
325
- console.log(chalk.cyan(` brew upgrade ${APP_NAME}`));
329
+ console.log(chalk.cyan(` ${getBrewUpgradeCommand()}`));
326
330
  console.log(chalk.dim("\nThis ensures the update goes through your organization's Homebrew tap."));
327
331
  // #1874 Task 7: brew upgrade runs out-of-band, so we can't recycle automatically.
328
332
  // Nudge the one command that applies it to the Chrome extension immediately.
@@ -330,6 +334,10 @@ function updateViaBrew(targetPath: string, expectedVersion: string): void {
330
334
  console.log(chalk.dim("(otherwise it takes effect the next time you open Chrome)."));
331
335
  }
332
336
 
337
+ export function getBrewUpgradeCommand(): string {
338
+ return `brew upgrade --cask ${APP_NAME}`;
339
+ }
340
+
333
341
  /**
334
342
  * Download a release binary to a target path, replacing an existing file.
335
343
  */
package/src/cli.ts CHANGED
@@ -78,6 +78,7 @@ const commands: CommandEntry[] = [
78
78
  { name: "manager", load: () => import("./commands/manager").then(m => m.default) },
79
79
  { name: "office", load: () => import("./commands/office").then(m => m.default) },
80
80
  { name: "plugin", load: () => import("./commands/plugin").then(m => m.default) },
81
+ { name: "profile", load: () => import("./commands/profile").then(m => m.default) },
81
82
  { name: "setup", load: () => import("./commands/setup").then(m => m.default) },
82
83
  { name: "shell", load: () => import("./commands/shell").then(m => m.default) },
83
84
  { name: "ssh", load: () => import("./commands/ssh").then(m => m.default) },
@@ -0,0 +1,72 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { Args, Command, Flags } from "@f5-sales-demo/pi-utils/cli";
3
+ import { machineProfileService } from "../person-profile/machine-profile";
4
+ import { resetProfileTargets } from "../person-profile/private-store";
5
+ import { personProfileService } from "../person-profile/service";
6
+
7
+ type ProfileTarget = "person" | "computer" | "all";
8
+
9
+ export default class Profile extends Command {
10
+ static description = "Inspect or reset private person and computer profiles";
11
+ static args = {
12
+ action: Args.string({ description: "Profile action", options: ["status", "reset"], required: true }),
13
+ target: Args.string({ description: "Reset target", options: ["person", "computer", "all"] }),
14
+ };
15
+ static flags = {
16
+ json: Flags.boolean({ description: "Output machine-readable status", default: false }),
17
+ yes: Flags.boolean({ description: "Confirm permanent reset", default: false }),
18
+ };
19
+
20
+ async run(): Promise<void> {
21
+ const { args, flags } = await this.parse(Profile);
22
+ if (args.action === "status") {
23
+ if (flags.yes) throw new Error("profile status does not accept --yes");
24
+ if (args.target) throw new Error("profile status does not accept a target");
25
+ const result = {
26
+ person: await personProfileService.status(),
27
+ computer: await machineProfileService.status(),
28
+ };
29
+ if (flags.json) {
30
+ process.stdout.write(`${JSON.stringify(result)}\n`);
31
+ return;
32
+ }
33
+ for (const [name, status] of Object.entries(result)) {
34
+ const details = [
35
+ status.schemaVersion === undefined ? undefined : `schema v${status.schemaVersion}`,
36
+ status.permissions?.file ? `file ${status.permissions.file}` : undefined,
37
+ status.permissions?.directory ? `directory ${status.permissions.directory}` : undefined,
38
+ status.reason,
39
+ ].filter(Boolean);
40
+ process.stdout.write(`${name}: ${status.status}${details.length ? ` (${details.join(", ")})` : ""}\n`);
41
+ if (status.remedy) process.stdout.write(` remedy: ${status.remedy}\n`);
42
+ }
43
+ return;
44
+ }
45
+
46
+ const target = args.target as ProfileTarget | undefined;
47
+ if (!target) throw new Error("profile reset requires person, computer, or all");
48
+ if (flags.json) throw new Error("profile reset does not accept --json");
49
+ const services =
50
+ target === "all"
51
+ ? [personProfileService, machineProfileService]
52
+ : [target === "person" ? personProfileService : machineProfileService];
53
+ process.stdout.write(
54
+ `Profiles selected for permanent reset:\n${services.map(service => `- ${service.path}`).join("\n")}\n`,
55
+ );
56
+ if (!flags.yes) {
57
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
58
+ throw new Error("Non-interactive profile reset requires --yes");
59
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
60
+ try {
61
+ const answer = await prompt.question('Type "reset" to continue: ');
62
+ if (answer !== "reset") throw new Error("Profile reset cancelled");
63
+ } finally {
64
+ prompt.close();
65
+ }
66
+ }
67
+ const results = await resetProfileTargets(services);
68
+ for (const service of services) {
69
+ process.stdout.write(`${service.path}: ${results.get(service.path) ? "reset" : "already missing"}\n`);
70
+ }
71
+ }
72
+ }
@@ -15,7 +15,7 @@ import { loadCapability } from "../../discovery";
15
15
  import { getExtensionNameFromPath, getPreloadedPluginRoots } from "../../discovery/helpers";
16
16
  import type { ExecOptions } from "../../exec/exec";
17
17
  import { execCommand } from "../../exec/exec";
18
- import { type ProfileCollector, personProfileService } from "../../person-profile/service";
18
+ import { personProfileService } from "../../person-profile/service";
19
19
  import type { CustomMessage } from "../../session/messages";
20
20
  import { EventBus } from "../../utils/event-bus";
21
21
  import { getAllPluginExtensionPaths } from "../plugins/loader";
@@ -121,6 +121,7 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
121
121
  config: import("./types").ProviderConfig;
122
122
  sourceId: string;
123
123
  }> = [];
124
+ readonly personProfile: ExtensionAPI["personProfile"];
124
125
 
125
126
  constructor(
126
127
  public readonly pi: typeof import("@f5-sales-demo/xcsh"),
@@ -128,7 +129,18 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
128
129
  private readonly runtime: IExtensionRuntime,
129
130
  private readonly cwd: string,
130
131
  public readonly events: EventBus,
131
- ) {}
132
+ ) {
133
+ const registrant = extension.resolvedPath;
134
+ this.personProfile = Object.freeze({
135
+ get: () => personProfileService.get(),
136
+ registerCollector: collector => {
137
+ if (!collector || typeof collector.collect !== "function")
138
+ throw new Error("Invalid person profile collector");
139
+ personProfileService.registerProfileCollector(collector, registrant);
140
+ },
141
+ unregisterCollector: (id: string) => personProfileService.unregisterProfileCollector(id, registrant),
142
+ });
143
+ }
132
144
 
133
145
  on<F extends HandlerFn>(event: string, handler: F): void {
134
146
  const list = this.extension.handlers.get(event) ?? [];
@@ -249,35 +261,6 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
249
261
  return this.runtime.setSessionName(name);
250
262
  }
251
263
 
252
- registerProfileCollector(collector: ProfileCollector): void {
253
- if (!collector || typeof collector.collect !== "function") throw new Error("Invalid person profile collector");
254
- personProfileService.registerProfileCollector(
255
- {
256
- ...collector,
257
- async collect(signal) {
258
- const result = await collector.collect(signal);
259
- if ("facts" in result) return result;
260
- // Legacy flat output does not distinguish queried values from heuristic inference.
261
- // Keep it as candidates; modern collectors can return explicit facts/observations.
262
- return {
263
- facts: {},
264
- observations: Object.entries(result).map(([field, value]) => ({
265
- field: field as keyof typeof result,
266
- value,
267
- source: collector.id,
268
- kind: "inferred" as const,
269
- observedAt: new Date().toISOString(),
270
- })),
271
- };
272
- },
273
- },
274
- this.extension.resolvedPath,
275
- );
276
- }
277
- unregisterProfileCollector(id: string): boolean {
278
- return personProfileService.unregisterProfileCollector(id, this.extension.resolvedPath);
279
- }
280
-
281
264
  registerProvider(name: string, config: import("./types").ProviderConfig): void {
282
265
  this.runtime.pendingProviderRegistrations.push({ name, config, sourceId: this.extension.path });
283
266
  }
@@ -1013,6 +1013,13 @@ export interface ExtensionAPI {
1013
1013
  /** Injected pi-coding-agent exports for accessing SDK utilities */
1014
1014
  pi: typeof piCodingAgent;
1015
1015
 
1016
+ /** Canonical, provenance-aware person profile integration. */
1017
+ readonly personProfile: {
1018
+ get(): Promise<import("../../person-profile/schema").PersonProfile>;
1019
+ registerCollector(collector: import("../../person-profile/service").ExtensionProfileCollector): void;
1020
+ unregisterCollector(id: string): boolean;
1021
+ };
1022
+
1016
1023
  // =========================================================================
1017
1024
  // Event Subscription
1018
1025
  // =========================================================================
@@ -1222,9 +1229,6 @@ export interface ExtensionAPI {
1222
1229
  * });
1223
1230
  */
1224
1231
  registerProvider(name: string, config: ProviderConfig): void;
1225
- registerProfileCollector(collector: import("../../person-profile/service").ProfileCollector): void;
1226
- unregisterProfileCollector(id: string): boolean;
1227
-
1228
1232
  /** Shared event bus for extension communication. */
1229
1233
  events: EventBus;
1230
1234
  }
package/src/index.ts CHANGED
@@ -40,7 +40,7 @@ export * from "./modes/components";
40
40
  export * from "./modes/theme/theme";
41
41
  export type { PersonProfile, UserProfile, UserProfileObservation } from "./person-profile/schema";
42
42
  export type { ProfileCollection, ProfileCollector } from "./person-profile/service";
43
- export { loadProfile, PersonProfileService } from "./person-profile/service";
43
+ export { PersonProfileService } from "./person-profile/service";
44
44
  export * from "./routing/classifier";
45
45
  export * from "./routing/coordinator";
46
46
  export * from "./routing/delegation";
@@ -17,17 +17,17 @@ export interface BuildInfo {
17
17
  }
18
18
 
19
19
  export const BUILD_INFO: BuildInfo = {
20
- "version": "21.30.1",
21
- "commit": "22d9767e595b2a3d2e57a62b192bd582fc9b00eb",
22
- "shortCommit": "22d9767",
23
- "branch": "main",
24
- "tag": "v21.30.1",
25
- "commitDate": "2026-09-17T15:05:21+00:00",
26
- "buildDate": "2026-09-17T16:16:04.516Z",
20
+ "version": "21.32.0",
21
+ "commit": "318283a955aea8725f8119d68e14db6ea2d8b3e7",
22
+ "shortCommit": "318283a",
23
+ "branch": "unknown",
24
+ "tag": "v21.32.0",
25
+ "commitDate": "2026-09-17T18:23:21Z",
26
+ "buildDate": "2026-09-17T23:24:55.633Z",
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/22d9767e595b2a3d2e57a62b192bd582fc9b00eb",
32
- "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v21.30.1"
31
+ "commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/318283a955aea8725f8119d68e14db6ea2d8b3e7",
32
+ "releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v21.32.0"
33
33
  };