takibibase 1.14.1 → 1.15.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 +6 -5
- package/SKILL.md +13 -3
- package/package.json +2 -2
- package/takibi.mjs +103 -22
package/README.md
CHANGED
|
@@ -5,14 +5,14 @@ npx takibibase … # zero-install: always the latest release
|
|
|
5
5
|
npm i -g takibibase # or install once, then run `takibi …`
|
|
6
6
|
```
|
|
7
7
|
|
|
8
|
-
Needs Node.js
|
|
8
|
+
Needs Node.js 20+. This directory is also the npm package source:
|
|
9
9
|
`takibi.mjs` stays a single zero-dependency file so the
|
|
10
10
|
`/downloads/takibi.mjs` copy and the published package never drift.
|
|
11
11
|
|
|
12
12
|
Thin client-side enablement for agents. The CLI calls the project, task,
|
|
13
13
|
document, and Notes API routes; its behavior is documented in `SKILL.md`.
|
|
14
14
|
|
|
15
|
-
- `takibi.mjs` — the CLI. Single-file Node ≥
|
|
15
|
+
- `takibi.mjs` — the CLI. Single-file Node ≥ 20, zero dependencies.
|
|
16
16
|
Run it from a checkout as `node tools/takibi/takibi.mjs …`, or alias it:
|
|
17
17
|
`alias takibi='node <checkout>/tools/takibi/takibi.mjs'`.
|
|
18
18
|
- `SKILL.md` — the `takibi-use` skill. Short on purpose; the CLI carries
|
|
@@ -44,8 +44,10 @@ document, and Notes API routes; its behavior is documented in `SKILL.md`.
|
|
|
44
44
|
## Give an agent
|
|
45
45
|
|
|
46
46
|
Key file in place + `takibi skill --install` (puts the `takibi-use` skill
|
|
47
|
-
into the detected agent skills dirs; `--dir
|
|
48
|
-
|
|
47
|
+
into the detected agent skills dirs; repeat `--dir <skills-root>` per
|
|
48
|
+
custom box (`--dir` replaces detection unless a harness flag is also
|
|
49
|
+
passed), `--force` refreshes an older copy). Zero-install alternative:
|
|
50
|
+
`takibi skill` prints the skill —
|
|
49
51
|
paste it into the agent's first prompt. Acceptance: a fresh agent asks,
|
|
50
52
|
searches, lists tasks, appends/searches notes, and uses `notes list --all`
|
|
51
53
|
to find the current version before curation. Reads are free; task writes,
|
|
@@ -54,7 +56,6 @@ conversation; account-only routes stay yours.
|
|
|
54
56
|
|
|
55
57
|
## Known edges
|
|
56
58
|
|
|
57
|
-
- `tasks get` filters the board list client-side (no single-task GET).
|
|
58
59
|
- `doc download`/`delete`/`retry` refuse client-side with a pointer —
|
|
59
60
|
Bearer keys can never reach account-only routes (the server answers
|
|
60
61
|
those 401, not 403). `doc upload` is the exception: keys holding the
|
package/SKILL.md
CHANGED
|
@@ -66,6 +66,15 @@ error hints.
|
|
|
66
66
|
unassigned unless `--assignee` names someone; the source keeps its
|
|
67
67
|
status, holder, and history, and nothing carries over. Never move a
|
|
68
68
|
done card back to To do to continue it — follow up instead.
|
|
69
|
+
- `takibi tasks create --title "…"` — open a fresh card on the board
|
|
70
|
+
(`--body`, `--due <date>`, `--assignee` with the same name rules as
|
|
71
|
+
follow-up). Needs the create cap; naming anyone other than yourself
|
|
72
|
+
needs Orchestrator. The card lands at To do, unassigned unless
|
|
73
|
+
`--assignee` names someone — claim it before driving it.
|
|
74
|
+
- `takibi tasks update <id> [--title …] [--body …] [--due …]` — amend a
|
|
75
|
+
card without spinning a follow-up (at least one flag; empty `--due`
|
|
76
|
+
clears the date). A holder edits its own card; an orchestrator may
|
|
77
|
+
edit any card on the board. Title/body edits need the create cap.
|
|
69
78
|
- `@name` in a task names the handoff target: the FIRST explicit
|
|
70
79
|
`assign to @name` (or the first @name clearly meaning "who gets the
|
|
71
80
|
card next"). Later @ names describing someone else's downstream step
|
|
@@ -166,13 +175,14 @@ abstains honestly instead of answering halfway.
|
|
|
166
175
|
- Notes export/keep/remove need owner approval already given in the
|
|
167
176
|
conversation. Export only creates a draft; verify it before adding
|
|
168
177
|
content to Sources.
|
|
169
|
-
- Task claim/status/handoff/artifact/escalation/follow-up writes only with owner approval
|
|
178
|
+
- Task create/update/claim/status/handoff/artifact/escalation/follow-up writes only with owner approval
|
|
170
179
|
already given in conversation. Claim-first: a plain key must claim a card before
|
|
171
180
|
moving or touching it; only orchestrators/owners accept (`review→done`),
|
|
172
181
|
assign or release cards they do not hold, or archive. A holder may hand
|
|
173
182
|
its own card to another eligible profile (`tasks handoff`). Title/body edits need the create cap —
|
|
174
|
-
claim-only keys can drive a card but cannot rewrite its text.
|
|
175
|
-
the create cap too, and any granted creator may spin
|
|
183
|
+
claim-only keys can drive a card but cannot rewrite its text. Creates and
|
|
184
|
+
follow-ups need the create cap too, and any granted creator may spin a
|
|
185
|
+
follow-up off a card it does not hold.
|
|
176
186
|
- Boards need a whole-collection grant: folder-only or tag-only keys 403
|
|
177
187
|
on every task route (the full-collection server message says so verbatim
|
|
178
188
|
when the key holds the required cap; keys lacking the cap get the generic
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "takibibase",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"description": "Thin CLI for the Takibi API: ask, search, tasks, docs, notes. Single file, zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"README.md"
|
|
13
13
|
],
|
|
14
14
|
"engines": {
|
|
15
|
-
"node": ">=
|
|
15
|
+
"node": ">=20"
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
18
|
"test": "node --test notes.test.mjs skill.test.mjs tasks.test.mjs upload.test.mjs"
|
package/takibi.mjs
CHANGED
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
* fallback and for custom aliases. UUIDs pass straight through.
|
|
18
18
|
* `folders` lists granted folders via GET /v1/projects/:id/folders the
|
|
19
19
|
* same way (single-grant keys may omit --project; the CLI defaults it).
|
|
20
|
-
* -
|
|
20
|
+
* - Task ids are board-wide: `tasks get` reads GET /v1/tasks/:id straight
|
|
21
|
+
* off (no --project needed); `tasks create` posts POST /v1/tasks and
|
|
22
|
+
* `tasks update` patches PATCH /v1/tasks/:id, both project-scoped.
|
|
21
23
|
* - Account-only routes (doc download, deletes) answer 401, not 403, to
|
|
22
24
|
* Bearer [REDACTED]; `doc download` refuses client-side with a hint. Uploads
|
|
23
25
|
* accept keys holding documents:ingest (`doc upload`); keys without it 403.
|
|
@@ -37,7 +39,7 @@ import { homedir } from 'node:os';
|
|
|
37
39
|
import { basename, join } from 'node:path';
|
|
38
40
|
|
|
39
41
|
/** Baked fallback; the published package re-reads package.json next door. */
|
|
40
|
-
const BAKED_VERSION = '1.
|
|
42
|
+
const BAKED_VERSION = '1.15.0';
|
|
41
43
|
const CLI_INFO = (() => {
|
|
42
44
|
try {
|
|
43
45
|
const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
|
|
@@ -145,6 +147,10 @@ async function maybeNotifyUpdate({ json = false } = {}) {
|
|
|
145
147
|
? 'hint: download the latest takibi.mjs from https://takibibase.com/downloads/takibi.mjs'
|
|
146
148
|
: 'hint: `npm i -g takibibase@latest`, or `npx takibibase@latest` for one-shot runs.',
|
|
147
149
|
);
|
|
150
|
+
// Stale skills misroute crews (the upgrade bumps skill copy too), so
|
|
151
|
+
// the version nudge carries the refresh recipe. Single-file copies
|
|
152
|
+
// carry no SKILL.md to install — nothing to refresh there.
|
|
153
|
+
if (!CLI_INFO.singleFile) err('hint: then `takibi skill --install --force` refreshes agent skills (repeat `--dir <root>` per custom box).');
|
|
148
154
|
} catch {
|
|
149
155
|
// Offline, broken cache, slow registry: stay silent, run the command.
|
|
150
156
|
}
|
|
@@ -361,8 +367,10 @@ Commands:
|
|
|
361
367
|
search -q "..." Ranked chunks (snippets + metadata, no full text)
|
|
362
368
|
tasks list Board cards for the project
|
|
363
369
|
tasks get <id> One card with body + artifacts
|
|
370
|
+
tasks create Open a fresh card [--title/--body/--assignee/--due]
|
|
364
371
|
tasks claim <id> Claim an open to-do card (atomic, one winner)
|
|
365
372
|
tasks status <id> <st> Move a card: todo | in_progress | review | done [--note <text>]
|
|
373
|
+
tasks update <id> Amend a card's title, body, or due date [--title/--body/--due]
|
|
366
374
|
tasks handoff <id> <profile> Hand a card you hold to another profile [--review --note <text>]
|
|
367
375
|
tasks follow-up <id> Spin a fresh linked todo off any card [--title/--body/--assignee/--due]
|
|
368
376
|
tasks escalate <id> Ask the Founder for help on a held card [--reason <text>]
|
|
@@ -406,6 +414,11 @@ Per-command options:
|
|
|
406
414
|
tasks escalate: --reason <text> (required, max 500 chars — what do you need from the Founder?)
|
|
407
415
|
tasks follow-up: --title <text> (required, max 300 chars), --body <text> (max 20000 chars),
|
|
408
416
|
--assignee <name-or-uuid>, --due <date>; the source card is never modified
|
|
417
|
+
tasks create: --title <text> (required, max 300 chars), --body <text> (max 20000 chars),
|
|
418
|
+
--assignee <name-or-uuid>, --due <date>; needs the create cap,
|
|
419
|
+
lands at To do unassigned unless --assignee names someone
|
|
420
|
+
tasks update: --title/--body/--due (at least one; same limits as create;
|
|
421
|
+
empty --due clears the date)
|
|
409
422
|
tasks get: sent-back cards carry previousReviewNote, the last handoff note
|
|
410
423
|
tasks artifact add <url>: --note <text> --size <bytes> --hash <hex> (links only)
|
|
411
424
|
tasks artifact add --markdown <file|->: --title <text> --note <text> (content from file or stdin -, max 50000 chars; title max 140 chars)
|
|
@@ -422,6 +435,7 @@ Per-command options:
|
|
|
422
435
|
notes keep/remove: <id> <expected-version> from notes list --all
|
|
423
436
|
projects: --add <name> <uuid>
|
|
424
437
|
skill: --install [--force] [--agents] [--claude] [--codex] [--dir <skills-root>]
|
|
438
|
+
(repeat --dir per custom box; --dir alone skips detection)
|
|
425
439
|
|
|
426
440
|
Setup: save the owner-provided key (one <publicId>.<secret> line) to
|
|
427
441
|
~/.takibi/key (chmod 600). The API defaults to Takibi's servers; only
|
|
@@ -432,7 +446,7 @@ to probe the API and \`takibi projects\` to see this key's scope
|
|
|
432
446
|
Agents: \`takibi skill --install\` puts the takibi-use skill where
|
|
433
447
|
assistants look for it.
|
|
434
448
|
|
|
435
|
-
Reads are free. Export stamps included notes. Task claim/status/handoff/artifact/escalation/follow-up writes and doc uploads
|
|
449
|
+
Reads are free. Export stamps included notes. Task create/update/claim/status/handoff/artifact/escalation/follow-up writes and doc uploads
|
|
436
450
|
need owner approval in conversation (uploads also need the Uploads grant on the key).
|
|
437
451
|
Account-only routes (delete, retry, download originals, PATCH docs/projects)
|
|
438
452
|
are never the agent's to call — ask the account owner.
|
|
@@ -729,16 +743,47 @@ async function cmdTasks(tokens, globals) {
|
|
|
729
743
|
if (sub === 'get') {
|
|
730
744
|
const [id, extra] = rest;
|
|
731
745
|
if (!id || extra) throw usageError('Usage: takibi tasks get <id>');
|
|
746
|
+
// Ids are board-wide: no --project to resolve (same as follow-up).
|
|
747
|
+
const ctx = await ctxFor(globals, { needKey: true, resolveProject: false });
|
|
748
|
+
const data = await api('GET', `/v1/tasks/${id}`, { ...ctx, label: 'tasks get' });
|
|
749
|
+
if (ctx.json) out(JSON.stringify({ task: data.task }, null, 2));
|
|
750
|
+
else renderTask(data.task);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (sub === 'create') {
|
|
754
|
+
const usage = 'Usage: takibi tasks create --title <text> [--body <text>] [--assignee <name-or-uuid>] [--due <date>]';
|
|
755
|
+
let title;
|
|
756
|
+
let body;
|
|
757
|
+
let assignee;
|
|
758
|
+
let due;
|
|
759
|
+
for (let i = 0; i < rest.length; i++) {
|
|
760
|
+
const f = rest[i];
|
|
761
|
+
if (f === '--title') title = rest[++i];
|
|
762
|
+
else if (f === '--body') body = rest[++i];
|
|
763
|
+
else if (f === '--assignee') assignee = rest[++i];
|
|
764
|
+
else if (f === '--due') due = rest[++i];
|
|
765
|
+
else throw usageError(`Unexpected ${JSON.stringify(f)}. ${usage}`);
|
|
766
|
+
}
|
|
767
|
+
// Validate everything before touching the key or network, mirroring
|
|
768
|
+
// the server limits so failures name the flag instead of 400ing.
|
|
769
|
+
if (title === undefined || title.trim() === '') throw usageError('--title needs non-empty text.');
|
|
770
|
+
if (title.trim().length > 300) throw usageError('--title is max 300 chars.');
|
|
771
|
+
if (body !== undefined && body.length > 20000) throw usageError('--body is max 20000 chars.');
|
|
772
|
+
if (due !== undefined && due.trim() === '') throw usageError('--due needs a date.');
|
|
773
|
+
if (assignee !== undefined && String(assignee).trim() === '') throw usageError('--assignee needs a profile name or UUID.');
|
|
732
774
|
const ctx = await ctxFor(globals, { needKey: true });
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
tasks = await listTasks(ctx, true);
|
|
737
|
-
found = tasks.find((t) => t.id === id);
|
|
775
|
+
if (assignee !== undefined) {
|
|
776
|
+
const v = String(assignee).trim();
|
|
777
|
+
assignee = UUID_RE.test(v) ? v : (await resolveAssignee(v, ctx, 'tasks create')).id;
|
|
738
778
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
779
|
+
const data = await api('POST', '/v1/tasks', {
|
|
780
|
+
...ctx,
|
|
781
|
+
query: qparams([['projectId', ctx.projectId]]),
|
|
782
|
+
body: { title: title.trim(), body, assigneeProfileId: assignee, dueAt: due },
|
|
783
|
+
label: 'tasks create',
|
|
784
|
+
});
|
|
785
|
+
if (ctx.json) out(JSON.stringify(data, null, 2));
|
|
786
|
+
else renderTask(data.task);
|
|
742
787
|
return;
|
|
743
788
|
}
|
|
744
789
|
if (sub === 'claim') {
|
|
@@ -803,6 +848,41 @@ async function cmdTasks(tokens, globals) {
|
|
|
803
848
|
else renderTask(data.task);
|
|
804
849
|
return;
|
|
805
850
|
}
|
|
851
|
+
if (sub === 'update') {
|
|
852
|
+
const usage = 'Usage: takibi tasks update <id> [--title <text>] [--body <text>] [--due <date>]';
|
|
853
|
+
const [id, ...flagTokens] = rest;
|
|
854
|
+
if (!id || id.startsWith('--')) throw usageError(usage);
|
|
855
|
+
let title;
|
|
856
|
+
let body;
|
|
857
|
+
let due;
|
|
858
|
+
for (let i = 0; i < flagTokens.length; i++) {
|
|
859
|
+
const f = flagTokens[i];
|
|
860
|
+
if (f === '--title') title = flagTokens[++i];
|
|
861
|
+
else if (f === '--body') body = flagTokens[++i];
|
|
862
|
+
else if (f === '--due') due = flagTokens[++i];
|
|
863
|
+
else throw usageError(`Unexpected ${JSON.stringify(f)}. ${usage}`);
|
|
864
|
+
}
|
|
865
|
+
if (title === undefined && body === undefined && due === undefined) {
|
|
866
|
+
throw usageError(`Nothing to change — pass at least one of --title, --body, --due. ${usage}`);
|
|
867
|
+
}
|
|
868
|
+
if (title !== undefined && title.trim() === '') throw usageError('--title needs non-empty text.');
|
|
869
|
+
if (title !== undefined && title.trim().length > 300) throw usageError('--title is max 300 chars.');
|
|
870
|
+
if (body !== undefined && body.length > 20000) throw usageError('--body is max 20000 chars.');
|
|
871
|
+
// An explicit empty --due clears the date (the server treats '' and
|
|
872
|
+
// null as clear). Create keeps rejecting it: omit and clear are the
|
|
873
|
+
// same on POST.
|
|
874
|
+
if (due !== undefined && due.trim() === '') due = null;
|
|
875
|
+
const ctx = await ctxFor(globals, { needKey: true });
|
|
876
|
+
const data = await api('PATCH', `/v1/tasks/${id}`, {
|
|
877
|
+
...ctx,
|
|
878
|
+
query: qparams([['projectId', ctx.projectId]]),
|
|
879
|
+
body: { title: title?.trim(), body, dueAt: due },
|
|
880
|
+
label: 'tasks update',
|
|
881
|
+
});
|
|
882
|
+
if (ctx.json) out(JSON.stringify(data, null, 2));
|
|
883
|
+
else renderTask(data.task);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
806
886
|
if (sub === 'artifact' || sub === 'artifacts') {
|
|
807
887
|
const LINK_USAGE = 'Usage: takibi tasks artifact add <taskId> <url> [--note <t> --size <n> --hash <h>]';
|
|
808
888
|
const MD_USAGE = 'Usage: takibi tasks artifact add <taskId> --markdown <file|-> [--title <t>] [--note <t>]';
|
|
@@ -945,7 +1025,7 @@ async function cmdTasks(tokens, globals) {
|
|
|
945
1025
|
// Validate everything before touching the key or network, mirroring
|
|
946
1026
|
// the server limits so failures name the flag instead of 400ing.
|
|
947
1027
|
if (title === undefined || title.trim() === '') throw usageError('--title needs non-empty text.');
|
|
948
|
-
if (title.length > 300) throw usageError('--title is max 300 chars.');
|
|
1028
|
+
if (title.trim().length > 300) throw usageError('--title is max 300 chars.');
|
|
949
1029
|
if (body !== undefined && body.length > 20000) throw usageError('--body is max 20000 chars.');
|
|
950
1030
|
if (due !== undefined && due.trim() === '') throw usageError('--due needs a date.');
|
|
951
1031
|
if (assignee !== undefined && String(assignee).trim() === '') throw usageError('--assignee needs a profile name or UUID.');
|
|
@@ -970,7 +1050,7 @@ async function cmdTasks(tokens, globals) {
|
|
|
970
1050
|
else renderTask(data.task);
|
|
971
1051
|
return;
|
|
972
1052
|
}
|
|
973
|
-
throw usageError(`Unknown tasks command ${JSON.stringify(sub)}. Use list | get | claim | status | handoff | follow-up | escalate | deescalate | artifact add | artifact show.`);
|
|
1053
|
+
throw usageError(`Unknown tasks command ${JSON.stringify(sub)}. Use list | get | create | claim | status | update | handoff | follow-up | escalate | deescalate | artifact add | artifact show.`);
|
|
974
1054
|
}
|
|
975
1055
|
|
|
976
1056
|
async function cmdDoc(tokens, globals) {
|
|
@@ -1437,7 +1517,7 @@ const SKILL_TARGETS = [
|
|
|
1437
1517
|
{ flag: '--codex', harness: '.codex', dir: '.codex/skills' },
|
|
1438
1518
|
];
|
|
1439
1519
|
|
|
1440
|
-
const SKILL_USAGE = 'Usage: takibi skill [--install [--force] [--agents] [--claude] [--codex] [--dir <skills-root>]]';
|
|
1520
|
+
const SKILL_USAGE = 'Usage: takibi skill [--install [--force] [--agents] [--claude] [--codex] [--dir <skills-root>]…] (repeat --dir per custom box)';
|
|
1441
1521
|
|
|
1442
1522
|
/** First-run skill nudge. Stderr only (stdout stays clean), best-effort,
|
|
1443
1523
|
* never fails the command. Skipped for --json, in CI, with
|
|
@@ -1464,7 +1544,7 @@ function maybeNudgeSkill({ json = false } = {}) {
|
|
|
1464
1544
|
// Read-only home: nudge once per process run instead of daily.
|
|
1465
1545
|
}
|
|
1466
1546
|
err('takibi: no takibi-use skill installed for your agents.');
|
|
1467
|
-
err('hint: `takibi skill --install` puts it where assistants look.');
|
|
1547
|
+
err('hint: `takibi skill --install` puts it where assistants look (repeat `--dir <skills-root>` per custom box; `--force` refreshes).');
|
|
1468
1548
|
} catch {
|
|
1469
1549
|
// Nudges never fail commands.
|
|
1470
1550
|
}
|
|
@@ -1473,19 +1553,20 @@ function maybeNudgeSkill({ json = false } = {}) {
|
|
|
1473
1553
|
function cmdSkill(tokens, globals) {
|
|
1474
1554
|
let install = false;
|
|
1475
1555
|
let force = false;
|
|
1476
|
-
|
|
1556
|
+
const customRoots = [];
|
|
1477
1557
|
const picked = new Set();
|
|
1478
1558
|
for (let i = 0; i < tokens.length; i++) {
|
|
1479
1559
|
const t = tokens[i];
|
|
1480
1560
|
if (t === '--install') install = true;
|
|
1481
1561
|
else if (t === '--force') force = true;
|
|
1482
1562
|
else if (t === '--dir') {
|
|
1483
|
-
|
|
1484
|
-
if (!
|
|
1563
|
+
const root = tokens[++i];
|
|
1564
|
+
if (!root) throw usageError('`--dir` needs a skills root. ' + SKILL_USAGE);
|
|
1565
|
+
customRoots.push(root);
|
|
1485
1566
|
} else if (t === '--agents' || t === '--claude' || t === '--codex') picked.add(t);
|
|
1486
1567
|
else throw usageError(`Unexpected ${JSON.stringify(t)}. ${SKILL_USAGE}`);
|
|
1487
1568
|
}
|
|
1488
|
-
if (!install && (force || picked.size > 0 ||
|
|
1569
|
+
if (!install && (force || picked.size > 0 || customRoots.length > 0)) {
|
|
1489
1570
|
throw usageError(`Those flags need \`--install\`. ${SKILL_USAGE}`);
|
|
1490
1571
|
}
|
|
1491
1572
|
const text = skillText();
|
|
@@ -1504,17 +1585,17 @@ function cmdSkill(tokens, globals) {
|
|
|
1504
1585
|
}
|
|
1505
1586
|
const home = skillHome();
|
|
1506
1587
|
let targets;
|
|
1507
|
-
if (
|
|
1588
|
+
if (customRoots.length > 0 && picked.size === 0) targets = [];
|
|
1508
1589
|
else if (picked.size === 0) {
|
|
1509
1590
|
const detected = SKILL_TARGETS.filter((t) => existsSync(join(home, t.harness)));
|
|
1510
1591
|
targets = detected.length > 0 ? detected : [SKILL_TARGETS[0]];
|
|
1511
1592
|
} else {
|
|
1512
1593
|
targets = SKILL_TARGETS.filter((t) => picked.has(t.flag));
|
|
1513
1594
|
}
|
|
1514
|
-
|
|
1595
|
+
for (const root of customRoots) targets.push({ flag: '--dir', custom: true, root });
|
|
1515
1596
|
const results = [];
|
|
1516
1597
|
for (const t of targets) {
|
|
1517
|
-
const dir = t.custom ? join(
|
|
1598
|
+
const dir = t.custom ? join(t.root, SKILL_NAME) : join(home, t.dir, SKILL_NAME);
|
|
1518
1599
|
const dest = join(dir, SKILL_FILE);
|
|
1519
1600
|
if (existsSync(dest)) {
|
|
1520
1601
|
let same = false;
|