pi-profiles-manager 1.1.1 → 1.1.5
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/commands.test.ts +29 -13
- package/src/commands.ts +53 -57
- package/src/extension.test.ts +46 -17
- package/src/extension.ts +21 -10
package/package.json
CHANGED
package/src/commands.test.ts
CHANGED
|
@@ -70,16 +70,27 @@ describe("registerCommands", () => {
|
|
|
70
70
|
);
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
-
it("registers
|
|
73
|
+
it("registers slash-command aliases for every profile action", () => {
|
|
74
74
|
const pi = mockPi();
|
|
75
75
|
const mgr = new ProfileManager(pi, mockCtx());
|
|
76
76
|
mgr.setConfig(configWithProfiles([]));
|
|
77
77
|
|
|
78
78
|
registerCommands(pi, mgr, vi.fn() as any, vi.fn() as any);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
|
|
80
|
+
for (const action of [
|
|
81
|
+
"list",
|
|
82
|
+
"status",
|
|
83
|
+
"sync",
|
|
84
|
+
"use",
|
|
85
|
+
"save",
|
|
86
|
+
"next",
|
|
87
|
+
"off",
|
|
88
|
+
]) {
|
|
89
|
+
expect(pi.registerCommand).toHaveBeenCalledWith(
|
|
90
|
+
`profiles:${action}`,
|
|
91
|
+
expect.any(Object),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
83
94
|
});
|
|
84
95
|
|
|
85
96
|
it("completes actions first and profile names after use or save", () => {
|
|
@@ -267,23 +278,28 @@ describe("registerCommands", () => {
|
|
|
267
278
|
expect(openTui).not.toHaveBeenCalled();
|
|
268
279
|
});
|
|
269
280
|
|
|
270
|
-
it("profiles
|
|
281
|
+
it("profiles action aliases delegate through the existing action handling", async () => {
|
|
271
282
|
const pi = mockPi();
|
|
272
283
|
const ctx = mockCtx();
|
|
273
284
|
const mgr = new ProfileManager(pi, ctx);
|
|
285
|
+
const save = vi.fn(async () => {});
|
|
274
286
|
const sync = vi.fn(async () => {});
|
|
275
287
|
const openTui = vi.fn(async () => {});
|
|
276
|
-
mgr.setConfig(configWithProfiles([]));
|
|
288
|
+
mgr.setConfig(configWithProfiles(["alpha"]));
|
|
277
289
|
|
|
278
|
-
registerCommands(pi, mgr,
|
|
290
|
+
registerCommands(pi, mgr, save as any, vi.fn() as any, openTui, sync);
|
|
279
291
|
|
|
280
|
-
const
|
|
281
|
-
(
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
await
|
|
292
|
+
const handlerFor = (command: string) =>
|
|
293
|
+
(pi.registerCommand as any).mock.calls.find(
|
|
294
|
+
(c: any[]) => c[0] === command,
|
|
295
|
+
)[1].handler;
|
|
296
|
+
await handlerFor("profiles:save")("snapshot", ctx);
|
|
297
|
+
await handlerFor("profiles:sync")("", ctx);
|
|
298
|
+
await handlerFor("profiles:list")("", ctx);
|
|
285
299
|
|
|
300
|
+
expect(save).toHaveBeenCalledWith(ctx, "snapshot");
|
|
286
301
|
expect(sync).toHaveBeenCalledWith(ctx);
|
|
302
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith("alpha");
|
|
287
303
|
expect(openTui).not.toHaveBeenCalled();
|
|
288
304
|
});
|
|
289
305
|
});
|
package/src/commands.ts
CHANGED
|
@@ -30,19 +30,52 @@ export function registerCommands(
|
|
|
30
30
|
openTui: OpenTuiFn = async () => {},
|
|
31
31
|
sync: SyncFn = async () => {},
|
|
32
32
|
) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
const handleAction = async (args: string, ctx: ExtensionCommandContext) => {
|
|
34
|
+
manager.setContext(ctx);
|
|
35
|
+
const { verb, name } = parseCommand(args);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
if (verb === "list") {
|
|
39
|
+
return ctx.ui.notify(manager.names().join("\n") || "No profiles");
|
|
40
|
+
}
|
|
41
|
+
if (verb === "status") {
|
|
42
|
+
const active = manager.state.get(ctx.sessionManager.getSessionId());
|
|
43
|
+
return ctx.ui.notify(active?.profile ?? "none");
|
|
44
|
+
}
|
|
45
|
+
if (verb === "sync") {
|
|
37
46
|
await sync(ctx);
|
|
38
|
-
|
|
39
|
-
ctx.ui.notify(
|
|
40
|
-
error instanceof Error ? error.message : String(error),
|
|
41
|
-
"error",
|
|
42
|
-
);
|
|
47
|
+
return;
|
|
43
48
|
}
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
if (verb === "use") {
|
|
50
|
+
if (!name) {
|
|
51
|
+
return ctx.ui.notify("Usage: /profiles use <name>", "error");
|
|
52
|
+
}
|
|
53
|
+
await manager.use(name);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (verb === "save") {
|
|
57
|
+
if (!name) {
|
|
58
|
+
return ctx.ui.notify("Usage: /profiles save <name>", "error");
|
|
59
|
+
}
|
|
60
|
+
await _save(ctx, name);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (verb === "next") {
|
|
64
|
+
await manager.next();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (verb === "off") {
|
|
68
|
+
await manager.off();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
return openTui(ctx as ContextLike);
|
|
72
|
+
} catch (error: unknown) {
|
|
73
|
+
ctx.ui.notify(
|
|
74
|
+
error instanceof Error ? error.message : String(error),
|
|
75
|
+
"error",
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
46
79
|
|
|
47
80
|
pi.registerCommand("profiles", {
|
|
48
81
|
description: "Manage SDD model profiles",
|
|
@@ -65,51 +98,14 @@ export function registerCommands(
|
|
|
65
98
|
.filter((name) => name.startsWith(namePrefix))
|
|
66
99
|
.map((value) => ({ value, label: value }));
|
|
67
100
|
},
|
|
68
|
-
|
|
69
|
-
manager.setContext(ctx);
|
|
70
|
-
const { verb, name } = parseCommand(args);
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
if (verb === "list") {
|
|
74
|
-
return ctx.ui.notify(manager.names().join("\n") || "No profiles");
|
|
75
|
-
}
|
|
76
|
-
if (verb === "status") {
|
|
77
|
-
const active = manager.state.get(ctx.sessionManager.getSessionId());
|
|
78
|
-
return ctx.ui.notify(active?.profile ?? "none");
|
|
79
|
-
}
|
|
80
|
-
if (verb === "sync") {
|
|
81
|
-
await sync(ctx);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
if (verb === "use") {
|
|
85
|
-
if (!name) {
|
|
86
|
-
return ctx.ui.notify("Usage: /profiles use <name>", "error");
|
|
87
|
-
}
|
|
88
|
-
await manager.use(name);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
if (verb === "save") {
|
|
92
|
-
if (!name) {
|
|
93
|
-
return ctx.ui.notify("Usage: /profiles save <name>", "error");
|
|
94
|
-
}
|
|
95
|
-
await _save(ctx, name);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
if (verb === "next") {
|
|
99
|
-
await manager.next();
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
if (verb === "off") {
|
|
103
|
-
await manager.off();
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
return openTui(ctx as ContextLike);
|
|
107
|
-
} catch (error: unknown) {
|
|
108
|
-
ctx.ui.notify(
|
|
109
|
-
error instanceof Error ? error.message : String(error),
|
|
110
|
-
"error",
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
},
|
|
101
|
+
handler: handleAction,
|
|
114
102
|
});
|
|
103
|
+
|
|
104
|
+
for (const action of COMMAND_ACTIONS) {
|
|
105
|
+
pi.registerCommand(`profiles:${action}`, {
|
|
106
|
+
description: `PiProfiles: ${action}`,
|
|
107
|
+
handler: (args: string, ctx: ExtensionCommandContext) =>
|
|
108
|
+
handleAction(`${action} ${args}`.trim(), ctx),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
115
111
|
}
|
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
|
}));
|
|
@@ -266,7 +267,7 @@ describe("package extension", () => {
|
|
|
266
267
|
});
|
|
267
268
|
|
|
268
269
|
it.each(["return", "escape"])(
|
|
269
|
-
"keeps export
|
|
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,12 @@ describe("package extension", () => {
|
|
|
320
321
|
"piprofile:1:eyJfdHlwZSI6InBpcHJvZmlsZSIsInZlcnNpb24iOjEsInByb2ZpbGUiOnsibmFtZSI6ImFscGhhIiwib3JkZXIiOjAsImZhdm9yaXRlIjpmYWxzZX19",
|
|
321
322
|
);
|
|
322
323
|
expect(tuiState.texts).toContainEqual([
|
|
323
|
-
"
|
|
324
|
+
"piprofile:1:eyJfdHlwZSI6InBpcHJvZmlsZSIsInZlcnNpb24iOjEsInByb2ZpbGUiOnsibmFtZSI6ImFscGhhIiwib3JkZXIiOjAsImZhdm9yaXRlIjpmYWxzZX19",
|
|
325
|
+
1,
|
|
326
|
+
0,
|
|
327
|
+
]);
|
|
328
|
+
expect(tuiState.texts).toContainEqual([
|
|
329
|
+
expect.stringContaining("Copied profile 'alpha' to clipboard"),
|
|
324
330
|
1,
|
|
325
331
|
0,
|
|
326
332
|
]);
|
|
@@ -331,7 +337,7 @@ describe("package extension", () => {
|
|
|
331
337
|
expect(confirmationView).toBeDefined();
|
|
332
338
|
expect(handlerSettled).toBe(false);
|
|
333
339
|
|
|
334
|
-
confirmationView
|
|
340
|
+
confirmationView?.handleInput(dismissKey);
|
|
335
341
|
await handling;
|
|
336
342
|
|
|
337
343
|
expect(finishConfirmation).toBeDefined();
|
|
@@ -343,7 +349,7 @@ describe("package extension", () => {
|
|
|
343
349
|
},
|
|
344
350
|
);
|
|
345
351
|
|
|
346
|
-
it("
|
|
352
|
+
it("keeps export string visible and shows error if clipboard fails", async () => {
|
|
347
353
|
const syncFs = await import("node:fs");
|
|
348
354
|
vi.mocked(syncFs.readFileSync).mockReturnValue(JSON.stringify({
|
|
349
355
|
version: 1,
|
|
@@ -358,10 +364,27 @@ describe("package extension", () => {
|
|
|
358
364
|
const handler = (vi.mocked(pi.registerCommand).mock.calls as any[]).find(
|
|
359
365
|
([name]: [string]) => name === "profiles",
|
|
360
366
|
)[1].handler as (args: string, ctx: any) => Promise<void>;
|
|
367
|
+
|
|
368
|
+
let confirmationView: { handleInput(data: string): void } | undefined;
|
|
369
|
+
let finishConfirmation: ((value: unknown) => void) | undefined;
|
|
361
370
|
const custom = vi
|
|
362
371
|
.fn()
|
|
363
372
|
.mockResolvedValueOnce("alpha")
|
|
364
373
|
.mockResolvedValueOnce("export")
|
|
374
|
+
.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) => {
|
|
375
|
+
return await new Promise((resolve) => {
|
|
376
|
+
finishConfirmation = resolve;
|
|
377
|
+
confirmationView = factory(
|
|
378
|
+
{ requestRender: vi.fn() },
|
|
379
|
+
{
|
|
380
|
+
fg: (_color: string, text: string) => text,
|
|
381
|
+
bold: (text: string) => text,
|
|
382
|
+
},
|
|
383
|
+
{},
|
|
384
|
+
resolve,
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
})
|
|
365
388
|
.mockResolvedValueOnce(null);
|
|
366
389
|
const ctx = {
|
|
367
390
|
sessionManager: { getSessionId: () => "session-1" },
|
|
@@ -369,22 +392,28 @@ describe("package extension", () => {
|
|
|
369
392
|
ui: { custom, notify: vi.fn(), setStatus: vi.fn() },
|
|
370
393
|
};
|
|
371
394
|
|
|
372
|
-
|
|
395
|
+
let handlerSettled = false;
|
|
396
|
+
const handling = handler("", ctx).then(() => { handlerSettled = true; });
|
|
397
|
+
await vi.waitFor(() => expect(custom).toHaveBeenCalledTimes(3));
|
|
373
398
|
|
|
374
|
-
expect(
|
|
375
|
-
"
|
|
376
|
-
"error",
|
|
377
|
-
);
|
|
378
|
-
expect(custom).toHaveBeenCalledTimes(3);
|
|
379
|
-
expect(tuiState.texts).not.toContainEqual([
|
|
380
|
-
"Copied profile 'alpha' to clipboard.",
|
|
399
|
+
expect(tuiState.texts).toContainEqual([
|
|
400
|
+
"piprofile:1:eyJfdHlwZSI6InBpcHJvZmlsZSIsInZlcnNpb24iOjEsInByb2ZpbGUiOnsibmFtZSI6ImFscGhhIiwib3JkZXIiOjAsImZhdm9yaXRlIjpmYWxzZX19",
|
|
381
401
|
1,
|
|
382
402
|
0,
|
|
383
403
|
]);
|
|
384
|
-
expect(
|
|
385
|
-
"
|
|
386
|
-
|
|
387
|
-
|
|
404
|
+
expect(tuiState.texts).toContainEqual([
|
|
405
|
+
expect.stringContaining("Failed to copy profile 'alpha' to clipboard"),
|
|
406
|
+
1,
|
|
407
|
+
0,
|
|
408
|
+
]);
|
|
409
|
+
expect(confirmationView).toBeDefined();
|
|
410
|
+
expect(handlerSettled).toBe(false);
|
|
411
|
+
|
|
412
|
+
confirmationView?.handleInput("return");
|
|
413
|
+
await handling;
|
|
414
|
+
|
|
415
|
+
expect(finishConfirmation).toBeDefined();
|
|
416
|
+
expect(handlerSettled).toBe(true);
|
|
388
417
|
});
|
|
389
418
|
|
|
390
419
|
it("imports typed profile strings into the authoritative config without legacy storage", async () => {
|
package/src/extension.ts
CHANGED
|
@@ -174,12 +174,26 @@ 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", theme.bold(`Copying profile '${profileName}' to clipboard...`)), 1, 0);
|
|
181
|
+
|
|
180
182
|
container.addChild(new DynamicBorder((value: string) => theme.fg("accent", value)));
|
|
181
|
-
container.addChild(
|
|
183
|
+
container.addChild(statusText);
|
|
184
|
+
container.addChild(new Text(theme.fg("accent", exportString), 1, 0));
|
|
182
185
|
container.addChild(new DynamicBorder((value: string) => theme.fg("accent", value)));
|
|
186
|
+
|
|
187
|
+
copyPromise.then(() => {
|
|
188
|
+
statusText.setText(theme.fg("accent", theme.bold(`Copied profile '${profileName}' to clipboard.`)));
|
|
189
|
+
container.invalidate();
|
|
190
|
+
tui.requestRender();
|
|
191
|
+
}).catch((error) => {
|
|
192
|
+
statusText.setText(theme.fg("error", theme.bold(`Failed to copy profile '${profileName}' to clipboard: ${error instanceof Error ? error.message : String(error)}`)));
|
|
193
|
+
container.invalidate();
|
|
194
|
+
tui.requestRender();
|
|
195
|
+
});
|
|
196
|
+
|
|
183
197
|
return {
|
|
184
198
|
render: (width: number) => container.render(width),
|
|
185
199
|
invalidate: () => container.invalidate(),
|
|
@@ -509,12 +523,9 @@ export default function extension(pi: PiLike) {
|
|
|
509
523
|
favorite: selected === manager.config.defaultProfile,
|
|
510
524
|
},
|
|
511
525
|
})).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
|
-
}
|
|
526
|
+
const exportString = `piprofile:1:${encoded}`;
|
|
527
|
+
const copyPromise = copyToClipboard(exportString);
|
|
528
|
+
await showExportDialog(ctx, selected, exportString, copyPromise);
|
|
518
529
|
continue;
|
|
519
530
|
}
|
|
520
531
|
if (action === "delete") {
|