@papillon6814/copi 0.4.2 → 0.5.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/README.md +1 -1
- package/dist/src/resource-picker.d.ts +54 -0
- package/dist/src/resource-picker.js +239 -0
- package/dist/src/wizard.js +11 -9
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ copi migrate --resolve [--dry-run]
|
|
|
26
26
|
copi help
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
`setup` requires an interactive terminal. It first asks where copi may look: the Pi agent directories (`~/.pi/agent`, `~/.pi/agent-*`, `~/.pi/agent_*` and `~/.agents`; the default), this project only, or both. Settings and packages declared by those Pi profiles are expanded too, so configured paths elsewhere under `HOME` may be read; setup never walks your whole `HOME`. Symbolic links are not followed, and worktree checkout copies (`.worktrees`, `.claude/worktrees`, `.config/superpowers/worktrees`, `.treehouse`) are skipped. Selecting the current project scopes discovery to that project; when the current directory is `HOME` itself, only the Pi agent directories are offered. Candidates with the same kind, name and content are merged into one row marked `(+N identical copies)`; the kept row prefers a source under `~/.pi` or `~/.agents`. Copi then asks for explicit authorization before searching; declining performs no search. Search warnings are summarized (for example, resources found outside the selected roots are counted rather than listed one by one), and missing files are skipped silently.
|
|
29
|
+
`setup` requires an interactive terminal. It first asks where copi may look: the Pi agent directories (`~/.pi/agent`, `~/.pi/agent-*`, `~/.pi/agent_*` and `~/.agents`; the default), this project only, or both. Settings and packages declared by those Pi profiles are expanded too, so configured paths elsewhere under `HOME` may be read; setup never walks your whole `HOME`. Symbolic links are not followed, and worktree checkout copies (`.worktrees`, `.claude/worktrees`, `.config/superpowers/worktrees`, `.treehouse`) are skipped. Selecting the current project scopes discovery to that project; when the current directory is `HOME` itself, only the Pi agent directories are offered. Candidates with the same kind, name and content are merged into one row marked `(+N identical copies)`; the kept row prefers a source under `~/.pi` or `~/.agents`. Copi then asks for explicit authorization before searching; declining performs no search. Search warnings are summarized (for example, resources found outside the selected roots are counted rather than listed one by one), and missing files are skipped silently. The resource list is split into Skills, Agents, Instructions and Settings tabs (←/→ switch tabs, ↑/↓ move, Tab selects (Space types into the filter), typing filters the current tab, Enter confirms the selection across all tabs); below the list a masked preview of the focused row shows the first lines of the file (or a skill's `SKILL.md`) with size, modification date and destination. Selecting a row only makes it a candidate for review; nothing is saved until the final confirmation.
|
|
30
30
|
|
|
31
31
|
Candidates are listed with a source label, size and modification date, and entries whose name matches an already saved resource are marked `already saved`. After you select resources, name conflicts are resolved right away, before any preview: keep one of the same-named selections, rename the display name only (the files, body and destination are not changed), skip the resource (keeping the saved copy), or go back to the list.
|
|
32
32
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Prompt } from "@clack/core";
|
|
2
|
+
import { CANCEL_SYMBOL } from "@clack/prompts";
|
|
3
|
+
import type { ResourceCandidate, ResourceKind } from "./types.ts";
|
|
4
|
+
export type ResourceTab = "Skills" | "Agents" | "Instructions" | "Settings";
|
|
5
|
+
export declare function tabOf(kind: ResourceKind): ResourceTab;
|
|
6
|
+
export interface PickerRow {
|
|
7
|
+
candidate: ResourceCandidate;
|
|
8
|
+
/** Already safe for display (callers pass safeDisplay output). */
|
|
9
|
+
label: string;
|
|
10
|
+
/** Already safe for display: size · modified · destination. */
|
|
11
|
+
details: string;
|
|
12
|
+
}
|
|
13
|
+
export interface PickerOptions {
|
|
14
|
+
message: string;
|
|
15
|
+
rows: readonly PickerRow[];
|
|
16
|
+
initialValues?: readonly string[];
|
|
17
|
+
/** Masks and neutralizes control characters; applied to every preview line. */
|
|
18
|
+
sanitize: (text: string) => string;
|
|
19
|
+
previewLines?: number;
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
input?: NodeJS.ReadableStream;
|
|
22
|
+
output?: NodeJS.WritableStream;
|
|
23
|
+
}
|
|
24
|
+
/** The file whose head the preview shows: the file itself, or a skill directory's regular SKILL.md (never a symlink). */
|
|
25
|
+
export declare function previewFile(candidate: ResourceCandidate): string | undefined;
|
|
26
|
+
/** Masked head of the preview file: at most `lines` lines, plus a "… N more lines" marker when truncated. */
|
|
27
|
+
export declare function previewText(candidate: ResourceCandidate, lines: number, sanitize: (text: string) => string): {
|
|
28
|
+
title: string;
|
|
29
|
+
body: string[];
|
|
30
|
+
};
|
|
31
|
+
/** Cut to `width` terminal columns (CJK counts double), never splitting a code point. */
|
|
32
|
+
export declare function clip(text: string, width: number): string;
|
|
33
|
+
/** Keep head and tail within `width` columns, so trailing markers such as "(+1 identical copy)" stay visible. */
|
|
34
|
+
export declare function clipMiddle(text: string, width: number): string;
|
|
35
|
+
/**
|
|
36
|
+
* A tabbed multi-select for setup: ←/→ switch kind tabs, ↑/↓ move, Tab toggles, typing filters the current tab,
|
|
37
|
+
* Backspace deletes, Enter confirms the selection across all tabs. Shows a masked preview of the focused row.
|
|
38
|
+
*/
|
|
39
|
+
export declare class ResourcePickerPrompt extends Prompt<string[]> {
|
|
40
|
+
private readonly options;
|
|
41
|
+
readonly tabs: ResourceTab[];
|
|
42
|
+
tab: number;
|
|
43
|
+
cursor: number;
|
|
44
|
+
filter: string;
|
|
45
|
+
readonly selected: Set<string>;
|
|
46
|
+
private readonly rowsByTab;
|
|
47
|
+
private readonly previews;
|
|
48
|
+
constructor(options: PickerOptions);
|
|
49
|
+
visibleRows(): PickerRow[];
|
|
50
|
+
private handleKey;
|
|
51
|
+
private preview;
|
|
52
|
+
draw(): string;
|
|
53
|
+
}
|
|
54
|
+
export declare function resourcePicker(options: PickerOptions): Promise<string[] | typeof CANCEL_SYMBOL>;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { isCancel as isCoreCancel, Prompt } from "@clack/core";
|
|
2
|
+
import { CANCEL_SYMBOL, S_BAR, S_BAR_END, S_CHECKBOX_INACTIVE, S_CHECKBOX_SELECTED, symbol } from "@clack/prompts";
|
|
3
|
+
import stringWidth from "fast-string-width";
|
|
4
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { styleText } from "node:util";
|
|
7
|
+
import { maskSensitiveText } from "./inspect.js";
|
|
8
|
+
const tabOrder = ["Skills", "Agents", "Instructions", "Settings"];
|
|
9
|
+
export function tabOf(kind) {
|
|
10
|
+
if (kind === "skill")
|
|
11
|
+
return "Skills";
|
|
12
|
+
if (kind === "subagent")
|
|
13
|
+
return "Agents";
|
|
14
|
+
if (kind === "instruction")
|
|
15
|
+
return "Instructions";
|
|
16
|
+
return "Settings";
|
|
17
|
+
}
|
|
18
|
+
const previewReadBytes = 64 * 1024;
|
|
19
|
+
/** The file whose head the preview shows: the file itself, or a skill directory's regular SKILL.md (never a symlink). */
|
|
20
|
+
export function previewFile(candidate) {
|
|
21
|
+
try {
|
|
22
|
+
const stat = lstatSync(candidate.sourcePath);
|
|
23
|
+
if (stat.isFile())
|
|
24
|
+
return candidate.sourcePath;
|
|
25
|
+
if (!stat.isDirectory())
|
|
26
|
+
return undefined;
|
|
27
|
+
const skill = path.join(candidate.sourcePath, "SKILL.md");
|
|
28
|
+
return lstatSync(skill, { throwIfNoEntry: false })?.isFile() ? skill : undefined;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Masked head of the preview file: at most `lines` lines, plus a "… N more lines" marker when truncated. */
|
|
35
|
+
export function previewText(candidate, lines, sanitize) {
|
|
36
|
+
const file = previewFile(candidate);
|
|
37
|
+
if (!file)
|
|
38
|
+
return { title: "no previewable file", body: [] };
|
|
39
|
+
const title = sanitize(path.relative(path.dirname(candidate.sourcePath), file) || path.basename(file));
|
|
40
|
+
let fd;
|
|
41
|
+
try {
|
|
42
|
+
// No symlink following and no blocking on FIFOs swapped in after the lstat above.
|
|
43
|
+
fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
44
|
+
if (!fstatSync(fd).isFile())
|
|
45
|
+
return { title, body: ["[unreadable]"] };
|
|
46
|
+
const buffer = Buffer.alloc(previewReadBytes);
|
|
47
|
+
const read = readSync(fd, buffer, 0, previewReadBytes, 0);
|
|
48
|
+
const bytes = buffer.subarray(0, read);
|
|
49
|
+
if (bytes.includes(0))
|
|
50
|
+
return { title, body: ["[binary file]"] };
|
|
51
|
+
let text;
|
|
52
|
+
// Streaming decode drops a multi-byte character cut at the read limit instead of rejecting valid UTF-8.
|
|
53
|
+
try {
|
|
54
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes, { stream: read === previewReadBytes });
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return { title, body: ["[not UTF-8 text]"] };
|
|
58
|
+
}
|
|
59
|
+
const all = maskSensitiveText(text).replace(/\r\n/g, "\n").split("\n");
|
|
60
|
+
if (all.at(-1) === "")
|
|
61
|
+
all.pop();
|
|
62
|
+
const shown = all.slice(0, lines).map(sanitize);
|
|
63
|
+
const more = all.length - lines;
|
|
64
|
+
if (more > 0)
|
|
65
|
+
shown.push(`… ${more}${read === previewReadBytes ? "+" : ""} more ${more === 1 && read < previewReadBytes ? "line" : "lines"}`);
|
|
66
|
+
return { title, body: shown };
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return { title, body: ["[unreadable]"] };
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
if (fd !== undefined)
|
|
73
|
+
closeSync(fd);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Cut to `width` terminal columns (CJK counts double), never splitting a code point. */
|
|
77
|
+
export function clip(text, width) {
|
|
78
|
+
if (stringWidth(text) <= width)
|
|
79
|
+
return text;
|
|
80
|
+
let out = "";
|
|
81
|
+
for (const char of text) {
|
|
82
|
+
if (stringWidth(out + char) > width - 1)
|
|
83
|
+
break;
|
|
84
|
+
out += char;
|
|
85
|
+
}
|
|
86
|
+
return `${out}…`;
|
|
87
|
+
}
|
|
88
|
+
/** Keep head and tail within `width` columns, so trailing markers such as "(+1 identical copy)" stay visible. */
|
|
89
|
+
export function clipMiddle(text, width) {
|
|
90
|
+
if (stringWidth(text) <= width)
|
|
91
|
+
return text;
|
|
92
|
+
const chars = [...text];
|
|
93
|
+
let head = "", tail = "";
|
|
94
|
+
for (let i = 0, j = chars.length - 1; i <= j;) {
|
|
95
|
+
if (stringWidth(head) <= stringWidth(tail)) {
|
|
96
|
+
if (stringWidth(head + chars[i] + tail) > width - 1)
|
|
97
|
+
break;
|
|
98
|
+
head += chars[i++];
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
if (stringWidth(head + chars[j] + tail) > width - 1)
|
|
102
|
+
break;
|
|
103
|
+
tail = chars[j--] + tail;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return `${head}…${tail}`;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* A tabbed multi-select for setup: ←/→ switch kind tabs, ↑/↓ move, Tab toggles, typing filters the current tab,
|
|
110
|
+
* Backspace deletes, Enter confirms the selection across all tabs. Shows a masked preview of the focused row.
|
|
111
|
+
*/
|
|
112
|
+
export class ResourcePickerPrompt extends Prompt {
|
|
113
|
+
options;
|
|
114
|
+
tabs;
|
|
115
|
+
tab = 0;
|
|
116
|
+
cursor = 0;
|
|
117
|
+
filter = "";
|
|
118
|
+
selected;
|
|
119
|
+
rowsByTab;
|
|
120
|
+
previews = new Map();
|
|
121
|
+
constructor(options) {
|
|
122
|
+
super({
|
|
123
|
+
render() { return this.draw(); },
|
|
124
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
125
|
+
...(options.input ? { input: options.input } : {}),
|
|
126
|
+
...(options.output ? { output: options.output } : {}),
|
|
127
|
+
}, false);
|
|
128
|
+
this.options = options;
|
|
129
|
+
this.rowsByTab = new Map(tabOrder.map((tab) => [tab, options.rows.filter((row) => tabOf(row.candidate.kind) === tab)]));
|
|
130
|
+
this.tabs = tabOrder.filter((tab) => (this.rowsByTab.get(tab) ?? []).length > 0);
|
|
131
|
+
const known = new Set(options.rows.map((row) => row.candidate.id));
|
|
132
|
+
this.selected = new Set((options.initialValues ?? []).filter((id) => known.has(id)));
|
|
133
|
+
this.value = [...this.selected];
|
|
134
|
+
// Coming back to the list (e.g. "Go back"), open the first tab that holds a previous selection.
|
|
135
|
+
const firstSelectedTab = this.tabs.findIndex((tab) => (this.rowsByTab.get(tab) ?? []).some((row) => this.selected.has(row.candidate.id)));
|
|
136
|
+
if (firstSelectedTab >= 0)
|
|
137
|
+
this.tab = firstSelectedTab;
|
|
138
|
+
this.on("key", (char, key) => this.handleKey(char, key.name));
|
|
139
|
+
}
|
|
140
|
+
visibleRows() {
|
|
141
|
+
const rows = this.rowsByTab.get(this.tabs[this.tab]) ?? [];
|
|
142
|
+
const needle = this.filter.toLowerCase();
|
|
143
|
+
return needle ? rows.filter((row) => row.label.toLowerCase().includes(needle)) : rows;
|
|
144
|
+
}
|
|
145
|
+
handleKey(char, name) {
|
|
146
|
+
const rows = this.visibleRows();
|
|
147
|
+
if (name === "left" || name === "right") {
|
|
148
|
+
if (this.tabs.length > 0)
|
|
149
|
+
this.tab = (this.tab + (name === "left" ? this.tabs.length - 1 : 1)) % this.tabs.length;
|
|
150
|
+
this.cursor = 0;
|
|
151
|
+
this.filter = "";
|
|
152
|
+
}
|
|
153
|
+
else if (name === "up") {
|
|
154
|
+
this.cursor = rows.length === 0 ? 0 : (this.cursor + rows.length - 1) % rows.length;
|
|
155
|
+
}
|
|
156
|
+
else if (name === "down") {
|
|
157
|
+
this.cursor = rows.length === 0 ? 0 : (this.cursor + 1) % rows.length;
|
|
158
|
+
}
|
|
159
|
+
else if (name === "tab") {
|
|
160
|
+
const row = rows[this.cursor];
|
|
161
|
+
if (row) {
|
|
162
|
+
if (this.selected.has(row.candidate.id))
|
|
163
|
+
this.selected.delete(row.candidate.id);
|
|
164
|
+
else
|
|
165
|
+
this.selected.add(row.candidate.id);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else if (name === "backspace") {
|
|
169
|
+
this.filter = this.filter.slice(0, -1);
|
|
170
|
+
this.cursor = 0;
|
|
171
|
+
}
|
|
172
|
+
else if (name !== "return" && char !== undefined && [...char].length === 1 && this.options.sanitize(char) === char && char >= " ") {
|
|
173
|
+
this.filter += char;
|
|
174
|
+
this.cursor = 0;
|
|
175
|
+
}
|
|
176
|
+
// Keep the submitted value in canonical row order.
|
|
177
|
+
this.value = this.options.rows.map((row) => row.candidate.id).filter((id) => this.selected.has(id));
|
|
178
|
+
}
|
|
179
|
+
preview(row) {
|
|
180
|
+
let cached = this.previews.get(row.candidate.id);
|
|
181
|
+
if (!cached) {
|
|
182
|
+
cached = previewText(row.candidate, this.options.previewLines ?? 6, this.options.sanitize);
|
|
183
|
+
this.previews.set(row.candidate.id, cached);
|
|
184
|
+
}
|
|
185
|
+
return cached;
|
|
186
|
+
}
|
|
187
|
+
draw() {
|
|
188
|
+
const width = Math.max(20, (process.stdout.columns ?? 100) - 4);
|
|
189
|
+
const bar = styleText("cyan", S_BAR);
|
|
190
|
+
const title = `${symbol(this.state)} ${this.options.message}`;
|
|
191
|
+
if (this.state === "submit")
|
|
192
|
+
return `${styleText("gray", S_BAR)}\n${title}\n${styleText("gray", S_BAR)} ${styleText("dim", `${this.selected.size} items selected`)}`;
|
|
193
|
+
if (this.state === "cancel")
|
|
194
|
+
return `${styleText("gray", S_BAR)}\n${title}\n${styleText("gray", S_BAR)} ${styleText(["strikethrough", "dim"], `${this.selected.size} items selected`)}`;
|
|
195
|
+
const lines = [styleText("gray", S_BAR), title];
|
|
196
|
+
const tabLine = this.tabs.map((tab, index) => {
|
|
197
|
+
const rows = this.rowsByTab.get(tab) ?? [];
|
|
198
|
+
const picked = rows.filter((row) => this.selected.has(row.candidate.id)).length;
|
|
199
|
+
const text = `${tab} ${rows.length}${picked ? ` (${picked} selected)` : ""}`;
|
|
200
|
+
return index === this.tab ? styleText(["inverse", "bold"], ` ${text} `) : styleText("dim", ` ${text} `);
|
|
201
|
+
}).join(styleText("gray", "│"));
|
|
202
|
+
lines.push(`${bar} ${tabLine}`);
|
|
203
|
+
lines.push(`${bar} ${styleText("dim", "Search:")} ${clip(this.options.sanitize(this.filter), width - 10)}${styleText("inverse", " ")}`);
|
|
204
|
+
const rows = this.visibleRows();
|
|
205
|
+
const maxRows = Math.max(3, Math.min(10, (process.stdout.rows ?? 30) - 20));
|
|
206
|
+
const start = Math.min(Math.max(0, this.cursor - Math.floor(maxRows / 2)), Math.max(0, rows.length - maxRows));
|
|
207
|
+
if (rows.length === 0)
|
|
208
|
+
lines.push(`${bar} ${styleText("yellow", "No matches found")}`);
|
|
209
|
+
if (start > 0)
|
|
210
|
+
lines.push(`${bar} ${styleText("dim", "…")}`);
|
|
211
|
+
for (const [offset, row] of rows.slice(start, start + maxRows).entries()) {
|
|
212
|
+
const index = start + offset;
|
|
213
|
+
const box = this.selected.has(row.candidate.id) ? styleText("green", S_CHECKBOX_SELECTED) : styleText("dim", S_CHECKBOX_INACTIVE);
|
|
214
|
+
// Middle truncation keeps both the name and trailing markers such as "(+1 identical copy)".
|
|
215
|
+
const text = clipMiddle(row.label, width - 4);
|
|
216
|
+
lines.push(`${bar} ${box} ${index === this.cursor ? styleText("underline", text) : text}`);
|
|
217
|
+
}
|
|
218
|
+
if (start + maxRows < rows.length)
|
|
219
|
+
lines.push(`${bar} ${styleText("dim", "…")}`);
|
|
220
|
+
const focused = rows[this.cursor];
|
|
221
|
+
if (focused) {
|
|
222
|
+
const preview = this.preview(focused);
|
|
223
|
+
lines.push(bar);
|
|
224
|
+
lines.push(`${bar} ${styleText("dim", clipMiddle(`── Preview: ${preview.title} (masked) · ${focused.details} ──`, width))}`);
|
|
225
|
+
for (const line of preview.body)
|
|
226
|
+
lines.push(`${bar} ${styleText("dim", clip(line, width))}`);
|
|
227
|
+
}
|
|
228
|
+
lines.push(`${bar} ${styleText("dim", "←/→ tabs • ↑/↓ move • Tab: select • Enter: confirm • Type: to search")}`);
|
|
229
|
+
lines.push(styleText("cyan", S_BAR_END));
|
|
230
|
+
return lines.join("\n");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
export async function resourcePicker(options) {
|
|
234
|
+
const result = await new ResourcePickerPrompt(options).prompt();
|
|
235
|
+
// Translate core's cancel into the symbol @clack/prompts' isCancel recognizes, even if two core copies were installed.
|
|
236
|
+
if (isCoreCancel(result))
|
|
237
|
+
return CANCEL_SYMBOL;
|
|
238
|
+
return result ?? [];
|
|
239
|
+
}
|
package/dist/src/wizard.js
CHANGED
|
@@ -8,6 +8,7 @@ import { candidateHint, candidateLabels, collapseIdenticalCandidates, contentFin
|
|
|
8
8
|
import { inspectPreparedResource, maskSensitiveText } from "./inspect.js";
|
|
9
9
|
import { assertNameAvailable, assertNoNameConflicts, findNameConflicts, nameKey } from "./names.js";
|
|
10
10
|
import { executeApply, planApply } from "./apply.js";
|
|
11
|
+
import { resourcePicker } from "./resource-picker.js";
|
|
11
12
|
/** Mask display metadata too, and render controls literally rather than executing VT sequences. */
|
|
12
13
|
export function safeDisplay(value) {
|
|
13
14
|
return maskSensitiveText(value)
|
|
@@ -507,20 +508,21 @@ export async function runSetup(libraryDir, roots) {
|
|
|
507
508
|
"(suspected secrets need approval one by one; No skips that resource) \u2192 final confirmation.",
|
|
508
509
|
`Copies are saved to ${displayPath(libraryDir, homeDir)} only if you answer Yes at the final confirmation.`,
|
|
509
510
|
"Ctrl-C at any point saves nothing, and your original files are never changed.",
|
|
510
|
-
"
|
|
511
|
+
"Tabs group resources by kind; the masked preview below the list follows the focused row.",
|
|
511
512
|
].join("\n"), "Choosing resources");
|
|
512
513
|
let previousSelection = [];
|
|
513
514
|
let resolvedCandidates;
|
|
514
515
|
while (true) {
|
|
515
|
-
const selectedIds = await
|
|
516
|
-
message:
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
516
|
+
const selectedIds = await resourcePicker({
|
|
517
|
+
message: `Select resources to review and save as copied snapshots ${multiselectKeyHint}`,
|
|
518
|
+
rows: sortedCandidates.map((candidate) => ({
|
|
519
|
+
candidate,
|
|
520
|
+
label: safeDisplay(candidateLabelMap.get(candidate.id) + identicalCopiesSuffix(identicalCopies.get(candidate.id))),
|
|
521
|
+
details: safeDisplay(candidateHint(candidate, candidateStats.get(candidate.id) ?? {}, homeDir, undefined, 0)),
|
|
522
|
+
})),
|
|
522
523
|
initialValues: previousSelection,
|
|
523
|
-
|
|
524
|
+
sanitize: safeDisplay,
|
|
525
|
+
...inheritedSignal(),
|
|
524
526
|
});
|
|
525
527
|
if (isCancel(selectedIds)) {
|
|
526
528
|
cancel("Setup cancelled; no resources were saved.");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@papillon6814/copi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Collect and organize reusable Pi coding-agent resources",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -26,8 +26,10 @@
|
|
|
26
26
|
"check": "npm run build && npm test && npm pack --dry-run"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
+
"@clack/core": "1.5.1",
|
|
29
30
|
"@clack/prompts": "^1.8.1",
|
|
30
|
-
"diff": "^9.0.0"
|
|
31
|
+
"diff": "^9.0.0",
|
|
32
|
+
"fast-string-width": "^3.0.2"
|
|
31
33
|
},
|
|
32
34
|
"devDependencies": {
|
|
33
35
|
"@types/node": "^22.15.30",
|