pi-ui-extend 1.0.2 → 1.0.4

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/README.md CHANGED
@@ -439,7 +439,7 @@ cd /path/to/project
439
439
  pix
440
440
  ```
441
441
 
442
- `/idx-update` updates the globally installed `indexer-cli`; it does not refresh a project's index.
442
+ When `indexer-cli` is installed and available on `PATH`, Pix checks it at startup and runs its official updater when needed. Pix silently skips this check when `idx` is absent. `/idx-update` remains available for a manual retry; neither flow refreshes a project's index.
443
443
 
444
444
  ### An extension behaves differently
445
445
 
package/dist/app/app.d.ts CHANGED
@@ -72,7 +72,7 @@ export declare class PiUiExtendApp {
72
72
  private applyPixConfig;
73
73
  private updateOutputFilters;
74
74
  start(): Promise<void>;
75
- private checkPixUpdateOnStartup;
75
+ private checkUpdatesOnStartup;
76
76
  private bindCurrentSession;
77
77
  private awaitCurrentSessionExtensions;
78
78
  private activateRuntime;
package/dist/app/app.js CHANGED
@@ -43,6 +43,7 @@ import { AppTerminalController } from "./terminal/terminal-controller.js";
43
43
  import { TerminalBellSoundController } from "./terminal/terminal-bell-sound-controller.js";
44
44
  import { AppToastController } from "./rendering/toast-controller.js";
45
45
  import { checkPiUpdate, checkPixUpdate, formatPixStartupUpdateDialog, formatPiStartupUpdateToast } from "./cli/update.js";
46
+ import { checkAndUpdateIdxOnStartup, formatIdxStartupUpdateNotice } from "./cli/startup-checks.js";
46
47
  import { AppVoiceController } from "./input/voice-controller.js";
47
48
  import { createIsolatedExtensionEventBus } from "./extensions/extension-event-bus.js";
48
49
  import { setAppIconTheme } from "./icons.js";
@@ -864,9 +865,21 @@ export class PiUiExtendApp {
864
865
  await this.sessionLifecycle.start();
865
866
  this.modelUsageController.startPolling();
866
867
  this.nerdFontController.ensureInstalledOnStartup();
867
- this.checkPixUpdateOnStartup();
868
+ this.checkUpdatesOnStartup();
868
869
  }
869
- checkPixUpdateOnStartup() {
870
+ checkUpdatesOnStartup() {
871
+ void checkAndUpdateIdxOnStartup()
872
+ .then((result) => {
873
+ if (!this.running)
874
+ return;
875
+ const notice = formatIdxStartupUpdateNotice(result);
876
+ if (!notice)
877
+ return;
878
+ this.showToast(notice, result.status === "updated" ? "success" : "warning");
879
+ })
880
+ .catch(() => {
881
+ // Startup update checks should never interrupt the TUI.
882
+ });
870
883
  void checkPiUpdate()
871
884
  .then((result) => {
872
885
  if (result.status !== "newer")
@@ -3,7 +3,29 @@ export type StartupAvailabilityIssue = {
3
3
  kind: "warning" | "error";
4
4
  message: string;
5
5
  };
6
+ export type IdxStartupUpdateStatus = "current" | "updated" | "checked" | "skipped" | "unavailable" | "failed";
7
+ export type IdxStartupUpdateResult = {
8
+ status: IdxStartupUpdateStatus;
9
+ previousVersion?: string;
10
+ currentVersion?: string;
11
+ reason?: string;
12
+ };
13
+ type IdxUpdateCommandResult = {
14
+ code: number | null;
15
+ signal: NodeJS.Signals | null;
16
+ stdout: string;
17
+ stderr: string;
18
+ timedOut?: boolean;
19
+ };
20
+ export type IdxStartupUpdateOptions = {
21
+ timeoutMs?: number;
22
+ env?: NodeJS.ProcessEnv;
23
+ runUpdate?: (timeoutMs: number, env: NodeJS.ProcessEnv) => Promise<IdxUpdateCommandResult>;
24
+ };
6
25
  export declare function collectStartupAvailabilityIssues(runtime: AgentSessionRuntime): Promise<StartupAvailabilityIssue[]>;
7
26
  export declare function checkSelectedModelAuthAvailability(runtime: AgentSessionRuntime): StartupAvailabilityIssue[];
8
27
  export declare function checkPiCliAvailability(pathValue?: string): Promise<StartupAvailabilityIssue[]>;
28
+ export declare function checkAndUpdateIdxOnStartup(options?: IdxStartupUpdateOptions): Promise<IdxStartupUpdateResult>;
29
+ export declare function formatIdxStartupUpdateNotice(result: IdxStartupUpdateResult): string | undefined;
9
30
  export declare function checkPiToolsSuiteExtensionAvailability(extensionsResult: LoadExtensionsResult): StartupAvailabilityIssue[];
31
+ export {};
@@ -1,8 +1,12 @@
1
+ import { spawn } from "node:child_process";
1
2
  import { access } from "node:fs/promises";
2
3
  import { delimiter, join } from "node:path";
3
4
  import { constants as fsConstants } from "node:fs";
4
5
  const PI_CLI_COMMAND = "pi";
5
6
  const PI_TOOLS_SUITE_EXTENSION_ID = "pi-tools-suite";
7
+ const IDX_CLI_COMMAND = "idx";
8
+ const IDX_UPDATE_TIMEOUT_MS = 600_000;
9
+ const MAX_IDX_UPDATE_OUTPUT_BYTES = 32_000;
6
10
  export async function collectStartupAvailabilityIssues(runtime) {
7
11
  return [
8
12
  ...(await checkPiCliAvailability()),
@@ -33,6 +37,60 @@ export async function checkPiCliAvailability(pathValue = process.env.PATH ?? "")
33
37
  message: "pi CLI is not available on PATH. Run `pix install` or add pi to PATH before starting pix.",
34
38
  }];
35
39
  }
40
+ export async function checkAndUpdateIdxOnStartup(options = {}) {
41
+ const env = options.env ?? process.env;
42
+ const disabledReason = startupVersionCheckDisabledReason(env);
43
+ if (disabledReason)
44
+ return { status: "skipped", reason: disabledReason };
45
+ if (!options.runUpdate && !(await executableExistsOnPath(IDX_CLI_COMMAND, env.PATH ?? ""))) {
46
+ return { status: "unavailable" };
47
+ }
48
+ let commandResult;
49
+ try {
50
+ commandResult = await (options.runUpdate ?? runIdxUpdate)(options.timeoutMs ?? IDX_UPDATE_TIMEOUT_MS, env);
51
+ }
52
+ catch (error) {
53
+ if (isCommandNotFoundError(error)) {
54
+ return { status: "unavailable" };
55
+ }
56
+ return { status: "failed", reason: errorMessage(error) };
57
+ }
58
+ const output = [commandResult.stdout, commandResult.stderr].filter(Boolean).join("\n").trim();
59
+ if (commandResult.timedOut) {
60
+ return { status: "failed", reason: compactCommandFailure("idx update timed out", output) };
61
+ }
62
+ if (commandResult.code !== 0) {
63
+ const termination = commandResult.signal
64
+ ? `idx update terminated by signal ${commandResult.signal}`
65
+ : `idx update exited with code ${commandResult.code ?? "unknown"}`;
66
+ return { status: "failed", reason: compactCommandFailure(termination, output) };
67
+ }
68
+ const updatedVersions = parseUpdatedIdxVersions(output);
69
+ if (updatedVersions)
70
+ return { status: "updated", ...updatedVersions };
71
+ const currentVersion = parseCurrentIdxVersion(output);
72
+ if (currentVersion)
73
+ return { status: "current", currentVersion };
74
+ return { status: "checked" };
75
+ }
76
+ export function formatIdxStartupUpdateNotice(result) {
77
+ switch (result.status) {
78
+ case "updated": {
79
+ if (!result.currentVersion)
80
+ return "idx was updated to the latest version.";
81
+ const previousVersion = result.previousVersion ? ` from ${result.previousVersion}` : "";
82
+ return `idx updated${previousVersion} to ${result.currentVersion}.`;
83
+ }
84
+ case "unavailable":
85
+ return undefined;
86
+ case "failed":
87
+ return `idx startup update failed: ${result.reason ?? "unknown error"}`;
88
+ case "current":
89
+ case "checked":
90
+ case "skipped":
91
+ return undefined;
92
+ }
93
+ }
36
94
  export function checkPiToolsSuiteExtensionAvailability(extensionsResult) {
37
95
  if (extensionsResult.extensions.some(isPiToolsSuiteExtension))
38
96
  return [];
@@ -64,6 +122,95 @@ async function executableExistsOnPath(command, pathValue) {
64
122
  }
65
123
  return false;
66
124
  }
125
+ async function runIdxUpdate(timeoutMs, env) {
126
+ return await new Promise((resolve, reject) => {
127
+ const invocation = idxUpdateInvocation(env);
128
+ const child = spawn(invocation.command, invocation.args, {
129
+ env,
130
+ stdio: ["ignore", "pipe", "pipe"],
131
+ windowsHide: true,
132
+ });
133
+ let stdout = "";
134
+ let stderr = "";
135
+ let settled = false;
136
+ let timedOut = false;
137
+ const timer = setTimeout(() => {
138
+ timedOut = true;
139
+ child.kill();
140
+ }, timeoutMs);
141
+ timer.unref();
142
+ child.stdout?.on("data", (chunk) => {
143
+ stdout = appendBoundedOutput(stdout, chunk.toString());
144
+ });
145
+ child.stderr?.on("data", (chunk) => {
146
+ stderr = appendBoundedOutput(stderr, chunk.toString());
147
+ });
148
+ child.on("error", (error) => {
149
+ if (settled)
150
+ return;
151
+ settled = true;
152
+ clearTimeout(timer);
153
+ reject(error);
154
+ });
155
+ child.on("close", (code, signal) => {
156
+ if (settled)
157
+ return;
158
+ settled = true;
159
+ clearTimeout(timer);
160
+ resolve({ code, signal, stdout, stderr, ...(timedOut ? { timedOut: true } : {}) });
161
+ });
162
+ });
163
+ }
164
+ function idxUpdateInvocation(env) {
165
+ if (process.platform !== "win32")
166
+ return { command: IDX_CLI_COMMAND, args: ["update"] };
167
+ const commandProcessor = env.ComSpec ?? env.COMSPEC ?? process.env.ComSpec ?? process.env.COMSPEC ?? "cmd.exe";
168
+ return { command: commandProcessor, args: ["/d", "/s", "/c", `${IDX_CLI_COMMAND} update`] };
169
+ }
170
+ function appendBoundedOutput(existing, addition) {
171
+ const combined = `${existing}${addition}`;
172
+ if (Buffer.byteLength(combined, "utf8") <= MAX_IDX_UPDATE_OUTPUT_BYTES)
173
+ return combined;
174
+ return Buffer.from(combined, "utf8").subarray(-MAX_IDX_UPDATE_OUTPUT_BYTES).toString("utf8").replace(/^\uFFFD/u, "");
175
+ }
176
+ function parseUpdatedIdxVersions(output) {
177
+ const match = output.match(/(?:Updating|Updated) indexer-cli\s+v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\s*(?:→|->)\s*v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/iu);
178
+ const previousVersion = match?.[1];
179
+ const currentVersion = match?.[2];
180
+ if (!previousVersion || !currentVersion)
181
+ return undefined;
182
+ return { previousVersion, currentVersion };
183
+ }
184
+ function parseCurrentIdxVersion(output) {
185
+ return output.match(/already up to date\s*\(v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\)/iu)?.[1];
186
+ }
187
+ function startupVersionCheckDisabledReason(env) {
188
+ if (truthyEnv(env.PI_OFFLINE))
189
+ return "PI_OFFLINE is set";
190
+ if (truthyEnv(env.PI_SKIP_VERSION_CHECK))
191
+ return "PI_SKIP_VERSION_CHECK is set";
192
+ if (truthyEnv(env.PIX_SKIP_VERSION_CHECK))
193
+ return "PIX_SKIP_VERSION_CHECK is set";
194
+ return undefined;
195
+ }
196
+ function truthyEnv(value) {
197
+ if (!value)
198
+ return false;
199
+ return !["0", "false", "no", "off"].includes(value.toLowerCase());
200
+ }
201
+ function isCommandNotFoundError(error) {
202
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
203
+ }
204
+ function compactCommandFailure(prefix, output) {
205
+ if (!output)
206
+ return prefix;
207
+ const singleLine = output.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).slice(-4).join(" | ");
208
+ const summary = singleLine.length > 900 ? `${singleLine.slice(0, 897)}...` : singleLine;
209
+ return `${prefix}: ${summary}`;
210
+ }
211
+ function errorMessage(error) {
212
+ return error instanceof Error ? error.message : String(error);
213
+ }
67
214
  function isPiToolsSuiteExtension(extension) {
68
215
  return [
69
216
  extension.path,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ui-extend",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Pix: a workspace-first terminal UI for Pi with tabs, readable tool activity, voice input, and bundled agent tools.",
5
5
  "private": false,
6
6
  "repository": {