cawdev-cli 1.0.8-beta → 1.0.9-beta
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 -1
- package/lib/roadmap-format.mjs +96 -1
- package/mcp/README.md +1 -1
- package/mcp/server.mjs +83 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,4 +5,9 @@ and spawning processes are Node's home turf, and a file with no dependencies is
|
|
|
5
5
|
one you can read before running it against your repositories.
|
|
6
6
|
visit https://cawdev.com to learn more
|
|
7
7
|
|
|
8
|
-
https://github.com/4dali/cawdev-cli
|
|
8
|
+
https://github.com/4dali/cawdev-cli
|
|
9
|
+
## jira/
|
|
10
|
+
|
|
11
|
+
`jira/import.mjs` brings a Jira Cloud project's issues, epics, sprints and
|
|
12
|
+
comments into a cawdev project, once and again without duplicates (R337). Run from a checkout; see
|
|
13
|
+
"Leaving Jira" in GUIDE.md. Its tests: `node --test "tools/jira/*.test.mjs"`.
|
package/lib/roadmap-format.mjs
CHANGED
|
@@ -232,6 +232,32 @@ export function parseRoadmap(text) {
|
|
|
232
232
|
}
|
|
233
233
|
continue;
|
|
234
234
|
}
|
|
235
|
+
// R351. The date shape is checked here, where the line is read: a
|
|
236
|
+
// `Due:` that is not a date is a hand edit, and --live would otherwise
|
|
237
|
+
// report it as drift from a database that never held it.
|
|
238
|
+
const due = /^Due:\s*(.+)$/.exec(line.trim());
|
|
239
|
+
if (due) {
|
|
240
|
+
entry.due = due[1].trim();
|
|
241
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(entry.due) || Number.isNaN(Date.parse(entry.due))) {
|
|
242
|
+
problems.push(`${block.heading}: Due "${due[1].trim()}" is not a date written 2026-11-01.`);
|
|
243
|
+
}
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
// R358. The version the card means to ship in; a free string, as `Version:` is.
|
|
247
|
+
const target = /^Target:\s*(.+)$/.exec(line.trim());
|
|
248
|
+
if (target) {
|
|
249
|
+
entry.targetVersion = target[1].trim();
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const estimate = /^Estimate:\s*(.+)$/.exec(line.trim());
|
|
253
|
+
if (estimate) {
|
|
254
|
+
const n = Number(estimate[1].trim());
|
|
255
|
+
entry.estimate = n;
|
|
256
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
257
|
+
problems.push(`${block.heading}: Estimate "${estimate[1].trim()}" is not a positive whole number.`);
|
|
258
|
+
}
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
235
261
|
const development = /^Development:\s*(.+)$/.exec(line.trim());
|
|
236
262
|
if (development) {
|
|
237
263
|
entry.development = statusFromDisplay(development[1].trim());
|
|
@@ -262,6 +288,16 @@ export function parseRoadmap(text) {
|
|
|
262
288
|
entry.afterRefs = tokens.map(({ ref }) => ref);
|
|
263
289
|
continue;
|
|
264
290
|
}
|
|
291
|
+
const parent = /^Parent:\s*(\S+)\s*$/.exec(line.trim());
|
|
292
|
+
if (parent) {
|
|
293
|
+
// R336. One ref, always written in full (R300, i12). A token that is
|
|
294
|
+
// not a card is left as ordinary body text rather than eaten.
|
|
295
|
+
const token = parseRefToken(parent[1]);
|
|
296
|
+
if (token) {
|
|
297
|
+
entry.parentRef = token.ref;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
265
301
|
const tags = /^Tags:\s*(.+)$/.exec(line.trim());
|
|
266
302
|
if (tags) {
|
|
267
303
|
// R315. Already normalised by the server (lowercase, no `#`) by the
|
|
@@ -356,6 +392,45 @@ function* splitBlocks(text) {
|
|
|
356
392
|
if (entry) yield entry;
|
|
357
393
|
}
|
|
358
394
|
|
|
395
|
+
/** The ref of the card this one sits under, from either shape — R336. */
|
|
396
|
+
function parentRefOf(entry) {
|
|
397
|
+
const ref = entry.parent?.ref ?? entry.parentRef;
|
|
398
|
+
return ref ? (parseRefToken(ref)?.ref ?? null) : null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Files each child directly after its parent, within one section — R336.
|
|
403
|
+
*
|
|
404
|
+
* Pure. A child keeps its place among its siblings (they stay in the order
|
|
405
|
+
* given), and a child whose parent is in some other section stays exactly
|
|
406
|
+
* where it was: the file groups by section first, and an epic spread over
|
|
407
|
+
* phases is still one epic by its `Parent:` lines.
|
|
408
|
+
*/
|
|
409
|
+
export function nestUnderParents(list) {
|
|
410
|
+
const refs = new Map(list.map((entry) => [entryRef(entry), entry]));
|
|
411
|
+
const nestedUnder = (entry) => {
|
|
412
|
+
const parent = parentRefOf(entry);
|
|
413
|
+
const card = parent && refs.get(parent);
|
|
414
|
+
// A parent that is itself a child is bad data; leave the child where it is
|
|
415
|
+
// rather than lose it behind a card that is not a root.
|
|
416
|
+
return card && !parentRefOf(card) ? parent : null;
|
|
417
|
+
};
|
|
418
|
+
const children = new Map();
|
|
419
|
+
for (const entry of list) {
|
|
420
|
+
const parent = nestedUnder(entry);
|
|
421
|
+
if (parent) {
|
|
422
|
+
if (!children.has(parent)) children.set(parent, []);
|
|
423
|
+
children.get(parent).push(entry);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const out = [];
|
|
427
|
+
for (const entry of list) {
|
|
428
|
+
if (nestedUnder(entry)) continue;
|
|
429
|
+
out.push(entry, ...(children.get(entryRef(entry)) ?? []));
|
|
430
|
+
}
|
|
431
|
+
return out;
|
|
432
|
+
}
|
|
433
|
+
|
|
359
434
|
/**
|
|
360
435
|
* Writes entries as ROADMAP.md's entry sections.
|
|
361
436
|
*
|
|
@@ -385,7 +460,7 @@ export function renderRoadmap(entries, { preamble, sectionOrder = [] }) {
|
|
|
385
460
|
const parts = [preamble.trimEnd(), ''];
|
|
386
461
|
for (const section of ordered) {
|
|
387
462
|
parts.push('---', '', `## ${section}`, '');
|
|
388
|
-
for (const entry of bySection.get(section)) {
|
|
463
|
+
for (const entry of nestUnderParents(bySection.get(section))) {
|
|
389
464
|
parts.push(renderEntry(entry), '');
|
|
390
465
|
}
|
|
391
466
|
}
|
|
@@ -443,6 +518,19 @@ function renderEntry(entry) {
|
|
|
443
518
|
lines.push(`Priority: ${entry.priority}`);
|
|
444
519
|
}
|
|
445
520
|
|
|
521
|
+
// R351. Two facts, written only when the card carries them, after the
|
|
522
|
+
// priority: when, then how big.
|
|
523
|
+
if (entry.due) {
|
|
524
|
+
lines.push(`Due: ${entry.due}`);
|
|
525
|
+
}
|
|
526
|
+
if (entry.estimate != null && entry.estimate > 0) {
|
|
527
|
+
lines.push(`Estimate: ${entry.estimate}`);
|
|
528
|
+
}
|
|
529
|
+
// R358. Where it means to ship; dropped once it has, because `Version:` then says.
|
|
530
|
+
if (entry.targetVersion && !entry.version) {
|
|
531
|
+
lines.push(`Target: ${entry.targetVersion}`);
|
|
532
|
+
}
|
|
533
|
+
|
|
446
534
|
// Each status writes only what it currently claims. A MERGED entry keeps its
|
|
447
535
|
// branch in the database as history, but the file must not print it: the
|
|
448
536
|
// branch is expected to be deleted, and `--live` would then be reading a claim
|
|
@@ -492,6 +580,13 @@ function renderEntry(entry) {
|
|
|
492
580
|
.map(({ ref }) => ref);
|
|
493
581
|
lines.push(`After: ${written.join(', ')}`);
|
|
494
582
|
}
|
|
583
|
+
{
|
|
584
|
+
// R336. The API gives `parent` as {number, ref, title}; the parser gives
|
|
585
|
+
// `parentRef`. Always the full ref, and no line at all without a parent, so
|
|
586
|
+
// no existing card's text changes.
|
|
587
|
+
const parent = parentRefOf(entry);
|
|
588
|
+
if (parent) lines.push(`Parent: ${parent}`);
|
|
589
|
+
}
|
|
495
590
|
if (entry.tags?.length) {
|
|
496
591
|
// R315. Sorted, so the export does not churn on the set's own order.
|
|
497
592
|
lines.push(`Tags: ${[...entry.tags].sort().join(', ')}`);
|
package/mcp/README.md
CHANGED
|
@@ -163,7 +163,7 @@ what you are allowed to do.
|
|
|
163
163
|
| `roadmap_list` | Entries, optionally by status. `brief` omits bodies — use it to survey. |
|
|
164
164
|
| `roadmap_get` | One entry in full: its body, **the plan agreed for it**, and the discussion on it. |
|
|
165
165
|
| `roadmap_create` | Create an entry; the platform allocates its permanent number. |
|
|
166
|
-
| `roadmap_update` | Title, body, section, related ids, the cards it starts coding after (`after` — R181), and who it is assigned to (`assignee` — R331: a username, `""` clears; `roadmap_list`/`issue_list` take `assignee` too, and `me` means the token's owner). |
|
|
166
|
+
| `roadmap_update` | Title, body, section, related ids, the cards it starts coding after (`after` — R181), when it is due and how big it is (`due` — R351: an ISO date, `""` clears; `estimate`: a positive number, `0` clears), the version it is meant for (`targetVersion` — R358, `""` clears; since R362 it must be one the project defined under Project settings → Versions, and any other name is refused), and who it is assigned to (`assignee` — R331: a username, `""` clears; `roadmap_list`/`issue_list` take `assignee` too, and `me` means the token's owner). |
|
|
167
167
|
| `roadmap_comment` | Say something *beside* an entry rather than inside it. |
|
|
168
168
|
| `roadmap_set_status` | Move an entry, carrying whatever the status requires. |
|
|
169
169
|
| `roadmap_decline` | Decline with a reason. The only exit. |
|
package/mcp/server.mjs
CHANGED
|
@@ -692,6 +692,26 @@ const TOOLS = [
|
|
|
692
692
|
description:
|
|
693
693
|
'How much it matters; it does not reorder the board. Leave out for MEDIUM.',
|
|
694
694
|
},
|
|
695
|
+
due: {
|
|
696
|
+
type: 'string',
|
|
697
|
+
description: 'When it is due, written 2026-11-01. A fact on the card; it moves nothing. Leave out for none.',
|
|
698
|
+
},
|
|
699
|
+
estimate: {
|
|
700
|
+
type: 'integer',
|
|
701
|
+
minimum: 1,
|
|
702
|
+
description: "How big it is, in the team's own unit (points, days — the platform does not say). Leave out for none.",
|
|
703
|
+
},
|
|
704
|
+
targetVersion: {
|
|
705
|
+
type: 'string',
|
|
706
|
+
description: 'The version it is meant to ship in — one the project DEFINED under Project settings → Versions (R362); any other name is refused. The release corrects it if it lands elsewhere. Leave out for none.',
|
|
707
|
+
},
|
|
708
|
+
parent: {
|
|
709
|
+
type: 'string',
|
|
710
|
+
description:
|
|
711
|
+
'The card this one sits under (an epic), e.g. "R300". One level only: the parent ' +
|
|
712
|
+
'must be the same kind and have no parent itself, and this card must have no ' +
|
|
713
|
+
'children. An open card cannot be filed under a SHIPPED or DECLINED one.',
|
|
714
|
+
},
|
|
695
715
|
},
|
|
696
716
|
required: ['title'],
|
|
697
717
|
},
|
|
@@ -701,7 +721,7 @@ const TOOLS = [
|
|
|
701
721
|
method: 'POST',
|
|
702
722
|
body: pick(args, [
|
|
703
723
|
'title', 'body', 'status', 'branch', 'merge', 'version', 'reason', 'section', 'related',
|
|
704
|
-
'after', 'sprint', 'tags', 'assignee', 'priority',
|
|
724
|
+
'after', 'sprint', 'tags', 'assignee', 'priority', 'due', 'estimate', 'targetVersion', 'parent',
|
|
705
725
|
]),
|
|
706
726
|
});
|
|
707
727
|
return `Created ${refOf(entry)} in ${slug}.\n\n${formatEntry(entry, {})}`;
|
|
@@ -830,8 +850,8 @@ const TOOLS = [
|
|
|
830
850
|
{
|
|
831
851
|
name: 'roadmap_update',
|
|
832
852
|
description:
|
|
833
|
-
'Edit an entry\'s title, body, section, sprint, tags, related refs or the cards it
|
|
834
|
-
'after. Use roadmap_set_status to move it.',
|
|
853
|
+
'Edit an entry\'s title, body, section, sprint, tags, parent, related refs or the cards it ' +
|
|
854
|
+
'starts after. Use roadmap_set_status to move it.',
|
|
835
855
|
inputSchema: {
|
|
836
856
|
type: 'object',
|
|
837
857
|
properties: {
|
|
@@ -882,6 +902,25 @@ const TOOLS = [
|
|
|
882
902
|
description:
|
|
883
903
|
'How much it matters; it does not reorder the board. Leave out for MEDIUM (leave out to leave it alone).',
|
|
884
904
|
},
|
|
905
|
+
due: {
|
|
906
|
+
type: 'string',
|
|
907
|
+
description: 'When it is due, written 2026-11-01. Leave out to leave it alone; "" clears it.',
|
|
908
|
+
},
|
|
909
|
+
estimate: {
|
|
910
|
+
type: 'integer',
|
|
911
|
+
minimum: 0,
|
|
912
|
+
description: "How big it is, in the team's own unit. Leave out to leave it alone; 0 clears it.",
|
|
913
|
+
},
|
|
914
|
+
targetVersion: {
|
|
915
|
+
type: 'string',
|
|
916
|
+
description: 'The version it is meant to ship in — one the project defined (R362); any other name is refused. Leave out to leave it alone; "" clears it.',
|
|
917
|
+
},
|
|
918
|
+
parent: {
|
|
919
|
+
type: 'string',
|
|
920
|
+
description:
|
|
921
|
+
'The card this one sits under (an epic), e.g. "R300". "" takes the card out of its ' +
|
|
922
|
+
'parent; leave out to leave it alone. Same rules as roadmap_create.',
|
|
923
|
+
},
|
|
885
924
|
},
|
|
886
925
|
required: ['number'],
|
|
887
926
|
},
|
|
@@ -890,7 +929,10 @@ const TOOLS = [
|
|
|
890
929
|
const entry = await api(config, `/api/projects/${slug}/roadmap/${args.number}`, {
|
|
891
930
|
method: 'PATCH',
|
|
892
931
|
// pick keeps a present 0, which is how a card leaves its sprint.
|
|
893
|
-
body: pick(args, [
|
|
932
|
+
body: pick(args, [
|
|
933
|
+
'title', 'body', 'section', 'related', 'after', 'sprint', 'tags', 'assignee', 'priority',
|
|
934
|
+
'due', 'estimate', 'targetVersion', 'parent',
|
|
935
|
+
]),
|
|
894
936
|
});
|
|
895
937
|
return `Updated ${refOf(entry)}.\n\n${formatEntry(entry, {})}`;
|
|
896
938
|
},
|
|
@@ -1154,8 +1196,22 @@ const TOOLS = [
|
|
|
1154
1196
|
`run: ${run.state}${run.runnerName ? ` on ${run.runnerName}` : ''}`,
|
|
1155
1197
|
`started by ${run.startedByEmail}`,
|
|
1156
1198
|
'',
|
|
1157
|
-
formatEntry(entry, { comments, plan: plans?.[0] ?? null }),
|
|
1158
1199
|
);
|
|
1200
|
+
// R336. The epic this card was cut from, so a session sees the context.
|
|
1201
|
+
// Its own try, like the history above: a parent that cannot be read (a
|
|
1202
|
+
// 404, no grant) costs the paragraph and never the call.
|
|
1203
|
+
if (entry.parent) {
|
|
1204
|
+
const epic = await api(config, `/api/projects/${project}/roadmap/${entry.parent.ref}`)
|
|
1205
|
+
.catch(() => null);
|
|
1206
|
+
if (epic) {
|
|
1207
|
+
lines.push(
|
|
1208
|
+
`This card is part of ${entry.parent.ref} — ${epic.title}:`,
|
|
1209
|
+
epicExcerpt(epic.body),
|
|
1210
|
+
'',
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
lines.push(formatEntry(entry, { comments, plan: plans?.[0] ?? null }));
|
|
1159
1215
|
|
|
1160
1216
|
// What happened the other times. A session is told the entry and the
|
|
1161
1217
|
// branch but not that two previous runs on this card failed, which is
|
|
@@ -2173,6 +2229,15 @@ function originWord(origin) {
|
|
|
2173
2229
|
}
|
|
2174
2230
|
}
|
|
2175
2231
|
|
|
2232
|
+
/** The first 600 characters of an epic's body, cut at a word — R336. */
|
|
2233
|
+
function epicExcerpt(body) {
|
|
2234
|
+
const text = (body ?? '').trim();
|
|
2235
|
+
if (text.length <= 600) return text;
|
|
2236
|
+
const cut = text.slice(0, 600);
|
|
2237
|
+
const space = cut.lastIndexOf(' ');
|
|
2238
|
+
return `${(space > 0 ? cut.slice(0, space) : cut).trimEnd()}…`;
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2176
2241
|
function formatEntry(entry, { brief, comments, plan }) {
|
|
2177
2242
|
const lines = [`${refOf(entry)} — ${entry.title}`, ` status: ${entry.statusDisplay}`];
|
|
2178
2243
|
if (entry.branch) lines.push(` branch: ${entry.branch}`);
|
|
@@ -2189,6 +2254,11 @@ function formatEntry(entry, { brief, comments, plan }) {
|
|
|
2189
2254
|
if (entry.priority && entry.priority !== 'MEDIUM') {
|
|
2190
2255
|
lines.push(` priority: ${entry.priority}`);
|
|
2191
2256
|
}
|
|
2257
|
+
// R351. Both printed only when the card carries them, as the export does.
|
|
2258
|
+
if (entry.due) lines.push(` due: ${entry.due}`);
|
|
2259
|
+
if (entry.estimate) lines.push(` estimate: ${entry.estimate}`);
|
|
2260
|
+
// R358. Printed until the card ships; `version` says where it did.
|
|
2261
|
+
if (entry.targetVersion && !entry.version) lines.push(` target: ${entry.targetVersion}`);
|
|
2192
2262
|
// R257. Always printed when the card has one, so a reader's shape does not
|
|
2193
2263
|
// depend on which tool printed the card; a closed sprint says so.
|
|
2194
2264
|
if (entry.sprint) {
|
|
@@ -2214,6 +2284,14 @@ function formatEntry(entry, { brief, comments, plan }) {
|
|
|
2214
2284
|
.join(', ')}`,
|
|
2215
2285
|
);
|
|
2216
2286
|
}
|
|
2287
|
+
// R336. The epic this card sits under, and the cards under this one.
|
|
2288
|
+
if (entry.parent) lines.push(` parent: ${entry.parent.ref} — ${entry.parent.title}`);
|
|
2289
|
+
if (entry.children?.length) {
|
|
2290
|
+
const progress = entry.progress ? `${entry.progress.done} of ${entry.progress.total} done — ` : '';
|
|
2291
|
+
lines.push(
|
|
2292
|
+
` children: ${progress}${entry.children.map((c) => `${c.ref} (${c.status})`).join(', ')}`,
|
|
2293
|
+
);
|
|
2294
|
+
}
|
|
2217
2295
|
// Said in a survey, where the comments themselves are not fetched: an entry
|
|
2218
2296
|
// with an argument attached should be visibly different from one without,
|
|
2219
2297
|
// even in a list. Omitted at zero rather than written as "comments: 0".
|