process-graph 1.2.0 → 1.4.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.
Files changed (56) hide show
  1. package/README.md +25 -20
  2. package/dist/bin.js +4 -3
  3. package/dist/cli.d.ts +87 -75
  4. package/dist/cli.js +4 -2
  5. package/dist/data-file-BVMxkTug.js +95 -0
  6. package/dist/{dist-BxHnxpcT.js → dist-C8zDuW-y.js} +293 -510
  7. package/dist/dist-DipyhCvb.js +4 -0
  8. package/dist/{dist-BpCBmnMl.js → dist-arkejVz2.js} +2 -112
  9. package/dist/graphql-3DSUSUvL.d.ts +17 -0
  10. package/dist/{index-CoA6vXqe.d.ts → index-4f4PVH7K.d.ts} +23 -2
  11. package/dist/index-Bb90VKMc.d.ts +71 -0
  12. package/dist/{index-DxWOY8f9.d.ts → index-CR8xUK3S.d.ts} +5 -572
  13. package/dist/{index-CptUHq6F.d.ts → index-Cp5selgs.d.ts} +2 -1
  14. package/dist/{graphql-C1j5wQFz.d.ts → index-Cs1CWlEg.d.ts} +3 -16
  15. package/dist/{index-BH1aFqkJ.d.ts → index-D4CnOhNf.d.ts} +1 -1
  16. package/dist/index-DoMIav3D.d.ts +118 -0
  17. package/dist/{index-CmZOswkx.d.ts → index-Dvz_I_uQ.d.ts} +17 -8
  18. package/dist/{index-BUheRLdG.d.ts → index-LWdwpjZG.d.ts} +5 -24
  19. package/dist/{index-CrM5qWUA.d.ts → index-RwwlDYZ7.d.ts} +2 -2
  20. package/dist/index.d.ts +25 -4
  21. package/dist/index.js +4 -3
  22. package/dist/layout.d.ts +1 -1
  23. package/dist/project-CP_ye6KI.js +27 -0
  24. package/dist/react-flow.d.ts +5 -5
  25. package/dist/react-flow.js +1 -1
  26. package/dist/react.d.ts +2 -1
  27. package/dist/react.js +1 -24
  28. package/dist/{router-DvGGLnNt.js → router-Dk91O_9p.js} +2 -2
  29. package/dist/{schema-CVnS6HiX.js → schema-BPT8fmfa.js} +2 -2
  30. package/dist/schema-DVAttrJg.js +126 -0
  31. package/dist/source/github/node.d.ts +1 -1
  32. package/dist/source/github.d.ts +3 -2
  33. package/dist/source/github.js +38 -1
  34. package/dist/{source-DRDpEA8Q.js → source-lTKshktI.js} +24 -163
  35. package/dist/store/node.d.ts +27 -2
  36. package/dist/store/node.js +2 -2
  37. package/dist/store/wasm/worker.js +2 -2
  38. package/dist/store/wasm.d.ts +1 -1
  39. package/dist/{store-BJILV4JS.js → store-DKzvLGgl.js} +1 -1
  40. package/dist/store.d.ts +1 -1
  41. package/dist/store.js +2 -2
  42. package/dist/sync-CkaJO3OQ.js +213 -0
  43. package/dist/sync.d.ts +26 -0
  44. package/dist/sync.js +16 -0
  45. package/dist/theme-values-BJJhAkUn-ORyRUrPf.js +114 -0
  46. package/dist/theme-values-BybZ-cle.d.ts +116 -0
  47. package/dist/ui/theme.d.ts +2 -0
  48. package/dist/ui/theme.js +2 -0
  49. package/dist/ui.d.ts +6 -120
  50. package/dist/ui.js +2 -1
  51. package/dist/version-DMNBcTZ-.js +5 -0
  52. package/dist/view-DTeNUhGG.js +392 -0
  53. package/package.json +34 -23
  54. package/dist/dist-Cw63tKU9.js +0 -3
  55. package/dist/open-BF-UHqeW.js +0 -29
  56. package/dist/parse-CuipC_2K.js +0 -41
@@ -1,20 +1,23 @@
1
- import { a as validateOverlay, d as overlayStatuses, f as participantKinds } from "./schema-CVnS6HiX.js";
1
+ import { a as validateOverlay, d as overlayStatuses, f as participantKinds } from "./schema-BPT8fmfa.js";
2
2
  import { t as validate } from "./validate-DF3mQZww.js";
3
- import { n as parseModel, t as ModelParseError } from "./parse-CuipC_2K.js";
3
+ import { f as terminalJson, h as parseModel, m as ModelParseError, n as viewText, p as terminalText } from "./view-DTeNUhGG.js";
4
4
  import { c as deriveStatuses, f as toStateMachine, g as handoffs, h as toFlowchart, i as readySet, n as criticalPath, p as toSwimlane, r as impact, s as explainStatus, t as blockedBy, u as toSequence } from "./queries-C8kIDRiY.js";
5
5
  import { S as UnknownNodeError, f as layeredFacts, n as illegalCycles, u as factsFrom, x as CycleError } from "./control-DiygoEhm.js";
6
- import { l as assembleSync, t as createGitHubIssuesSource, u as createGraphSourceRouter } from "./source-DRDpEA8Q.js";
6
+ import { r as filterStates, s as orderFields } from "./schema-DVAttrJg.js";
7
+ import { l as createGraphSourceRouter, t as createGitHubIssuesSource } from "./source-lTKshktI.js";
8
+ import { t as GraphSourceError } from "./errors-BZe5WzCn.js";
7
9
  import { t as githubTokenFromEnvironment } from "./node-AchI1T-K.js";
8
- import { g as emptyModel } from "./store-BJILV4JS.js";
9
- import { t as createGraphStoreRouter } from "./router-DvGGLnNt.js";
10
- import { t as openNodeGraphStore } from "./open-BF-UHqeW.js";
10
+ import { g as emptyModel } from "./store-DKzvLGgl.js";
11
+ import { t as createGraphStoreRouter } from "./router-Dk91O_9p.js";
12
+ import { i as openNodeGraphStore, t as prepareDataFile } from "./data-file-BVMxkTug.js";
13
+ import { a as scopesBySource, i as InvalidScopeError, n as UnknownSourceError, r as syncScopes, t as InterruptedSyncError } from "./sync-CkaJO3OQ.js";
11
14
  import { t as exitCodes } from "./exit-codes-0ultCqdS.js";
12
15
  import { ORPCError, createRouterClient } from "@orpc/server";
13
- import { Argument, Command, CommanderError, Option } from "commander";
14
- import { chmodSync, closeSync, mkdirSync, openSync, readFileSync, statSync } from "node:fs";
16
+ import { readFileSync } from "node:fs";
17
+ import { join } from "node:path";
18
+ import { Argument, Command, CommanderError, InvalidArgumentError, Option } from "commander";
15
19
  import { readFile } from "node:fs/promises";
16
20
  import { homedir } from "node:os";
17
- import { dirname, join } from "node:path";
18
21
  //#region ../graph-cli/dist/errors.js
19
22
  /** A failure the command line reports with its own exit status and message, and nothing else. */
20
23
  var CliError = class extends Error {
@@ -46,6 +49,16 @@ const issueLines = (data) => {
46
49
  return data.issues.flatMap((issue) => typeof issue === "object" && issue !== null && "code" in issue && "message" in issue && typeof issue.code === "string" && typeof issue.message === "string" ? [`${issue.code}: ${issue.message}`] : []);
47
50
  };
48
51
  /**
52
+ * The error underneath a source's failure, for a failure's cause. A source called in process through oRPC fails with an `ORPCError` whose cause is the `GraphSourceError` the source threw, whose own cause, when it has one, is what went wrong underneath, such as a token port's error. Those two layers say nothing more than the failure's message and exit status already do, so the innermost error is the one worth keeping.
53
+ * @param error - What the source call threw.
54
+ * @returns The `GraphSourceError`'s cause when it has one, otherwise that `GraphSourceError`; anything else as it is.
55
+ */
56
+ const underlyingCause = (error) => {
57
+ const sourceError = error instanceof ORPCError && error.cause instanceof GraphSourceError ? error.cause : error;
58
+ if (!(sourceError instanceof GraphSourceError)) return error;
59
+ return sourceError.cause === void 0 ? sourceError : sourceError.cause;
60
+ };
61
+ /**
49
62
  * Classifies anything a command threw into its exit status and the message for standard error. A commander usage error has already printed its own message, so it is reported with an empty one.
50
63
  * @param error - What was thrown.
51
64
  * @returns The exit status and message.
@@ -406,6 +419,150 @@ const why = async (session, id) => {
406
419
  chain
407
420
  };
408
421
  };
422
+ /**
423
+ * An element's direct sub-tasks, from the `child-of` relations a sync keeps, as the store's hierarchy query answers.
424
+ * @param session - The store and model.
425
+ * @param id - The element asked about.
426
+ * @throws CliError with `notFound` when the model or the element does not exist.
427
+ */
428
+ const children = async (session, id) => {
429
+ const { model } = await loadRun(session);
430
+ const element = requireElement(model, id);
431
+ const found = await session.store.v1.query.children({
432
+ modelId: session.modelId,
433
+ element: id
434
+ }, callOptions(session.signal));
435
+ return {
436
+ element: {
437
+ id,
438
+ label: labelOf(element)
439
+ },
440
+ children: summarise(model, found.children)
441
+ };
442
+ };
443
+ /**
444
+ * The elements an element is a sub-task of, nearest first, from the `child-of` relations a sync keeps, as the store's hierarchy query answers.
445
+ * @param session - The store and model.
446
+ * @param id - The element asked about.
447
+ * @throws CliError with `notFound` when the model or the element does not exist.
448
+ */
449
+ const ancestors = async (session, id) => {
450
+ const { model } = await loadRun(session);
451
+ const element = requireElement(model, id);
452
+ const found = await session.store.v1.query.ancestors({
453
+ modelId: session.modelId,
454
+ element: id
455
+ }, callOptions(session.signal));
456
+ return {
457
+ element: {
458
+ id,
459
+ label: labelOf(element)
460
+ },
461
+ ancestors: found.ancestors.map((link) => ({
462
+ id: link.element,
463
+ label: labelOf(requireElement(model, link.element)),
464
+ distance: link.distance
465
+ }))
466
+ };
467
+ };
468
+ //#endregion
469
+ //#region ../graph-cli/dist/sync.js
470
+ /** The short direction names the command line takes for an order's direction. */
471
+ const directionNames = {
472
+ asc: "ascending",
473
+ desc: "descending"
474
+ };
475
+ /**
476
+ * Thrown by `sync` when a source fails part way. Its exit status and message are the source's failure, classified as `failureOf` classifies it; its cause is the error underneath that failure, such as the token port's own error; and what to run next is data, in `resume` and `remaining`, never text.
477
+ */
478
+ var SyncInterruptedError = class extends CliError {
479
+ resume;
480
+ remaining;
481
+ saved;
482
+ /**
483
+ * @param exitCode - The exit status the source's failure maps to.
484
+ * @param message - The source's failure.
485
+ * @param interruption - Where the sync carries on.
486
+ * @param options - The underlying error, kept as the cause.
487
+ */
488
+ constructor(exitCode, message, interruption, options) {
489
+ super(exitCode, message, options);
490
+ this.name = "SyncInterruptedError";
491
+ this.resume = interruption.resume;
492
+ this.remaining = interruption.remaining;
493
+ this.saved = interruption.saved;
494
+ }
495
+ };
496
+ /** A usage error for a scope that names no source or a source nothing reads, or nothing for any other error. */
497
+ const usageError = (error) => {
498
+ if (error instanceof InvalidScopeError) return new CliError(exitCodes.usage, error.message, { cause: error });
499
+ if (error instanceof UnknownSourceError) return new CliError(exitCodes.usage, `no source reads "${error.prefix}" scopes`, { cause: error });
500
+ };
501
+ /**
502
+ * Reads every page of the scopes from their sources and merges each into the session's model as it is read, creating the model when the store has none: participants, bound activities, transitions, relations (sub-issues, assignees, repository membership) and run state. Records already in the model are replaced by identifier and the rest are kept, so syncing again refreshes what changed.
503
+ *
504
+ * When a source fails part way, the pages read before the failure are already written, and the error carries where to carry on: the request that resumes the failing source after the cursor of its last page read, and the scopes of the sources not reached.
505
+ * @param session - The store and model.
506
+ * @param sources - The source for each identifier prefix.
507
+ * @param request - The scopes, a cursor to resume after, and a filter.
508
+ * @returns What was written, and the gaps the sources reported.
509
+ * @throws CliError with `usage` for a scope with no source prefix, a prefix no source serves, or `after` with scopes of more than one source; and SyncInterruptedError, with the source's failure and where to carry on, when a source fails part way.
510
+ */
511
+ const sync = async (session, sources, request) => {
512
+ const { scopes, after, filter } = request;
513
+ let groups;
514
+ try {
515
+ groups = scopesBySource(scopes);
516
+ } catch (error) {
517
+ throw usageError(error) ?? error;
518
+ }
519
+ const [first] = groups;
520
+ if (after !== void 0 && groups.length > 1) throw new CliError(exitCodes.usage, "--after resumes one source's sync: give only that source's scopes");
521
+ const resume = after === void 0 || first === void 0 ? void 0 : {
522
+ modelId: session.modelId,
523
+ scopes,
524
+ ...filter === void 0 ? {} : { filter },
525
+ source: first.prefix,
526
+ after
527
+ };
528
+ try {
529
+ const result = await syncScopes({
530
+ store: session.store,
531
+ sources,
532
+ modelId: session.modelId,
533
+ scopes,
534
+ filter,
535
+ resume,
536
+ signal: session.signal
537
+ });
538
+ return {
539
+ modelId: session.modelId,
540
+ revision: result.revision,
541
+ participants: result.participants,
542
+ activities: result.activities,
543
+ transitions: result.transitions,
544
+ relations: result.relations,
545
+ gaps: result.gaps
546
+ };
547
+ } catch (error) {
548
+ const usage = usageError(error);
549
+ if (usage !== void 0) throw usage;
550
+ if (!(error instanceof InterruptedSyncError)) throw error;
551
+ const failure = failureOf(error.cause);
552
+ const { resume: from, remaining, saved } = error;
553
+ const failing = groups.find((group) => group.prefix === from.source);
554
+ if (failing === void 0) throw new Error(`unreachable: the interrupted source "${from.source}" is one of the sync's own`, { cause: error });
555
+ throw new SyncInterruptedError(failure.exitCode, failure.message, {
556
+ resume: {
557
+ scopes: failing.scopes,
558
+ ...from.after === void 0 ? {} : { after: from.after },
559
+ ...filter === void 0 ? {} : { filter }
560
+ },
561
+ remaining,
562
+ saved
563
+ }, { cause: underlyingCause(error.cause) });
564
+ }
565
+ };
409
566
  //#endregion
410
567
  //#region ../graph-cli/dist/render.js
411
568
  /**
@@ -419,6 +576,10 @@ const renderReady = (result) => lines(elementRows(result.elements));
419
576
  const renderBlocked = (result) => lines(elementRows(result.blockedBy));
420
577
  /** `impact`: each element that depends on the one asked about. */
421
578
  const renderImpact = (result) => lines(elementRows(result.impacted));
579
+ /** `children`: each direct sub-task's identifier and label. */
580
+ const renderChildren = (result) => lines(elementRows(result.children));
581
+ /** `ancestors`: each element above the one asked about, nearest first, with its distance. */
582
+ const renderAncestors = (result) => lines(result.ancestors.map((link) => `${link.id}\t${link.label}\t${String(link.distance)}`));
422
583
  /** `path`: the critical path, one element per line in dependency order. */
423
584
  const renderPath = (result) => lines(elementRows(result.path));
424
585
  /** `cycles`: each illegal cycle as a closed path. */
@@ -470,329 +631,34 @@ const renderSetFact = (result) => lines([`set fact ${result.fact} to ${result.va
470
631
  /** `facts`: each recorded artefact as `artefact`, its id and `provided` or `absent`, then each named fact as `fact`, its name and its value, each sorted by id or name. */
471
632
  const renderFacts = (result) => lines([...Object.entries(result.facts.artefacts).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([id, provided]) => `artefact\t${id}\t${provided ? "provided" : "absent"}`), ...Object.entries(result.facts.values).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([name, value]) => `fact\t${name}\t${valueText(value)}`)]);
472
633
  /** `sync`: what was written, then each gap as its reason, target and message. */
473
- const renderSync = (result) => lines([`synced ${String(result.activities)} activities, ${String(result.transitions)} transitions and ${String(result.participants)} participants into "${result.modelId}" at revision ${String(result.revision)}`, ...result.gaps.map((gap) => `gap\t${gap.reason}\t${gap.target}\t${gap.message}`)]);
474
- /** `validate`: `valid`, or each issue as its code, its JSON pointer when it has one, and its message. */
475
- const renderValidate = (result) => result.valid ? lines(["valid"]) : lines(result.issues.map((issue) => issue.pointer === void 0 ? `${issue.code}\t${issue.message}` : `${issue.code}\t${issue.pointer === "" ? "/" : issue.pointer}\t${issue.message}`));
476
- //#endregion
477
- //#region ../graph-cli/dist/terminal-text.js
478
- /**
479
- * The characters that must never reach a terminal as they are, by Unicode general category: controls (`Cc`, where ESC starts the sequences that clear the screen, move the cursor or, through OSC 52, set the clipboard, and the C1 controls some terminals read as the same sequences), format characters (`Cf`, which holds every bidirectional control, the invisible operators, the soft hyphen, the interlinear annotation characters and the tag characters, all of which reorder, hide or change what is shown), the line and paragraph separators (`Zl`, `Zp`), and the unassigned and private-use code points (`Cn`, `Co`), which no font shows reliably. Text from a source (an issue title, a label, an actor's name) can carry any of them. A category, not a list of ranges, so a character Unicode adds to one of them is covered without a change here.
480
- *
481
- * Three are allowed: line feed and tab, which every text output uses, and the zero-width non-joiner and joiner (U+200C, U+200D), formats that emoji sequences (a family, a profession) and scripts such as Persian, Hindi and Malayalam need to be written correctly, and that cannot reorder or hide text on their own.
482
- */
483
- const unsafeCharacters = /[[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\p{Cn}\p{Co}]--[\n\t\u200C\u200D]]/gv;
484
- /** Code points are written in hexadecimal. */
485
- const hexadecimal = 16;
486
- /** A JSON `\u` escape always has four hexadecimal digits. */
487
- const jsonEscapeDigits = 4;
488
- /**
489
- * A character's code point.
490
- * @param character - One character, as the unsafe-character pattern matches it.
491
- * @returns Its code point.
492
- * @throws Error when given an empty string, which the pattern never matches.
493
- */
494
- const codePointOf = (character) => {
495
- const point = character.codePointAt(0);
496
- if (point === void 0) throw new Error("an unsafe character match was empty");
497
- return point;
498
- };
499
- /**
500
- * Text made safe to write to a terminal. Each unsafe character (see `unsafeCharacters`) is written as the visible escape `\u{<hex>}` of its code point, such as `\u{1b}` for ESC, `\u{202e}` for a right-to-left override or `\u{e0041}` for a tag character, so nothing is lost and nothing acts on the terminal. Line feed, tab, the zero-width joiners and every other character, accented letters, CJK, Arabic and emoji included, pass through. A literal `\u{...}` already in the text is left as it is, so the escaped form is for reading, not for reversing.
501
- * @param text - The text as written.
502
- * @returns The text with every unsafe character escaped.
503
- */
504
- const terminalText = (text) => text.replaceAll(unsafeCharacters, (character) => `\\u{${codePointOf(character).toString(hexadecimal)}}`);
505
- /**
506
- * A value as JSON that is also safe to write to a terminal. `JSON.stringify` escapes the C0 controls but writes every other unsafe character (see `unsafeCharacters`) as it is; this writes those as `\u` escapes too, a surrogate pair for a code point beyond the Basic Multilingual Plane, which leaves the JSON, and every value parsed from it, exactly the same.
507
- * @param value - The value to write.
508
- * @returns The JSON text, indented by two spaces and ending in a line break.
509
- */
510
- const terminalJson = (value) => `${JSON.stringify(value, null, 2).replaceAll(unsafeCharacters, (character) => character.split("").map((unit) => `\\u${unit.charCodeAt(0).toString(hexadecimal).padStart(jsonEscapeDigits, "0")}`).join(""))}\n`;
511
- //#endregion
512
- //#region ../graph-cli/dist/emit/text.js
513
- /**
514
- * The Mermaid entity code for a character that would end or change the meaning of a label: quotes, statement and edge-label delimiters, the entity marker itself and HTML.
515
- */
516
- const mermaidEntity = (character) => {
517
- switch (character) {
518
- case "#": return "#35;";
519
- case "\"": return "#quot;";
520
- case "<": return "#lt;";
521
- case ">": return "#gt;";
522
- case "&": return "#amp;";
523
- case ";": return "#59;";
524
- case "|": return "#124;";
525
- case ":": return "#58;";
526
- default: return character;
527
- }
528
- };
529
- /**
530
- * Text made safe to place inside any Mermaid label: a line break becomes a space, every control and invisible format character is written as its visible `\u{<hex>}` escape (see `terminalText`), and every character that Mermaid reads as syntax is written as its entity code, so a label from a source (an issue title, a repository name) can never break the diagram or act on the terminal it is printed to.
531
- * @param text - The label as written.
532
- * @returns The label as Mermaid source.
533
- */
534
- const mermaidText = (text) => terminalText(text.replaceAll(/[\r\n]/gu, " ")).replaceAll(/[#"<>&;|:]/gu, mermaidEntity);
535
- /**
536
- * A Graphviz DOT double-quoted string: line breaks written as DOT's own centred line break, every control and invisible format character as its visible `\u{<hex>}` escape (see `terminalText`), then backslashes, the escapes' included, and quotes escaped. Any identifier or label is valid DOT once quoted this way, and reads back as the escaped text.
537
- * @param text - The text as written.
538
- * @returns The quoted DOT string, quotes included.
539
- */
540
- const dotString = (text) => `"${text.split(/\r?\n/u).map((line) => terminalText(line).replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")).join("\\n")}"`;
541
- //#endregion
542
- //#region ../graph-cli/dist/emit/mermaid.js
543
- /**
544
- * Model identifiers are free text (`github:org/repo#12`), which Mermaid does not accept as a node identifier, so every emitter names things by position in the view instead: a prefix and the index. The view's order is the model's order, so the same model always gives the same names.
545
- */
546
- const positionalIds = (prefix, ids) => new Map(ids.map((id, index) => [id, `${prefix}${String(index)}`]));
547
- /** Reads a name the emitter itself assigned, which exists for every identifier the view lists. */
548
- const nameIn = (names, id) => {
549
- const name = names.get(id);
550
- if (name === void 0) throw new Error(`"${id}" is not part of the view being emitted`);
551
- return name;
552
- };
553
- const indent$1 = " ";
554
- /** Each flowchart shape in Mermaid's syntax around a node name and quoted label: a stadium for a terminator, a rectangle for a process, a rhombus for a decision and a fork bar for a fork or join. */
555
- const mermaidShapes = {
556
- terminator: (name, label) => `${name}(["${label}"])`,
557
- process: (name, label) => `${name}["${label}"]`,
558
- decision: (name, label) => `${name}{"${label}"}`,
559
- bar: (name, label) => `${name}@{ shape: fork, label: "${label}" }`
560
- };
561
- const mermaidNode = (name, node) => mermaidShapes[node.shape](name, mermaidText(node.label));
562
- /** The edges of a flowchart, a hand-off (an edge whose ends are in different lanes) drawn thick. */
563
- const mermaidEdges = (view, names, isHandoff) => view.edges.map((edge) => {
564
- const arrow = isHandoff(edge.from, edge.to) ? "==>" : "-->";
565
- const label = edge.label === void 0 ? "" : `|"${mermaidText(edge.label)}"|`;
566
- return `${indent$1}${nameIn(names, edge.from)} ${arrow}${label} ${nameIn(names, edge.to)}`;
567
- });
568
- /**
569
- * A flowchart as a Mermaid `flowchart`, top to bottom. Nodes are declared in the view's order, then edges, each with its guard as the edge label.
570
- * @param view - The projected flowchart.
571
- * @returns Mermaid source, ending with a line break.
572
- */
573
- const flowchartToMermaid = (view) => {
574
- const names = positionalIds("n", view.nodes.map((node) => node.id));
575
- return `${[
576
- "flowchart TD",
577
- ...view.nodes.map((node) => `${indent$1}${mermaidNode(nameIn(names, node.id), node)}`),
578
- ...mermaidEdges(view, names, () => false)
579
- ].join("\n")}\n`;
580
- };
581
- /**
582
- * A swimlane as a Mermaid `flowchart`, left to right, with one subgraph per lane in the view's lane order (an empty lane is an empty subgraph, so it stays visible) and nodes with no actor outside every lane. Hand-offs, the edges that change lane, are drawn thick.
583
- * @param view - The projected swimlane.
584
- * @returns Mermaid source, ending with a line break.
585
- */
586
- const swimlaneToMermaid = (view) => {
587
- const names = positionalIds("n", view.nodes.map((node) => node.id));
588
- const nodes = new Map(view.nodes.map((node) => [node.id, node]));
589
- const laneOf = /* @__PURE__ */ new Map();
590
- view.lanes.forEach((lane, index) => {
591
- for (const id of lane.nodes) laneOf.set(id, index);
592
- });
593
- const declare = (id, depth) => {
594
- const node = nodes.get(id);
595
- if (node === void 0) throw new Error(`lane member "${id}" is not a node of the view`);
596
- return `${depth}${mermaidNode(nameIn(names, id), node)}`;
597
- };
598
- return `${[
599
- "flowchart LR",
600
- ...view.lanes.flatMap((lane, index) => [
601
- `${indent$1}subgraph lane${String(index)}["${mermaidText(lane.label)}"]`,
602
- ...lane.nodes.map((id) => declare(id, indent$1.repeat(2))),
603
- `${indent$1}end`
604
- ]),
605
- ...view.unassigned.map((id) => declare(id, indent$1)),
606
- ...mermaidEdges(view, names, (from, to) => {
607
- const fromLane = laneOf.get(from);
608
- const toLane = laneOf.get(to);
609
- return fromLane !== void 0 && toLane !== void 0 && fromLane !== toLane;
610
- })
611
- ].join("\n")}\n`;
612
- };
613
- /** A state machine transition's label: its event, its guard in brackets and its effect after a slash, each only when present. */
614
- const transitionLabel = (transition) => [
615
- transition.event,
616
- transition.guard === void 0 ? void 0 : `[${transition.guard}]`,
617
- transition.effect === void 0 ? void 0 : `/ ${transition.effect}`
618
- ].filter((part) => part !== void 0).join(" ");
634
+ const renderSync = (result) => lines([`synced ${String(result.activities)} activities, ${String(result.transitions)} transitions, ${String(result.relations)} relations and ${String(result.participants)} participants into "${result.modelId}" at revision ${String(result.revision)}`, ...result.gaps.map((gap) => `gap\t${gap.reason}\t${gap.target}\t${gap.message}`)]);
619
635
  /**
620
- * A state machine as a Mermaid `stateDiagram-v2`. Initial and final pseudo-states are Mermaid's `[*]`; every other state is declared with its label. Each transition carries its event, guard and effect.
621
- * @param view - The projected state machine.
622
- * @returns Mermaid source, ending with a line break.
636
+ * The `sync` options that give a filter, as they would be typed again to resume under it, since a cursor is valid only under the filter it was issued with.
637
+ * @param filter - The filter, if any.
638
+ * @returns The options, each with a leading space, or the empty string.
623
639
  */
624
- const stateMachineToMermaid = (view) => {
625
- const drawn = view.states.filter((state) => state.role === "state");
626
- const names = new Map([...positionalIds("s", drawn.map((state) => state.id)), ...view.states.filter((state) => state.role !== "state").map((state) => [state.id, "[*]"])]);
627
- return `${[
628
- "stateDiagram-v2",
629
- ...drawn.map((state) => `${indent$1}state "${mermaidText(state.label)}" as ${nameIn(names, state.id)}`),
630
- ...view.transitions.map((transition) => {
631
- const label = transitionLabel(transition);
632
- return `${indent$1}${nameIn(names, transition.source)} --> ${nameIn(names, transition.target)}${label === "" ? "" : ` : ${mermaidText(label)}`}`;
633
- })
634
- ].join("\n")}\n`;
635
- };
636
- /** The Mermaid keyword that separates one operand of a fragment from the next, for the operators with more than one operand. */
637
- const operandSeparator = (operator) => operator === "par" ? "and" : "else";
640
+ const filterOptions = (filter) => [
641
+ ...(filter?.states ?? []).map((state) => ` --state ${state}`),
642
+ ...(filter?.labels ?? []).map((label) => ` --label ${label}`),
643
+ ...filter?.order === void 0 ? [] : [` --order ${filter.order.field} --direction ${filter.order.direction === directionNames.desc ? "desc" : "asc"}`]
644
+ ].join("");
638
645
  /**
639
- * A sequence diagram as a Mermaid `sequenceDiagram`. Lifelines are declared in order of first appearance, labelled through `participantLabel`; messages follow in scenario order, wrapped in the fragments the projection drew (`alt` operands separated by `else`, `par` operands by `and`), each operand headed by its guard.
640
- * @param view - The projected sequence diagram.
641
- * @param participantLabel - The label of a lifeline's participant.
642
- * @returns Mermaid source, ending with a line break.
646
+ * A sync a source interrupted: the source's failure, whether the pages read before it were saved, and the commands that carry on, `resume with: sync <scopes> --after <cursor>` for the failing source and `then: sync <scopes>` for the sources not reached.
647
+ * @param message - The source's failure.
648
+ * @param interruption - Where the sync carries on.
649
+ * @returns The lines for standard error, without a final line break.
643
650
  */
644
- const sequenceToMermaid = (view, participantLabel) => {
645
- const names = positionalIds("p", view.lifelines);
646
- const items = (list, depth) => list.flatMap((item) => {
647
- const prefix = indent$1.repeat(depth);
648
- if (item.kind === "message") {
649
- const { message } = item;
650
- return [`${prefix}${nameIn(names, message.from)}->>${nameIn(names, message.to)}: ${mermaidText(message.label)}`];
651
- }
652
- return [...item.operands.flatMap((operand, index) => {
653
- const keyword = index === 0 ? item.operator : operandSeparator(item.operator);
654
- const guard = operand.guard === void 0 ? "" : ` ${mermaidText(operand.guard)}`;
655
- return [`${prefix}${keyword}${guard}`, ...items(operand.items, depth + 1)];
656
- }), `${prefix}end`];
657
- });
658
- return `${[
659
- "sequenceDiagram",
660
- ...view.lifelines.map((lifeline) => `${indent$1}participant ${nameIn(names, lifeline)} as ${mermaidText(participantLabel(lifeline))}`),
661
- ...items(view.items, 1)
662
- ].join("\n")}\n`;
663
- };
664
- //#endregion
665
- //#region ../graph-cli/dist/emit/dot.js
666
- const indent = " ";
667
- /** A DOT attribute list from name and value pairs, each value quoted; empty when there are none. */
668
- const attributes = (pairs) => pairs.length === 0 ? "" : ` [${pairs.map(([name, value]) => `${name}=${dotString(value)}`).join(", ")}]`;
669
- /** A flowchart node's DOT shape: an oval terminator, a box process, a diamond decision, and a filled bar for a fork or join. */
670
- const nodeShapes = {
671
- terminator: [["shape", "oval"]],
672
- process: [["shape", "box"]],
673
- decision: [["shape", "diamond"]],
674
- bar: [
675
- ["shape", "box"],
676
- ["style", "filled"],
677
- ["fillcolor", "black"],
678
- ["fontcolor", "white"]
679
- ]
680
- };
681
- const dotNode = (node, depth) => `${depth}${dotString(node.id)}${attributes([["label", node.label], ...nodeShapes[node.shape]])};`;
682
- /** The edges of a flowchart, a hand-off (an edge whose ends are in different lanes) drawn bold. */
683
- const dotEdges = (view, isHandoff) => view.edges.map((edge) => `${indent}${dotString(edge.from)} -> ${dotString(edge.to)}${attributes([...edge.label === void 0 ? [] : [["label", edge.label]], ...isHandoff(edge.from, edge.to) ? [["style", "bold"]] : []])};`);
684
- /**
685
- * A flowchart as a Graphviz `digraph`, top to bottom. Nodes are named by their model identifiers, so the DOT source reads back to the model directly.
686
- * @param view - The projected flowchart.
687
- * @returns DOT source, ending with a line break.
688
- */
689
- const flowchartToDot = (view) => `${[
690
- `digraph ${dotString("flowchart")} {`,
691
- `${indent}rankdir="TB";`,
692
- ...view.nodes.map((node) => dotNode(node, indent)),
693
- ...dotEdges(view, () => false),
694
- "}"
695
- ].join("\n")}\n`;
696
- /**
697
- * A swimlane as a Graphviz `digraph`, left to right, with one `cluster` subgraph per lane in the view's lane order and nodes with no actor outside every lane. An empty lane holds one invisible point so Graphviz still draws it. Hand-offs, the edges that change lane, are drawn bold.
698
- * @param view - The projected swimlane.
699
- * @returns DOT source, ending with a line break.
700
- */
701
- const swimlaneToDot = (view) => {
702
- const nodes = new Map(view.nodes.map((node) => [node.id, node]));
703
- const laneOf = /* @__PURE__ */ new Map();
704
- view.lanes.forEach((lane, index) => {
705
- for (const id of lane.nodes) laneOf.set(id, index);
706
- });
707
- const declare = (id, depth) => {
708
- const node = nodes.get(id);
709
- if (node === void 0) throw new Error(`lane member "${id}" is not a node of the view`);
710
- return dotNode(node, depth);
711
- };
712
- return `${[
713
- `digraph ${dotString("swimlane")} {`,
714
- `${indent}rankdir="LR";`,
715
- ...view.lanes.flatMap((lane, index) => {
716
- const cluster = `cluster_lane${String(index)}`;
717
- return [
718
- `${indent}subgraph ${dotString(cluster)} {`,
719
- `${indent.repeat(2)}label=${dotString(lane.label)};`,
720
- ...lane.nodes.length === 0 ? [`${indent.repeat(2)}${dotString(`${cluster}:empty`)}${attributes([["shape", "point"], ["style", "invis"]])};`] : lane.nodes.map((id) => declare(id, indent.repeat(2))),
721
- `${indent}}`
722
- ];
723
- }),
724
- ...view.unassigned.map((id) => declare(id, indent)),
725
- ...dotEdges(view, (from, to) => {
726
- const fromLane = laneOf.get(from);
727
- const toLane = laneOf.get(to);
728
- return fromLane !== void 0 && toLane !== void 0 && fromLane !== toLane;
729
- }),
730
- "}"
731
- ].join("\n")}\n`;
732
- };
733
- /** A state's DOT shape: a filled point for an initial pseudo-state, a double circle for a final one, a rounded box for a state. */
734
- const stateShape = (state) => {
735
- if (state.role === "initial") return [["label", ""], ["shape", "point"]];
736
- if (state.role === "final") return [["label", ""], ["shape", "doublecircle"]];
651
+ const renderSyncInterruption = (message, interruption) => {
652
+ const { resume, remaining, saved } = interruption;
737
653
  return [
738
- ["label", state.label],
739
- ["shape", "box"],
740
- ["style", "rounded"]
741
- ];
742
- };
743
- /**
744
- * A state machine as a Graphviz `digraph`, left to right. Each transition is labelled with its event, guard and effect.
745
- * @param view - The projected state machine.
746
- * @returns DOT source, ending with a line break.
747
- */
748
- const stateMachineToDot = (view) => `${[
749
- `digraph ${dotString("state machine")} {`,
750
- `${indent}rankdir="LR";`,
751
- ...view.states.map((state) => `${indent}${dotString(state.id)}${attributes(stateShape(state))};`),
752
- ...view.transitions.map((transition) => {
753
- const label = transitionLabel(transition);
754
- return `${indent}${dotString(transition.source)} -> ${dotString(transition.target)}${attributes(label === "" ? [] : [["label", label]])};`;
755
- }),
756
- "}"
757
- ].join("\n")}\n`;
758
- const placeMessages = (items, fragments) => items.flatMap((item) => item.kind === "message" ? [{
759
- message: item.message,
760
- fragments
761
- }] : item.operands.flatMap((operand) => placeMessages(operand.items, [...fragments, operand.guard === void 0 ? item.operator : `${item.operator} [${operand.guard}]`])));
762
- /**
763
- * A sequence diagram as a Graphviz `digraph`. DOT has no sequence notation, so the diagram is drawn as a grid: a column per lifeline, headed by its participant's label, with one row per message in scenario order. Each message is an arrow across its row, numbered by its step and prefixed with the fragments around it (operator and guard, outermost first), since DOT cannot draw a box around arrows.
764
- * @param view - The projected sequence diagram.
765
- * @param participantLabel - The label of a lifeline's participant.
766
- * @returns DOT source, ending with a line break.
767
- */
768
- const sequenceToDot = (view, participantLabel) => {
769
- const columns = new Map(view.lifelines.map((lifeline, index) => [lifeline, index]));
770
- const column = (lifeline) => {
771
- const index = columns.get(lifeline);
772
- if (index === void 0) throw new Error(`"${lifeline}" is not a lifeline of the view`);
773
- return index;
774
- };
775
- const head = (index) => dotString(`p${String(index)}`);
776
- const cell = (index, row) => dotString(`p${String(index)}.${String(row)}`);
777
- const placed = placeMessages(view.items, []);
778
- const rows = placed.map((_, row) => row + 1);
779
- const pointAttributes = attributes([["shape", "point"], ["width", "0"]]);
780
- return `${[
781
- `digraph ${dotString("sequence")} {`,
782
- `${indent}rankdir="TB";`,
783
- ...view.lifelines.map((lifeline, index) => `${indent}${head(index)}${attributes([["label", participantLabel(lifeline)], ["shape", "box"]])};`),
784
- `${indent}{ rank="same"; ${view.lifelines.map((_, index) => `${head(index)};`).join(" ")} }`,
785
- ...view.lifelines.slice(1).map((_, index) => `${indent}${head(index)} -> ${head(index + 1)}${attributes([["style", "invis"]])};`),
786
- ...rows.flatMap((row) => [...view.lifelines.map((_, index) => `${indent}${cell(index, row)}${pointAttributes};`), `${indent}{ rank="same"; ${view.lifelines.map((_, index) => `${cell(index, row)};`).join(" ")} }`]),
787
- ...rows.length === 0 ? [] : view.lifelines.map((_, index) => `${indent}${[head(index), ...rows.map((row) => cell(index, row))].join(" -> ")}${attributes([["style", "dashed"], ["arrowhead", "none"]])};`),
788
- ...placed.map(({ message, fragments }, index) => {
789
- const row = index + 1;
790
- const text = fragments.length === 0 ? message.label : `${fragments.join(" / ")}: ${message.label}`;
791
- return `${indent}${cell(column(message.from), row)} -> ${cell(column(message.to), row)}${attributes([["label", `${String(row)}. ${text}`], ["constraint", "false"]])};`;
792
- }),
793
- "}"
794
- ].join("\n")}\n`;
654
+ message,
655
+ ...saved ? ["the pages read before the failure are saved"] : [],
656
+ `resume with: sync ${resume.scopes.join(" ")}${resume.after === void 0 ? "" : ` --after ${resume.after}`}${filterOptions(resume.filter)}`,
657
+ ...remaining.length === 0 ? [] : [`then: sync ${remaining.join(" ")}${filterOptions(resume.filter)}`]
658
+ ].join("\n");
795
659
  };
660
+ /** `validate`: `valid`, or each issue as its code, its JSON pointer when it has one, and its message. */
661
+ const renderValidate = (result) => result.valid ? lines(["valid"]) : lines(result.issues.map((issue) => issue.pointer === void 0 ? `${issue.code}\t${issue.message}` : `${issue.code}\t${issue.pointer === "" ? "/" : issue.pointer}\t${issue.message}`));
796
662
  //#endregion
797
663
  //#region ../graph-cli/dist/show.js
798
664
  /** The views `show` draws, as named on the command line. */
@@ -802,7 +668,7 @@ const viewNames = [
802
668
  "state",
803
669
  "sequence"
804
670
  ];
805
- /** The formats `show` writes. */
671
+ /** The formats `show` writes, graph-export's, with JSON named first. */
806
672
  const formatNames = [
807
673
  "json",
808
674
  "mermaid",
@@ -849,22 +715,33 @@ const project = (model, request) => {
849
715
  projection: toSequence(model, scenarioOf(model, request.scenario))
850
716
  };
851
717
  };
718
+ /** A view as `show` names it, as the projection layer names its notation. */
719
+ const notationViewOf = (projected) => {
720
+ if (projected.view === "flowchart") return {
721
+ notation: "flowchart",
722
+ view: projected.projection
723
+ };
724
+ if (projected.view === "swimlane") return {
725
+ notation: "swimlane",
726
+ view: projected.projection
727
+ };
728
+ if (projected.view === "state") return {
729
+ notation: "stateMachine",
730
+ view: projected.projection
731
+ };
732
+ return {
733
+ notation: "sequence",
734
+ view: projected.projection
735
+ };
736
+ };
852
737
  /**
853
- * Writes a projected view as Mermaid or DOT source.
738
+ * Writes a projected view as Mermaid or DOT source, or as JSON, through graph-export's `viewText`, the writer the web app's downloads use too.
854
739
  * @param model - The model, for participant labels.
855
740
  * @param projected - The view.
856
- * @param format - `mermaid` or `dot`.
857
- * @returns The diagram source.
741
+ * @param format - `mermaid`, `dot` or `json`.
742
+ * @returns The text.
858
743
  */
859
- const emit = (model, projected, format) => {
860
- const labels = new Map(model.participants.map((participant) => [participant.id, participant.label]));
861
- const participantLabel = (id) => labels.get(id) ?? id;
862
- const mermaid = format === "mermaid";
863
- if (projected.view === "flowchart") return mermaid ? flowchartToMermaid(projected.projection) : flowchartToDot(projected.projection);
864
- if (projected.view === "swimlane") return mermaid ? swimlaneToMermaid(projected.projection) : swimlaneToDot(projected.projection);
865
- if (projected.view === "state") return mermaid ? stateMachineToMermaid(projected.projection) : stateMachineToDot(projected.projection);
866
- return mermaid ? sequenceToMermaid(projected.projection, participantLabel) : sequenceToDot(projected.projection, participantLabel);
867
- };
744
+ const emit = (model, projected, format) => viewText(model, notationViewOf(projected), format);
868
745
  /**
869
746
  * Draws the session's model as a view in a format.
870
747
  * @param session - The store and model.
@@ -873,7 +750,7 @@ const emit = (model, projected, format) => {
873
750
  const show = async (session, request) => {
874
751
  const { model } = await loadRun(session);
875
752
  const projected = project(model, request);
876
- const text = request.format === "json" ? `${JSON.stringify(projected.projection, null, 2)}\n` : emit(model, projected, request.format);
753
+ const text = emit(model, projected, request.format);
877
754
  return {
878
755
  ...projected,
879
756
  format: request.format,
@@ -881,108 +758,6 @@ const show = async (session, request) => {
881
758
  };
882
759
  };
883
760
  //#endregion
884
- //#region ../graph-cli/dist/sync.js
885
- const scopeSeparator = ":";
886
- /** Groups scopes by their source prefix, keeping the order in which each prefix first appears. */
887
- const bySource = (scopes) => {
888
- const groups = /* @__PURE__ */ new Map();
889
- for (const scope of scopes) {
890
- const separator = scope.indexOf(scopeSeparator);
891
- if (separator <= 0) throw new CliError(exitCodes.usage, `"${scope}" is not a scope: write <source>:<scope>, such as github:owner/repo`);
892
- const prefix = scope.slice(0, separator);
893
- groups.set(prefix, [...groups.get(prefix) ?? [], scope]);
894
- }
895
- return [...groups].map(([prefix, grouped]) => ({
896
- prefix,
897
- scopes: grouped
898
- }));
899
- };
900
- /** Bound activities are the only elements a sync assembles, since a page's activities are bound by schema. */
901
- const boundActivities = (pages) => {
902
- const { model, overlay, gaps } = assembleSync(pages);
903
- const activities = model.elements.flatMap((element) => element.kind === "activity" && element.source !== void 0 ? [{
904
- ...element,
905
- source: element.source
906
- }] : []);
907
- return {
908
- items: {
909
- participants: model.participants,
910
- activities,
911
- transitions: model.transitions,
912
- overlay
913
- },
914
- gaps
915
- };
916
- };
917
- /**
918
- * Reads every page of the scopes from their sources and merges them into the session's model in one write, creating the model when the store has none. Records already in the model are replaced by identifier and the rest are kept, so syncing again refreshes what changed.
919
- *
920
- * When a source fails part way, the pages read before the failure are still written, and the error says how to resume: the failing source's scopes with `--after` and the cursor of its last page read.
921
- * @param session - The store and model.
922
- * @param sources - The source for each identifier prefix.
923
- * @param request - The scopes, and a cursor to resume after.
924
- * @returns What was written, and the gaps the sources reported.
925
- * @throws CliError with `usage` for a scope with no source prefix, a prefix no source serves, or `after` with scopes of more than one source; and with the source's failure, the resume command added, when a source refuses the sync.
926
- */
927
- const sync = async (session, sources, request) => {
928
- const groups = bySource(request.scopes);
929
- if (request.after !== void 0 && groups.length > 1) throw new CliError(exitCodes.usage, "--after resumes one source's sync: give only that source's scopes");
930
- const clients = groups.map((group) => {
931
- const client = sources(group.prefix);
932
- if (client === void 0) throw new CliError(exitCodes.usage, `no source reads "${group.prefix}" scopes`);
933
- return {
934
- ...group,
935
- client
936
- };
937
- });
938
- const pages = [];
939
- const write = async () => {
940
- const { items, gaps } = boundActivities(pages);
941
- const { revision } = await session.store.v1.items.upsert({
942
- modelId: session.modelId,
943
- items
944
- }, callOptions(session.signal));
945
- return {
946
- modelId: session.modelId,
947
- revision,
948
- participants: items.participants.length,
949
- activities: items.activities.length,
950
- transitions: items.transitions.length,
951
- gaps
952
- };
953
- };
954
- /** Reads the sources in order, each only after the one before has finished, since a source's failure must stop the ones after it. */
955
- const readFrom = async (index) => {
956
- const group = clients.at(index);
957
- if (group === void 0) return;
958
- let cursor = index === 0 ? request.after : void 0;
959
- try {
960
- const stream = await group.client.v1.sync.stream(cursor === void 0 ? { scope: group.scopes } : {
961
- scope: group.scopes,
962
- after: cursor
963
- }, callOptions(session.signal));
964
- for await (const page of stream) {
965
- pages.push(page);
966
- cursor = page.next;
967
- }
968
- } catch (error) {
969
- const failure = failureOf(error);
970
- const saved = pages.length > 0;
971
- if (saved) await write();
972
- const later = clients.slice(index + 1).flatMap((next) => next.scopes);
973
- throw new CliError(failure.exitCode, [
974
- failure.message,
975
- ...saved ? ["the pages read before the failure are saved"] : [],
976
- `resume with: sync ${group.scopes.join(" ")}${cursor === void 0 ? "" : ` --after ${cursor}`}`,
977
- ...later.length === 0 ? [] : [`then: sync ${later.join(" ")}`]
978
- ].join("\n"), { cause: error });
979
- }
980
- await readFrom(index + 1);
981
- };
982
- await readFrom(0);
983
- return write();
984
- };
985
- //#endregion
986
761
  //#region ../graph-cli/dist/validate.js
987
762
  /** A cycle that no decision closes, as an issue: status is undefined on it. */
988
763
  const cycleIssues = (model) => illegalCycles(model).map((cycle) => ({
@@ -1064,6 +839,44 @@ const choice = (values, value, name) => {
1064
839
  if (!isOneOf(values, value)) throw new Error(`--${name} was not checked against its choices`);
1065
840
  return value;
1066
841
  };
842
+ /** The direction names `--direction` takes. */
843
+ const directions = Object.keys(directionNames);
844
+ const isDirection = (value) => isOneOf(directions, value);
845
+ /**
846
+ * A commander option parser that collects every occurrence of an option, in order, so the option can be repeated.
847
+ * @param value - This occurrence's value.
848
+ * @param previous - The values collected before it.
849
+ */
850
+ const collect = (value, previous) => [...previous ?? [], value];
851
+ /**
852
+ * A commander option parser that checks each value against its choices and collects every occurrence, in order. Commander's own `choices` replaces an option's parser, so the two cannot be combined.
853
+ * @param choices - The values allowed.
854
+ */
855
+ const collectChoice = (choices) => (value, previous) => {
856
+ if (!isOneOf(choices, value)) throw new InvalidArgumentError(`Allowed choices are ${choices.join(", ")}.`);
857
+ return [...previous ?? [], value];
858
+ };
859
+ /**
860
+ * Reads `sync`'s filter options into the source contract's filter, or nothing when none is given.
861
+ * @param options - The `sync` command's options.
862
+ * @returns The filter.
863
+ * @throws CliError with `usage` for `--direction` without `--order`.
864
+ */
865
+ const syncFilterOf = (options) => {
866
+ const { state, label, order, direction } = options;
867
+ const states = Array.isArray(state) ? state.filter((value) => isOneOf(filterStates, value)) : [];
868
+ const labels = Array.isArray(label) ? label.filter(isText) : [];
869
+ if (direction !== void 0 && order === void 0) throw new CliError(exitCodes.usage, "--direction orders by --order: give both");
870
+ const filter = {
871
+ ...states.length === 0 ? {} : { states },
872
+ ...labels.length === 0 ? {} : { labels },
873
+ ...order === void 0 ? {} : { order: {
874
+ field: choice(orderFields, order, "order"),
875
+ direction: directionNames[isDirection(direction) ? direction : "asc"]
876
+ } }
877
+ };
878
+ return Object.keys(filter).length === 0 ? void 0 : filter;
879
+ };
1067
880
  /** The word that forgets a named fact rather than giving it a value. */
1068
881
  const unknownValue = "unknown";
1069
882
  /** A number as JSON writes one, so a value that reads as a number is never also meant as text. */
@@ -1134,17 +947,24 @@ const runCli = async (args, ports, identity) => {
1134
947
  await opened.close();
1135
948
  }
1136
949
  };
1137
- program.command("sync").description("read the scopes from their sources and merge them into the model").argument("<scope...>", "source-qualified scopes, such as github:owner or github:owner/repo").option("--after <cursor>", "resume an interrupted sync after this cursor").action(async (scopes, options, command) => {
950
+ program.command("sync").description("read the scopes from their sources and merge them into the model").argument("<scope...>", "source-qualified scopes, such as github:owner or github:owner/repo").option("--after <cursor>", "resume an interrupted sync after this cursor").addOption(new Option("--state <state>", `list only work in this state, one of ${filterStates.join(", ")}; repeat for more than one`).argParser(collectChoice(filterStates))).addOption(new Option("--label <label>", "list only work carrying this label; repeat to keep work carrying any of them").argParser(collect)).addOption(new Option("--order <field>", "list the work in this order").choices(orderFields)).addOption(new Option("--direction <direction>", "the direction of --order; ascending when not given").choices(directions)).addHelpText("after", "\nA filter narrows what each scope lists, not the work it depends on. A source that cannot filter a scope, such as a GitHub Projects v2 board, refuses the sync. A cursor from --after is valid only with the filter it was issued under.").action(async (scopes, options, command) => {
1138
951
  const { after } = options;
952
+ const filter = syncFilterOf(options);
1139
953
  await withSession(async (session) => {
1140
- const result = await sync(session, ports.source, typeof after === "string" ? {
1141
- scopes,
1142
- after
1143
- } : { scopes });
1144
- return {
1145
- json: result,
1146
- text: renderSync(result)
1147
- };
954
+ try {
955
+ const result = await sync(session, ports.source, {
956
+ scopes,
957
+ ...typeof after === "string" ? { after } : {},
958
+ ...filter === void 0 ? {} : { filter }
959
+ });
960
+ return {
961
+ json: result,
962
+ text: renderSync(result)
963
+ };
964
+ } catch (error) {
965
+ if (error instanceof SyncInterruptedError) throw new CliError(error.exitCode, renderSyncInterruption(error.message, error), { cause: error });
966
+ throw error;
967
+ }
1148
968
  })(command);
1149
969
  });
1150
970
  program.command("ready").description("list the work that can start now").action(async (_options, command) => {
@@ -1174,6 +994,24 @@ const runCli = async (args, ports, identity) => {
1174
994
  };
1175
995
  })(command);
1176
996
  });
997
+ program.command("children").description("list an element's sub-tasks: the work a source reports as its children").argument("<id>", "the element").action(async (id, _options, command) => {
998
+ await withSession(async (session) => {
999
+ const result = await children(session, id);
1000
+ return {
1001
+ json: result,
1002
+ text: renderChildren(result)
1003
+ };
1004
+ })(command);
1005
+ });
1006
+ program.command("ancestors").description("list what an element is a sub-task of, nearest first, with how many levels up each is").argument("<id>", "the element").action(async (id, _options, command) => {
1007
+ await withSession(async (session) => {
1008
+ const result = await ancestors(session, id);
1009
+ return {
1010
+ json: result,
1011
+ text: renderAncestors(result)
1012
+ };
1013
+ })(command);
1014
+ });
1177
1015
  program.command("path").description("list the critical path: the longest chain of work still ahead").action(async (_options, command) => {
1178
1016
  await withSession(async (session) => {
1179
1017
  const result = await path(session);
@@ -1388,61 +1226,6 @@ const runCli = async (args, ports, identity) => {
1388
1226
  }
1389
1227
  return exitCode;
1390
1228
  };
1391
- /** The permission bits of a file mode, without its type. */
1392
- const permissionBits = 511;
1393
- /** The bits that give the file's group or anyone else any access. */
1394
- const groupAndOtherBits = 63;
1395
- /** The SQLite files a store at `path` uses: the database and its write-ahead log and shared-memory index. */
1396
- const storeFiles = (path) => [
1397
- path,
1398
- `${path}-wal`,
1399
- `${path}-shm`
1400
- ];
1401
- /** Whether an error is the file system's report that a path does not exist, or already does. */
1402
- const hasCode = (error, code) => error instanceof Error && "code" in error && error.code === code;
1403
- /**
1404
- * Narrows a path's mode to `mode` when its group or others have any access.
1405
- * @param path - The path.
1406
- * @param mode - The private mode it should have.
1407
- * @returns Whether the mode was changed; `false` too when the path does not exist.
1408
- */
1409
- const tighten = (path, mode) => {
1410
- let current;
1411
- try {
1412
- current = statSync(path).mode & permissionBits;
1413
- } catch (error) {
1414
- if (hasCode(error, "ENOENT")) return false;
1415
- throw error;
1416
- }
1417
- if ((current & groupAndOtherBits) === 0) return false;
1418
- chmodSync(path, mode);
1419
- return true;
1420
- };
1421
- /**
1422
- * Makes a data file ready for SQLite to open. The directory is created when missing, private when it is the command line's own. The database file is created private when missing, before SQLite opens it, so the file and the sidecars SQLite derives from it are never readable by others, whatever the umask; a file that already exists is left alone unless the directory is the command line's own. In its own directory, an existing directory or store file that group or others can read is narrowed to the private mode.
1423
- * @param path - The database file.
1424
- * @param options - How much of the path the command line owns, and the platform.
1425
- * @returns One warning line naming what was narrowed, or nothing.
1426
- */
1427
- const prepareDataFile = (path, options) => {
1428
- const directory = dirname(path);
1429
- if (options.platform === "win32") {
1430
- mkdirSync(directory, { recursive: true });
1431
- return [];
1432
- }
1433
- mkdirSync(directory, options.ownsDirectory ? {
1434
- recursive: true,
1435
- mode: 448
1436
- } : { recursive: true });
1437
- try {
1438
- closeSync(openSync(path, "wx", 384));
1439
- } catch (error) {
1440
- if (!hasCode(error, "EEXIST")) throw error;
1441
- }
1442
- if (!options.ownsDirectory) return [];
1443
- const narrowed = [...tighten(directory, 448) ? [directory] : [], ...storeFiles(path).filter((file) => tighten(file, 384))];
1444
- return narrowed.length === 0 ? [] : [`made ${narrowed.join(", ")} readable by its owner only, since other users could read the synced work in it`];
1445
- };
1446
1229
  //#endregion
1447
1230
  //#region ../graph-cli/dist/node.js
1448
1231
  /** The variable that names the data file outright. */
@@ -1485,7 +1268,7 @@ const packageVersion = (manifest) => {
1485
1268
  return parsed.version;
1486
1269
  };
1487
1270
  /**
1488
- * The ports for a Node process: the SQLite store over `node:sqlite` (creating the data file's directory when needed), the GitHub issues source for `github:` scopes, the file system and the process's streams.
1271
+ * The ports for a Node process: the SQLite store over `node:sqlite` (creating the data file's directory when needed, private when the command line or the host owns it), the GitHub issues source for `github:` scopes, the file system and the process's streams.
1489
1272
  * @param options - Replacements for any of the defaults.
1490
1273
  * @returns The ports.
1491
1274
  */
@@ -1493,7 +1276,7 @@ const nodePorts = (options = {}) => {
1493
1276
  const env = options.env ?? process.env;
1494
1277
  const token = options.token ?? githubTokenFromEnvironment({ env });
1495
1278
  const home = options.home ?? homedir();
1496
- const ownDataPath = xdgDataPath(env, home);
1279
+ const ownDataPath = options.dataPath ?? xdgDataPath(env, home);
1497
1280
  return {
1498
1281
  stdout: options.stdout ?? ((text) => {
1499
1282
  process.stdout.write(text);
@@ -1501,7 +1284,7 @@ const nodePorts = (options = {}) => {
1501
1284
  stderr: options.stderr ?? ((text) => {
1502
1285
  process.stderr.write(text);
1503
1286
  }),
1504
- defaultDataPath: defaultDataPath(env, home),
1287
+ defaultDataPath: options.dataPath ?? defaultDataPath(env, home),
1505
1288
  openStore: (path) => {
1506
1289
  const warnings = path === inMemory ? [] : prepareDataFile(path, {
1507
1290
  ownsDirectory: path === ownDataPath,
@@ -1540,4 +1323,4 @@ const runProcess = async (identity) => {
1540
1323
  return runCli(process.argv.slice(2), nodePorts({ signal: controller.signal }), identity);
1541
1324
  };
1542
1325
  //#endregion
1543
- export { add as A, failureOf as B, blocked as C, path as D, impactOf as E, setState as F, findRun as I, loadRun as L, factsOf as M, setArtefact as N, ready as O, setFact as P, runFacts as R, swimlaneToMermaid as S, handoffsOf as T, stateMachineToDot as _, defaultModelId as a, sequenceToMermaid as b, validateStored as c, formatNames as d, project as f, sequenceToDot as g, flowchartToDot as h, runProcess as i, artefactStates as j, why as k, sync as l, viewNames as m, nodePorts as n, runCli as o, show as p, packageVersion as r, validateDocument as s, defaultDataPath as t, emit as u, swimlaneToDot as v, cycles as w, stateMachineToMermaid as x, flowchartToMermaid as y, CliError as z };
1326
+ export { setState as A, ready as C, factsOf as D, artefactStates as E, failureOf as F, loadRun as M, runFacts as N, setArtefact as O, CliError as P, path as S, add as T, blocked as _, defaultModelId as a, handoffsOf as b, validateStored as c, project as d, show as f, ancestors as g, sync as h, runProcess as i, findRun as j, setFact as k, emit as l, SyncInterruptedError as m, nodePorts as n, runCli as o, viewNames as p, packageVersion as r, validateDocument as s, defaultDataPath as t, formatNames as u, children as v, why as w, impactOf as x, cycles as y };