pi-profiles-manager 1.1.4 → 1.1.6
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 +1 -1
- package/src/extension.test.ts +68 -19
- package/src/extension.ts +25 -11
package/package.json
CHANGED
package/src/extension.test.ts
CHANGED
|
@@ -34,6 +34,7 @@ vi.mock("@earendil-works/pi-tui", () => ({
|
|
|
34
34
|
},
|
|
35
35
|
Text: class {
|
|
36
36
|
constructor(...args: unknown[]) { tuiState.texts.push(args); }
|
|
37
|
+
setText(text: string) { tuiState.texts.push([text, 1, 0]); }
|
|
37
38
|
},
|
|
38
39
|
matchesKey: (data: string, key: string) => data === key,
|
|
39
40
|
}));
|
|
@@ -265,8 +266,8 @@ describe("package extension", () => {
|
|
|
265
266
|
);
|
|
266
267
|
});
|
|
267
268
|
|
|
268
|
-
it.each(["return", "escape"])(
|
|
269
|
-
"keeps export
|
|
269
|
+
it.each(["return", "escape", "a", "space"])(
|
|
270
|
+
"keeps export string visible until %s dismisses it",
|
|
270
271
|
async (dismissKey) => {
|
|
271
272
|
const syncFs = await import("node:fs");
|
|
272
273
|
vi.mocked(syncFs.readFileSync).mockReturnValue(JSON.stringify({
|
|
@@ -290,7 +291,7 @@ describe("package extension", () => {
|
|
|
290
291
|
.fn()
|
|
291
292
|
.mockResolvedValueOnce("alpha")
|
|
292
293
|
.mockResolvedValueOnce("export")
|
|
293
|
-
.mockImplementationOnce(async (factory:
|
|
294
|
+
.mockImplementationOnce(async (factory: (tui: { requestRender: () => void }, theme: { fg: (color: string, text: string) => string; bold: (text: string) => string }, kb: unknown, done: (value: unknown) => void) => { handleInput(data: string): void }, options: unknown) => {
|
|
294
295
|
confirmationOptions = options;
|
|
295
296
|
return await new Promise((resolve) => {
|
|
296
297
|
finishConfirmation = resolve;
|
|
@@ -320,7 +321,27 @@ describe("package extension", () => {
|
|
|
320
321
|
"piprofile:1:eyJfdHlwZSI6InBpcHJvZmlsZSIsInZlcnNpb24iOjEsInByb2ZpbGUiOnsibmFtZSI6ImFscGhhIiwib3JkZXIiOjAsImZhdm9yaXRlIjpmYWxzZX19",
|
|
321
322
|
);
|
|
322
323
|
expect(tuiState.texts).toContainEqual([
|
|
323
|
-
"
|
|
324
|
+
"📤 Profile Export: 'alpha'",
|
|
325
|
+
1,
|
|
326
|
+
0,
|
|
327
|
+
]);
|
|
328
|
+
expect(tuiState.texts).toContainEqual([
|
|
329
|
+
"Select string below to copy:",
|
|
330
|
+
1,
|
|
331
|
+
0,
|
|
332
|
+
]);
|
|
333
|
+
expect(tuiState.texts).toContainEqual([
|
|
334
|
+
"piprofile:1:eyJfdHlwZSI6InBpcHJvZmlsZSIsInZlcnNpb24iOjEsInByb2ZpbGUiOnsibmFtZSI6ImFscGhhIiwib3JkZXIiOjAsImZhdm9yaXRlIjpmYWxzZX19",
|
|
335
|
+
1,
|
|
336
|
+
0,
|
|
337
|
+
]);
|
|
338
|
+
expect(tuiState.texts).toContainEqual([
|
|
339
|
+
"[ Press any key or Esc to close ]",
|
|
340
|
+
1,
|
|
341
|
+
0,
|
|
342
|
+
]);
|
|
343
|
+
expect(tuiState.texts).toContainEqual([
|
|
344
|
+
"✓ Copy command sent to terminal clipboard.",
|
|
324
345
|
1,
|
|
325
346
|
0,
|
|
326
347
|
]);
|
|
@@ -331,19 +352,19 @@ describe("package extension", () => {
|
|
|
331
352
|
expect(confirmationView).toBeDefined();
|
|
332
353
|
expect(handlerSettled).toBe(false);
|
|
333
354
|
|
|
334
|
-
confirmationView
|
|
355
|
+
confirmationView?.handleInput(dismissKey);
|
|
335
356
|
await handling;
|
|
336
357
|
|
|
337
358
|
expect(finishConfirmation).toBeDefined();
|
|
338
359
|
expect(handlerSettled).toBe(true);
|
|
339
360
|
expect(ctx.ui.notify).not.toHaveBeenCalledWith(
|
|
340
|
-
"
|
|
361
|
+
"✓ Copy command sent to terminal clipboard.",
|
|
341
362
|
"info",
|
|
342
363
|
);
|
|
343
364
|
},
|
|
344
365
|
);
|
|
345
366
|
|
|
346
|
-
it("
|
|
367
|
+
it("keeps export string visible and shows error if clipboard fails", async () => {
|
|
347
368
|
const syncFs = await import("node:fs");
|
|
348
369
|
vi.mocked(syncFs.readFileSync).mockReturnValue(JSON.stringify({
|
|
349
370
|
version: 1,
|
|
@@ -358,10 +379,27 @@ describe("package extension", () => {
|
|
|
358
379
|
const handler = (vi.mocked(pi.registerCommand).mock.calls as any[]).find(
|
|
359
380
|
([name]: [string]) => name === "profiles",
|
|
360
381
|
)[1].handler as (args: string, ctx: any) => Promise<void>;
|
|
382
|
+
|
|
383
|
+
let confirmationView: { handleInput(data: string): void } | undefined;
|
|
384
|
+
let finishConfirmation: ((value: unknown) => void) | undefined;
|
|
361
385
|
const custom = vi
|
|
362
386
|
.fn()
|
|
363
387
|
.mockResolvedValueOnce("alpha")
|
|
364
388
|
.mockResolvedValueOnce("export")
|
|
389
|
+
.mockImplementationOnce(async (factory: (tui: { requestRender: () => void }, theme: { fg: (color: string, text: string) => string; bold: (text: string) => string }, kb: unknown, done: (value: unknown) => void) => { handleInput(data: string): void }, _options: unknown) => {
|
|
390
|
+
return await new Promise((resolve) => {
|
|
391
|
+
finishConfirmation = resolve;
|
|
392
|
+
confirmationView = factory(
|
|
393
|
+
{ requestRender: vi.fn() },
|
|
394
|
+
{
|
|
395
|
+
fg: (_color: string, text: string) => text,
|
|
396
|
+
bold: (text: string) => text,
|
|
397
|
+
},
|
|
398
|
+
{},
|
|
399
|
+
resolve,
|
|
400
|
+
);
|
|
401
|
+
});
|
|
402
|
+
})
|
|
365
403
|
.mockResolvedValueOnce(null);
|
|
366
404
|
const ctx = {
|
|
367
405
|
sessionManager: { getSessionId: () => "session-1" },
|
|
@@ -369,22 +407,33 @@ describe("package extension", () => {
|
|
|
369
407
|
ui: { custom, notify: vi.fn(), setStatus: vi.fn() },
|
|
370
408
|
};
|
|
371
409
|
|
|
372
|
-
|
|
410
|
+
let handlerSettled = false;
|
|
411
|
+
const handling = handler("", ctx).then(() => { handlerSettled = true; });
|
|
412
|
+
await vi.waitFor(() => expect(custom).toHaveBeenCalledTimes(3));
|
|
373
413
|
|
|
374
|
-
expect(
|
|
375
|
-
"
|
|
376
|
-
"error",
|
|
377
|
-
);
|
|
378
|
-
expect(custom).toHaveBeenCalledTimes(3);
|
|
379
|
-
expect(tuiState.texts).not.toContainEqual([
|
|
380
|
-
"Copied profile 'alpha' to clipboard.",
|
|
414
|
+
expect(tuiState.texts).toContainEqual([
|
|
415
|
+
"📤 Profile Export: 'alpha'",
|
|
381
416
|
1,
|
|
382
417
|
0,
|
|
383
418
|
]);
|
|
384
|
-
expect(
|
|
385
|
-
"
|
|
386
|
-
|
|
387
|
-
|
|
419
|
+
expect(tuiState.texts).toContainEqual([
|
|
420
|
+
"piprofile:1:eyJfdHlwZSI6InBpcHJvZmlsZSIsInZlcnNpb24iOjEsInByb2ZpbGUiOnsibmFtZSI6ImFscGhhIiwib3JkZXIiOjAsImZhdm9yaXRlIjpmYWxzZX19",
|
|
421
|
+
1,
|
|
422
|
+
0,
|
|
423
|
+
]);
|
|
424
|
+
expect(tuiState.texts).toContainEqual([
|
|
425
|
+
expect.stringContaining("Failed to copy: clipboard unavailable"),
|
|
426
|
+
1,
|
|
427
|
+
0,
|
|
428
|
+
]);
|
|
429
|
+
expect(confirmationView).toBeDefined();
|
|
430
|
+
expect(handlerSettled).toBe(false);
|
|
431
|
+
|
|
432
|
+
confirmationView?.handleInput("return");
|
|
433
|
+
await handling;
|
|
434
|
+
|
|
435
|
+
expect(finishConfirmation).toBeDefined();
|
|
436
|
+
expect(handlerSettled).toBe(true);
|
|
388
437
|
});
|
|
389
438
|
|
|
390
439
|
it("imports typed profile strings into the authoritative config without legacy storage", async () => {
|
package/src/extension.ts
CHANGED
|
@@ -174,17 +174,34 @@ function prompt(ctx: ContextLike, title: string, initial = ""): Promise<string |
|
|
|
174
174
|
}, { overlay: true });
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
function
|
|
178
|
-
return (ctx.ui.custom as
|
|
179
|
-
const container = new Container() as
|
|
177
|
+
function showExportDialog(ctx: ContextLike, profileName: string, exportString: string, copyPromise: Promise<void>): Promise<void> {
|
|
178
|
+
return (ctx.ui.custom as (factory: unknown, options?: unknown) => Promise<void>)((tui: { requestRender: () => void }, theme: { fg: (color: string, text: string) => string; bold: (text: string) => string }, _kb: unknown, done: (value: void) => void) => {
|
|
179
|
+
const container = new Container() as { addChild: (child: unknown) => void; render: (width: number) => unknown; invalidate: () => void };
|
|
180
|
+
const statusText = new Text(theme.fg("accent", "Copying to clipboard..."), 1, 0);
|
|
181
|
+
|
|
180
182
|
container.addChild(new DynamicBorder((value: string) => theme.fg("accent", value)));
|
|
181
|
-
container.addChild(new Text(theme.fg("accent", theme.bold(
|
|
183
|
+
container.addChild(new Text(theme.fg("accent", theme.bold(`📤 Profile Export: '${profileName}'`)), 1, 0));
|
|
184
|
+
container.addChild(statusText);
|
|
185
|
+
container.addChild(new Text(theme.fg("dim", "Select string below to copy:"), 1, 0));
|
|
186
|
+
container.addChild(new Text(theme.fg("accent", exportString), 1, 0));
|
|
187
|
+
container.addChild(new Text(theme.fg("dim", "[ Press any key or Esc to close ]"), 1, 0));
|
|
182
188
|
container.addChild(new DynamicBorder((value: string) => theme.fg("accent", value)));
|
|
189
|
+
|
|
190
|
+
copyPromise.then(() => {
|
|
191
|
+
statusText.setText(theme.fg("accent", "✓ Copy command sent to terminal clipboard."));
|
|
192
|
+
container.invalidate();
|
|
193
|
+
tui.requestRender();
|
|
194
|
+
}).catch((error) => {
|
|
195
|
+
statusText.setText(theme.fg("error", `Failed to copy: ${error instanceof Error ? error.message : String(error)}`));
|
|
196
|
+
container.invalidate();
|
|
197
|
+
tui.requestRender();
|
|
198
|
+
});
|
|
199
|
+
|
|
183
200
|
return {
|
|
184
201
|
render: (width: number) => container.render(width),
|
|
185
202
|
invalidate: () => container.invalidate(),
|
|
186
203
|
handleInput: (data: string) => {
|
|
187
|
-
|
|
204
|
+
done();
|
|
188
205
|
tui.requestRender();
|
|
189
206
|
},
|
|
190
207
|
};
|
|
@@ -509,12 +526,9 @@ export default function extension(pi: PiLike) {
|
|
|
509
526
|
favorite: selected === manager.config.defaultProfile,
|
|
510
527
|
},
|
|
511
528
|
})).toString("base64");
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
} catch (error) {
|
|
516
|
-
ctx.ui.notify(`Failed to copy profile '${selected}' to clipboard: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
517
|
-
}
|
|
529
|
+
const exportString = `piprofile:1:${encoded}`;
|
|
530
|
+
const copyPromise = copyToClipboard(exportString);
|
|
531
|
+
await showExportDialog(ctx, selected, exportString, copyPromise);
|
|
518
532
|
continue;
|
|
519
533
|
}
|
|
520
534
|
if (action === "delete") {
|