@aliou/pi-ts-aperture 0.1.0 → 0.2.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.
@@ -57,14 +57,10 @@ jobs:
57
57
 
58
58
  let title = 'Version Packages';
59
59
  let commit = 'Version Packages';
60
- if (releases.length === 1) {
61
- const { name, newVersion } = releases[0];
62
- title = 'Updating ' + name + ' to version ' + newVersion;
63
- commit = name + '@' + newVersion;
64
- } else if (releases.length > 1) {
65
- const summary = releases.map(r => r.name + '@' + r.newVersion).join(', ');
66
- title = 'Updating ' + summary;
67
- commit = summary;
60
+ if (releases.length >= 1) {
61
+ const version = releases[0].newVersion;
62
+ title = version;
63
+ commit = version;
68
64
  }
69
65
 
70
66
  fs.appendFileSync(process.env.GITHUB_OUTPUT, 'title=' + title + '\n');
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @aliou/pi-ts-aperture
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 926f0a9: Improve `/aperture:setup` provider and connectivity flow.
8
+
9
+ - Add URL health check during setup (`/v1/models`) before provider selection, with retry/cancel UX.
10
+ - Build provider choices from Pi's runtime model registry so extension-registered providers (for example `pi-synthetic`) appear in the setup list.
11
+
12
+ ### Patch Changes
13
+
14
+ - 2263fc2: mark pi SDK peer deps as optional to prevent koffi OOM in Gondolin VMs
15
+
3
16
  ## 0.1.0
4
17
 
5
18
  ### Minor Changes
package/README.md CHANGED
@@ -7,7 +7,7 @@ Aperture handles API key injection and request routing server-side. This extensi
7
7
  ## Setup
8
8
 
9
9
  ```bash
10
- pi install @aliou/pi-ts-aperture
10
+ pi install npm:@aliou/pi-ts-aperture
11
11
  ```
12
12
 
13
13
  Then run the setup wizard:
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aliou/pi-ts-aperture",
3
3
  "description": "Route Pi LLM providers through Tailscale Aperture",
4
- "version": "0.1.0",
4
+ "version": "0.2.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/aliou/pi-ts-aperture"
@@ -36,6 +36,14 @@
36
36
  "husky": "^9.1.7",
37
37
  "typescript": "^5.9.3"
38
38
  },
39
+ "peerDependenciesMeta": {
40
+ "@mariozechner/pi-coding-agent": {
41
+ "optional": true
42
+ },
43
+ "@mariozechner/pi-ai": {
44
+ "optional": true
45
+ }
46
+ },
39
47
  "scripts": {
40
48
  "typecheck": "tsc --noEmit",
41
49
  "lint": "biome check",
@@ -8,15 +8,15 @@
8
8
  */
9
9
 
10
10
  import { FuzzySelector } from "@aliou/pi-utils-settings";
11
- import { getProviders } from "@mariozechner/pi-ai";
12
11
  import type {
13
12
  ExtensionAPI,
14
13
  ExtensionContext,
15
14
  } from "@mariozechner/pi-coding-agent";
16
15
  import { getSettingsListTheme } from "@mariozechner/pi-coding-agent";
17
- import type { Component } from "@mariozechner/pi-tui";
16
+ import type { Component, TUI } from "@mariozechner/pi-tui";
18
17
  import { Input, Key, matchesKey } from "@mariozechner/pi-tui";
19
18
  import { configLoader } from "../config";
19
+ import { checkApertureHealth } from "../lib/health";
20
20
 
21
21
  function normalizeUrl(url: string): string {
22
22
  let result = url.trim();
@@ -27,6 +27,102 @@ function normalizeUrl(url: string): string {
27
27
  return result.replace(/\/v1\/?$/, "").replace(/\/+$/, "");
28
28
  }
29
29
 
30
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
31
+
32
+ /**
33
+ * Shows a spinner while verifying the Aperture URL is reachable.
34
+ * Kicks off the health check on construction and resolves done(boolean)
35
+ * when the check completes.
36
+ */
37
+ class HealthCheckSpinner implements Component {
38
+ private theme: ReturnType<typeof getSettingsListTheme>;
39
+ private url: string;
40
+ private tui: TUI;
41
+ // true = healthy, false = failed (retry), undefined = cancelled
42
+ private done: (result: boolean | undefined) => void;
43
+ private frame = 0;
44
+ private timer: ReturnType<typeof setInterval>;
45
+ private result: { ok: boolean; error?: string } | null = null;
46
+
47
+ constructor(
48
+ theme: ReturnType<typeof getSettingsListTheme>,
49
+ url: string,
50
+ tui: TUI,
51
+ done: (result: boolean | undefined) => void,
52
+ ) {
53
+ this.theme = theme;
54
+ this.url = url;
55
+ this.tui = tui;
56
+ this.done = done;
57
+
58
+ // Animate spinner at ~80ms per frame.
59
+ this.timer = setInterval(() => {
60
+ this.frame = (this.frame + 1) % SPINNER_FRAMES.length;
61
+ this.tui.requestRender();
62
+ }, 80);
63
+
64
+ // Fire the check.
65
+ checkApertureHealth(url).then((res) => {
66
+ clearInterval(this.timer);
67
+ this.result = res;
68
+ this.tui.requestRender();
69
+
70
+ // On success, auto-advance after a brief pause.
71
+ // On failure, wait for user input (see handleInput).
72
+ if (res.ok) {
73
+ setTimeout(() => this.done(true), 600);
74
+ }
75
+ });
76
+ }
77
+
78
+ render(_width: number): string[] {
79
+ const lines: string[] = [];
80
+ lines.push(this.theme.label(" Aperture Setup", true));
81
+ lines.push("");
82
+
83
+ if (!this.result) {
84
+ const spinner = SPINNER_FRAMES[this.frame];
85
+ lines.push(
86
+ this.theme.hint(` ${spinner} Checking connection to ${this.url}...`),
87
+ );
88
+ } else if (this.result.ok) {
89
+ lines.push(this.theme.hint(` Connected to ${this.url}`));
90
+ } else {
91
+ lines.push(
92
+ this.theme.hint(` Could not reach ${this.url}: ${this.result.error}`),
93
+ );
94
+ lines.push("");
95
+ lines.push(
96
+ this.theme.hint(
97
+ " Make sure the URL is correct and you are connected to the tailnet.",
98
+ ),
99
+ );
100
+ lines.push("");
101
+ lines.push(this.theme.hint(" Enter: try another URL · Esc: cancel"));
102
+ }
103
+
104
+ lines.push("");
105
+ return lines;
106
+ }
107
+
108
+ invalidate() {}
109
+
110
+ handleInput(data: string) {
111
+ // Only handle input after a failed check.
112
+ if (!this.result || this.result.ok) return;
113
+
114
+ if (matchesKey(data, Key.enter)) {
115
+ this.done(false); // retry
116
+ } else if (matchesKey(data, Key.escape)) {
117
+ this.done(undefined); // cancel wizard
118
+ }
119
+ }
120
+
121
+ dispose() {
122
+ clearInterval(this.timer);
123
+ }
124
+ }
125
+
30
126
  /**
31
127
  * Simple input prompt component for the base URL step.
32
128
  */
@@ -171,17 +267,39 @@ export function registerSetupCommand(
171
267
  const config = configLoader.getConfig();
172
268
  const settingsTheme = getSettingsListTheme();
173
269
 
174
- // Step 1: base URL
175
- const baseUrl = await ctx.ui.custom<string | undefined>(
176
- (_tui, _theme, _kb, done) => {
177
- return new UrlPrompt(settingsTheme, config.baseUrl, done);
178
- },
179
- );
270
+ // Step 1: base URL + health check loop.
271
+ // On failure, loop back to the URL prompt so the user can retry.
272
+ let baseUrl: string | undefined;
273
+ while (true) {
274
+ baseUrl = await ctx.ui.custom<string | undefined>(
275
+ (_tui, _theme, _kb, done) => {
276
+ return new UrlPrompt(
277
+ settingsTheme,
278
+ baseUrl ?? config.baseUrl,
279
+ done,
280
+ );
281
+ },
282
+ );
283
+
284
+ if (!baseUrl) return;
285
+ const urlToCheck = baseUrl;
286
+
287
+ const result = await ctx.ui.custom<boolean | undefined>(
288
+ (tui, _theme, _kb, done) => {
289
+ return new HealthCheckSpinner(settingsTheme, urlToCheck, tui, done);
290
+ },
291
+ );
180
292
 
181
- if (!baseUrl) return;
293
+ if (result === true) break; // healthy, proceed
294
+ if (result === undefined) return; // cancelled
295
+ }
182
296
 
183
297
  // Step 2: select providers
184
- const knownProviders = getProviders();
298
+ // Use model registry so custom/extension providers are included.
299
+ const knownProviders = Array.from(
300
+ new Set(ctx.modelRegistry.getAll().map((model) => model.provider)),
301
+ ).sort((a, b) => a.localeCompare(b));
302
+
185
303
  const providers = await ctx.ui.custom<string[] | undefined>(
186
304
  (_tui, _theme, _kb, done) => {
187
305
  return new ProviderMultiSelect(
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Health check for the Aperture gateway.
3
+ *
4
+ * Hits GET <baseUrl>/v1/models to verify the gateway is reachable.
5
+ * Uses native fetch (no extra dependencies).
6
+ */
7
+
8
+ export interface HealthCheckResult {
9
+ ok: boolean;
10
+ error?: string;
11
+ }
12
+
13
+ export async function checkApertureHealth(
14
+ baseUrl: string,
15
+ ): Promise<HealthCheckResult> {
16
+ const url = `${baseUrl.replace(/\/+$/, "")}/v1/models`;
17
+ try {
18
+ const res = await fetch(url, {
19
+ method: "GET",
20
+ signal: AbortSignal.timeout(5000),
21
+ });
22
+ if (!res.ok) {
23
+ return { ok: false, error: `HTTP ${res.status} ${res.statusText}` };
24
+ }
25
+ return { ok: true };
26
+ } catch (e: unknown) {
27
+ const msg = e instanceof Error ? e.message : String(e);
28
+ return { ok: false, error: msg };
29
+ }
30
+ }