@turri-dev/cli 0.0.0-stage → 0.1.2-next.48dacfc
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/.pi/themes/turri-dark.json +80 -0
- package/.pi/themes/turri-light.json +82 -0
- package/LICENSE +21 -0
- package/README.md +272 -2
- package/assets/jellyfish/dark-00.png +0 -0
- package/assets/jellyfish/dark-01.png +0 -0
- package/assets/jellyfish/dark-02.png +0 -0
- package/assets/jellyfish/dark-03.png +0 -0
- package/assets/jellyfish/dark-04.png +0 -0
- package/assets/jellyfish/dark-05.png +0 -0
- package/assets/jellyfish/dark-06.png +0 -0
- package/assets/jellyfish/dark-07.png +0 -0
- package/assets/jellyfish/dark-08.png +0 -0
- package/assets/jellyfish/dark-09.png +0 -0
- package/assets/jellyfish/dark-10.png +0 -0
- package/assets/jellyfish/dark-11.png +0 -0
- package/assets/jellyfish/dark-12.png +0 -0
- package/assets/jellyfish/dark-13.png +0 -0
- package/assets/jellyfish/dark-14.png +0 -0
- package/assets/jellyfish/fallback.txt +252 -0
- package/assets/jellyfish/light-00.png +0 -0
- package/assets/jellyfish/light-01.png +0 -0
- package/assets/jellyfish/light-02.png +0 -0
- package/assets/jellyfish/light-03.png +0 -0
- package/assets/jellyfish/light-04.png +0 -0
- package/assets/jellyfish/light-05.png +0 -0
- package/assets/jellyfish/light-06.png +0 -0
- package/assets/jellyfish/light-07.png +0 -0
- package/assets/jellyfish/light-08.png +0 -0
- package/assets/jellyfish/light-09.png +0 -0
- package/assets/jellyfish/light-10.png +0 -0
- package/assets/jellyfish/light-11.png +0 -0
- package/assets/jellyfish/light-12.png +0 -0
- package/assets/jellyfish/light-13.png +0 -0
- package/assets/jellyfish/light-14.png +0 -0
- package/bin/turri.mjs +29 -0
- package/dist/ask.js +149 -0
- package/dist/brand.js +50 -0
- package/dist/btw.js +174 -0
- package/dist/chat-models.js +37 -0
- package/dist/chat-skills.js +18 -0
- package/dist/cli.js +178 -0
- package/dist/config-command.js +197 -0
- package/dist/config-edit.js +367 -0
- package/dist/config.js +489 -0
- package/dist/conform-cli.js +29 -0
- package/dist/cultivate-cli.js +110 -0
- package/dist/drift-cli.js +25 -0
- package/dist/environment.js +70 -0
- package/dist/extension.js +2372 -0
- package/dist/genome.js +301 -0
- package/dist/graph.js +246 -0
- package/dist/identity.js +35 -0
- package/dist/init-config.js +511 -0
- package/dist/judge.js +62 -0
- package/dist/lineage-cli.js +13 -0
- package/dist/loop-cli.js +132 -0
- package/dist/loop-tools.js +225 -0
- package/dist/mature-tools.js +193 -0
- package/dist/model-check.js +117 -0
- package/dist/module-report.js +9 -0
- package/dist/pi-launch.js +31 -0
- package/dist/pi.js +813 -0
- package/dist/plain/check.js +76 -0
- package/dist/plain/concepts.js +213 -0
- package/dist/plain/errors.js +43 -0
- package/dist/plain/files.js +57 -0
- package/dist/plain/highlight.js +195 -0
- package/dist/plain/index.js +15 -0
- package/dist/plain/liquid.js +129 -0
- package/dist/plain/markdown.js +208 -0
- package/dist/plain/parser.js +752 -0
- package/dist/plain/plain.tmLanguage.json +124 -0
- package/dist/plain/spec.js +291 -0
- package/dist/plain/stdlib.js +44 -0
- package/dist/question-dialog.js +125 -0
- package/dist/questioning.js +109 -0
- package/dist/quiet.js +125 -0
- package/dist/regenerate/activity.js +187 -0
- package/dist/regenerate/agent-env.js +190 -0
- package/dist/regenerate/agent-shell.js +114 -0
- package/dist/regenerate/arbiter.js +133 -0
- package/dist/regenerate/audit.js +203 -0
- package/dist/regenerate/cases.js +83 -0
- package/dist/regenerate/change-view.js +102 -0
- package/dist/regenerate/choose-conformance.js +117 -0
- package/dist/regenerate/chunk-plan.js +625 -0
- package/dist/regenerate/codebase.js +75 -0
- package/dist/regenerate/command-check.js +312 -0
- package/dist/regenerate/compaction.js +20 -0
- package/dist/regenerate/confine.js +310 -0
- package/dist/regenerate/conform.js +133 -0
- package/dist/regenerate/conformance-agents.js +955 -0
- package/dist/regenerate/conformance.js +936 -0
- package/dist/regenerate/conversation-writes.js +100 -0
- package/dist/regenerate/credentials.js +88 -0
- package/dist/regenerate/debug-log.js +228 -0
- package/dist/regenerate/debugger.js +90 -0
- package/dist/regenerate/delivery.js +19 -0
- package/dist/regenerate/diagnose-job.js +40 -0
- package/dist/regenerate/diagnose.js +81 -0
- package/dist/regenerate/diff-view.js +240 -0
- package/dist/regenerate/drift.js +194 -0
- package/dist/regenerate/engine.js +118 -0
- package/dist/regenerate/environment-error.js +220 -0
- package/dist/regenerate/job-log.js +102 -0
- package/dist/regenerate/job-panel.js +336 -0
- package/dist/regenerate/jobs-tree.js +300 -0
- package/dist/regenerate/jobs.js +427 -0
- package/dist/regenerate/lanes.js +32 -0
- package/dist/regenerate/lineage.js +384 -0
- package/dist/regenerate/loop.js +1435 -0
- package/dist/regenerate/masking.js +95 -0
- package/dist/regenerate/notes.js +397 -0
- package/dist/regenerate/os-sandbox.js +243 -0
- package/dist/regenerate/paths.js +165 -0
- package/dist/regenerate/plain-palette.js +83 -0
- package/dist/regenerate/prompt.js +704 -0
- package/dist/regenerate/records.js +294 -0
- package/dist/regenerate/regenerate.js +598 -0
- package/dist/regenerate/regression.js +181 -0
- package/dist/regenerate/release.js +262 -0
- package/dist/regenerate/requires.js +241 -0
- package/dist/regenerate/resources.js +164 -0
- package/dist/regenerate/self-check.js +111 -0
- package/dist/regenerate/services.js +155 -0
- package/dist/regenerate/shell.js +320 -0
- package/dist/regenerate/side-panel.js +278 -0
- package/dist/regenerate/spec-map-view.js +574 -0
- package/dist/regenerate/spec-map.js +561 -0
- package/dist/regenerate/spec-steps.js +527 -0
- package/dist/regenerate/state.js +10 -0
- package/dist/regenerate/steer.js +91 -0
- package/dist/regenerate/steering.js +119 -0
- package/dist/regenerate/stop-report.js +161 -0
- package/dist/regenerate/submit-work.js +71 -0
- package/dist/regenerate/test-kinds.js +124 -0
- package/dist/regenerate/test-scripts.js +695 -0
- package/dist/regenerate/testing.js +406 -0
- package/dist/regenerate/transcript-view.js +1 -0
- package/dist/regenerate/unit-gate.js +20 -0
- package/dist/regenerate/unit-review.js +347 -0
- package/dist/regenerate/web.js +256 -0
- package/dist/regenerate/workspace.js +99 -0
- package/dist/regenerate-cli.js +68 -0
- package/dist/release-agent.js +114 -0
- package/dist/release-cli.js +131 -0
- package/dist/release-tools.js +148 -0
- package/dist/review/live.js +165 -0
- package/dist/rules.js +47 -0
- package/dist/self-invocation.js +110 -0
- package/dist/setup.js +245 -0
- package/dist/skill-progress.js +78 -0
- package/dist/skill.js +149 -0
- package/dist/skills.js +74 -0
- package/dist/spec-diff-cli.js +37 -0
- package/dist/spec-tools.js +155 -0
- package/dist/stack.js +158 -0
- package/dist/stacks.js +97 -0
- package/dist/stdlib.js +100 -0
- package/dist/terminal-images.js +46 -0
- package/dist/tools.js +242 -0
- package/dist/validate-cli.js +52 -0
- package/docs/README.md +58 -0
- package/docs/authoring-plain.md +245 -0
- package/docs/config.md +353 -0
- package/docs/deploy.md +109 -0
- package/docs/development.md +88 -0
- package/docs/headless.md +148 -0
- package/docs/images/how-turri-works.svg +50 -0
- package/docs/images/turri-banner.png +0 -0
- package/docs/installation.md +119 -0
- package/docs/parser.md +108 -0
- package/docs/regenerating.md +1116 -0
- package/docs/sandbox.md +233 -0
- package/docs/skills-and-gates.md +254 -0
- package/docs/theming.md +76 -0
- package/docs/using.md +454 -0
- package/genome/rules/bullet-continuation.md +69 -0
- package/genome/rules/concise-specs-examples.md +431 -0
- package/genome/rules/concise-specs-integration-examples.md +270 -0
- package/genome/rules/concise-specs.md +80 -0
- package/genome/rules/definitions.md +107 -0
- package/genome/rules/examples/integration-embedded-testing/run_conformance_tests_java.sh +163 -0
- package/genome/rules/examples/integration-embedded-testing/run_unittests_java.sh +51 -0
- package/genome/rules/exported-concepts.md +39 -0
- package/genome/rules/func-specs.md +129 -0
- package/genome/rules/impl-reqs.md +68 -0
- package/genome/rules/import-modules.md +48 -0
- package/genome/rules/integration-embedded-testing.md +276 -0
- package/genome/rules/integration-embedded.md +171 -0
- package/genome/rules/integration-standalone.md +135 -0
- package/genome/rules/integrations.md +137 -0
- package/genome/rules/linked-resources.md +69 -0
- package/genome/rules/module-structure.md +105 -0
- package/genome/rules/project-model.md +88 -0
- package/genome/rules/required-concepts.md +44 -0
- package/genome/rules/requires-modules.md +80 -0
- package/genome/rules/stdlib.md +73 -0
- package/genome/rules/test-reqs.md +57 -0
- package/genome/skills/add-acceptance-test/SKILL.md +100 -0
- package/genome/skills/add-concept/SKILL.md +71 -0
- package/genome/skills/add-functional-spec/SKILL.md +84 -0
- package/genome/skills/add-functional-specs/SKILL.md +118 -0
- package/genome/skills/add-implementation-requirement/SKILL.md +86 -0
- package/genome/skills/add-resource/SKILL.md +105 -0
- package/genome/skills/add-template/SKILL.md +64 -0
- package/genome/skills/add-test-requirement/SKILL.md +73 -0
- package/genome/skills/analyze-2-func-specs/SKILL.md +101 -0
- package/genome/skills/analyze-func-specs/SKILL.md +123 -0
- package/genome/skills/analyze-if-func-spec-too-complex/SKILL.md +211 -0
- package/genome/skills/break-down-func-spec/SKILL.md +205 -0
- package/genome/skills/brood/SKILL.md +229 -0
- package/genome/skills/brood/references/intent.md +35 -0
- package/genome/skills/brood/references/product.md +101 -0
- package/genome/skills/brood/references/stack.md +61 -0
- package/genome/skills/brood/references/testing.md +56 -0
- package/genome/skills/bud/SKILL.md +148 -0
- package/genome/skills/bud/references/checklist.md +44 -0
- package/genome/skills/bud/references/intent.md +30 -0
- package/genome/skills/check-plain-env/SKILL.md +285 -0
- package/genome/skills/consolidate-concepts/SKILL.md +192 -0
- package/genome/skills/create-import-module/SKILL.md +104 -0
- package/genome/skills/create-requires-module/SKILL.md +109 -0
- package/genome/skills/debug-specs/SKILL.md +188 -0
- package/genome/skills/implement-conformance-testing-script/SKILL.md +238 -0
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_cypress.ps1 +337 -0
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_golang.ps1 +111 -0
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_java.sh +136 -0
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.ps1 +103 -0
- package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.sh +150 -0
- package/genome/skills/implement-unit-testing-script/SKILL.md +160 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_flutter.ps1 +87 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_golang.ps1 +73 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_java.sh +72 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_python.ps1 +81 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_python.sh +98 -0
- package/genome/skills/implement-unit-testing-script/assets/run_unittests_react.ps1 +87 -0
- package/genome/skills/init-plain-project/SKILL.md +117 -0
- package/genome/skills/init-plain-project/references/checklist.md +30 -0
- package/genome/skills/mature/SKILL.md +126 -0
- package/genome/skills/refactor-module/SKILL.md +196 -0
- package/genome/skills/resolve-section-ownership/SKILL.md +64 -0
- package/genome/skills/resolve-spec-conflict/SKILL.md +96 -0
- package/genome/skills/spawn/SKILL.md +163 -0
- package/package.json +68 -4
- package/stdlib/backend/common.plain +25 -0
- package/stdlib/backend/conformance/common.plain +12 -0
- package/stdlib/backend/conformance/go-gin.plain +9 -0
- package/stdlib/backend/conformance/go-nethttp.plain +9 -0
- package/stdlib/backend/conformance/java-springboot.plain +9 -0
- package/stdlib/backend/conformance/python-django.plain +9 -0
- package/stdlib/backend/conformance/python-fastapi.plain +9 -0
- package/stdlib/backend/conformance/python-flask.plain +9 -0
- package/stdlib/backend/conformance/rust-axum.plain +9 -0
- package/stdlib/backend/conformance/typescript-express.plain +9 -0
- package/stdlib/backend/conformance/typescript-fastify.plain +9 -0
- package/stdlib/backend/conformance/typescript-nestjs.plain +9 -0
- package/stdlib/backend/go-gin.plain +18 -0
- package/stdlib/backend/go-nethttp.plain +18 -0
- package/stdlib/backend/java-springboot.plain +19 -0
- package/stdlib/backend/python-django.plain +19 -0
- package/stdlib/backend/python-fastapi.plain +23 -0
- package/stdlib/backend/python-flask.plain +23 -0
- package/stdlib/backend/rust-axum.plain +17 -0
- package/stdlib/backend/typescript-express.plain +19 -0
- package/stdlib/backend/typescript-fastify.plain +19 -0
- package/stdlib/backend/typescript-nestjs.plain +19 -0
- package/stdlib/cli/common.plain +17 -0
- package/stdlib/cli/conformance/common.plain +11 -0
- package/stdlib/cli/conformance/go-cobra.plain +9 -0
- package/stdlib/cli/conformance/go-flag.plain +9 -0
- package/stdlib/cli/conformance/java-picocli.plain +9 -0
- package/stdlib/cli/conformance/python-argparse.plain +9 -0
- package/stdlib/cli/conformance/python-click.plain +9 -0
- package/stdlib/cli/conformance/python-typer.plain +9 -0
- package/stdlib/cli/conformance/rust-clap.plain +9 -0
- package/stdlib/cli/conformance/typescript-commander.plain +9 -0
- package/stdlib/cli/go-cobra.plain +14 -0
- package/stdlib/cli/go-flag.plain +14 -0
- package/stdlib/cli/java-picocli.plain +15 -0
- package/stdlib/cli/python-argparse.plain +18 -0
- package/stdlib/cli/python-click.plain +16 -0
- package/stdlib/cli/python-typer.plain +16 -0
- package/stdlib/cli/rust-clap.plain +14 -0
- package/stdlib/cli/typescript-commander.plain +17 -0
- package/stdlib/frontend/angular.plain +17 -0
- package/stdlib/frontend/common.plain +17 -0
- package/stdlib/frontend/conformance/angular.plain +5 -0
- package/stdlib/frontend/conformance/common.plain +11 -0
- package/stdlib/frontend/conformance/react-vite.plain +5 -0
- package/stdlib/frontend/conformance/sveltekit-spa.plain +5 -0
- package/stdlib/frontend/conformance/vue-vite.plain +5 -0
- package/stdlib/frontend/react-vite.plain +16 -0
- package/stdlib/frontend/sveltekit-spa.plain +16 -0
- package/stdlib/frontend/vue-vite.plain +16 -0
- package/stdlib/lang/go.plain +13 -0
- package/stdlib/lang/java.plain +12 -0
- package/stdlib/lang/python.plain +18 -0
- package/stdlib/lang/rust.plain +12 -0
- package/stdlib/lang/typescript.plain +13 -0
- package/stdlib/library/common.plain +15 -0
- package/stdlib/library/conformance/common.plain +9 -0
- package/stdlib/library/conformance/go.plain +9 -0
- package/stdlib/library/conformance/java-gradle.plain +9 -0
- package/stdlib/library/conformance/java-maven.plain +9 -0
- package/stdlib/library/conformance/python.plain +9 -0
- package/stdlib/library/conformance/rust.plain +9 -0
- package/stdlib/library/conformance/typescript.plain +9 -0
- package/stdlib/library/go.plain +12 -0
- package/stdlib/library/java-gradle.plain +13 -0
- package/stdlib/library/java-maven.plain +13 -0
- package/stdlib/library/python.plain +13 -0
- package/stdlib/library/rust.plain +12 -0
- package/stdlib/library/typescript.plain +15 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
|
|
3
|
+
"name": "***plain",
|
|
4
|
+
"patterns": [
|
|
5
|
+
{
|
|
6
|
+
"include": "#keywords"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"include": "#strings"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"include": "#variables"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"include": "#classes"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"include": "#comments"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"include": "#functions"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"include": "#modules"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"include": "#punctuation"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"include": "#strings"
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"repository": {
|
|
34
|
+
"keywords": {
|
|
35
|
+
"patterns": [
|
|
36
|
+
{
|
|
37
|
+
"name": "entity.name.function.plain",
|
|
38
|
+
"match": "\\*\\*\\*(definitions|implementation reqs|test reqs|functional specs|acceptance tests)\\*\\*\\*"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "meta.frontmatter.plain",
|
|
42
|
+
"begin": "^---$",
|
|
43
|
+
"end": "^---$",
|
|
44
|
+
"patterns": [
|
|
45
|
+
{
|
|
46
|
+
"name": "keyword.control.import.plain",
|
|
47
|
+
"match": "^\\s*(description):"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "keyword.control.import.plain",
|
|
51
|
+
"match": "^\\s*(required_concepts):"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "keyword.control.import.plain",
|
|
55
|
+
"match": "^\\s*(exported_concepts):"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "keyword.control.import.plain",
|
|
59
|
+
"match": "^\\s*(import):"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "keyword.control.requires.plain",
|
|
63
|
+
"match": "^\\s*(requires):"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"match": "^\\s*-\\s+([a-zA-Z_][a-zA-Z0-9_]*)",
|
|
67
|
+
"captures": {
|
|
68
|
+
"1": {
|
|
69
|
+
"name": "entity.name.type.class.plain"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "variable.parameter.plain",
|
|
75
|
+
"match": ":[^\\s]+?:"
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"name": "variable.parameter.plain",
|
|
81
|
+
"match": ":[^\\s]+?:"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"name": "comment.line.plain",
|
|
85
|
+
"match": "^>.*$"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"name": "entity.name.function.plain",
|
|
89
|
+
"match": "\\\"\\{\\{.*?\\}\\}\\\""
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"name": "entity.name.module.plain",
|
|
93
|
+
"begin": "\\{%\\s*include",
|
|
94
|
+
"end": "%\\}"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"name": "entity.punctuation.definition.bullet.plain",
|
|
98
|
+
"match": "^\\s*-"
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
"strings": {
|
|
103
|
+
"patterns": [
|
|
104
|
+
{
|
|
105
|
+
"name": "string.quoted.other.plain",
|
|
106
|
+
"match": "\\[[^)]+\\]\\([^\\]]+\\)"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"name": "string.quoted.double.plain",
|
|
110
|
+
"match": "\"[^\"]*\""
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"name": "string.quoted.other.backtick.plain",
|
|
114
|
+
"match": "`[^`]*`"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"name": "string.quoted.other.angle.plain",
|
|
118
|
+
"match": "<[^<>]+>"
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
"scopeName": "source.plain"
|
|
124
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { InvalidFridArgument } from "./errors.js";
|
|
3
|
+
/** Port of plain_spec.py. */
|
|
4
|
+
export const DEFINITIONS = "definitions";
|
|
5
|
+
export const NON_FUNCTIONAL_REQUIREMENTS = "implementation reqs";
|
|
6
|
+
export const TEST_REQUIREMENTS = "test reqs";
|
|
7
|
+
export const FUNCTIONAL_REQUIREMENTS = "functional specs";
|
|
8
|
+
export const ACCEPTANCE_TESTS = "acceptance_tests";
|
|
9
|
+
export const ACCEPTANCE_TEST_HEADING = "acceptance tests";
|
|
10
|
+
/**
|
|
11
|
+
* Where a module's tree carries the functional specs it inherits through `requires`: every
|
|
12
|
+
* functional spec of every module it requires, transitively, bottom of the chain first.
|
|
13
|
+
* Kept apart from its own, which are numbered 1, 2, 3 on their own.
|
|
14
|
+
*/
|
|
15
|
+
export const REQUIRED_FUNCTIONAL_SPECS = "required functional specs";
|
|
16
|
+
export const ALLOWED_SPECIFICATION_HEADINGS = [
|
|
17
|
+
DEFINITIONS,
|
|
18
|
+
NON_FUNCTIONAL_REQUIREMENTS,
|
|
19
|
+
TEST_REQUIREMENTS,
|
|
20
|
+
FUNCTIONAL_REQUIREMENTS,
|
|
21
|
+
ACCEPTANCE_TEST_HEADING,
|
|
22
|
+
];
|
|
23
|
+
export const ALLOWED_IMPORT_SPECIFICATION_HEADINGS = [
|
|
24
|
+
DEFINITIONS,
|
|
25
|
+
NON_FUNCTIONAL_REQUIREMENTS,
|
|
26
|
+
TEST_REQUIREMENTS,
|
|
27
|
+
];
|
|
28
|
+
/**
|
|
29
|
+
* The functional specs a module inherits through `requires`, rendered: each with its
|
|
30
|
+
* module, its id there, and its acceptance tests.
|
|
31
|
+
*/
|
|
32
|
+
export function getInheritedFunctionalSpecs(tree, replaceCodeVariables = true) {
|
|
33
|
+
const items = tree[REQUIRED_FUNCTIONAL_SPECS] ?? [];
|
|
34
|
+
return items.map((item) => {
|
|
35
|
+
const codeVariables = {};
|
|
36
|
+
const tests = item[ACCEPTANCE_TESTS];
|
|
37
|
+
return {
|
|
38
|
+
module: item.module,
|
|
39
|
+
frid: item.frid,
|
|
40
|
+
spec: getSpecificationItemMarkdown(item, codeVariables, replaceCodeVariables),
|
|
41
|
+
acceptanceTests: tests?.map((test) => getSpecificationItemMarkdown(test, codeVariables, replaceCodeVariables)),
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
export function getSpecificationItems(tree, heading) {
|
|
46
|
+
const value = tree[heading];
|
|
47
|
+
return Array.isArray(value) ? value : undefined;
|
|
48
|
+
}
|
|
49
|
+
export function hasAcceptanceTests(tree) {
|
|
50
|
+
for (const requirement of getSpecificationItems(tree, FUNCTIONAL_REQUIREMENTS) ?? []) {
|
|
51
|
+
const tests = requirement[ACCEPTANCE_TESTS];
|
|
52
|
+
if (tests !== undefined && tests.length > 0)
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
export function collectSpecificationLinkedResources(specification, specificationHeading, linkedResourcesList) {
|
|
58
|
+
for (const resource of specification.linked_resources ?? []) {
|
|
59
|
+
let resourceFound = false;
|
|
60
|
+
for (const resourceMap of linkedResourcesList) {
|
|
61
|
+
if (resource.text === resourceMap.text) {
|
|
62
|
+
if (resource.target !== resourceMap.target) {
|
|
63
|
+
throw new Error(`The file ${resource.target} is linked to multiple linked resources with the same text: ${resource.text}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else if (resource.target === resourceMap.target) {
|
|
67
|
+
if (resource.text !== resourceMap.text) {
|
|
68
|
+
throw new Error(`The linked resource ${resource.text} is linked to multiple files: ${resourceMap.target}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (resourceFound) {
|
|
75
|
+
throw new Error(`Duplicate linked resource found: ${resource.text} (${resource.target})`);
|
|
76
|
+
}
|
|
77
|
+
resourceFound = true;
|
|
78
|
+
resourceMap.sections.push(specificationHeading);
|
|
79
|
+
}
|
|
80
|
+
if (!resourceFound) {
|
|
81
|
+
linkedResourcesList.push({
|
|
82
|
+
text: resource.text,
|
|
83
|
+
target: resource.target,
|
|
84
|
+
sections: [specificationHeading],
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export function collectLinkedResourcesInSection(section, linkedResourcesList, specificationsList, includeAcceptanceTests, frid) {
|
|
90
|
+
let toCollect = [DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS];
|
|
91
|
+
if (specificationsList && specificationsList.length > 0) {
|
|
92
|
+
toCollect = toCollect.filter((heading) => specificationsList.includes(heading));
|
|
93
|
+
}
|
|
94
|
+
for (const heading of toCollect) {
|
|
95
|
+
for (const requirement of getSpecificationItems(section, heading) ?? []) {
|
|
96
|
+
collectSpecificationLinkedResources(requirement, heading, linkedResourcesList);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const functionalRequirements = getSpecificationItems(section, FUNCTIONAL_REQUIREMENTS);
|
|
100
|
+
if (functionalRequirements !== undefined &&
|
|
101
|
+
(!specificationsList ||
|
|
102
|
+
specificationsList.length === 0 ||
|
|
103
|
+
specificationsList.includes(FUNCTIONAL_REQUIREMENTS))) {
|
|
104
|
+
let count = 0;
|
|
105
|
+
for (const requirement of functionalRequirements) {
|
|
106
|
+
collectSpecificationLinkedResources(requirement, FUNCTIONAL_REQUIREMENTS, linkedResourcesList);
|
|
107
|
+
count += 1;
|
|
108
|
+
const currentFrid = String(count);
|
|
109
|
+
const tests = requirement[ACCEPTANCE_TESTS];
|
|
110
|
+
if (tests !== undefined && includeAcceptanceTests && (frid === null || frid === currentFrid)) {
|
|
111
|
+
for (const test of tests) {
|
|
112
|
+
collectSpecificationLinkedResources(test, FUNCTIONAL_REQUIREMENTS, linkedResourcesList);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// FRIDs increment, so once the requested one is reached nothing after it counts.
|
|
116
|
+
if (frid !== null && currentFrid === frid)
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
export function collectLinkedResources(tree, linkedResourcesList, specificationsList, includeAcceptanceTests, frid = null) {
|
|
123
|
+
if (tree === null || typeof tree !== "object" || Array.isArray(tree)) {
|
|
124
|
+
throw new TypeError("[plain_source_tree must be a dictionary.");
|
|
125
|
+
}
|
|
126
|
+
if (frid !== null && ![...getFrids(tree)].includes(frid)) {
|
|
127
|
+
throw new Error(`frid ${frid} does not exist.`);
|
|
128
|
+
}
|
|
129
|
+
const result = collectLinkedResourcesInSection(tree, linkedResourcesList, specificationsList, includeAcceptanceTests, frid);
|
|
130
|
+
linkedResourcesList.sort((a, b) => (a.text < b.text ? -1 : a.text > b.text ? 1 : 0));
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The FRID iterator: one functionality ID per functional spec, 1, 2, 3 in order.
|
|
135
|
+
* Everything that walks the spec one functionality at a time - rendering, ranges, context
|
|
136
|
+
* building - is driven by this.
|
|
137
|
+
*/
|
|
138
|
+
export function* getFrids(tree) {
|
|
139
|
+
const functionalRequirements = getSpecificationItems(tree, FUNCTIONAL_REQUIREMENTS) ?? [];
|
|
140
|
+
for (let id = 1; id <= functionalRequirements.length; id += 1)
|
|
141
|
+
yield String(id);
|
|
142
|
+
}
|
|
143
|
+
export function getFirstFrid(tree) {
|
|
144
|
+
const first = getFrids(tree).next();
|
|
145
|
+
return first.done ? null : first.value;
|
|
146
|
+
}
|
|
147
|
+
export function getNextFrid(tree, frid) {
|
|
148
|
+
const iterator = getFrids(tree);
|
|
149
|
+
for (const candidate of iterator) {
|
|
150
|
+
if (candidate === frid) {
|
|
151
|
+
const next = iterator.next();
|
|
152
|
+
return next.done ? null : next.value;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
throw new Error(`Functionality ${frid} does not exist.`);
|
|
156
|
+
}
|
|
157
|
+
export function getPreviousFrid(tree, frid) {
|
|
158
|
+
let previous = null;
|
|
159
|
+
for (const candidate of getFrids(tree)) {
|
|
160
|
+
if (candidate === frid)
|
|
161
|
+
return previous;
|
|
162
|
+
previous = candidate;
|
|
163
|
+
}
|
|
164
|
+
throw new Error(`Functionality ${frid} does not exist.`);
|
|
165
|
+
}
|
|
166
|
+
export function getFridsBefore(tree, targetFrid) {
|
|
167
|
+
const before = [];
|
|
168
|
+
for (const frid of getFrids(tree)) {
|
|
169
|
+
if (frid === targetFrid)
|
|
170
|
+
break;
|
|
171
|
+
before.push(frid);
|
|
172
|
+
}
|
|
173
|
+
return before;
|
|
174
|
+
}
|
|
175
|
+
export function getSpecificationItemMarkdown(specificationItem, codeVariables, replaceCodeVariables) {
|
|
176
|
+
let markdown = specificationItem.markdown;
|
|
177
|
+
for (const codeVariable of specificationItem.code_variables ?? []) {
|
|
178
|
+
if (codeVariable.name in codeVariables) {
|
|
179
|
+
if (codeVariables[codeVariable.name] !== codeVariable.value) {
|
|
180
|
+
throw new Error(`Code variable ${codeVariable.name} has multiple values: ${codeVariables[codeVariable.name]} and ${codeVariable.value}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
codeVariables[codeVariable.name] = codeVariable.value;
|
|
185
|
+
}
|
|
186
|
+
if (replaceCodeVariables) {
|
|
187
|
+
markdown = markdown.split(`{{ ${codeVariable.name} }}`).join(codeVariable.value);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return markdown;
|
|
191
|
+
}
|
|
192
|
+
function getSpecificationsFromPlainSourceTree(frid, tree, definitions, nonFunctionalRequirements, testRequirements, functionalRequirements, acceptanceTests, codeVariables, replaceCodeVariables) {
|
|
193
|
+
let returnFrid = null;
|
|
194
|
+
const requirements = getSpecificationItems(tree, FUNCTIONAL_REQUIREMENTS);
|
|
195
|
+
if (requirements !== undefined && requirements.length > 0) {
|
|
196
|
+
let count = 0;
|
|
197
|
+
for (const requirement of requirements) {
|
|
198
|
+
count += 1;
|
|
199
|
+
const currentFrid = String(count);
|
|
200
|
+
functionalRequirements.push(getSpecificationItemMarkdown(requirement, codeVariables, replaceCodeVariables));
|
|
201
|
+
if (currentFrid === frid) {
|
|
202
|
+
for (const test of requirement[ACCEPTANCE_TESTS] ?? []) {
|
|
203
|
+
acceptanceTests.push(getSpecificationItemMarkdown(test, codeVariables, replaceCodeVariables));
|
|
204
|
+
}
|
|
205
|
+
returnFrid = currentFrid;
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (returnFrid !== null) {
|
|
211
|
+
const prepend = (target, heading) => {
|
|
212
|
+
const source = getSpecificationItems(tree, heading);
|
|
213
|
+
if (source === undefined)
|
|
214
|
+
return;
|
|
215
|
+
target.unshift(...source.map((item) => getSpecificationItemMarkdown(item, codeVariables, replaceCodeVariables)));
|
|
216
|
+
};
|
|
217
|
+
prepend(definitions, DEFINITIONS);
|
|
218
|
+
prepend(nonFunctionalRequirements, NON_FUNCTIONAL_REQUIREMENTS);
|
|
219
|
+
prepend(testRequirements, TEST_REQUIREMENTS);
|
|
220
|
+
}
|
|
221
|
+
return returnFrid;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Build the rendering context for one functionality: every definition and
|
|
225
|
+
* implementation/test req in scope, the functionalities up to and including this one,
|
|
226
|
+
* and this one's acceptance tests.
|
|
227
|
+
*/
|
|
228
|
+
export function getSpecificationsForFrid(tree, frid, replaceCodeVariables = true) {
|
|
229
|
+
const definitions = [];
|
|
230
|
+
const nonFunctionalRequirements = [];
|
|
231
|
+
const testRequirements = [];
|
|
232
|
+
const functionalRequirements = [];
|
|
233
|
+
const acceptanceTests = [];
|
|
234
|
+
const codeVariables = {};
|
|
235
|
+
const result = getSpecificationsFromPlainSourceTree(frid, tree, definitions, nonFunctionalRequirements, testRequirements, functionalRequirements, acceptanceTests, codeVariables, replaceCodeVariables);
|
|
236
|
+
if (result === null)
|
|
237
|
+
throw new Error(`Functionality ${frid} does not exist.`);
|
|
238
|
+
const specifications = {
|
|
239
|
+
[DEFINITIONS]: definitions,
|
|
240
|
+
[NON_FUNCTIONAL_REQUIREMENTS]: nonFunctionalRequirements,
|
|
241
|
+
[TEST_REQUIREMENTS]: testRequirements,
|
|
242
|
+
[FUNCTIONAL_REQUIREMENTS]: functionalRequirements,
|
|
243
|
+
};
|
|
244
|
+
if (acceptanceTests.length > 0)
|
|
245
|
+
specifications[ACCEPTANCE_TESTS] = acceptanceTests;
|
|
246
|
+
return [specifications, Object.keys(codeVariables).length > 0 ? codeVariables : null];
|
|
247
|
+
}
|
|
248
|
+
/** Iterate every functionality with the context that should be rendered for it. */
|
|
249
|
+
export function* iterateFunctionalSpecs(tree, replaceCodeVariables = true) {
|
|
250
|
+
for (const frid of getFrids(tree)) {
|
|
251
|
+
const [specifications, codeVariables] = getSpecificationsForFrid(tree, frid, replaceCodeVariables);
|
|
252
|
+
yield { frid, specifications, codeVariables };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
export function hashText(text) {
|
|
256
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
257
|
+
}
|
|
258
|
+
export function getHashValue(specifications) {
|
|
259
|
+
return hashText(JSON.stringify(specifications, null, 4));
|
|
260
|
+
}
|
|
261
|
+
function getFridsRange(tree, start, end) {
|
|
262
|
+
const frids = [...getFrids(tree)];
|
|
263
|
+
start = String(start);
|
|
264
|
+
if (!frids.includes(start)) {
|
|
265
|
+
throw new InvalidFridArgument(`Invalid start functionality ID: ${start}. Valid IDs are: ${JSON.stringify(frids)}.`);
|
|
266
|
+
}
|
|
267
|
+
let endIdx;
|
|
268
|
+
if (end !== undefined) {
|
|
269
|
+
end = String(end);
|
|
270
|
+
if (!frids.includes(end)) {
|
|
271
|
+
throw new InvalidFridArgument(`Invalid end functionality ID: ${end}. Valid IDs are: ${JSON.stringify(frids)}.`);
|
|
272
|
+
}
|
|
273
|
+
endIdx = frids.indexOf(end) + 1;
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
endIdx = frids.length;
|
|
277
|
+
}
|
|
278
|
+
const startIdx = frids.indexOf(start);
|
|
279
|
+
if (startIdx >= endIdx) {
|
|
280
|
+
throw new InvalidFridArgument(`Start functionality ID: ${start} must be before end functionality ID: ${end}.`);
|
|
281
|
+
}
|
|
282
|
+
return frids.slice(startIdx, endIdx);
|
|
283
|
+
}
|
|
284
|
+
export function getRenderRange(renderRange, tree) {
|
|
285
|
+
const parts = renderRange.split(",");
|
|
286
|
+
const rangeEnd = parts.length === 2 ? parts[1] : parts[0];
|
|
287
|
+
return getFridsRange(tree, parts[0], rangeEnd);
|
|
288
|
+
}
|
|
289
|
+
export function getRenderRangeFrom(start, tree) {
|
|
290
|
+
return getFridsRange(tree, start);
|
|
291
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { ModuleDoesNotExistError, PlainSyntaxError } from "./errors.js";
|
|
5
|
+
import { openFrom } from "./files.js";
|
|
6
|
+
/**
|
|
7
|
+
* The ***plain standard library's names. `stdlib/` is a reserved prefix: `stdlib/backend/python-fastapi`
|
|
8
|
+
* is always turri's own template, in its `stdlib/` folder - never a module of the project's. A
|
|
9
|
+
* project that has a module at such a path gets an error, not a quiet choice between the two: which one
|
|
10
|
+
* a spec builds on must never depend on what happens to be on disk beside it.
|
|
11
|
+
*
|
|
12
|
+
* This is turri's, not the reference parser's: the Python parser has no standard library.
|
|
13
|
+
*/
|
|
14
|
+
export const STDLIB_PREFIX = "stdlib/";
|
|
15
|
+
/** The folder `stdlib/` is in: turri's own. */
|
|
16
|
+
export const STDLIB_ROOT = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
17
|
+
export function isStdlibModule(moduleName) {
|
|
18
|
+
return moduleName.replace(/^\.\//, "").startsWith(STDLIB_PREFIX);
|
|
19
|
+
}
|
|
20
|
+
function sameFile(a, b) {
|
|
21
|
+
try {
|
|
22
|
+
return realpathSync(a) === realpathSync(b);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** A standard library module's text - refused when a folder the spec is resolved from has a module of that name too. */
|
|
29
|
+
export function readStdlibModule(moduleName, templateDirs) {
|
|
30
|
+
const name = moduleName.replace(/^\.\//, "");
|
|
31
|
+
const file = `${name}.plain`;
|
|
32
|
+
const own = join(STDLIB_ROOT, file);
|
|
33
|
+
for (const dir of templateDirs) {
|
|
34
|
+
const local = join(dir, file);
|
|
35
|
+
if (existsSync(local) && !sameFile(local, own)) {
|
|
36
|
+
throw new PlainSyntaxError(`Plain syntax error: '${name}' is a reserved name - modules under ${STDLIB_PREFIX} are turri's standard library - ` +
|
|
37
|
+
`but ${local} is a module of that name. Rename it, or the folder it is in.`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const text = openFrom([STDLIB_ROOT], file);
|
|
41
|
+
if (text === null)
|
|
42
|
+
throw new ModuleDoesNotExistError(`Module does not exist (${name}): the standard library has no ${name}.`);
|
|
43
|
+
return text;
|
|
44
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Input, matchesKey, truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
2
|
+
/** Lines kept for the conversation above the dialog, so it never takes the whole screen. */
|
|
3
|
+
const KEPT_ABOVE = 4;
|
|
4
|
+
/** **bold** and `code`, as the brief's markdown means them. */
|
|
5
|
+
function styled(text, style) {
|
|
6
|
+
return text.replace(/\*\*([^*]+)\*\*/g, (_all, inner) => style.bold(inner)).replace(/`([^`]+)`/g, (_all, inner) => style.accent(inner));
|
|
7
|
+
}
|
|
8
|
+
export class QuestionDialog {
|
|
9
|
+
options;
|
|
10
|
+
input;
|
|
11
|
+
selected = 0;
|
|
12
|
+
/** The brief's first line in its window. */
|
|
13
|
+
scroll = 0;
|
|
14
|
+
/** How far the brief scrolls, and its window's height - as last drawn. */
|
|
15
|
+
maxScroll = 0;
|
|
16
|
+
window = 0;
|
|
17
|
+
answered = false;
|
|
18
|
+
focusedNow = false;
|
|
19
|
+
constructor(options) {
|
|
20
|
+
this.options = options;
|
|
21
|
+
if (options.options === undefined || options.options.length === 0) {
|
|
22
|
+
this.input = new Input({ prompt: "> " });
|
|
23
|
+
this.input.onSubmit = (value) => this.finish(value.trim() === "" ? undefined : value);
|
|
24
|
+
this.input.onEscape = () => this.finish(undefined);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
this.input = undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
get focused() {
|
|
31
|
+
return this.focusedNow;
|
|
32
|
+
}
|
|
33
|
+
set focused(value) {
|
|
34
|
+
this.focusedNow = value;
|
|
35
|
+
if (this.input !== undefined)
|
|
36
|
+
this.input.focused = value;
|
|
37
|
+
}
|
|
38
|
+
finish(answer) {
|
|
39
|
+
if (this.answered)
|
|
40
|
+
return;
|
|
41
|
+
this.answered = true;
|
|
42
|
+
this.options.done(answer);
|
|
43
|
+
}
|
|
44
|
+
scrollBy(lines) {
|
|
45
|
+
const next = Math.max(0, Math.min(this.maxScroll, this.scroll + lines));
|
|
46
|
+
if (next === this.scroll)
|
|
47
|
+
return;
|
|
48
|
+
this.scroll = next;
|
|
49
|
+
this.options.requestRender();
|
|
50
|
+
}
|
|
51
|
+
render(width) {
|
|
52
|
+
const { style } = this.options;
|
|
53
|
+
const inner = Math.max(10, width - 2);
|
|
54
|
+
const pad = (line) => truncateToWidth(` ${line}`, width);
|
|
55
|
+
const context = (this.options.context ?? "").trim();
|
|
56
|
+
const brief = context === "" ? [] : context.split("\n").flatMap((line) => (line.trim() === "" ? [""] : wrapTextWithAnsi(styled(line, style), inner)));
|
|
57
|
+
const question = wrapTextWithAnsi(style.bold(style.accent(this.options.question)), inner);
|
|
58
|
+
const choices = this.options.options ?? [];
|
|
59
|
+
const body = this.input !== undefined
|
|
60
|
+
? this.input.render(inner)
|
|
61
|
+
: choices.map((choice, index) => (index === this.selected ? style.accent(`› ${choice}`) : ` ${choice}`));
|
|
62
|
+
const hint = this.input !== undefined
|
|
63
|
+
? "enter answers · esc dismisses"
|
|
64
|
+
: "↑↓ choose · enter picks · esc dismisses";
|
|
65
|
+
// The brief gets what the question, the options and the hints leave of the screen.
|
|
66
|
+
const fixed = question.length + body.length + 4;
|
|
67
|
+
const room = Math.max(3, this.options.rows() - KEPT_ABOVE - fixed);
|
|
68
|
+
const scrolls = brief.length > room;
|
|
69
|
+
this.window = scrolls ? room - 1 : brief.length;
|
|
70
|
+
this.maxScroll = scrolls ? brief.length - this.window : 0;
|
|
71
|
+
this.scroll = Math.min(this.scroll, this.maxScroll);
|
|
72
|
+
const lines = [];
|
|
73
|
+
if (brief.length > 0) {
|
|
74
|
+
lines.push(...brief.slice(this.scroll, this.scroll + this.window).map(pad));
|
|
75
|
+
if (scrolls) {
|
|
76
|
+
const last = Math.min(brief.length, this.scroll + this.window);
|
|
77
|
+
const more = this.scroll === 0 ? " - more below" : last === brief.length ? " - the end" : "";
|
|
78
|
+
lines.push(pad(style.muted(`── lines ${this.scroll + 1}-${last} of ${brief.length}${more} · PgUp/PgDn or shift+↑↓ scroll ──`)));
|
|
79
|
+
}
|
|
80
|
+
lines.push("");
|
|
81
|
+
}
|
|
82
|
+
lines.push(...question.map(pad), ...body.map(pad), "", pad(style.muted(hint)));
|
|
83
|
+
return lines;
|
|
84
|
+
}
|
|
85
|
+
handleInput(data) {
|
|
86
|
+
// The brief scrolls whatever has the keys.
|
|
87
|
+
if (matchesKey(data, "pageDown"))
|
|
88
|
+
return this.scrollBy(Math.max(1, this.window - 1));
|
|
89
|
+
if (matchesKey(data, "pageUp"))
|
|
90
|
+
return this.scrollBy(-Math.max(1, this.window - 1));
|
|
91
|
+
if (matchesKey(data, "shift+down"))
|
|
92
|
+
return this.scrollBy(1);
|
|
93
|
+
if (matchesKey(data, "shift+up"))
|
|
94
|
+
return this.scrollBy(-1);
|
|
95
|
+
if (this.input !== undefined) {
|
|
96
|
+
this.input.handleInput(data);
|
|
97
|
+
this.options.requestRender();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const choices = this.options.options ?? [];
|
|
101
|
+
if (matchesKey(data, "up")) {
|
|
102
|
+
this.selected = (this.selected - 1 + choices.length) % choices.length;
|
|
103
|
+
this.options.requestRender();
|
|
104
|
+
}
|
|
105
|
+
else if (matchesKey(data, "down")) {
|
|
106
|
+
this.selected = (this.selected + 1) % choices.length;
|
|
107
|
+
this.options.requestRender();
|
|
108
|
+
}
|
|
109
|
+
else if (matchesKey(data, "enter")) {
|
|
110
|
+
this.finish(choices[this.selected]);
|
|
111
|
+
}
|
|
112
|
+
else if (matchesKey(data, "escape")) {
|
|
113
|
+
this.finish(undefined);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
handleMouse(event) {
|
|
117
|
+
if (event.type !== "wheel" || event.wheelDelta === undefined || this.maxScroll === 0)
|
|
118
|
+
return undefined;
|
|
119
|
+
this.scrollBy(event.wheelDelta);
|
|
120
|
+
return { handled: true };
|
|
121
|
+
}
|
|
122
|
+
invalidate() {
|
|
123
|
+
this.input?.invalidate();
|
|
124
|
+
}
|
|
125
|
+
}
|