pi-do-always 0.4.5 → 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 +12 -0
- package/extensions/pi-do-always/index.ts +13 -4
- package/extensions/pi-do-always/tasks.ts +63 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -113,6 +113,18 @@ Fields:
|
|
|
113
113
|
In the object form you can also configure the selector shortcut:
|
|
114
114
|
|
|
115
115
|
- `shortcut` (optional) — key that opens the selector, e.g. `"f4"`. Set to `null` to disable the shortcut. Defaults to `F4`. The project file's value wins over the global one.
|
|
116
|
+
- `merge` (optional) — how project tasks combine with the global tasks: `"override"` (default) replaces a global task with the same `name`; `"append"` keeps the global tasks and only adds new project task names (a cascade, like CSS). The project file's value wins over the global one; when neither sets it, the default is `override` (the historical behavior).
|
|
117
|
+
|
|
118
|
+
Example project file that only *adds* tasks without overriding the global set:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"merge": "append",
|
|
123
|
+
"tasks": [
|
|
124
|
+
{ "name": "deploy", "category": "Ops", "prompt": "Deploy this project to staging." }
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
```
|
|
116
128
|
|
|
117
129
|
Reload Pi (or start a new session) after editing a config file.
|
|
118
130
|
|
|
@@ -71,22 +71,31 @@ import {
|
|
|
71
71
|
* Project-local tasks override global tasks with the same name; new ones are appended.
|
|
72
72
|
* Falls back to DEFAULT_TASKS when nothing is defined.
|
|
73
73
|
*/
|
|
74
|
-
function loadConfig(cwd: string): {
|
|
74
|
+
function loadConfig(cwd: string): {
|
|
75
|
+
tasks: DoAlwaysTask[];
|
|
76
|
+
shortcut: string | null;
|
|
77
|
+
merge: "append" | "override";
|
|
78
|
+
} {
|
|
75
79
|
const globalPath = join(getAgentDir(), "do-always.json");
|
|
76
80
|
const projectPath = join(cwd, CONFIG_DIR_NAME, "do-always.json");
|
|
77
81
|
|
|
78
82
|
const global = existsSync(globalPath)
|
|
79
83
|
? parseConfig(readFileSync(globalPath, "utf-8"), globalPath)
|
|
80
|
-
: { tasks: [], shortcut: undefined };
|
|
84
|
+
: { tasks: [], shortcut: undefined, merge: undefined };
|
|
81
85
|
const project = existsSync(projectPath)
|
|
82
86
|
? parseConfig(readFileSync(projectPath, "utf-8"), projectPath)
|
|
83
|
-
: { tasks: [], shortcut: undefined };
|
|
87
|
+
: { tasks: [], shortcut: undefined, merge: undefined };
|
|
88
|
+
|
|
89
|
+
// The project file's merge mode wins; otherwise the global value; otherwise
|
|
90
|
+
// override (the historical behavior), so existing configs are unaffected.
|
|
91
|
+
const mode = project.merge ?? global.merge ?? "override";
|
|
84
92
|
|
|
85
93
|
return {
|
|
86
94
|
// Order the merged list by category so the selector numbers, digit-pick,
|
|
87
95
|
// `/do-always <n>`, and `list` all share one consistent order.
|
|
88
|
-
tasks: orderTasksByCategory(mergeTasks(global.tasks, project.tasks, DEFAULT_TASKS)),
|
|
96
|
+
tasks: orderTasksByCategory(mergeTasks(global.tasks, project.tasks, DEFAULT_TASKS, mode)),
|
|
89
97
|
shortcut: resolveShortcut(global.shortcut, project.shortcut),
|
|
98
|
+
merge: mode,
|
|
90
99
|
};
|
|
91
100
|
}
|
|
92
101
|
|
|
@@ -32,8 +32,17 @@ export interface DoAlwaysTask {
|
|
|
32
32
|
* A config file can be a bare array of tasks, or {"tasks": [...], "shortcut": ...}.
|
|
33
33
|
* `shortcut` is a key id string (e.g. "f4", "ctrl+shift+p"), or null to disable
|
|
34
34
|
* the keyboard shortcut.
|
|
35
|
+
* `merge` controls how project tasks combine with global tasks:
|
|
36
|
+
* `override` (default) replaces a global task with the same name;
|
|
37
|
+
* `append` keeps globals and only adds new project task names (a cascade).
|
|
35
38
|
*/
|
|
36
|
-
export type DoAlwaysConfig =
|
|
39
|
+
export type DoAlwaysConfig =
|
|
40
|
+
| DoAlwaysTask[]
|
|
41
|
+
| {
|
|
42
|
+
tasks: DoAlwaysTask[];
|
|
43
|
+
shortcut?: string | null;
|
|
44
|
+
merge?: "append" | "override";
|
|
45
|
+
};
|
|
37
46
|
|
|
38
47
|
/** Shortcut used when neither config file specifies one. */
|
|
39
48
|
export const DEFAULT_SHORTCUT = "f4";
|
|
@@ -46,6 +55,11 @@ export interface ParsedDoAlwaysConfig {
|
|
|
46
55
|
* disabled, undefined when the file does not set one.
|
|
47
56
|
*/
|
|
48
57
|
shortcut: string | null | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* The `merge` field, if present: "append" or "override", undefined when the
|
|
60
|
+
* file does not set one.
|
|
61
|
+
*/
|
|
62
|
+
merge?: "append" | "override" | undefined;
|
|
49
63
|
}
|
|
50
64
|
|
|
51
65
|
/**
|
|
@@ -196,14 +210,18 @@ export function parseConfig(
|
|
|
196
210
|
}
|
|
197
211
|
|
|
198
212
|
let shortcut: string | null | undefined;
|
|
213
|
+
let merge: "append" | "override" | undefined;
|
|
199
214
|
if (!Array.isArray(data) && "shortcut" in data) {
|
|
200
215
|
const s = data.shortcut;
|
|
201
216
|
if (s === null) shortcut = null;
|
|
202
217
|
else if (typeof s === "string") shortcut = s.trim() === "" ? null : s.trim();
|
|
203
218
|
else onError(`do-always: ignoring invalid "shortcut" in ${path} (expected a key string or null)`);
|
|
204
219
|
}
|
|
220
|
+
if (!Array.isArray(data) && "merge" in data) {
|
|
221
|
+
merge = parseMerge(data.merge, path, onError);
|
|
222
|
+
}
|
|
205
223
|
|
|
206
|
-
return { tasks, shortcut };
|
|
224
|
+
return { tasks, shortcut, merge };
|
|
207
225
|
}
|
|
208
226
|
|
|
209
227
|
const KEY_MODIFIERS = new Set(["ctrl", "shift", "alt", "super"]);
|
|
@@ -242,12 +260,53 @@ export function resolveShortcut(
|
|
|
242
260
|
return DEFAULT_SHORTCUT;
|
|
243
261
|
}
|
|
244
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Parse the optional `merge` field: "append" or "override" (case-insensitive),
|
|
265
|
+
* or undefined when absent. A non-string or unrecognized value is ignored with
|
|
266
|
+
* a warning, so it never silently changes behavior.
|
|
267
|
+
*/
|
|
268
|
+
function parseMerge(
|
|
269
|
+
raw: unknown,
|
|
270
|
+
path: string,
|
|
271
|
+
onError: (message: string) => void,
|
|
272
|
+
): "append" | "override" | undefined {
|
|
273
|
+
if (raw === undefined) return undefined;
|
|
274
|
+
if (typeof raw !== "string") {
|
|
275
|
+
onError(`do-always: ignoring invalid "merge" in ${path} (expected "append" or "override")`);
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
const v = raw.trim().toLowerCase();
|
|
279
|
+
if (v === "append" || v === "override") return v;
|
|
280
|
+
onError(`do-always: ignoring invalid "merge" in ${path} (expected "append" or "override")`);
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
|
|
245
284
|
/**
|
|
246
285
|
* Merge project-local tasks over global tasks.
|
|
247
|
-
*
|
|
286
|
+
*
|
|
287
|
+
* When `mode` is "override" (default), a project task with a name matching a
|
|
288
|
+
* global task replaces it; new names are appended. When "append", globals are
|
|
289
|
+
* kept as-is and only new (non-duplicate) project task names are appended.
|
|
248
290
|
* Returns fallback when the merged result is empty.
|
|
249
291
|
*/
|
|
250
|
-
export function mergeTasks(
|
|
292
|
+
export function mergeTasks(
|
|
293
|
+
globalTasks: DoAlwaysTask[],
|
|
294
|
+
projectTasks: DoAlwaysTask[],
|
|
295
|
+
fallback: DoAlwaysTask[],
|
|
296
|
+
mode: "append" | "override" = "override",
|
|
297
|
+
): DoAlwaysTask[] {
|
|
298
|
+
if (mode === "append") {
|
|
299
|
+
const merged = [...globalTasks];
|
|
300
|
+
const names = new Set(merged.map((t) => t.name));
|
|
301
|
+
for (const task of projectTasks) {
|
|
302
|
+
if (!names.has(task.name)) {
|
|
303
|
+
merged.push(task);
|
|
304
|
+
names.add(task.name);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return merged.length > 0 ? merged : fallback;
|
|
308
|
+
}
|
|
309
|
+
|
|
251
310
|
const merged = [...globalTasks];
|
|
252
311
|
for (const task of projectTasks) {
|
|
253
312
|
const idx = merged.findIndex((t) => t.name === task.name);
|