trekoon 0.3.1 → 0.3.2
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/.agents/skills/trekoon/SKILL.md +126 -14
- package/.agents/skills/trekoon/reference/execution-with-team.md +213 -0
- package/.agents/skills/trekoon/reference/execution.md +210 -0
- package/.agents/skills/trekoon/reference/planning.md +244 -0
- package/README.md +20 -9
- package/docs/ai-agents.md +59 -26
- package/package.json +2 -2
- package/src/board/assets/app.js +5 -0
- package/src/board/assets/components/EpicsOverview.js +13 -0
- package/src/board/assets/components/Workspace.js +27 -12
- package/src/board/assets/components/helpers.js +3 -2
- package/src/board/assets/runtime/delegation.js +69 -1
- package/src/board/assets/state/actions.js +27 -1
- package/src/board/assets/state/store.js +37 -8
- package/src/board/assets/state/utils.js +42 -0
- package/src/board/assets/styles/board.css +68 -0
- package/src/commands/skills.ts +39 -32
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isValidTransition } from "../state/utils.js";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Event delegation system for the board runtime.
|
|
3
5
|
*
|
|
@@ -89,6 +91,31 @@ export function createDelegation(rootElement, actions) {
|
|
|
89
91
|
return;
|
|
90
92
|
}
|
|
91
93
|
|
|
94
|
+
// -- Status filter pills ---------------------------------------------------
|
|
95
|
+
const epicFilterEl = target.closest("[data-toggle-epic-status-filter]");
|
|
96
|
+
if (epicFilterEl) {
|
|
97
|
+
actions.toggleEpicStatusFilter(epicFilterEl.dataset.toggleEpicStatusFilter);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const taskFilterEl = target.closest("[data-toggle-task-status-filter]");
|
|
102
|
+
if (taskFilterEl) {
|
|
103
|
+
actions.toggleTaskStatusFilter(taskFilterEl.dataset.toggleTaskStatusFilter);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const resetEpicFilterEl = target.closest("[data-reset-epic-filter]");
|
|
108
|
+
if (resetEpicFilterEl) {
|
|
109
|
+
actions.resetEpicFilter();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const resetTaskFilterEl = target.closest("[data-reset-task-filter]");
|
|
114
|
+
if (resetTaskFilterEl) {
|
|
115
|
+
actions.resetTaskFilter();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
92
119
|
// -- Navigation -----------------------------------------------------------
|
|
93
120
|
|
|
94
121
|
const navEl = target.closest("[data-nav]");
|
|
@@ -276,6 +303,15 @@ export function createDelegation(rootElement, actions) {
|
|
|
276
303
|
// ---------------------------------------------------------------------------
|
|
277
304
|
// Drag-and-drop delegation
|
|
278
305
|
// ---------------------------------------------------------------------------
|
|
306
|
+
let draggedTaskStatus = null;
|
|
307
|
+
|
|
308
|
+
function cleanupDragFeedback() {
|
|
309
|
+
draggedTaskStatus = null;
|
|
310
|
+
for (const el of rootElement.querySelectorAll(".board-drop-valid, .board-drop-invalid")) {
|
|
311
|
+
el.classList.remove("board-drop-valid", "board-drop-invalid");
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
279
315
|
function handleDragstart(event) {
|
|
280
316
|
const draggable = event.target.closest("[data-draggable-task]");
|
|
281
317
|
if (!draggable) return;
|
|
@@ -290,11 +326,28 @@ export function createDelegation(rootElement, actions) {
|
|
|
290
326
|
|
|
291
327
|
event.dataTransfer?.setData("text/task-id", taskId);
|
|
292
328
|
event.dataTransfer?.setData("text/plain", taskId);
|
|
329
|
+
draggedTaskStatus = actions.getTaskStatus(taskId);
|
|
293
330
|
}
|
|
294
331
|
|
|
295
332
|
function handleDragover(event) {
|
|
296
|
-
|
|
333
|
+
const column = event.target.closest("[data-drop-status]");
|
|
334
|
+
if (!column) return;
|
|
335
|
+
|
|
336
|
+
const targetStatus = column.dataset.dropStatus;
|
|
337
|
+
if (draggedTaskStatus && isValidTransition(draggedTaskStatus, targetStatus)) {
|
|
297
338
|
event.preventDefault();
|
|
339
|
+
column.classList.add("board-drop-valid");
|
|
340
|
+
column.classList.remove("board-drop-invalid");
|
|
341
|
+
} else if (draggedTaskStatus && targetStatus !== draggedTaskStatus) {
|
|
342
|
+
column.classList.add("board-drop-invalid");
|
|
343
|
+
column.classList.remove("board-drop-valid");
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function handleDragleave(event) {
|
|
348
|
+
const column = event.target.closest("[data-drop-status]");
|
|
349
|
+
if (column && !column.contains(event.relatedTarget)) {
|
|
350
|
+
column.classList.remove("board-drop-valid", "board-drop-invalid");
|
|
298
351
|
}
|
|
299
352
|
}
|
|
300
353
|
|
|
@@ -309,7 +362,18 @@ export function createDelegation(rootElement, actions) {
|
|
|
309
362
|
event.dataTransfer?.getData("text/task-id") ||
|
|
310
363
|
event.dataTransfer?.getData("text/plain");
|
|
311
364
|
const nextStatus = column.dataset.dropStatus;
|
|
365
|
+
|
|
366
|
+
if (draggedTaskStatus && !isValidTransition(draggedTaskStatus, nextStatus)) {
|
|
367
|
+
cleanupDragFeedback();
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
312
371
|
actions.dropTaskStatus(taskId, nextStatus);
|
|
372
|
+
cleanupDragFeedback();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function handleDragend() {
|
|
376
|
+
cleanupDragFeedback();
|
|
313
377
|
}
|
|
314
378
|
|
|
315
379
|
// ---------------------------------------------------------------------------
|
|
@@ -321,7 +385,9 @@ export function createDelegation(rootElement, actions) {
|
|
|
321
385
|
rootElement.addEventListener("submit", handleSubmit);
|
|
322
386
|
rootElement.addEventListener("dragstart", handleDragstart);
|
|
323
387
|
rootElement.addEventListener("dragover", handleDragover);
|
|
388
|
+
rootElement.addEventListener("dragleave", handleDragleave);
|
|
324
389
|
rootElement.addEventListener("drop", handleDrop);
|
|
390
|
+
rootElement.addEventListener("dragend", handleDragend);
|
|
325
391
|
rootElement.addEventListener("keydown", handleDelegatedKeydown);
|
|
326
392
|
window.addEventListener("keydown", handleKeydown);
|
|
327
393
|
|
|
@@ -335,7 +401,9 @@ export function createDelegation(rootElement, actions) {
|
|
|
335
401
|
rootElement.removeEventListener("submit", handleSubmit);
|
|
336
402
|
rootElement.removeEventListener("dragstart", handleDragstart);
|
|
337
403
|
rootElement.removeEventListener("dragover", handleDragover);
|
|
404
|
+
rootElement.removeEventListener("dragleave", handleDragleave);
|
|
338
405
|
rootElement.removeEventListener("drop", handleDrop);
|
|
406
|
+
rootElement.removeEventListener("dragend", handleDragend);
|
|
339
407
|
rootElement.removeEventListener("keydown", handleDelegatedKeydown);
|
|
340
408
|
window.removeEventListener("keydown", handleKeydown);
|
|
341
409
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { copyTextToClipboard } from "../runtime/clipboard.js";
|
|
2
|
-
import { orderEpicsNewestFirst } from "./store.js";
|
|
2
|
+
import { DEFAULT_STATUS_FILTER, orderEpicsNewestFirst } from "./store.js";
|
|
3
3
|
|
|
4
4
|
function cloneSnapshot(snapshot) {
|
|
5
5
|
if (typeof structuredClone === "function") {
|
|
@@ -404,6 +404,10 @@ export function createBoardActions(options) {
|
|
|
404
404
|
removeDependency(sourceId, dependsOnId) {
|
|
405
405
|
api.removeDependency(sourceId, dependsOnId, (snapshot) => removeDependencyInSnapshot(snapshot, sourceId, dependsOnId, normalizeSnapshot));
|
|
406
406
|
},
|
|
407
|
+
getTaskStatus(taskId) {
|
|
408
|
+
const task = getTaskById(taskId);
|
|
409
|
+
return task?.status ?? null;
|
|
410
|
+
},
|
|
407
411
|
dropTaskStatus(taskId, nextStatus) {
|
|
408
412
|
const task = getTaskById(taskId);
|
|
409
413
|
if (!task || !nextStatus || task.status === nextStatus) {
|
|
@@ -426,6 +430,28 @@ export function createBoardActions(options) {
|
|
|
426
430
|
cascadeEpicStatusInSnapshot(snapshot, epicId, normalizedStatus, normalizeSnapshot),
|
|
427
431
|
);
|
|
428
432
|
},
|
|
433
|
+
toggleEpicStatusFilter(status) {
|
|
434
|
+
const current = store.epicStatusFilter || { ...DEFAULT_STATUS_FILTER };
|
|
435
|
+
store.epicStatusFilter = { ...current, [status]: !current[status] };
|
|
436
|
+
persist();
|
|
437
|
+
rerender();
|
|
438
|
+
},
|
|
439
|
+
toggleTaskStatusFilter(status) {
|
|
440
|
+
const current = store.taskStatusFilter || { ...DEFAULT_STATUS_FILTER };
|
|
441
|
+
store.taskStatusFilter = { ...current, [status]: !current[status] };
|
|
442
|
+
persist();
|
|
443
|
+
rerender();
|
|
444
|
+
},
|
|
445
|
+
resetEpicFilter() {
|
|
446
|
+
store.epicStatusFilter = { ...DEFAULT_STATUS_FILTER };
|
|
447
|
+
persist();
|
|
448
|
+
rerender();
|
|
449
|
+
},
|
|
450
|
+
resetTaskFilter() {
|
|
451
|
+
store.taskStatusFilter = { ...DEFAULT_STATUS_FILTER };
|
|
452
|
+
persist();
|
|
453
|
+
rerender();
|
|
454
|
+
},
|
|
429
455
|
handleKeydown(event) {
|
|
430
456
|
const boardState = getBoardState();
|
|
431
457
|
const activeElement = document.activeElement;
|
|
@@ -9,6 +9,18 @@ function normalizeSearch(value) {
|
|
|
9
9
|
|
|
10
10
|
// --- Persistence helpers ---
|
|
11
11
|
|
|
12
|
+
export const DEFAULT_STATUS_FILTER = { todo: true, blocked: true, in_progress: true, done: false };
|
|
13
|
+
|
|
14
|
+
function readStatusFilter(raw) {
|
|
15
|
+
if (typeof raw !== "object" || raw === null) return { ...DEFAULT_STATUS_FILTER };
|
|
16
|
+
return {
|
|
17
|
+
todo: typeof raw.todo === "boolean" ? raw.todo : DEFAULT_STATUS_FILTER.todo,
|
|
18
|
+
blocked: typeof raw.blocked === "boolean" ? raw.blocked : DEFAULT_STATUS_FILTER.blocked,
|
|
19
|
+
in_progress: typeof raw.in_progress === "boolean" ? raw.in_progress : DEFAULT_STATUS_FILTER.in_progress,
|
|
20
|
+
done: typeof raw.done === "boolean" ? raw.done : DEFAULT_STATUS_FILTER.done,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
export function readStoredState() {
|
|
13
25
|
try {
|
|
14
26
|
return JSON.parse(localStorage.getItem(STATE_STORAGE_KEY) || "{}");
|
|
@@ -77,13 +89,25 @@ export function orderEpicsNewestFirst(epics) {
|
|
|
77
89
|
|
|
78
90
|
// --- Derived state selectors ---
|
|
79
91
|
|
|
92
|
+
/** Recently-done epics stay visible for 24h even when the "done" filter is off. */
|
|
93
|
+
const DONE_GRACE_PERIOD_MS = 86400000;
|
|
94
|
+
|
|
80
95
|
const selectVisibleEpics = createSelector(
|
|
81
|
-
(s) => [s.snapshot?.epics, s.searchQuery],
|
|
82
|
-
(epics, searchQuery) => {
|
|
96
|
+
(s) => [s.snapshot?.epics, s.searchQuery, s.epicStatusFilter],
|
|
97
|
+
(epics, searchQuery, epicStatusFilter) => {
|
|
83
98
|
if (!epics) return [];
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
const filtered = epics.filter((epic) => {
|
|
101
|
+
if (epic.status === "done") {
|
|
102
|
+
if (!epicStatusFilter.done && (now - epic.updatedAt) > DONE_GRACE_PERIOD_MS) return false;
|
|
103
|
+
} else if (!epicStatusFilter[epic.status]) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
});
|
|
84
108
|
const matchingEpics = searchQuery.length === 0
|
|
85
|
-
?
|
|
86
|
-
:
|
|
109
|
+
? filtered
|
|
110
|
+
: filtered.filter((epic) => epic.searchText.includes(searchQuery));
|
|
87
111
|
|
|
88
112
|
return orderEpicsNewestFirst(matchingEpics);
|
|
89
113
|
},
|
|
@@ -100,11 +124,12 @@ const selectTasksInScope = createSelector(
|
|
|
100
124
|
);
|
|
101
125
|
|
|
102
126
|
const selectVisibleTasks = createSelector(
|
|
103
|
-
(s) => [selectTasksInScope(s), s.searchQuery],
|
|
104
|
-
(tasksInScope, searchQuery) => {
|
|
127
|
+
(s) => [selectTasksInScope(s), s.searchQuery, s.taskStatusFilter],
|
|
128
|
+
(tasksInScope, searchQuery, taskStatusFilter) => {
|
|
129
|
+
const filtered = tasksInScope.filter((task) => taskStatusFilter[task.status]);
|
|
105
130
|
return searchQuery.length === 0
|
|
106
|
-
?
|
|
107
|
-
:
|
|
131
|
+
? filtered
|
|
132
|
+
: filtered.filter((task) => task.searchText.includes(searchQuery));
|
|
108
133
|
},
|
|
109
134
|
);
|
|
110
135
|
|
|
@@ -287,6 +312,8 @@ export function createStore(initialSnapshot, options = {}) {
|
|
|
287
312
|
notice: null,
|
|
288
313
|
isMutating: false,
|
|
289
314
|
notesPanelOpen: storedState.notesPanelOpen === true,
|
|
315
|
+
epicStatusFilter: readStatusFilter(storedState.epicStatusFilter),
|
|
316
|
+
taskStatusFilter: readStatusFilter(storedState.taskStatusFilter),
|
|
290
317
|
};
|
|
291
318
|
|
|
292
319
|
/** @type {Set<(state: object) => void>} */
|
|
@@ -306,6 +333,8 @@ export function createStore(initialSnapshot, options = {}) {
|
|
|
306
333
|
view: state.view,
|
|
307
334
|
selectedTaskId: state.selectedTaskId,
|
|
308
335
|
notesPanelOpen: state.notesPanelOpen,
|
|
336
|
+
epicStatusFilter: state.epicStatusFilter,
|
|
337
|
+
taskStatusFilter: state.taskStatusFilter,
|
|
309
338
|
});
|
|
310
339
|
}
|
|
311
340
|
|
|
@@ -220,3 +220,45 @@ export function escapeHtml(value) {
|
|
|
220
220
|
.replaceAll(">", ">")
|
|
221
221
|
.replaceAll('"', """);
|
|
222
222
|
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Valid status transitions mirroring the backend state machine (src/domain/types.ts).
|
|
226
|
+
* @type {Map<string, Set<string>>}
|
|
227
|
+
*/
|
|
228
|
+
export const VALID_TRANSITIONS = new Map([
|
|
229
|
+
["todo", new Set(["in_progress", "blocked"])],
|
|
230
|
+
["in_progress", new Set(["done", "blocked"])],
|
|
231
|
+
["blocked", new Set(["in_progress", "todo"])],
|
|
232
|
+
["done", new Set(["in_progress"])],
|
|
233
|
+
]);
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get the list of statuses a node can transition to from its current status.
|
|
237
|
+
* @param {string} currentStatus
|
|
238
|
+
* @returns {string[]}
|
|
239
|
+
*/
|
|
240
|
+
export function getValidTargets(currentStatus) {
|
|
241
|
+
const targets = VALID_TRANSITIONS.get(currentStatus);
|
|
242
|
+
return targets ? Array.from(targets) : [];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Check whether transitioning from one status to another is valid.
|
|
247
|
+
* @param {string} from
|
|
248
|
+
* @param {string} to
|
|
249
|
+
* @returns {boolean}
|
|
250
|
+
*/
|
|
251
|
+
export function isValidTransition(from, to) {
|
|
252
|
+
const targets = VALID_TRANSITIONS.get(from);
|
|
253
|
+
return targets ? targets.has(to) : false;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Return the current status plus all valid targets, useful for populating
|
|
258
|
+
* status select dropdowns.
|
|
259
|
+
* @param {string} currentStatus
|
|
260
|
+
* @returns {string[]}
|
|
261
|
+
*/
|
|
262
|
+
export function getSelectableStatuses(currentStatus) {
|
|
263
|
+
return [currentStatus, ...getValidTargets(currentStatus)];
|
|
264
|
+
}
|
|
@@ -1929,3 +1929,71 @@ textarea,
|
|
|
1929
1929
|
.\!px-2 { padding-inline: 0.5rem !important; }
|
|
1930
1930
|
.\!text-xs { font-size: 0.75rem !important; line-height: 1rem !important; }
|
|
1931
1931
|
.\!min-h-0 { min-height: 0 !important; }
|
|
1932
|
+
|
|
1933
|
+
/* --- Drag-and-drop transition feedback --- */
|
|
1934
|
+
.board-drop-valid {
|
|
1935
|
+
outline: 2px solid var(--board-accent);
|
|
1936
|
+
outline-offset: -2px;
|
|
1937
|
+
background: rgba(var(--board-accent-rgb, 99, 102, 241), 0.06);
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
.board-drop-invalid {
|
|
1941
|
+
outline: 2px dashed rgba(239, 68, 68, 0.4);
|
|
1942
|
+
outline-offset: -2px;
|
|
1943
|
+
opacity: 0.6;
|
|
1944
|
+
cursor: not-allowed;
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
/* --- Filter pills --- */
|
|
1948
|
+
.board-filter-bar {
|
|
1949
|
+
display: flex;
|
|
1950
|
+
flex-wrap: wrap;
|
|
1951
|
+
align-items: center;
|
|
1952
|
+
gap: 0.375rem;
|
|
1953
|
+
margin-top: 0.75rem;
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
.board-filter-pill {
|
|
1957
|
+
display: inline-flex;
|
|
1958
|
+
align-items: center;
|
|
1959
|
+
gap: 0.25rem;
|
|
1960
|
+
padding: 0.25rem 0.75rem;
|
|
1961
|
+
border-radius: 9999px;
|
|
1962
|
+
border: 1px solid var(--board-border);
|
|
1963
|
+
background: var(--board-surface-2);
|
|
1964
|
+
color: var(--board-text-muted);
|
|
1965
|
+
font-size: 0.6875rem;
|
|
1966
|
+
font-weight: 600;
|
|
1967
|
+
text-transform: uppercase;
|
|
1968
|
+
letter-spacing: 0.14em;
|
|
1969
|
+
cursor: pointer;
|
|
1970
|
+
transition: opacity 0.2s, border-color 0.2s, background 0.2s;
|
|
1971
|
+
user-select: none;
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
.board-filter-pill:hover {
|
|
1975
|
+
border-color: var(--board-border-strong);
|
|
1976
|
+
background: rgba(255, 255, 255, 0.08);
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
.board-filter-pill:focus-visible {
|
|
1980
|
+
outline: none;
|
|
1981
|
+
box-shadow: 0 0 0 2px var(--board-border-strong);
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
.board-filter-pill--active {
|
|
1985
|
+
opacity: 1;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
.board-filter-pill--inactive {
|
|
1989
|
+
opacity: 0.4;
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
.board-filter-pill--reset {
|
|
1993
|
+
color: var(--board-accent);
|
|
1994
|
+
border-color: var(--board-accent);
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
.board-filter-pill--reset:hover {
|
|
1998
|
+
background: rgba(var(--board-accent-rgb, 99, 102, 241), 0.1);
|
|
1999
|
+
}
|
package/src/commands/skills.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { copyFileSync, existsSync, lstatSync, mkdirSync, readlinkSync, realpathSync, rmSync, symlinkSync } from "node:fs";
|
|
1
|
+
import { copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readlinkSync, realpathSync, rmSync, symlinkSync } from "node:fs";
|
|
2
2
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
|
|
@@ -79,6 +79,10 @@ function resolveBundledSkillFilePath(): string {
|
|
|
79
79
|
return fileURLToPath(new URL("../../.agents/skills/trekoon/SKILL.md", import.meta.url));
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
function resolveBundledSkillDirPath(): string {
|
|
83
|
+
return fileURLToPath(new URL("../../.agents/skills/trekoon", import.meta.url));
|
|
84
|
+
}
|
|
85
|
+
|
|
82
86
|
function toAbsolutePath(cwd: string, pathValue: string): string {
|
|
83
87
|
if (isAbsolute(pathValue)) {
|
|
84
88
|
return pathValue;
|
|
@@ -234,6 +238,7 @@ function resolveEditorConfigDir(cwd: string, editor: EditorName): string {
|
|
|
234
238
|
|
|
235
239
|
function installCanonicalSkill(cwd: string): CliResult | { sourcePath: string; installedPath: string; installedDir: string } {
|
|
236
240
|
const sourcePath: string = resolveBundledSkillFilePath();
|
|
241
|
+
const sourceDir: string = resolveBundledSkillDirPath();
|
|
237
242
|
if (!existsSync(sourcePath)) {
|
|
238
243
|
return failResult({
|
|
239
244
|
command: "skills.install",
|
|
@@ -255,6 +260,12 @@ function installCanonicalSkill(cwd: string): CliResult | { sourcePath: string; i
|
|
|
255
260
|
try {
|
|
256
261
|
mkdirSync(installedDir, { recursive: true });
|
|
257
262
|
copyFileSync(sourcePath, installedPath);
|
|
263
|
+
// Copy reference guides if they exist in the bundled source.
|
|
264
|
+
const sourceRefDir: string = join(sourceDir, "reference");
|
|
265
|
+
if (existsSync(sourceRefDir)) {
|
|
266
|
+
const installedRefDir: string = join(installedDir, "reference");
|
|
267
|
+
cpSync(sourceRefDir, installedRefDir, { recursive: true });
|
|
268
|
+
}
|
|
258
269
|
} catch (error: unknown) {
|
|
259
270
|
const message = error instanceof Error ? error.message : "Unknown skills install failure";
|
|
260
271
|
return failResult({
|
|
@@ -298,39 +309,28 @@ function replaceOrCreateSymlink(
|
|
|
298
309
|
|
|
299
310
|
const existing = lstatSync(linkPath);
|
|
300
311
|
if (!existing.isSymbolicLink()) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
error: {
|
|
310
|
-
code: "path_conflict",
|
|
311
|
-
message: "Symlink destination exists as a non-link path",
|
|
312
|
-
},
|
|
313
|
-
});
|
|
312
|
+
// Replace stale directory or file with symlink to the canonical location.
|
|
313
|
+
rmSync(linkPath, { recursive: true, force: true });
|
|
314
|
+
const boundaryFailure = revalidateLinkParentBoundary(repoRoot, linkPath, allowOutsideRepo);
|
|
315
|
+
if (boundaryFailure) {
|
|
316
|
+
return boundaryFailure;
|
|
317
|
+
}
|
|
318
|
+
symlinkSync(symlinkTarget, linkPath, "dir");
|
|
319
|
+
return null;
|
|
314
320
|
}
|
|
315
321
|
|
|
316
322
|
const existingRawTarget: string = readlinkSync(linkPath);
|
|
317
323
|
const existingAbsoluteTarget: string = toAbsolutePath(dirname(linkPath), existingRawTarget);
|
|
318
324
|
const expectedTarget: string = resolve(targetPath);
|
|
319
325
|
if (existingAbsoluteTarget !== expectedTarget) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
},
|
|
329
|
-
error: {
|
|
330
|
-
code: "path_conflict",
|
|
331
|
-
message: "Symlink destination points to a different target",
|
|
332
|
-
},
|
|
333
|
-
});
|
|
326
|
+
// Replace symlink pointing to a different target.
|
|
327
|
+
rmSync(linkPath, { force: true });
|
|
328
|
+
const boundaryFailure = revalidateLinkParentBoundary(repoRoot, linkPath, allowOutsideRepo);
|
|
329
|
+
if (boundaryFailure) {
|
|
330
|
+
return boundaryFailure;
|
|
331
|
+
}
|
|
332
|
+
symlinkSync(symlinkTarget, linkPath, "dir");
|
|
333
|
+
return null;
|
|
334
334
|
}
|
|
335
335
|
|
|
336
336
|
rmSync(linkPath, { force: true });
|
|
@@ -525,12 +525,16 @@ function updateEditorLink(
|
|
|
525
525
|
|
|
526
526
|
const entry = lstatSync(linkPath);
|
|
527
527
|
if (!entry.isSymbolicLink()) {
|
|
528
|
+
// Replace stale directory or file with symlink to the canonical location.
|
|
529
|
+
rmSync(linkPath, { recursive: true, force: true });
|
|
530
|
+
mkdirSync(dirname(linkPath), { recursive: true });
|
|
531
|
+
symlinkSync(symlinkTarget, linkPath, "dir");
|
|
528
532
|
return {
|
|
529
533
|
editor,
|
|
530
534
|
linkPath,
|
|
531
535
|
expectedTarget,
|
|
532
|
-
action: "
|
|
533
|
-
conflictCode:
|
|
536
|
+
action: "refreshed",
|
|
537
|
+
conflictCode: null,
|
|
534
538
|
existingTarget: null,
|
|
535
539
|
};
|
|
536
540
|
}
|
|
@@ -539,12 +543,15 @@ function updateEditorLink(
|
|
|
539
543
|
const existingTarget: string = toAbsolutePath(dirname(linkPath), existingRawTarget);
|
|
540
544
|
|
|
541
545
|
if (existingTarget !== expectedTarget) {
|
|
546
|
+
// Replace symlink pointing to a different target.
|
|
547
|
+
rmSync(linkPath, { force: true });
|
|
548
|
+
symlinkSync(symlinkTarget, linkPath, "dir");
|
|
542
549
|
return {
|
|
543
550
|
editor,
|
|
544
551
|
linkPath,
|
|
545
552
|
expectedTarget,
|
|
546
|
-
action: "
|
|
547
|
-
conflictCode:
|
|
553
|
+
action: "refreshed",
|
|
554
|
+
conflictCode: null,
|
|
548
555
|
existingTarget,
|
|
549
556
|
};
|
|
550
557
|
}
|