pi-runline 0.5.1 → 0.5.2

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.
@@ -75,7 +75,9 @@ export async function promptForCredentials(
75
75
  runlineDir: string,
76
76
  plugins: PluginSummary[],
77
77
  newlyEnabled: string[],
78
+ options: { force?: boolean } = {},
78
79
  ): Promise<string[]> {
80
+ const force = options.force === true;
79
81
  const config = readConfig(runlineDir);
80
82
  const connections = getConnections(config);
81
83
  const saved: string[] = [];
@@ -87,16 +89,17 @@ export async function promptForCredentials(
87
89
  const schema = plugin.connectionConfigSchema;
88
90
  if (isSchemaEmpty(schema)) continue; // no creds needed
89
91
 
90
- if (connectionFor(connections, name)) continue; // already configured
92
+ if (!force && connectionFor(connections, name)) continue; // already configured
91
93
 
92
94
  // Check env — if every required field has an env var set, skip the prompt.
95
+ // Skipped on `force` (the user explicitly asked to re-enter values).
93
96
  const requiredFields = Object.entries(schema!).filter(
94
97
  ([, f]) => f.required,
95
98
  );
96
99
  const allFromEnv = requiredFields.every(
97
100
  ([, f]) => f.env && process.env[f.env],
98
101
  );
99
- if (requiredFields.length > 0 && allFromEnv) continue;
102
+ if (!force && requiredFields.length > 0 && allFromEnv) continue;
100
103
 
101
104
  const wantSetup = await ctx.ui.confirm(
102
105
  `Set up ${name}?`,
@@ -137,7 +140,12 @@ export async function promptForCredentials(
137
140
  plugin: name,
138
141
  config: values,
139
142
  };
140
- connections.push(conn);
143
+ const existingIdx = connections.findIndex((c) => c.plugin === name);
144
+ if (existingIdx >= 0) {
145
+ connections[existingIdx] = conn;
146
+ } else {
147
+ connections.push(conn);
148
+ }
141
149
  saved.push(name);
142
150
  }
143
151
 
@@ -1,15 +1,25 @@
1
1
  import type { Theme } from "@mariozechner/pi-coding-agent";
2
2
  import type { Component, TUI } from "@mariozechner/pi-tui";
3
- import { fuzzyFilter, Input, visibleWidth } from "@mariozechner/pi-tui";
3
+ import {
4
+ fuzzyFilter,
5
+ Input,
6
+ Key,
7
+ matchesKey,
8
+ visibleWidth,
9
+ } from "@mariozechner/pi-tui";
4
10
 
5
11
  export interface PluginPickerItem {
6
12
  name: string;
7
13
  actionCount: number;
14
+ /** Plugin already has a stored connection — eligible for Ctrl-R reconfigure. */
15
+ connected?: boolean;
8
16
  }
9
17
 
10
18
  export interface PluginPickerResult {
11
19
  /** undefined = cancelled */
12
20
  selected?: string[];
21
+ /** Set when the user pressed Ctrl-R on a connected plugin. */
22
+ reconfigure?: string;
13
23
  }
14
24
 
15
25
  /**
@@ -18,7 +28,7 @@ export interface PluginPickerResult {
18
28
  * Keys:
19
29
  * ↑ / ↓ — move highlight
20
30
  * space — toggle current item
21
- * Ctrl-A toggle all (filtered view)
31
+ * alt-r reconfigure highlighted plugin (when connected)
22
32
  * enter — save and close
23
33
  * esc / C-c — cancel
24
34
  * type — fuzzy filter
@@ -70,7 +80,7 @@ export class PluginPicker implements Component {
70
80
  body.push(
71
81
  theme.fg(
72
82
  "dim",
73
- "type to filter · space toggle · ^A toggle all · enter save · esc cancel",
83
+ "type to filter · space toggle · alt+r reconfigure · enter save · esc cancel",
74
84
  ),
75
85
  );
76
86
  body.push("");
@@ -106,9 +116,12 @@ export class PluginPicker implements Component {
106
116
  ? theme.fg("success", box)
107
117
  : theme.fg("dim", box);
108
118
  const name = isCur ? theme.bold(item.name) : item.name;
119
+ const connectedTag = item.connected
120
+ ? theme.fg("success", " • connected")
121
+ : "";
109
122
  const count = theme.fg("dim", ` ${item.actionCount} actions`);
110
123
  const arrow = isCur ? theme.fg("accent", "❯ ") : " ";
111
- body.push(`${arrow}${boxColored} ${name}${count}`);
124
+ body.push(`${arrow}${boxColored} ${name}${connectedTag}${count}`);
112
125
  }
113
126
  for (let i = end - start; i < this.maxRows; i++) body.push("");
114
127
 
@@ -168,12 +181,13 @@ export class PluginPicker implements Component {
168
181
  }
169
182
  return;
170
183
  }
171
- if (data === "\x01") {
172
- // Ctrl-Atoggle all visible
173
- const allSelected = this.filtered.every((i) => this.selected.has(i.name));
174
- for (const i of this.filtered) {
175
- if (allSelected) this.selected.delete(i.name);
176
- else this.selected.add(i.name);
184
+ if (matchesKey(data, Key.alt("r"))) {
185
+ // Alt-Rreconfigure the highlighted plugin if it already has
186
+ // saved credentials. No-op otherwise. (Ctrl-R is reserved by pi
187
+ // for `app.session.rename` and never reaches handleInput.)
188
+ const item = this.filtered[this.cursor];
189
+ if (item?.connected) {
190
+ this.onDone({ reconfigure: item.name });
177
191
  }
178
192
  return;
179
193
  }
@@ -1,11 +1,12 @@
1
1
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
2
  import { Markdown, Text } from "@mariozechner/pi-tui";
3
3
  import { Type } from "@sinclair/typebox";
4
- import { Runline } from "runline";
4
+ import { discoverPlugins, Runline } from "runline";
5
5
  import { promptForCredentials } from "../connection-setup.js";
6
6
  import { createPluginPickerFactory } from "../plugin-picker.js";
7
7
  import {
8
8
  findRunlineDir,
9
+ getConnectedPluginNames,
9
10
  loadExtConfig,
10
11
  savePiPlugins,
11
12
  } from "../runline-resolve.js";
@@ -228,20 +229,26 @@ export default function (pi: ExtensionAPI) {
228
229
  return;
229
230
  }
230
231
 
231
- let rl: Runline;
232
+ // Load the full bundled catalog directly — `Runline.fromProject`
233
+ // gates builtins by `connections[].plugin`, which is the wrong
234
+ // surface for the picker (the picker is HOW you decide which
235
+ // plugins to enable in the first place).
236
+ let allPlugins: Awaited<ReturnType<typeof discoverPlugins>>;
232
237
  try {
233
- rl = await getRunline(ctx.cwd);
238
+ allPlugins = await discoverPlugins(runlineDir);
234
239
  } catch (err) {
235
240
  ctx.ui.notify(
236
- `runline failed to load: ${(err as Error).message}`,
241
+ `runline failed to load plugins: ${(err as Error).message}`,
237
242
  "error",
238
243
  );
239
244
  return;
240
245
  }
241
246
 
242
- const items = rl.plugins().map((p) => ({
247
+ const connectedNames = getConnectedPluginNames(runlineDir);
248
+ const items = allPlugins.map((p) => ({
243
249
  name: p.name,
244
250
  actionCount: p.actions.length,
251
+ connected: connectedNames.has(p.name),
245
252
  }));
246
253
  const { piPlugins } = loadExtConfig(runlineDir);
247
254
  const initial = piPlugins ?? [];
@@ -251,6 +258,27 @@ export default function (pi: ExtensionAPI) {
251
258
  { overlay: true, overlayOptions: { width: "80%", maxHeight: "80%" } },
252
259
  );
253
260
 
261
+ // Ctrl-R inside the picker — reconfigure a single plugin and stop.
262
+ // Selection state isn't saved (user didn't press enter); they can
263
+ // re-open `/runline-plugins` to make selection changes.
264
+ if (result.reconfigure) {
265
+ const target = result.reconfigure;
266
+ const updated = await promptForCredentials(
267
+ ctx,
268
+ runlineDir,
269
+ allPlugins,
270
+ [target],
271
+ { force: true },
272
+ );
273
+ ctx.ui.notify(
274
+ updated.length > 0
275
+ ? `credentials updated for ${target}`
276
+ : `reconfigure cancelled for ${target}`,
277
+ "info",
278
+ );
279
+ return;
280
+ }
281
+
254
282
  if (!result.selected) {
255
283
  ctx.ui.notify("plugin selection cancelled", "info");
256
284
  return;
@@ -270,7 +298,7 @@ export default function (pi: ExtensionAPI) {
270
298
  const saved = await promptForCredentials(
271
299
  ctx,
272
300
  runlineDir,
273
- rl.plugins(),
301
+ allPlugins,
274
302
  newlyEnabled,
275
303
  );
276
304
  if (saved.length > 0) {
@@ -60,6 +60,25 @@ export function loadExtConfig(runlineDir: string): RunlineExtConfig {
60
60
  }
61
61
  }
62
62
 
63
+ /** Plugin names that already have a saved connection in .runline/config.json */
64
+ export function getConnectedPluginNames(runlineDir: string): Set<string> {
65
+ const configPath = path.join(runlineDir, "config.json");
66
+ if (!fs.existsSync(configPath)) return new Set();
67
+ try {
68
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
69
+ const conns = Array.isArray(raw.connections) ? raw.connections : [];
70
+ return new Set(
71
+ conns
72
+ .map((c: { plugin?: unknown }) =>
73
+ typeof c.plugin === "string" ? c.plugin : null,
74
+ )
75
+ .filter((n: string | null): n is string => n !== null),
76
+ );
77
+ } catch {
78
+ return new Set();
79
+ }
80
+ }
81
+
63
82
  export function savePiPlugins(runlineDir: string, piPlugins: string[]): void {
64
83
  const configPath = path.join(runlineDir, "config.json");
65
84
  let raw: Record<string, unknown> = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-runline",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "Code mode for pi",
5
5
  "type": "commonjs",
6
6
  "keywords": [
@@ -22,7 +22,7 @@
22
22
  "lint:fix": "biome check --write extensions/"
23
23
  },
24
24
  "dependencies": {
25
- "runline": "^0.5.1"
25
+ "runline": "^0.5.2"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "@mariozechner/pi-coding-agent": "*",