@f5-sales-demo/xcsh 22.4.2 → 22.4.5

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": "22.4.2",
4
+ "version": "22.4.5",
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",
@@ -67,13 +67,13 @@
67
67
  },
68
68
  "dependencies": {
69
69
  "@agentclientprotocol/sdk": "1.4.0",
70
- "@f5-sales-demo/pi-agent-core": "22.4.2",
71
- "@f5-sales-demo/pi-ai": "22.4.2",
72
- "@f5-sales-demo/pi-natives": "22.4.2",
73
- "@f5-sales-demo/pi-resource-management": "22.4.2",
74
- "@f5-sales-demo/pi-tui": "22.4.2",
75
- "@f5-sales-demo/pi-utils": "22.4.2",
76
- "@f5-sales-demo/xcsh-stats": "22.4.2",
70
+ "@f5-sales-demo/pi-agent-core": "22.4.5",
71
+ "@f5-sales-demo/pi-ai": "22.4.5",
72
+ "@f5-sales-demo/pi-natives": "22.4.5",
73
+ "@f5-sales-demo/pi-resource-management": "22.4.5",
74
+ "@f5-sales-demo/pi-tui": "22.4.5",
75
+ "@f5-sales-demo/pi-utils": "22.4.5",
76
+ "@f5-sales-demo/xcsh-stats": "22.4.5",
77
77
  "@mozilla/readability": "^0.6",
78
78
  "@resvg/resvg-wasm": "2.6.2",
79
79
  "@sinclair/typebox": "0.34.52",
@@ -0,0 +1,118 @@
1
+ import { createHash } from "node:crypto";
2
+ import path from "node:path";
3
+
4
+ const REPO = "f5-sales-demo/xcsh";
5
+ const MAX_INSTALLER_BYTES = 256 * 1024;
6
+
7
+ export interface LinuxInstallerInput {
8
+ script: Buffer;
9
+ installDir: string;
10
+ version: string;
11
+ }
12
+
13
+ export interface LinuxStandaloneUpdateOptions {
14
+ targetPath: string;
15
+ arch?: string;
16
+ fetchImpl?(input: string | URL | Request, init?: RequestInit): Promise<Response>;
17
+ runInstaller?(input: LinuxInstallerInput): Promise<void>;
18
+ }
19
+
20
+ async function runInstaller(input: LinuxInstallerInput): Promise<void> {
21
+ const child = Bun.spawn(["/bin/sh", "-s", "--", "--binary", "--ref", `v${input.version}`], {
22
+ stdin: "pipe",
23
+ stdout: "inherit",
24
+ stderr: "inherit",
25
+ env: { ...process.env, PI_INSTALL_DIR: input.installDir },
26
+ });
27
+ try {
28
+ child.stdin.write(input.script);
29
+ await child.stdin.end();
30
+ } catch (error) {
31
+ child.kill();
32
+ await child.exited;
33
+ throw error;
34
+ }
35
+ const exitCode = await child.exited;
36
+ if (exitCode !== 0) throw new Error(`Verified Linux installer exited with status ${exitCode}`);
37
+ }
38
+
39
+ /** Use the published installer's complete integrity transaction for every Linux replacement. */
40
+ export async function updateLinuxStandalone(version: string, options: LinuxStandaloneUpdateOptions): Promise<void> {
41
+ if (!/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(version))
42
+ throw new Error("A stable published version is required for Linux standalone updates");
43
+ if (!path.posix.isAbsolute(options.targetPath) || path.posix.basename(options.targetPath) !== "xcsh")
44
+ throw new Error("Linux standalone target must be an absolute xcsh executable path");
45
+ const arch = options.arch ?? process.arch;
46
+ if (arch !== "x64" && arch !== "arm64") throw new Error("Unsupported Linux standalone update architecture");
47
+
48
+ const fetchImpl = options.fetchImpl ?? fetch;
49
+ const tag = `v${version}`;
50
+ const api = `https://api.github.com/repos/${REPO}`;
51
+ const releaseResponse = await fetchImpl(`${api}/releases/tags/${tag}`, {
52
+ signal: AbortSignal.timeout(30_000),
53
+ });
54
+ if (!releaseResponse.ok) throw new Error(`Cannot read immutable release metadata: ${releaseResponse.status}`);
55
+ const release = (await releaseResponse.json()) as {
56
+ tag_name?: unknown;
57
+ draft?: unknown;
58
+ prerelease?: unknown;
59
+ immutable?: unknown;
60
+ assets?: unknown;
61
+ } | null;
62
+ if (
63
+ release?.tag_name !== tag ||
64
+ release.draft !== false ||
65
+ release.prerelease !== false ||
66
+ release.immutable !== true
67
+ )
68
+ throw new Error("Linux self-update requires the exact published immutable release");
69
+ if (!Array.isArray(release.assets)) throw new Error("Linux release is missing integrity assets");
70
+ const binary = `xcsh-linux-${arch}`;
71
+ for (const name of [binary, `${binary}.sha256`, `${binary}.provenance.json`, `${binary}.provenance.json.sha256`]) {
72
+ const matches = release.assets.filter(asset => asset?.name === name);
73
+ if (
74
+ matches.length !== 1 ||
75
+ typeof matches[0].digest !== "string" ||
76
+ !/^sha256:[a-f0-9]{64}$/.test(matches[0].digest) ||
77
+ !Number.isInteger(matches[0].size) ||
78
+ matches[0].size <= 0
79
+ )
80
+ throw new Error("Linux release has incomplete or ambiguous integrity assets");
81
+ }
82
+
83
+ const sourceResponse = await fetchImpl(`${api}/contents/scripts/install.sh?ref=${tag}`, {
84
+ signal: AbortSignal.timeout(30_000),
85
+ });
86
+ if (!sourceResponse.ok) throw new Error(`Cannot read tagged installer source: ${sourceResponse.status}`);
87
+ const source = (await sourceResponse.json()) as {
88
+ type?: unknown;
89
+ path?: unknown;
90
+ encoding?: unknown;
91
+ size?: unknown;
92
+ sha?: unknown;
93
+ content?: unknown;
94
+ } | null;
95
+ if (
96
+ source?.type !== "file" ||
97
+ source.path !== "scripts/install.sh" ||
98
+ source.encoding !== "base64" ||
99
+ typeof source.size !== "number" ||
100
+ !Number.isInteger(source.size) ||
101
+ source.size <= 0 ||
102
+ source.size > MAX_INSTALLER_BYTES ||
103
+ typeof source.sha !== "string" ||
104
+ !/^[a-f0-9]{40}$/.test(source.sha) ||
105
+ typeof source.content !== "string"
106
+ )
107
+ throw new Error("Invalid tagged installer source metadata");
108
+ const script = Buffer.from(source.content, "base64");
109
+ const blobSha = createHash("sha1").update(`blob ${script.length}\0`).update(script).digest("hex");
110
+ if (script.length !== source.size || blobSha !== source.sha)
111
+ throw new Error("Tagged installer source does not match its Git blob identity");
112
+
113
+ await (options.runInstaller ?? runInstaller)({
114
+ script,
115
+ installDir: path.posix.dirname(options.targetPath),
116
+ version,
117
+ });
118
+ }
@@ -5,6 +5,7 @@ import path from "node:path";
5
5
  import { pipeline } from "node:stream/promises";
6
6
  import { APP_NAME, isEnoent, VERSION } from "@f5-sales-demo/pi-utils";
7
7
  import chalk from "chalk";
8
+ import { type LinuxInstallerInput, updateLinuxStandalone } from "./linux-standalone-update";
8
9
  import {
9
10
  createInstallReceipt,
10
11
  defaultInstallChannelDependencies,
@@ -76,6 +77,7 @@ export interface StandaloneReplacementOptions {
76
77
  writeFile?(file: string, contents: string): Promise<void>;
77
78
  unlink?(file: string): Promise<void>;
78
79
  validate?(executable: string, expectedVersion: string): Promise<void>;
80
+ runLinuxInstaller?(input: LinuxInstallerInput): Promise<void>;
79
81
  }
80
82
 
81
83
  async function ignoreMissing(unlink: (file: string) => Promise<void>, file: string): Promise<void> {
@@ -94,6 +96,15 @@ export async function replaceStandaloneExecutable(
94
96
  const targetPath = options.targetPath;
95
97
  const platform = options.platform ?? process.platform;
96
98
  const arch = options.arch ?? process.arch;
99
+ if (platform === "linux") {
100
+ if (arch !== "x64" && arch !== "arm64") throw new Error("Unsupported Linux standalone update architecture");
101
+ return updateLinuxStandalone(expectedVersion, {
102
+ targetPath,
103
+ arch,
104
+ fetchImpl: options.fetchImpl,
105
+ runInstaller: options.runLinuxInstaller,
106
+ });
107
+ }
97
108
  const binaryName = getBinaryName(platform, arch);
98
109
  const url = `https://github.com/${REPO}/releases/download/v${expectedVersion}/${binaryName}`;
99
110
  const tempPath = `${targetPath}.new-${process.pid}`;
package/src/cli.ts CHANGED
@@ -5,11 +5,14 @@ import { APP_NAME, initI18n, MIN_BUN_VERSION, registerLocales, t, VERSION } from
5
5
  * lightweight CLI runner from pi-utils.
6
6
  */
7
7
  import { CliUsageError, type CommandEntry, run } from "@f5-sales-demo/pi-utils/cli";
8
+ import { installCliOutputHandlers, writeCliOutput } from "@f5-sales-demo/pi-utils/cli-output";
8
9
  import { validateInlineFlagSyntax } from "./cli/flag-spec";
9
10
  import { findPrefixedCommand, launchFlagScopeMessage } from "./cli/root-command-routing";
10
11
  import { sandboxArgs, sandboxFlags, validateSandboxInvocation } from "./cli/sandbox-spec";
11
12
  import { locales } from "./locales/index";
12
13
 
14
+ installCliOutputHandlers();
15
+
13
16
  registerLocales(locales);
14
17
  initI18n();
15
18
 
@@ -97,7 +100,7 @@ async function showHelp(config: import("@f5-sales-demo/pi-utils/cli").CliConfig)
97
100
  renderRootHelp(config);
98
101
  const extra = getExtraHelpText();
99
102
  if (extra.trim().length > 0) {
100
- process.stdout.write(`\n${extra}\n`);
103
+ writeCliOutput(process.stdout, `\n${extra}\n`);
101
104
  }
102
105
  }
103
106
 
@@ -679,13 +679,32 @@ export async function discoverAndLoadExtensions(
679
679
  addPaths(await logger.time("ext:pluginPaths", () => getAllPluginExtensionPaths(cwd)));
680
680
 
681
681
  // 2b. Discover extension entry points from marketplace-cached plugins
682
+ const marketplaceDiagnostics: Array<{ path: string; error: string }> = [];
682
683
  await logger.time("ext:marketplaceRoots", async () => {
683
684
  for (const root of getPreloadedPluginRoots()) {
684
685
  try {
685
686
  const rootPath = await fs.realpath(root.path);
686
- const manifestPath = path.join(rootPath, ".xcsh-plugin", "plugin.json");
687
- const manifest = await Bun.file(manifestPath).json();
688
- const extensions = manifest?.extensions;
687
+ const canonicalPath = path.join(rootPath, ".xcsh-plugin", "plugin.json");
688
+ let canonical: { extensions?: unknown; lifecycle?: { integrations?: unknown } } | undefined;
689
+ try {
690
+ canonical = await Bun.file(canonicalPath).json();
691
+ } catch {
692
+ // A marketplace plugin may use package metadata only.
693
+ }
694
+
695
+ // The canonical manifest owns an explicitly declared extension list. Older marketplace
696
+ // plugins keep lifecycle metadata in that manifest and their extension declaration in
697
+ // package.json, so use the established package contract only when it is omitted.
698
+ let extensions = canonical?.extensions;
699
+ let declarationPath = canonicalPath;
700
+ if (extensions === undefined) {
701
+ const packagePath = path.join(rootPath, "package.json");
702
+ const pkg = await Bun.file(packagePath).json();
703
+ extensions = pkg?.xcsh?.extensions ?? pkg?.pi?.extensions;
704
+ declarationPath = packagePath;
705
+ }
706
+
707
+ let loadedEntries = 0;
689
708
  if (Array.isArray(extensions)) {
690
709
  for (const entry of extensions) {
691
710
  if (typeof entry !== "string") continue;
@@ -699,10 +718,22 @@ export async function discoverAndLoadExtensions(
699
718
  if (!resolved.startsWith(rootPath + path.sep) && resolved !== rootPath) continue;
700
719
  if (isDisabledName(getExtensionNameFromPath(resolved))) continue;
701
720
  addPath(resolved);
721
+ loadedEntries++;
702
722
  }
703
723
  }
704
- } catch {
705
- // No plugin manifest, invalid manifest, or missing entry point — skip
724
+
725
+ const integrations = canonical?.lifecycle?.integrations;
726
+ if (Array.isArray(integrations) && integrations.some(id => typeof id === "string") && loadedEntries === 0) {
727
+ marketplaceDiagnostics.push({
728
+ path: root.path,
729
+ error: `Marketplace plugin ${root.plugin} declares lifecycle integrations but no extension entrypoint could be loaded from ${declarationPath}`,
730
+ });
731
+ }
732
+ } catch (error) {
733
+ marketplaceDiagnostics.push({
734
+ path: root.path,
735
+ error: `Failed to read marketplace extension metadata: ${error instanceof Error ? error.message : String(error)}`,
736
+ });
706
737
  }
707
738
  }
708
739
  });
@@ -737,6 +768,7 @@ export async function discoverAndLoadExtensions(
737
768
 
738
769
  const resolvedEventBus = eventBus ?? new EventBus();
739
770
  const result = await logger.time("ext:loadLoop", () => loadExtensions(allPaths, cwd, resolvedEventBus));
771
+ result.errors.push(...marketplaceDiagnostics);
740
772
  await logger.time("ext:loadBundled", () => loadBundledExtensions(result, cwd, resolvedEventBus, isDisabledName));
741
773
  return result;
742
774
  }