@plune-ai/cli 0.25.0 → 0.26.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 +13 -2
- package/dist/cli.cjs +107 -14
- package/dist/reporter-core.cjs +31 -2
- package/dist/reporter-core.js +33 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -165,9 +165,20 @@ plune requirements import prd.md --sections Requirements --unit bullets --key-pr
|
|
|
165
165
|
|
|
166
166
|
`--sections` takes the heading or its number; `--unit bullets|subheadings` (default: sub-headings when the section has any); `--key-prefix` is text a bullet starts with that carries its key (`REQ-12: The system shall ...`; no pattern is accepted); `--priority-from subheading` makes the bullets under a sub-heading take its text as their priority.
|
|
167
167
|
|
|
168
|
-
**
|
|
168
|
+
**2c. Several tables in one run.** A PRD keeps its functional and non-functional requirements in different tables. Repeat `--table` and put each table's columns inside it, after the number and a colon:
|
|
169
169
|
|
|
170
|
-
|
|
170
|
+
```bash
|
|
171
|
+
plune requirements import prd.md \
|
|
172
|
+
--table "1:key=ID,text=Requirement,priority=Priority,fixed-category=functional" \
|
|
173
|
+
--table "2:key=ID,text=Requirement,fixed-category=non-functional" \
|
|
174
|
+
--map-priority "Must=Must-have"
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Inside a `--table` the names are `key`, `title` or `text` (one of the two), `description`, `priority`, `category`, `type`, `status`, `criteria`, plus `fixed-category=functional|non-functional|other` and `fixed-type=user_story|feature|epic` — one meaning for the whole table, used where a row has none of its own. Names are separated by commas, so a header that contains a comma cannot be named in this form. `--map-priority`, `--type` and `--map-category` are shared by all tables. A table number may be given once. The single-table form (`--table 0 --key-column ...`) keeps working, but the two forms are not mixed: `--key-column` and the other column flags are refused together with `--table "<n>:..."`. The plan shows `table #N "heading"` in front of each row.
|
|
178
|
+
|
|
179
|
+
**3. Read the plan, then write it.** Without `--yes` nothing is written. The report is the summary line (`create 3 · update 0 · unchanged 12 · skip 1`), the rows grouped by action with their line in the file, key, title and reason (a skipped row says why), the priority and category values seen and what they map to, and what was **not imported** (other tables, other sections, frontmatter keys). It ends with `dry run — nothing was written; add --yes to apply`. Add `--yes` to apply the same plan in one transaction. A row with a key matches the requirement of that key; one without matches by exact title; running the same file again changes nothing (every row is `unchanged`). A skipped row does not fail the command. Rows that are only placeholders (`[Behavior]`) are skipped with the reason `template placeholder`; a key that appears twice in the file is skipped with `first on line N, again on line M`. When every row is a placeholder the plan says `this file is an empty template` (`--json` has `"emptyTemplate": true`) and the exit code is `0`, like any plan whose rows are skipped: nothing is written, and nothing is an error.
|
|
180
|
+
|
|
181
|
+
`--json` prints the platform's answer instead. The project is the only one of the account, or `--project <id>` / `PLUNE_PROJECT` (a token only works in the project it was minted in). Exit `0` on success, `2` for a flag combination that cannot work (`--table` with `--sections`, `--title-column` with `--text-column`, a `--table "<n>:..."` spec with the single-table column flags), an unreadable or over-limit file, or a rejected token, `1` when the platform refuses (400, 403, 409 a key taken meanwhile, 413) or does not have the routes yet.
|
|
171
182
|
|
|
172
183
|
### A release gate: `--fail-on-failures`
|
|
173
184
|
|
package/dist/cli.cjs
CHANGED
|
@@ -2315,7 +2315,8 @@ function nodeTransport(url, init, idleMs = IDLE_MS) {
|
|
|
2315
2315
|
return new Promise((resolve10, reject) => {
|
|
2316
2316
|
const target = new URL(url);
|
|
2317
2317
|
const send = target.protocol === "https:" ? import_node_https.request : import_node_http.request;
|
|
2318
|
-
const
|
|
2318
|
+
const agent = POOLS[target.protocol === "https:" ? "https:" : "http:"];
|
|
2319
|
+
const req = send(target, { method: init.method, headers: init.headers, agent }, (res) => {
|
|
2319
2320
|
const chunks = [];
|
|
2320
2321
|
res.on("data", (chunk2) => chunks.push(chunk2));
|
|
2321
2322
|
res.on("error", reject);
|
|
@@ -2506,7 +2507,7 @@ function createClient(opts) {
|
|
|
2506
2507
|
}
|
|
2507
2508
|
};
|
|
2508
2509
|
}
|
|
2509
|
-
var import_node_fs2, import_node_stream, import_node_http, import_node_https, BACKOFF_BASE_MS, BACKOFF_CAP_MS, DEFAULT_RETRIES, RETRY_AFTER_CAP_MS, IDLE_MS, withoutLength, codeOfError, sleep;
|
|
2510
|
+
var import_node_fs2, import_node_stream, import_node_http, import_node_https, BACKOFF_BASE_MS, BACKOFF_CAP_MS, DEFAULT_RETRIES, RETRY_AFTER_CAP_MS, POOLS, IDLE_MS, withoutLength, codeOfError, sleep;
|
|
2510
2511
|
var init_client = __esm({
|
|
2511
2512
|
"src/reporter-core/client.ts"() {
|
|
2512
2513
|
"use strict";
|
|
@@ -2519,6 +2520,7 @@ var init_client = __esm({
|
|
|
2519
2520
|
BACKOFF_CAP_MS = 1e4;
|
|
2520
2521
|
DEFAULT_RETRIES = 2;
|
|
2521
2522
|
RETRY_AFTER_CAP_MS = 6e4;
|
|
2523
|
+
POOLS = { "http:": new import_node_http.Agent({ keepAlive: true }), "https:": new import_node_https.Agent({ keepAlive: true }) };
|
|
2522
2524
|
IDLE_MS = 3e5;
|
|
2523
2525
|
withoutLength = (headers) => Object.fromEntries(Object.entries(headers).filter(([name]) => name.toLowerCase() !== "content-length"));
|
|
2524
2526
|
codeOfError = (err) => {
|
|
@@ -3558,7 +3560,29 @@ async function startRun(passed, declared2) {
|
|
|
3558
3560
|
else await uploadBatched(jobs);
|
|
3559
3561
|
uploadMs += Date.now() - started;
|
|
3560
3562
|
}
|
|
3563
|
+
const sending = /* @__PURE__ */ new Set();
|
|
3564
|
+
let sendError;
|
|
3565
|
+
let answered = false;
|
|
3566
|
+
async function dispatch(work) {
|
|
3567
|
+
while (sending.size >= (answered ? SEND_CONCURRENCY : 1)) await Promise.race(sending);
|
|
3568
|
+
const task = work().catch((err) => {
|
|
3569
|
+
sendError ??= err;
|
|
3570
|
+
}).finally(() => sending.delete(task));
|
|
3571
|
+
sending.add(task);
|
|
3572
|
+
}
|
|
3573
|
+
async function settle() {
|
|
3574
|
+
while (sending.size > 0) await Promise.all([...sending]);
|
|
3575
|
+
if (sendError !== void 0) {
|
|
3576
|
+
const err = sendError;
|
|
3577
|
+
sendError = void 0;
|
|
3578
|
+
throw err;
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3561
3581
|
async function flush() {
|
|
3582
|
+
await flushBuffer();
|
|
3583
|
+
await settle();
|
|
3584
|
+
}
|
|
3585
|
+
async function flushBuffer() {
|
|
3562
3586
|
if (buffer.length === 0) return;
|
|
3563
3587
|
const pending = buffer;
|
|
3564
3588
|
buffer = [];
|
|
@@ -3566,6 +3590,9 @@ async function startRun(passed, declared2) {
|
|
|
3566
3590
|
defer(pending);
|
|
3567
3591
|
return;
|
|
3568
3592
|
}
|
|
3593
|
+
await dispatch(() => deliver(pending));
|
|
3594
|
+
}
|
|
3595
|
+
async function deliver(pending) {
|
|
3569
3596
|
await resolveKeys(pending.flatMap((p) => p.keys));
|
|
3570
3597
|
const submissions = [];
|
|
3571
3598
|
const unanswered = [];
|
|
@@ -3606,6 +3633,7 @@ async function startRun(passed, declared2) {
|
|
|
3606
3633
|
for (const k of told) stats[k] += counts[k];
|
|
3607
3634
|
progress();
|
|
3608
3635
|
await upload(out.ok ? out.body?.items : void 0, filesByKey);
|
|
3636
|
+
answered = true;
|
|
3609
3637
|
continue;
|
|
3610
3638
|
}
|
|
3611
3639
|
if (out.ok) {
|
|
@@ -3680,7 +3708,7 @@ async function startRun(passed, declared2) {
|
|
|
3680
3708
|
},
|
|
3681
3709
|
async add(result) {
|
|
3682
3710
|
buffer.push({ ...result, keys: bound(result.keys) });
|
|
3683
|
-
if (buffer.length >= batchSize) await
|
|
3711
|
+
if (buffer.length >= batchSize) await flushBuffer();
|
|
3684
3712
|
},
|
|
3685
3713
|
flush,
|
|
3686
3714
|
async finish(reason) {
|
|
@@ -3745,7 +3773,7 @@ async function startRun(passed, declared2) {
|
|
|
3745
3773
|
}
|
|
3746
3774
|
};
|
|
3747
3775
|
}
|
|
3748
|
-
var import_node_crypto3, import_node_fs5, import_node_path3, import_node_util2, RESOLVE_CHUNK, BATCH_MAX, DISCOVER_MAX, BATCH_DEFAULT, EXPECTED_MAX, KEY_MAX, TITLE_MAX, COUNTED, SCREENSHOT_TYPES, SCREENSHOT_BYTES, SCREENSHOTS_PER_RESULT, TEXT_BYTES, TEXTS_PER_RESULT, UTF8, TYPE_BY_EXTENSION, METADATA_TYPE2, NAME_MAX, SKIPPED, BUCKET_FOUND, KINDS, codeOf2, BATCH_BYTES, ENVELOPE_BYTES;
|
|
3776
|
+
var import_node_crypto3, import_node_fs5, import_node_path3, import_node_util2, RESOLVE_CHUNK, BATCH_MAX, DISCOVER_MAX, BATCH_DEFAULT, SEND_CONCURRENCY, EXPECTED_MAX, KEY_MAX, TITLE_MAX, COUNTED, SCREENSHOT_TYPES, SCREENSHOT_BYTES, SCREENSHOTS_PER_RESULT, TEXT_BYTES, TEXTS_PER_RESULT, UTF8, TYPE_BY_EXTENSION, METADATA_TYPE2, NAME_MAX, SKIPPED, BUCKET_FOUND, KINDS, codeOf2, BATCH_BYTES, ENVELOPE_BYTES;
|
|
3749
3777
|
var init_session = __esm({
|
|
3750
3778
|
"src/reporter-core/session.ts"() {
|
|
3751
3779
|
"use strict";
|
|
@@ -3765,6 +3793,7 @@ var init_session = __esm({
|
|
|
3765
3793
|
BATCH_MAX = 500;
|
|
3766
3794
|
DISCOVER_MAX = 500;
|
|
3767
3795
|
BATCH_DEFAULT = 500;
|
|
3796
|
+
SEND_CONCURRENCY = 4;
|
|
3768
3797
|
EXPECTED_MAX = 1e4;
|
|
3769
3798
|
KEY_MAX = 1024;
|
|
3770
3799
|
TITLE_MAX = 300;
|
|
@@ -6291,6 +6320,7 @@ __export(requirements_import_exports, {
|
|
|
6291
6320
|
formatReport: () => formatReport2,
|
|
6292
6321
|
handleRequirementsImport: () => handleRequirementsImport,
|
|
6293
6322
|
parseMap: () => parseMap,
|
|
6323
|
+
parseTableSpec: () => parseTableSpec,
|
|
6294
6324
|
reportRequirementsImportFailure: () => reportRequirementsImportFailure
|
|
6295
6325
|
});
|
|
6296
6326
|
function parseMap(flag2, raw) {
|
|
@@ -6306,25 +6336,63 @@ function parseMap(flag2, raw) {
|
|
|
6306
6336
|
if (Object.keys(out).length === 0) usage(`${flag2} is empty. Write it like ${flag2} "Critical=Must-have,High=Should-have".`);
|
|
6307
6337
|
return out;
|
|
6308
6338
|
}
|
|
6339
|
+
function parseTableSpec(spec) {
|
|
6340
|
+
const at = spec.indexOf(":");
|
|
6341
|
+
const index = (at < 0 ? spec : spec.slice(0, at)).trim();
|
|
6342
|
+
if (!/^\d+$/.test(index)) usage(`--table "${spec}": "${index}" is not a table number. ${SPEC_HELP}`);
|
|
6343
|
+
const t = { table: Number(index) };
|
|
6344
|
+
for (const pair of (at < 0 ? "" : spec.slice(at + 1)).split(",")) {
|
|
6345
|
+
if (pair.trim() === "") continue;
|
|
6346
|
+
const eq = pair.indexOf("=");
|
|
6347
|
+
const name = (eq < 0 ? pair : pair.slice(0, eq)).trim().toLowerCase();
|
|
6348
|
+
const value2 = eq < 0 ? "" : pair.slice(eq + 1).trim();
|
|
6349
|
+
const field = TABLE_SPEC_KEYS[name];
|
|
6350
|
+
if (field === void 0) return usage(`--table "${spec}": "${name}" is not a column name. Use ${Object.keys(TABLE_SPEC_KEYS).join(", ")}.`);
|
|
6351
|
+
if (value2 === "") usage(`--table "${spec}": "${name}" has no value. ${SPEC_HELP}`);
|
|
6352
|
+
if (field in t) usage(`--table "${spec}": "${name}" is given twice.`);
|
|
6353
|
+
if (field === "fixedCategory") oneOf("fixed-category", value2, ["functional", "non-functional", "other"]);
|
|
6354
|
+
if (field === "fixedType") oneOf("fixed-type", value2, ["user_story", "feature", "epic"]);
|
|
6355
|
+
t[field] = value2;
|
|
6356
|
+
}
|
|
6357
|
+
if (t["titleColumn"] !== void 0 && t["textColumn"] !== void 0) usage(`--table "${spec}": give title= or text=, not both.`);
|
|
6358
|
+
if (t["titleColumn"] === void 0 && t["textColumn"] === void 0) usage(`--table "${spec}": each table needs title=<header> or text=<header>.`);
|
|
6359
|
+
return t;
|
|
6360
|
+
}
|
|
6309
6361
|
function buildImportBody(f) {
|
|
6362
|
+
const tables = f.table === void 0 ? [] : Array.isArray(f.table) ? f.table : [f.table];
|
|
6310
6363
|
const columnFlags = COLUMN_FLAGS.filter(([k]) => f[k] !== void 0);
|
|
6311
6364
|
const sectionFlags = SECTION_FLAGS.filter(([k]) => f[k] !== void 0);
|
|
6312
|
-
const anyMapping =
|
|
6365
|
+
const anyMapping = tables.length > 0 || f.sections !== void 0 || columnFlags.length > 0 || sectionFlags.length > 0 || f.mapPriority !== void 0 || f.mapCategory !== void 0 || f.type !== void 0;
|
|
6313
6366
|
if (!anyMapping) return null;
|
|
6314
|
-
if (
|
|
6315
|
-
if (
|
|
6367
|
+
if (tables.length > 0 && f.sections !== void 0) usage("Give --table or --sections, not both: one import reads one table or one section.");
|
|
6368
|
+
if (tables.length === 0 && f.sections === void 0) {
|
|
6316
6369
|
usage("The mapping flags need --table <n> (a table) or --sections <heading|n> (a section). Run without flags to see which the file has.");
|
|
6317
6370
|
}
|
|
6318
6371
|
const body = {};
|
|
6319
6372
|
if (f.mapPriority !== void 0) body["priorityMap"] = parseMap("--map-priority", f.mapPriority);
|
|
6320
6373
|
if (f.type !== void 0) body["defaultType"] = oneOf("--type", f.type, ["user_story", "feature", "epic"]);
|
|
6321
|
-
if (
|
|
6322
|
-
if (
|
|
6374
|
+
if (tables.length > 1 || tables.some((t) => t.includes(":"))) {
|
|
6375
|
+
if (sectionFlags.length > 0) usage(`${sectionFlags.map(([, n]) => n).join(", ")} ${sectionFlags.length > 1 ? "are" : "is"} for --sections, not --table.`);
|
|
6376
|
+
if (columnFlags.length > 0) {
|
|
6377
|
+
usage(`${columnFlags.map(([, n]) => n).join(", ")} cannot be used with --table "<n>:...": put the column inside each --table, e.g. --table "3:key=ID,text=Requirement".`);
|
|
6378
|
+
}
|
|
6379
|
+
const specs = tables.map(parseTableSpec);
|
|
6380
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6381
|
+
for (const t of specs) {
|
|
6382
|
+
if (seen.has(t["table"])) usage(`Table ${String(t["table"])} is given twice in --table.`);
|
|
6383
|
+
seen.add(t["table"]);
|
|
6384
|
+
}
|
|
6385
|
+
body["mode"] = "table";
|
|
6386
|
+
const categoryMap = f.mapCategory !== void 0 ? parseMap("--map-category", f.mapCategory) : void 0;
|
|
6387
|
+
body["tables"] = specs.map((t) => categoryMap === void 0 ? t : { ...t, categoryMap });
|
|
6388
|
+
} else if (tables.length === 1) {
|
|
6389
|
+
const only = tables[0];
|
|
6390
|
+
if (!/^\d+$/.test(only)) usage(`--table is the number of the table in the list (0, 1, ...), not "${only}".`);
|
|
6323
6391
|
if (sectionFlags.length > 0) usage(`${sectionFlags.map(([, n]) => n).join(", ")} ${sectionFlags.length > 1 ? "are" : "is"} for --sections, not --table.`);
|
|
6324
6392
|
if (f.titleColumn !== void 0 && f.textColumn !== void 0) usage("Give --title-column or --text-column, not both.");
|
|
6325
6393
|
if (f.titleColumn === void 0 && f.textColumn === void 0) usage("--table needs --title-column <header> or --text-column <header>.");
|
|
6326
6394
|
body["mode"] = "table";
|
|
6327
|
-
body["table"] = Number(
|
|
6395
|
+
body["table"] = Number(only);
|
|
6328
6396
|
for (const [k, , key] of columnFlags) body[key] = f[k];
|
|
6329
6397
|
if (f.mapCategory !== void 0) body["categoryMap"] = parseMap("--map-category", f.mapCategory);
|
|
6330
6398
|
} else {
|
|
@@ -6434,13 +6502,19 @@ function formatDiscover(file, r) {
|
|
|
6434
6502
|
"",
|
|
6435
6503
|
"Pick one and map it (a dry run, nothing is written):",
|
|
6436
6504
|
` plune requirements import ${file} --table <n> --key-column <header> --title-column <header>`,
|
|
6437
|
-
` plune requirements import ${file} --sections <heading|n> --unit bullets|subheadings
|
|
6505
|
+
` plune requirements import ${file} --sections <heading|n> --unit bullets|subheadings`,
|
|
6506
|
+
"Several tables in one run, each with its own columns:",
|
|
6507
|
+
` plune requirements import ${file} --table "<n>:key=<header>,text=<header>,fixed-category=functional" --table "<m>:key=<header>,text=<header>"`
|
|
6438
6508
|
);
|
|
6439
6509
|
return out.join("\n") + "\n";
|
|
6440
6510
|
}
|
|
6441
6511
|
function formatReport2(report) {
|
|
6442
6512
|
const s = report.summary;
|
|
6443
6513
|
const out = [`create ${s.create} \xB7 update ${s.update} \xB7 unchanged ${s.unchanged} \xB7 skip ${s.skip}`];
|
|
6514
|
+
if (report.emptyTemplate === true) {
|
|
6515
|
+
const said = report.message !== void 0 && !/empty template/i.test(report.message) ? ` ${flat(report.message)}` : "";
|
|
6516
|
+
out.push("", `this file is an empty template: every row is a placeholder, nothing to import.${said}`);
|
|
6517
|
+
}
|
|
6444
6518
|
for (const action of ["create", "update", "skip", "unchanged"]) {
|
|
6445
6519
|
const rows = report.items.filter((i) => i.action === action);
|
|
6446
6520
|
if (rows.length === 0) continue;
|
|
@@ -6448,7 +6522,8 @@ function formatReport2(report) {
|
|
|
6448
6522
|
for (const i of rows.slice(0, SHOWN)) {
|
|
6449
6523
|
const changes = i.changes !== void 0 && i.changes.length > 0 ? `; changes: ${i.changes.join(", ")}` : "";
|
|
6450
6524
|
const notes = i.notes !== void 0 && i.notes.length > 0 ? `; note: ${i.notes.join("; ")}` : "";
|
|
6451
|
-
|
|
6525
|
+
const from = i.table === void 0 ? "" : `table #${i.table}${i.tableHeading ? ` "${cut2(flat(i.tableHeading), 40)}"` : ""} `;
|
|
6526
|
+
out.push(` ${from}line ${i.line} ${i.key ?? "-"} ${cut2(flat(i.title), 80)} - ${flat(i.reason)}${changes}${notes}`);
|
|
6452
6527
|
}
|
|
6453
6528
|
if (rows.length > SHOWN) out.push(` ... and ${rows.length - SHOWN} more (--json lists every row)`);
|
|
6454
6529
|
}
|
|
@@ -6472,7 +6547,7 @@ function reportRequirementsImportFailure(err, write) {
|
|
|
6472
6547
|
write(err.message + "\n");
|
|
6473
6548
|
return err.exitCode;
|
|
6474
6549
|
}
|
|
6475
|
-
var import_node_fs7, import_node_path4, MAX_MARKDOWN_BYTES, RequirementsImportError, usage, COLUMN_FLAGS, SECTION_FLAGS, oneOf, SUPPORT, SHOWN, flat, cut2, plural;
|
|
6550
|
+
var import_node_fs7, import_node_path4, MAX_MARKDOWN_BYTES, RequirementsImportError, usage, COLUMN_FLAGS, SECTION_FLAGS, oneOf, TABLE_SPEC_KEYS, SPEC_HELP, SUPPORT, SHOWN, flat, cut2, plural;
|
|
6476
6551
|
var init_requirements_import = __esm({
|
|
6477
6552
|
"src/cli/commands/requirements-import.ts"() {
|
|
6478
6553
|
"use strict";
|
|
@@ -6511,6 +6586,20 @@ var init_requirements_import = __esm({
|
|
|
6511
6586
|
["priorityFrom", "--priority-from"]
|
|
6512
6587
|
];
|
|
6513
6588
|
oneOf = (flag2, value2, allowed) => allowed.includes(value2) ? value2 : usage(`${flag2} must be ${allowed.join(" or ")}, not "${value2}".`);
|
|
6589
|
+
TABLE_SPEC_KEYS = {
|
|
6590
|
+
key: "keyColumn",
|
|
6591
|
+
title: "titleColumn",
|
|
6592
|
+
text: "textColumn",
|
|
6593
|
+
description: "descriptionColumn",
|
|
6594
|
+
priority: "priorityColumn",
|
|
6595
|
+
category: "categoryColumn",
|
|
6596
|
+
type: "typeColumn",
|
|
6597
|
+
status: "statusColumn",
|
|
6598
|
+
criteria: "acceptanceCriteriaColumn",
|
|
6599
|
+
"fixed-category": "fixedCategory",
|
|
6600
|
+
"fixed-type": "fixedType"
|
|
6601
|
+
};
|
|
6602
|
+
SPEC_HELP = 'Write it like --table "3:key=ID,text=Requirement,fixed-category=functional".';
|
|
6514
6603
|
SUPPORT = "This platform does not support requirements import yet. Nothing was sent to a project; try a newer Plune platform.";
|
|
6515
6604
|
SHOWN = 50;
|
|
6516
6605
|
flat = (s) => s.replace(/\s+/g, " ").trim();
|
|
@@ -7008,7 +7097,11 @@ function createProgram() {
|
|
|
7008
7097
|
const requirementsCommand = program.command("requirements").description("Requirements on the platform \u2014 import them from a Markdown file");
|
|
7009
7098
|
requirementsCommand.command("import").argument("<file>", "A Markdown file (UTF-8, at most 1 MiB)").description(
|
|
7010
7099
|
"Import requirements from a Markdown file. With no mapping flags it lists the tables and sections found; with them it is a dry run until --yes"
|
|
7011
|
-
).option(
|
|
7100
|
+
).option(
|
|
7101
|
+
"--table <n|spec>",
|
|
7102
|
+
'Read the table with this number (from the list); repeat as --table "<n>:key=ID,text=Requirement,fixed-category=functional" for several tables',
|
|
7103
|
+
(value2, previous = []) => [...previous, value2]
|
|
7104
|
+
).option("--key-column <header>", "Table: the column holding the requirement key").option("--title-column <header>", "Table: the column holding the title").option("--text-column <header>", "Table: one text column; the title is its first line (not with --title-column)").option("--description-column <header>", "Table: the description column").option("--priority-column <header>", "Table: the priority column").option("--category-column <header>", "Table: the category column").option("--type-column <header>", "Table: the type column").option("--status-column <header>", "Table: the status column (draft, active or done)").option("--criteria-column <header>", "Table: the acceptance criteria column").option("--map-priority <pairs>", 'Rename priority values, e.g. "Critical=Must-have,High=Should-have"').option("--map-category <pairs>", 'Table: rename category values, e.g. "Quality=non-functional"').option("--sections <heading|n>", "Read the section with this heading or number (from the list)").option("--unit <unit>", "Sections: bullets or subheadings (default: sub-headings if it has any)").option("--key-prefix <text>", "Sections: text a bullet or sub-heading starts with that carries its key").option("--priority-from <source>", "Sections: subheading \u2014 bullets take their sub-heading as priority").option("--type <type>", "What a row without a type becomes: user_story, feature or epic").option("--project <id>", "The project (default: PLUNE_PROJECT, else the only project of the account)").option("--yes", "Write it. Without it the import is a dry run", false).option("--json", "Print the platform\u2019s answer as JSON", false).action(async (file, options, command) => {
|
|
7012
7105
|
const verbose = command.optsWithGlobals().verbose === true;
|
|
7013
7106
|
const { handleRequirementsImport: handleRequirementsImport2, formatDiscover: formatDiscover2, formatReport: formatReport3, reportRequirementsImportFailure: reportRequirementsImportFailure2 } = await Promise.resolve().then(() => (init_requirements_import(), requirements_import_exports));
|
|
7014
7107
|
try {
|
package/dist/reporter-core.cjs
CHANGED
|
@@ -95,12 +95,14 @@ function parseRetryAfter(raw, now = Date.now()) {
|
|
|
95
95
|
if (!Number.isFinite(seconds)) return null;
|
|
96
96
|
return Math.min(Math.max(seconds, 0) * 1e3, RETRY_AFTER_CAP_MS);
|
|
97
97
|
}
|
|
98
|
+
var POOLS = { "http:": new import_node_http.Agent({ keepAlive: true }), "https:": new import_node_https.Agent({ keepAlive: true }) };
|
|
98
99
|
var IDLE_MS = 3e5;
|
|
99
100
|
function nodeTransport(url, init, idleMs = IDLE_MS) {
|
|
100
101
|
return new Promise((resolve2, reject) => {
|
|
101
102
|
const target = new URL(url);
|
|
102
103
|
const send = target.protocol === "https:" ? import_node_https.request : import_node_http.request;
|
|
103
|
-
const
|
|
104
|
+
const agent = POOLS[target.protocol === "https:" ? "https:" : "http:"];
|
|
105
|
+
const req = send(target, { method: init.method, headers: init.headers, agent }, (res) => {
|
|
104
106
|
const chunks = [];
|
|
105
107
|
res.on("data", (chunk2) => chunks.push(chunk2));
|
|
106
108
|
res.on("error", reject);
|
|
@@ -591,6 +593,7 @@ var RESOLVE_CHUNK = 500;
|
|
|
591
593
|
var BATCH_MAX = 500;
|
|
592
594
|
var DISCOVER_MAX = 500;
|
|
593
595
|
var BATCH_DEFAULT = 500;
|
|
596
|
+
var SEND_CONCURRENCY = 4;
|
|
594
597
|
var EXPECTED_MAX = 1e4;
|
|
595
598
|
var KEY_MAX = 1024;
|
|
596
599
|
var TITLE_MAX = 300;
|
|
@@ -1344,7 +1347,29 @@ async function startRun(passed, declared) {
|
|
|
1344
1347
|
else await uploadBatched(jobs);
|
|
1345
1348
|
uploadMs += Date.now() - started;
|
|
1346
1349
|
}
|
|
1350
|
+
const sending = /* @__PURE__ */ new Set();
|
|
1351
|
+
let sendError;
|
|
1352
|
+
let answered = false;
|
|
1353
|
+
async function dispatch(work) {
|
|
1354
|
+
while (sending.size >= (answered ? SEND_CONCURRENCY : 1)) await Promise.race(sending);
|
|
1355
|
+
const task = work().catch((err) => {
|
|
1356
|
+
sendError ??= err;
|
|
1357
|
+
}).finally(() => sending.delete(task));
|
|
1358
|
+
sending.add(task);
|
|
1359
|
+
}
|
|
1360
|
+
async function settle() {
|
|
1361
|
+
while (sending.size > 0) await Promise.all([...sending]);
|
|
1362
|
+
if (sendError !== void 0) {
|
|
1363
|
+
const err = sendError;
|
|
1364
|
+
sendError = void 0;
|
|
1365
|
+
throw err;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1347
1368
|
async function flush() {
|
|
1369
|
+
await flushBuffer();
|
|
1370
|
+
await settle();
|
|
1371
|
+
}
|
|
1372
|
+
async function flushBuffer() {
|
|
1348
1373
|
if (buffer.length === 0) return;
|
|
1349
1374
|
const pending = buffer;
|
|
1350
1375
|
buffer = [];
|
|
@@ -1352,6 +1377,9 @@ async function startRun(passed, declared) {
|
|
|
1352
1377
|
defer(pending);
|
|
1353
1378
|
return;
|
|
1354
1379
|
}
|
|
1380
|
+
await dispatch(() => deliver(pending));
|
|
1381
|
+
}
|
|
1382
|
+
async function deliver(pending) {
|
|
1355
1383
|
await resolveKeys(pending.flatMap((p) => p.keys));
|
|
1356
1384
|
const submissions = [];
|
|
1357
1385
|
const unanswered = [];
|
|
@@ -1392,6 +1420,7 @@ async function startRun(passed, declared) {
|
|
|
1392
1420
|
for (const k of told) stats[k] += counts[k];
|
|
1393
1421
|
progress();
|
|
1394
1422
|
await upload(out.ok ? out.body?.items : void 0, filesByKey);
|
|
1423
|
+
answered = true;
|
|
1395
1424
|
continue;
|
|
1396
1425
|
}
|
|
1397
1426
|
if (out.ok) {
|
|
@@ -1466,7 +1495,7 @@ async function startRun(passed, declared) {
|
|
|
1466
1495
|
},
|
|
1467
1496
|
async add(result) {
|
|
1468
1497
|
buffer.push({ ...result, keys: bound(result.keys) });
|
|
1469
|
-
if (buffer.length >= batchSize) await
|
|
1498
|
+
if (buffer.length >= batchSize) await flushBuffer();
|
|
1470
1499
|
},
|
|
1471
1500
|
flush,
|
|
1472
1501
|
async finish(reason) {
|
package/dist/reporter-core.js
CHANGED
|
@@ -33,8 +33,8 @@ function loadToken() {
|
|
|
33
33
|
// src/reporter-core/client.ts
|
|
34
34
|
import { createReadStream, openAsBlob } from "fs";
|
|
35
35
|
import { pipeline } from "stream";
|
|
36
|
-
import { request as httpRequest } from "http";
|
|
37
|
-
import { request as httpsRequest } from "https";
|
|
36
|
+
import { Agent as HttpAgent, request as httpRequest } from "http";
|
|
37
|
+
import { Agent as HttpsAgent, request as httpsRequest } from "https";
|
|
38
38
|
var BACKOFF_BASE_MS = 300;
|
|
39
39
|
var BACKOFF_CAP_MS = 1e4;
|
|
40
40
|
var DEFAULT_RETRIES = 2;
|
|
@@ -46,12 +46,14 @@ function parseRetryAfter(raw, now = Date.now()) {
|
|
|
46
46
|
if (!Number.isFinite(seconds)) return null;
|
|
47
47
|
return Math.min(Math.max(seconds, 0) * 1e3, RETRY_AFTER_CAP_MS);
|
|
48
48
|
}
|
|
49
|
+
var POOLS = { "http:": new HttpAgent({ keepAlive: true }), "https:": new HttpsAgent({ keepAlive: true }) };
|
|
49
50
|
var IDLE_MS = 3e5;
|
|
50
51
|
function nodeTransport(url, init, idleMs = IDLE_MS) {
|
|
51
52
|
return new Promise((resolve2, reject) => {
|
|
52
53
|
const target = new URL(url);
|
|
53
54
|
const send = target.protocol === "https:" ? httpsRequest : httpRequest;
|
|
54
|
-
const
|
|
55
|
+
const agent = POOLS[target.protocol === "https:" ? "https:" : "http:"];
|
|
56
|
+
const req = send(target, { method: init.method, headers: init.headers, agent }, (res) => {
|
|
55
57
|
const chunks = [];
|
|
56
58
|
res.on("data", (chunk2) => chunks.push(chunk2));
|
|
57
59
|
res.on("error", reject);
|
|
@@ -542,6 +544,7 @@ var RESOLVE_CHUNK = 500;
|
|
|
542
544
|
var BATCH_MAX = 500;
|
|
543
545
|
var DISCOVER_MAX = 500;
|
|
544
546
|
var BATCH_DEFAULT = 500;
|
|
547
|
+
var SEND_CONCURRENCY = 4;
|
|
545
548
|
var EXPECTED_MAX = 1e4;
|
|
546
549
|
var KEY_MAX = 1024;
|
|
547
550
|
var TITLE_MAX = 300;
|
|
@@ -1295,7 +1298,29 @@ async function startRun(passed, declared) {
|
|
|
1295
1298
|
else await uploadBatched(jobs);
|
|
1296
1299
|
uploadMs += Date.now() - started;
|
|
1297
1300
|
}
|
|
1301
|
+
const sending = /* @__PURE__ */ new Set();
|
|
1302
|
+
let sendError;
|
|
1303
|
+
let answered = false;
|
|
1304
|
+
async function dispatch(work) {
|
|
1305
|
+
while (sending.size >= (answered ? SEND_CONCURRENCY : 1)) await Promise.race(sending);
|
|
1306
|
+
const task = work().catch((err) => {
|
|
1307
|
+
sendError ??= err;
|
|
1308
|
+
}).finally(() => sending.delete(task));
|
|
1309
|
+
sending.add(task);
|
|
1310
|
+
}
|
|
1311
|
+
async function settle() {
|
|
1312
|
+
while (sending.size > 0) await Promise.all([...sending]);
|
|
1313
|
+
if (sendError !== void 0) {
|
|
1314
|
+
const err = sendError;
|
|
1315
|
+
sendError = void 0;
|
|
1316
|
+
throw err;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1298
1319
|
async function flush() {
|
|
1320
|
+
await flushBuffer();
|
|
1321
|
+
await settle();
|
|
1322
|
+
}
|
|
1323
|
+
async function flushBuffer() {
|
|
1299
1324
|
if (buffer.length === 0) return;
|
|
1300
1325
|
const pending = buffer;
|
|
1301
1326
|
buffer = [];
|
|
@@ -1303,6 +1328,9 @@ async function startRun(passed, declared) {
|
|
|
1303
1328
|
defer(pending);
|
|
1304
1329
|
return;
|
|
1305
1330
|
}
|
|
1331
|
+
await dispatch(() => deliver(pending));
|
|
1332
|
+
}
|
|
1333
|
+
async function deliver(pending) {
|
|
1306
1334
|
await resolveKeys(pending.flatMap((p) => p.keys));
|
|
1307
1335
|
const submissions = [];
|
|
1308
1336
|
const unanswered = [];
|
|
@@ -1343,6 +1371,7 @@ async function startRun(passed, declared) {
|
|
|
1343
1371
|
for (const k of told) stats[k] += counts[k];
|
|
1344
1372
|
progress();
|
|
1345
1373
|
await upload(out.ok ? out.body?.items : void 0, filesByKey);
|
|
1374
|
+
answered = true;
|
|
1346
1375
|
continue;
|
|
1347
1376
|
}
|
|
1348
1377
|
if (out.ok) {
|
|
@@ -1417,7 +1446,7 @@ async function startRun(passed, declared) {
|
|
|
1417
1446
|
},
|
|
1418
1447
|
async add(result) {
|
|
1419
1448
|
buffer.push({ ...result, keys: bound(result.keys) });
|
|
1420
|
-
if (buffer.length >= batchSize) await
|
|
1449
|
+
if (buffer.length >= batchSize) await flushBuffer();
|
|
1421
1450
|
},
|
|
1422
1451
|
flush,
|
|
1423
1452
|
async finish(reason) {
|
package/package.json
CHANGED