@jmcombs/pi-1password 1.0.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/LICENSE +21 -0
- package/README.md +143 -0
- package/data/shell-plugins.json +450 -0
- package/index.ts +853 -0
- package/package.json +53 -0
- package/ui/bordered-popups.ts +318 -0
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jmcombs/pi-1password",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "1Password integration for the Pi coding agent. Read secrets and run commands with 1Password credential injection via the op CLI.",
|
|
5
|
+
"homepage": "https://github.com/jmcombs/pi-extensions/tree/main/packages/1password",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/jmcombs/pi-extensions.git",
|
|
9
|
+
"directory": "packages/1password"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/jmcombs/pi-extensions/issues"
|
|
13
|
+
},
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "Jeremy Combs",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "./index.ts",
|
|
18
|
+
"types": "./index.ts",
|
|
19
|
+
"files": [
|
|
20
|
+
"index.ts",
|
|
21
|
+
"data/",
|
|
22
|
+
"ui/",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"keywords": [
|
|
27
|
+
"pi-package",
|
|
28
|
+
"pi-extension",
|
|
29
|
+
"1password",
|
|
30
|
+
"op",
|
|
31
|
+
"credentials",
|
|
32
|
+
"secrets"
|
|
33
|
+
],
|
|
34
|
+
"pi": {
|
|
35
|
+
"extensions": [
|
|
36
|
+
"./index.ts"
|
|
37
|
+
],
|
|
38
|
+
"image": "https://raw.githubusercontent.com/jmcombs/pi-extensions/main/assets/1password/preview.png"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=22.0.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
45
|
+
"@earendil-works/pi-tui": "*",
|
|
46
|
+
"typebox": "*"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@earendil-works/pi-coding-agent": "^0.75.4",
|
|
50
|
+
"@earendil-works/pi-tui": "*",
|
|
51
|
+
"typebox": "^1.1.37"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bordered Popup TUI Helpers
|
|
3
|
+
*
|
|
4
|
+
* These are reusable, self-contained helpers for creating polished,
|
|
5
|
+
* consistent bordered popups inside Pi extensions using `ctx.ui.custom({ overlay: true })`.
|
|
6
|
+
*
|
|
7
|
+
* They were originally developed in the 1Password extension and are provided
|
|
8
|
+
* here as a copy-paste starting point for any extension that needs rich
|
|
9
|
+
* interactive flows (select lists with live filtering, text input, confirms)
|
|
10
|
+
* that look and feel better than the basic `ctx.ui.select / input / confirm`.
|
|
11
|
+
*
|
|
12
|
+
* ## Usage
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* import {
|
|
16
|
+
* selectInBorderedPopup,
|
|
17
|
+
* confirmInBorderedPopup,
|
|
18
|
+
* inputInBorderedPopup,
|
|
19
|
+
* } from "./ui/bordered-popups.js";
|
|
20
|
+
*
|
|
21
|
+
* const choice = await selectInBorderedPopup(ctx, {
|
|
22
|
+
* title: "Select something",
|
|
23
|
+
* items: [...],
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* const confirmed = await confirmInBorderedPopup(ctx, {
|
|
27
|
+
* title: "Are you sure?",
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* const name = await inputInBorderedPopup(ctx, {
|
|
31
|
+
* title: "Enter name",
|
|
32
|
+
* prompt: "What should we call it?",
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* The helpers automatically handle:
|
|
37
|
+
* - Consistent 4-sided borders (╭─╮│╰╯) with stable right edge
|
|
38
|
+
* - Proper ANSI-aware padding using Pi's `truncateToWidth`
|
|
39
|
+
* - Live filtering on long lists
|
|
40
|
+
* - Back navigation ("← Go back") and Esc-to-cancel semantics
|
|
41
|
+
* - Theming consistent with the rest of Pi
|
|
42
|
+
*
|
|
43
|
+
* ## When to use
|
|
44
|
+
*
|
|
45
|
+
* Use these when you have:
|
|
46
|
+
* - Long lists that benefit from filtering
|
|
47
|
+
* - Multi-step wizards
|
|
48
|
+
* - Situations where the basic Pi UI dialogs feel too plain
|
|
49
|
+
*
|
|
50
|
+
* For very simple one-off prompts, the built-in `ctx.ui.select/input/confirm`
|
|
51
|
+
* are still perfectly acceptable and require less code.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
import type { ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
55
|
+
|
|
56
|
+
// No static imports from @earendil-works/pi-tui are used for types.
|
|
57
|
+
// We rely on inference for ctx.ui.custom callback parameters (sourced via the
|
|
58
|
+
// pi-coding-agent peer) and a minimal local facade for the runtime values
|
|
59
|
+
// obtained via dynamic import. This avoids duplicate module declarations
|
|
60
|
+
// (pi-tui types nested inside coding-agent vs. direct peer) that would
|
|
61
|
+
// otherwise break tsc strict under the project's monorepo layout.
|
|
62
|
+
|
|
63
|
+
/** Internal helper to render a consistent bordered box. */
|
|
64
|
+
export function renderBorderedBox(
|
|
65
|
+
width: number,
|
|
66
|
+
title: string,
|
|
67
|
+
bodyLines: string[],
|
|
68
|
+
footer: string | undefined,
|
|
69
|
+
theme: Pick<Theme, "fg" | "bold">,
|
|
70
|
+
truncateToWidthFn: (s: string, w: number, e?: string, pad?: boolean) => string,
|
|
71
|
+
): string[] {
|
|
72
|
+
const innerWidth = Math.max(20, width - 4);
|
|
73
|
+
const top = theme.fg("accent", "╭" + "─".repeat(width - 2) + "╮");
|
|
74
|
+
const bottom = theme.fg("accent", "╰" + "─".repeat(width - 2) + "╯");
|
|
75
|
+
|
|
76
|
+
const rawTitle = theme.fg("accent", theme.bold(title));
|
|
77
|
+
const titlePadded = truncateToWidthFn(rawTitle, innerWidth, "", true);
|
|
78
|
+
const borderedTitle = theme.fg("accent", "│ ") + titlePadded + theme.fg("accent", " │");
|
|
79
|
+
|
|
80
|
+
const borderedBody = bodyLines.map((line) => {
|
|
81
|
+
const padded = truncateToWidthFn(line || "", innerWidth, "", true);
|
|
82
|
+
return theme.fg("accent", "│ ") + padded + theme.fg("accent", " │");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const lines = [top, borderedTitle, ...borderedBody];
|
|
86
|
+
|
|
87
|
+
if (footer) {
|
|
88
|
+
const rawFooter = theme.fg("dim", footer);
|
|
89
|
+
const footerPadded = truncateToWidthFn(rawFooter, innerWidth, "", true);
|
|
90
|
+
lines.push(theme.fg("accent", "│ ") + footerPadded + theme.fg("accent", " │"));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
lines.push(bottom);
|
|
94
|
+
return lines;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* High-level helper for a filterable list inside a bordered popup.
|
|
99
|
+
* Returns the chosen `.value` or `null` (on cancel / Esc).
|
|
100
|
+
*/
|
|
101
|
+
export async function selectInBorderedPopup<T = string>(
|
|
102
|
+
ctx: ExtensionCommandContext,
|
|
103
|
+
opts: {
|
|
104
|
+
title: string;
|
|
105
|
+
items: { value: T; label: string; description?: string }[];
|
|
106
|
+
helpText?: string;
|
|
107
|
+
maxVisible?: number;
|
|
108
|
+
},
|
|
109
|
+
): Promise<T | null> {
|
|
110
|
+
const maxVis = opts.maxVisible ?? 14;
|
|
111
|
+
const help = opts.helpText ?? "↑↓ • Enter • Esc = cancel • Type to filter";
|
|
112
|
+
|
|
113
|
+
// Pure local interface (no `extends` of the real SelectList type) describing
|
|
114
|
+
// exactly the surface we use. Avoids pulling in conflicting .d.ts copies of
|
|
115
|
+
// pi-tui that exist via the coding-agent transitive dep vs. our direct peer.
|
|
116
|
+
interface SelectListHandle {
|
|
117
|
+
render(w: number): string[];
|
|
118
|
+
invalidate(): void;
|
|
119
|
+
handleInput(d: string): void;
|
|
120
|
+
onSelect: (item: { value: T }) => void;
|
|
121
|
+
onCancel: () => void;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Let inference provide the exact callback parameter types from the
|
|
125
|
+
// ExtensionCommandContext (via coding-agent peer). Explicit annotations
|
|
126
|
+
// referencing TUI/KeybindingsManager etc. from pi-tui trigger the
|
|
127
|
+
// "separate declarations of private property" tsc error in the monorepo.
|
|
128
|
+
return await ctx.ui.custom<T | null>(
|
|
129
|
+
async (tui, theme, _kb, done) => {
|
|
130
|
+
const piTui = (await import("@earendil-works/pi-tui")) as unknown as {
|
|
131
|
+
SelectList: new (
|
|
132
|
+
items: { value: T; label: string; description?: string }[],
|
|
133
|
+
maxVisible: number,
|
|
134
|
+
theme: unknown,
|
|
135
|
+
) => SelectListHandle;
|
|
136
|
+
Container: new () => { invalidate(): void };
|
|
137
|
+
truncateToWidth: (s: string, w: number, e?: string, pad?: boolean) => string;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const { SelectList, Container, truncateToWidth: truncateToWidthFn } = piTui;
|
|
141
|
+
|
|
142
|
+
let currentList: SelectListHandle | null = null;
|
|
143
|
+
|
|
144
|
+
function build() {
|
|
145
|
+
currentList = new SelectList(
|
|
146
|
+
opts.items.map((it) => ({
|
|
147
|
+
value: it.value,
|
|
148
|
+
label: it.label,
|
|
149
|
+
description: it.description,
|
|
150
|
+
})),
|
|
151
|
+
maxVis,
|
|
152
|
+
{
|
|
153
|
+
selectedPrefix: (t: string) => theme.fg("accent", t),
|
|
154
|
+
selectedText: (t: string) => theme.fg("accent", t),
|
|
155
|
+
description: (t: string) => theme.fg("muted", t),
|
|
156
|
+
scrollInfo: (t: string) => theme.fg("dim", t),
|
|
157
|
+
noMatch: (t: string) => theme.fg("warning", t),
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
currentList.onSelect = (item) => {
|
|
161
|
+
done(item.value);
|
|
162
|
+
};
|
|
163
|
+
currentList.onCancel = () => {
|
|
164
|
+
done(null);
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
build();
|
|
169
|
+
|
|
170
|
+
const container = new Container();
|
|
171
|
+
|
|
172
|
+
const popup: {
|
|
173
|
+
render(w: number): string[];
|
|
174
|
+
invalidate(): void;
|
|
175
|
+
handleInput?(d: string): void;
|
|
176
|
+
dispose?(): void;
|
|
177
|
+
} = {
|
|
178
|
+
render(width: number) {
|
|
179
|
+
const listLines = currentList ? currentList.render(Math.max(20, width - 4)) : [];
|
|
180
|
+
return renderBorderedBox(width, opts.title, listLines, help, theme, truncateToWidthFn);
|
|
181
|
+
},
|
|
182
|
+
invalidate() {
|
|
183
|
+
container.invalidate();
|
|
184
|
+
currentList?.invalidate();
|
|
185
|
+
},
|
|
186
|
+
handleInput(d: string) {
|
|
187
|
+
currentList?.handleInput(d);
|
|
188
|
+
tui.requestRender();
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
return popup;
|
|
193
|
+
},
|
|
194
|
+
{ overlay: true },
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Yes/No (or custom labels) confirmation inside a bordered popup. */
|
|
199
|
+
export async function confirmInBorderedPopup(
|
|
200
|
+
ctx: ExtensionCommandContext,
|
|
201
|
+
opts: {
|
|
202
|
+
title: string;
|
|
203
|
+
message?: string;
|
|
204
|
+
confirmLabel?: string;
|
|
205
|
+
cancelLabel?: string;
|
|
206
|
+
},
|
|
207
|
+
): Promise<boolean> {
|
|
208
|
+
const yes = opts.confirmLabel ?? "Yes";
|
|
209
|
+
const no = opts.cancelLabel ?? "No";
|
|
210
|
+
const items = [
|
|
211
|
+
{ value: true, label: yes },
|
|
212
|
+
{ value: false, label: no },
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
const choice = await selectInBorderedPopup(ctx, {
|
|
216
|
+
title: opts.title,
|
|
217
|
+
items,
|
|
218
|
+
helpText: "↑↓ • Enter to confirm • Esc = cancel",
|
|
219
|
+
maxVisible: 5,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
return choice === true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Bordered popup text input powered by Pi's Editor component.
|
|
227
|
+
* Good for free-text entry while staying inside the custom popup aesthetic.
|
|
228
|
+
*/
|
|
229
|
+
export async function inputInBorderedPopup(
|
|
230
|
+
ctx: ExtensionCommandContext,
|
|
231
|
+
opts: {
|
|
232
|
+
title: string;
|
|
233
|
+
prompt?: string;
|
|
234
|
+
defaultValue?: string;
|
|
235
|
+
helpText?: string;
|
|
236
|
+
},
|
|
237
|
+
): Promise<string | undefined> {
|
|
238
|
+
const help = opts.helpText ?? "Enter to confirm • Esc = cancel";
|
|
239
|
+
|
|
240
|
+
// Pure local interface (no extends) for the submit hook.
|
|
241
|
+
interface EditorHandle {
|
|
242
|
+
render(w: number): string[];
|
|
243
|
+
invalidate(): void;
|
|
244
|
+
handleInput(d: string): void;
|
|
245
|
+
setText(s: string): void;
|
|
246
|
+
onSubmit: (value: string) => void;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Inference for callback params (see selectInBorderedPopup for rationale).
|
|
250
|
+
return await ctx.ui.custom<string | undefined>(
|
|
251
|
+
async (tui, theme, _kb, done) => {
|
|
252
|
+
const piTui = (await import("@earendil-works/pi-tui")) as unknown as {
|
|
253
|
+
Editor: new (tui: unknown, theme: unknown) => EditorHandle;
|
|
254
|
+
matchesKey: (data: string, key: string) => boolean;
|
|
255
|
+
truncateToWidth: (s: string, w: number, e?: string, pad?: boolean) => string;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const { Editor, matchesKey, truncateToWidth: truncateToWidthFn } = piTui;
|
|
259
|
+
|
|
260
|
+
const editorTheme = {
|
|
261
|
+
borderColor: (s: string) => theme.fg("accent", s),
|
|
262
|
+
selectList: {
|
|
263
|
+
selectedPrefix: (t: string) => theme.fg("accent", t),
|
|
264
|
+
selectedText: (t: string) => theme.fg("accent", t),
|
|
265
|
+
description: (t: string) => theme.fg("muted", t),
|
|
266
|
+
scrollInfo: (t: string) => theme.fg("dim", t),
|
|
267
|
+
noMatch: (t: string) => theme.fg("warning", t),
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const editor = new Editor(tui, editorTheme);
|
|
272
|
+
|
|
273
|
+
if (opts.defaultValue) {
|
|
274
|
+
editor.setText(opts.defaultValue);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
editor.onSubmit = (value: string) => {
|
|
278
|
+
done(value.trim() || undefined);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const popup: {
|
|
282
|
+
render(w: number): string[];
|
|
283
|
+
invalidate(): void;
|
|
284
|
+
handleInput?(d: string): void;
|
|
285
|
+
dispose?(): void;
|
|
286
|
+
} = {
|
|
287
|
+
render(width: number) {
|
|
288
|
+
const innerWidth = Math.max(20, width - 4);
|
|
289
|
+
const body: string[] = [];
|
|
290
|
+
|
|
291
|
+
if (opts.prompt) {
|
|
292
|
+
body.push(theme.fg("text", opts.prompt));
|
|
293
|
+
body.push("");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const editorLines = editor.render(innerWidth);
|
|
297
|
+
body.push(...editorLines);
|
|
298
|
+
|
|
299
|
+
return renderBorderedBox(width, opts.title, body, help, theme, truncateToWidthFn);
|
|
300
|
+
},
|
|
301
|
+
invalidate() {
|
|
302
|
+
editor.invalidate();
|
|
303
|
+
},
|
|
304
|
+
handleInput(data: string) {
|
|
305
|
+
if (matchesKey(data, "escape")) {
|
|
306
|
+
done(undefined);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
editor.handleInput(data);
|
|
310
|
+
tui.requestRender();
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
return popup;
|
|
315
|
+
},
|
|
316
|
+
{ overlay: true },
|
|
317
|
+
);
|
|
318
|
+
}
|