@shortlink-org/portolan 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -205
- package/cli/init.mjs +285 -0
- package/cli/init.test.mjs +158 -0
- package/cli/portolan.mjs +37 -73
- package/index.html +19 -0
- package/package.json +5 -10
- package/plugins/README.md +66 -27
- package/plugins/portolan-go.wasm +0 -0
- package/portolan.json +52 -73
- package/public/landing/cats/bug-hunter.webp +0 -0
- package/public/landing/cats/compass-nap.webp +0 -0
- package/public/landing/cats/diagrammer.webp +0 -0
- package/public/landing/cats/docs-reader.webp +0 -0
- package/public/landing/cats/laptop.webp +0 -0
- package/public/landing/cats/map-inspector.webp +0 -0
- package/public/landing/cats/server-break.webp +0 -0
- package/public/landing/cats/star-mapper.webp +0 -0
- package/public/landing/cats/system-builder.webp +0 -0
- package/public/landing/cats/thread-tangle.webp +0 -0
- package/public/og.png +0 -0
- package/public/readme-header.webp +0 -0
- package/schema/portolan.schema.json +180 -0
- package/scripts/builtin-plugins.mjs +14 -0
- package/scripts/delivery-presets.mjs +353 -0
- package/scripts/gen.mjs +37 -4
- package/scripts/history.mjs +126 -0
- package/scripts/history.test.mjs +110 -0
- package/scripts/host-plugins/fetch-bsr.mjs +351 -0
- package/{plugins/fetch-bsr/options.schema.json → scripts/host-plugins/fetch-bsr.options.json} +4 -0
- package/scripts/host-plugins/fetch-bsr.test.mjs +212 -0
- package/scripts/host-plugins/fetch-csr.mjs +386 -0
- package/scripts/host-plugins/fetch-csr.test.mjs +178 -0
- package/scripts/host-plugins/fetch-git.mjs +379 -0
- package/scripts/host-plugins/fetch-git.test.mjs +210 -0
- package/scripts/local-api.mjs +27 -10
- package/scripts/local-api.test.mjs +85 -2
- package/scripts/manifest.mjs +3 -2
- package/scripts/package-smoke.mjs +24 -3
- package/scripts/plugin-host.mjs +77 -18
- package/scripts/plugin-host.test.mjs +50 -1
- package/scripts/plugin-wasm-worker.mjs +6 -3
- package/scripts/run-builtin.mjs +6 -26
- package/scripts/schema.mjs +6 -1
- package/src/app/App.tsx +24 -429
- package/src/app/Breadcrumbs.test.ts +7 -0
- package/src/app/Breadcrumbs.tsx +15 -2
- package/src/app/CatalogApp.tsx +436 -0
- package/src/app/Sidebar.tsx +0 -1
- package/src/chat/ChatPanel.tsx +98 -41
- package/src/chat/Composer.tsx +4 -2
- package/src/chat/Conversation.tsx +79 -15
- package/src/chat/Header.tsx +43 -9
- package/src/chat/Starter.tsx +64 -3
- package/src/chat/Steps.tsx +1 -1
- package/src/chat/flags.test.ts +51 -14
- package/src/chat/flags.ts +8 -2
- package/src/chat/page-context.test.ts +51 -0
- package/src/chat/page-context.ts +128 -0
- package/src/chat/prompt.test.ts +49 -1
- package/src/chat/prompt.ts +42 -1
- package/src/chat/tools.ts +7 -2
- package/src/chat/transport.ts +16 -4
- package/src/components/ProblemRow.tsx +182 -0
- package/src/data.ts +20 -5
- package/src/er/ErCanvas.tsx +1 -1
- package/src/graph/DependencyGraph.tsx +10 -1
- package/src/graph/FocusedEventGraph.tsx +1 -1
- package/src/graph/GraphToolbar.tsx +2 -4
- package/src/index.css +220 -2
- package/src/landing/DraggableReveal.tsx +168 -0
- package/src/landing/EstateGraph.tsx +28 -0
- package/src/landing/FlowPlayback.tsx +282 -0
- package/src/landing/HeroMap.tsx +128 -0
- package/src/landing/LandingPage.tsx +757 -0
- package/src/landing/ProductFrame.tsx +68 -0
- package/src/landing/ProductTour.tsx +371 -0
- package/src/landing/catalog.ts +25 -0
- package/src/landing/motion.tsx +52 -0
- package/src/lib/all-problems.ts +28 -0
- package/src/lib/local-api.ts +35 -1
- package/src/lib/setup-info.test.ts +8 -0
- package/src/lib/setup-info.ts +3 -2
- package/src/lib/source-code.ts +2 -1
- package/src/likec4/C4View.tsx +5 -0
- package/src/likec4/InteractiveView.tsx +5 -1
- package/src/map/ContextMapGraph.tsx +47 -20
- package/src/pages/ContextMap.tsx +5 -1
- package/src/pages/FlowDetail.tsx +24 -7
- package/src/pages/Overview.tsx +68 -12
- package/src/pages/Problems.tsx +6 -193
- package/src/pages/Settings.tsx +133 -108
- package/src/pages/settings/DeliverySettings.tsx +229 -0
- package/src/pages/settings/PreferencesSettings.tsx +101 -0
- package/src/routes.test.ts +20 -0
- package/src/routes.ts +13 -1
- package/vite.config.ts +17 -9
- package/catalog/enum_test.go +0 -46
- package/catalog/model.go +0 -932
- package/catalog/roundtrip_test.go +0 -185
- package/catalog/via_test.go +0 -38
- package/go.mod +0 -14
- package/go.sum +0 -14
- package/internal/commands/cargo.go +0 -154
- package/internal/commands/commands.go +0 -143
- package/internal/commands/commands_test.go +0 -323
- package/internal/commands/gradle.go +0 -77
- package/internal/commands/justfile.go +0 -82
- package/internal/commands/makefile.go +0 -106
- package/internal/commands/maven.go +0 -93
- package/internal/commands/packagejson.go +0 -159
- package/internal/commands/pyproject.go +0 -169
- package/internal/commands/taskfile.go +0 -101
- package/internal/commands/testdata/estate/.cargo/config.toml +0 -7
- package/internal/commands/testdata/estate/Makefile +0 -33
- package/internal/commands/testdata/estate/Taskfile.yml +0 -21
- package/internal/commands/testdata/estate/build.gradle.kts +0 -21
- package/internal/commands/testdata/estate/justfile +0 -20
- package/internal/commands/testdata/estate/package.json +0 -13
- package/internal/commands/testdata/estate/pom.xml +0 -29
- package/internal/commands/testdata/estate/pyproject.toml +0 -27
- package/internal/commands/testdata/estate/xtask/src/main.rs +0 -27
- package/internal/commands/testdata/golden/commands.json +0 -305
- package/internal/gohttp/analyze.go +0 -2500
- package/internal/gohttp/endpoints.go +0 -1067
- package/internal/gohttp/roots.go +0 -320
- package/internal/gohttp/typed.go +0 -143
- package/internal/goscan/constants.go +0 -85
- package/internal/goscan/goscan_test.go +0 -227
- package/internal/goscan/names.go +0 -52
- package/internal/goscan/parse_test.go +0 -11
- package/internal/goscan/source.go +0 -36
- package/internal/goscan/tree.go +0 -155
- package/internal/goscan/types.go +0 -99
- package/internal/wsdl/ids.go +0 -127
- package/internal/wsdl/ids_test.go +0 -21
- package/internal/wsdl/model.go +0 -62
- package/internal/wsdl/parse.go +0 -920
- package/internal/wsdl/parse_test.go +0 -133
- package/plugin/describe.go +0 -107
- package/plugin/describe_test.go +0 -114
- package/plugin/protocol.go +0 -117
- package/plugin/schematest/schematest.go +0 -126
- package/plugins/extract-adr/describe.go +0 -19
- package/plugins/extract-adr/describe_test.go +0 -11
- package/plugins/extract-adr/extract.go +0 -153
- package/plugins/extract-adr/extract_test.go +0 -350
- package/plugins/extract-adr/history.go +0 -99
- package/plugins/extract-adr/main.go +0 -65
- package/plugins/extract-adr/parse.go +0 -642
- package/plugins/extract-adr/parse_test.go +0 -405
- package/plugins/extract-asyncapi/describe.go +0 -19
- package/plugins/extract-asyncapi/describe_test.go +0 -11
- package/plugins/extract-asyncapi/extract.go +0 -315
- package/plugins/extract-asyncapi/extract_test.go +0 -197
- package/plugins/extract-asyncapi/main.go +0 -48
- package/plugins/extract-asyncapi/spec.go +0 -151
- package/plugins/extract-commands/describe.go +0 -19
- package/plugins/extract-commands/describe_test.go +0 -11
- package/plugins/extract-commands/extract.go +0 -79
- package/plugins/extract-commands/extract_test.go +0 -92
- package/plugins/extract-commands/main.go +0 -55
- package/plugins/extract-csr/avro.go +0 -258
- package/plugins/extract-csr/describe.go +0 -19
- package/plugins/extract-csr/describe_test.go +0 -11
- package/plugins/extract-csr/extract.go +0 -338
- package/plugins/extract-csr/extract_test.go +0 -374
- package/plugins/extract-csr/jsonschema.go +0 -343
- package/plugins/extract-csr/lock.go +0 -25
- package/plugins/extract-csr/main.go +0 -84
- package/plugins/extract-csr/subject.go +0 -102
- package/plugins/extract-flows/describe.go +0 -19
- package/plugins/extract-flows/describe_test.go +0 -11
- package/plugins/extract-flows/extract.go +0 -91
- package/plugins/extract-flows/main.go +0 -45
- package/plugins/extract-flows/parse.go +0 -593
- package/plugins/extract-flows/parse_test.go +0 -204
- package/plugins/extract-glossary/describe.go +0 -19
- package/plugins/extract-glossary/describe_test.go +0 -11
- package/plugins/extract-glossary/extract.go +0 -115
- package/plugins/extract-glossary/extract_test.go +0 -220
- package/plugins/extract-glossary/main.go +0 -59
- package/plugins/extract-glossary/parse.go +0 -214
- package/plugins/extract-glossary/parse_test.go +0 -203
- package/plugins/extract-go/aggregate.go +0 -214
- package/plugins/extract-go/client.go +0 -409
- package/plugins/extract-go/client_test.go +0 -266
- package/plugins/extract-go/describe.go +0 -19
- package/plugins/extract-go/describe_test.go +0 -11
- package/plugins/extract-go/enum.go +0 -195
- package/plugins/extract-go/enum_test.go +0 -82
- package/plugins/extract-go/event.go +0 -99
- package/plugins/extract-go/extract.go +0 -191
- package/plugins/extract-go/extract_test.go +0 -261
- package/plugins/extract-go/flow.go +0 -1441
- package/plugins/extract-go/flow_test.go +0 -609
- package/plugins/extract-go/httpclient.go +0 -174
- package/plugins/extract-go/httpclient_test.go +0 -296
- package/plugins/extract-go/ids.go +0 -92
- package/plugins/extract-go/layout.go +0 -225
- package/plugins/extract-go/layout_test.go +0 -125
- package/plugins/extract-go/lifecycle.go +0 -324
- package/plugins/extract-go/lifecycle_test.go +0 -108
- package/plugins/extract-go/main.go +0 -79
- package/plugins/extract-go/operation.go +0 -157
- package/plugins/extract-go/source.go +0 -316
- package/plugins/extract-go/transport.go +0 -321
- package/plugins/extract-go/transport_test.go +0 -145
- package/plugins/extract-go/wiring.go +0 -434
- package/plugins/extract-go-nats/describe.go +0 -19
- package/plugins/extract-go-nats/describe_test.go +0 -11
- package/plugins/extract-go-nats/extract.go +0 -177
- package/plugins/extract-go-nats/extract_test.go +0 -389
- package/plugins/extract-go-nats/index.go +0 -379
- package/plugins/extract-go-nats/main.go +0 -42
- package/plugins/extract-go-nats/resolve.go +0 -161
- package/plugins/extract-go-nats/sites.go +0 -224
- package/plugins/extract-graphql/describe.go +0 -19
- package/plugins/extract-graphql/describe_test.go +0 -11
- package/plugins/extract-graphql/extract.go +0 -433
- package/plugins/extract-graphql/extract_test.go +0 -256
- package/plugins/extract-graphql/ids.go +0 -49
- package/plugins/extract-graphql/lex.go +0 -237
- package/plugins/extract-graphql/main.go +0 -51
- package/plugins/extract-graphql/parse.go +0 -621
- package/plugins/extract-graphql/parse_test.go +0 -122
- package/plugins/extract-http-clients/describe.go +0 -19
- package/plugins/extract-http-clients/describe_test.go +0 -11
- package/plugins/extract-http-clients/extract.go +0 -705
- package/plugins/extract-http-clients/extract_test.go +0 -1263
- package/plugins/extract-http-clients/main.go +0 -42
- package/plugins/extract-openapi/describe.go +0 -19
- package/plugins/extract-openapi/describe_test.go +0 -11
- package/plugins/extract-openapi/discover.go +0 -243
- package/plugins/extract-openapi/extract.go +0 -526
- package/plugins/extract-openapi/extract_test.go +0 -545
- package/plugins/extract-openapi/main.go +0 -75
- package/plugins/extract-openapi/spec.go +0 -350
- package/plugins/extract-project/describe.go +0 -19
- package/plugins/extract-project/describe_test.go +0 -11
- package/plugins/extract-project/extract.go +0 -221
- package/plugins/extract-project/extract_test.go +0 -109
- package/plugins/extract-project/main.go +0 -41
- package/plugins/extract-proto/ast.go +0 -125
- package/plugins/extract-proto/consumes.go +0 -77
- package/plugins/extract-proto/describe.go +0 -19
- package/plugins/extract-proto/extract.go +0 -293
- package/plugins/extract-proto/extract_test.go +0 -459
- package/plugins/extract-proto/ids.go +0 -89
- package/plugins/extract-proto/ids_test.go +0 -57
- package/plugins/extract-proto/lex.go +0 -285
- package/plugins/extract-proto/main.go +0 -100
- package/plugins/extract-proto/module.go +0 -120
- package/plugins/extract-proto/parse.go +0 -720
- package/plugins/extract-proto/parse_test.go +0 -307
- package/plugins/extract-proto/provides.go +0 -236
- package/plugins/extract-proto/resolve.go +0 -222
- package/plugins/extract-proto/resolve_test.go +0 -119
- package/plugins/extract-redis/describe.go +0 -19
- package/plugins/extract-redis/describe_test.go +0 -11
- package/plugins/extract-redis/extract.go +0 -183
- package/plugins/extract-redis/extract_test.go +0 -168
- package/plugins/extract-redis/keyspaces.go +0 -469
- package/plugins/extract-redis/main.go +0 -44
- package/plugins/extract-river/describe.go +0 -19
- package/plugins/extract-river/describe_test.go +0 -11
- package/plugins/extract-river/extract.go +0 -507
- package/plugins/extract-river/extract_test.go +0 -132
- package/plugins/extract-river/main.go +0 -42
- package/plugins/extract-sql/ddl.go +0 -893
- package/plugins/extract-sql/ddl_test.go +0 -401
- package/plugins/extract-sql/describe.go +0 -19
- package/plugins/extract-sql/describe_test.go +0 -11
- package/plugins/extract-sql/layout.go +0 -221
- package/plugins/extract-sql/layout_test.go +0 -55
- package/plugins/extract-sql/lineage.go +0 -107
- package/plugins/extract-sql/main.go +0 -141
- package/plugins/extract-sql/maps.go +0 -564
- package/plugins/extract-sql/maps_java.go +0 -117
- package/plugins/extract-sql/maps_rust.go +0 -333
- package/plugins/extract-sql/maps_rust_test.go +0 -70
- package/plugins/extract-sql/maps_test.go +0 -204
- package/plugins/extract-sql/maps_ts.go +0 -398
- package/plugins/extract-sql/maps_ts_test.go +0 -136
- package/plugins/extract-sql/projection.go +0 -70
- package/plugins/extract-sql/projection_test.go +0 -50
- package/plugins/extract-sql/store.go +0 -420
- package/plugins/extract-sql/store_test.go +0 -233
- package/plugins/extract-sql/view.go +0 -295
- package/plugins/extract-watermill/describe.go +0 -19
- package/plugins/extract-watermill/describe_test.go +0 -11
- package/plugins/extract-watermill/extract.go +0 -1228
- package/plugins/extract-watermill/extract_test.go +0 -234
- package/plugins/extract-watermill/main.go +0 -41
- package/plugins/extract-wsdl/describe.go +0 -19
- package/plugins/extract-wsdl/describe_test.go +0 -11
- package/plugins/extract-wsdl/extract.go +0 -148
- package/plugins/extract-wsdl/extract_test.go +0 -45
- package/plugins/extract-wsdl/main.go +0 -53
- package/plugins/fetch-bsr/auth.go +0 -114
- package/plugins/fetch-bsr/bsr.go +0 -240
- package/plugins/fetch-bsr/cache.go +0 -66
- package/plugins/fetch-bsr/describe.go +0 -19
- package/plugins/fetch-bsr/fetch.go +0 -192
- package/plugins/fetch-bsr/fetch_test.go +0 -465
- package/plugins/fetch-bsr/lock.go +0 -71
- package/plugins/fetch-bsr/main.go +0 -117
- package/plugins/fetch-csr/auth.go +0 -95
- package/plugins/fetch-csr/cache.go +0 -69
- package/plugins/fetch-csr/describe.go +0 -19
- package/plugins/fetch-csr/describe_test.go +0 -11
- package/plugins/fetch-csr/fetch.go +0 -254
- package/plugins/fetch-csr/fetch_test.go +0 -494
- package/plugins/fetch-csr/lock.go +0 -95
- package/plugins/fetch-csr/main.go +0 -122
- package/plugins/fetch-csr/registry.go +0 -209
- package/plugins/fetch-git/cache.go +0 -65
- package/plugins/fetch-git/describe.go +0 -19
- package/plugins/fetch-git/describe_test.go +0 -11
- package/plugins/fetch-git/fetch.go +0 -181
- package/plugins/fetch-git/fetch_test.go +0 -332
- package/plugins/fetch-git/git.go +0 -169
- package/plugins/fetch-git/lock.go +0 -72
- package/plugins/fetch-git/main.go +0 -127
- package/plugins/fetch-git/offline.go +0 -39
- package/plugins/fetch-git/pin.go +0 -90
- package/plugins/fetch-git/pin_test.go +0 -74
- package/plugins/gen-backstage/describe.go +0 -17
- package/plugins/gen-backstage/main.go +0 -22
- package/plugins/gen-backstage/plugin.go +0 -473
- package/plugins/gen-backstage/plugin_test.go +0 -145
- package/plugins/gen-backstage.wasm +0 -0
- package/plugins/gen-markdown/adr.go +0 -162
- package/plugins/gen-markdown/aggregate.go +0 -362
- package/plugins/gen-markdown/canonical.go +0 -93
- package/plugins/gen-markdown/context.go +0 -90
- package/plugins/gen-markdown/coverage_test.go +0 -89
- package/plugins/gen-markdown/describe.go +0 -19
- package/plugins/gen-markdown/describe_test.go +0 -11
- package/plugins/gen-markdown/external.go +0 -71
- package/plugins/gen-markdown/flow.go +0 -277
- package/plugins/gen-markdown/glossary.go +0 -76
- package/plugins/gen-markdown/glossary_test.go +0 -148
- package/plugins/gen-markdown/llms.go +0 -302
- package/plugins/gen-markdown/main.go +0 -22
- package/plugins/gen-markdown/markdown.go +0 -254
- package/plugins/gen-markdown/markdown_test.go +0 -100
- package/plugins/gen-markdown/module.go +0 -107
- package/plugins/gen-markdown/plugin.go +0 -39
- package/plugins/gen-markdown/quality_test.go +0 -176
- package/plugins/gen-markdown/redis_test.go +0 -38
- package/plugins/gen-markdown/render.go +0 -344
- package/plugins/gen-markdown/render_test.go +0 -186
- package/plugins/gen-markdown/service.go +0 -456
- package/plugins/gen-markdown/source.go +0 -177
- package/plugins/gen-markdown/store.go +0 -201
- package/plugins/gen-markdown.wasm +0 -0
- package/plugins/gen-mermaid/describe.go +0 -17
- package/plugins/gen-mermaid/main.go +0 -22
- package/plugins/gen-mermaid/plugin.go +0 -104
- package/plugins/gen-mermaid/plugin_test.go +0 -33
- package/plugins/gen-mermaid.wasm +0 -0
- package/plugins/openapi/ids.go +0 -261
- package/plugins/openapi/ids_test.go +0 -98
- package/plugins/verify-codeowners/describe.go +0 -19
- package/plugins/verify-codeowners/describe_test.go +0 -11
- package/plugins/verify-codeowners/main.go +0 -75
- package/plugins/verify-codeowners/match.go +0 -85
- package/plugins/verify-codeowners/match_test.go +0 -47
- package/plugins/verify-codeowners/owners.go +0 -164
- package/plugins/verify-codeowners/owners_test.go +0 -225
- package/plugins/verify-codeowners/parse.go +0 -90
- package/plugins/verify-codeowners/parse_test.go +0 -62
- package/plugins/verify-otel/describe.go +0 -19
- package/plugins/verify-otel/describe_test.go +0 -11
- package/plugins/verify-otel/main.go +0 -53
- package/plugins/verify-otel/match.go +0 -336
- package/plugins/verify-otel/otlp.go +0 -200
- package/plugins/verify-otel/verify.go +0 -734
- package/plugins/verify-otel/verify_test.go +0 -460
- /package/{plugins/fetch-csr/options.schema.json → scripts/host-plugins/fetch-csr.options.json} +0 -0
- /package/{plugins/fetch-git/options.schema.json → scripts/host-plugins/fetch-git.options.json} +0 -0
package/README.md
CHANGED
|
@@ -1,17 +1,44 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="./public/readme-header.webp" alt="A software architecture landscape mapped by Portolan" width="100%" />
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
contexts, components, interfaces, events, flows, stores and ADRs, read out of
|
|
5
|
-
the code and specs that already describe them, and rendered as a navigable
|
|
6
|
-
site. DDD enriches the model when a repository really uses it; it is not a
|
|
7
|
-
prerequisite.
|
|
5
|
+
<h1 align="center">Portolan</h1>
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
branch comparison and source previews read immutable files from GitHub or
|
|
11
|
-
GitLab at runtime; local development uses a localhost-only control plane.
|
|
7
|
+
<p align="center"><strong>Your architecture, read from the code.</strong></p>
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
<p align="center">
|
|
10
|
+
Turn code, contracts, schemas, traces and ADRs into a validated, navigable map of your software estate.
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
<p align="center">
|
|
14
|
+
<a href="https://shortlink-org.github.io/portolan/landing">View the product tour</a>
|
|
15
|
+
·
|
|
16
|
+
<a href="https://shortlink-org.github.io/portolan/?catalog=example">Explore the example catalog</a>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
## Make architecture visible — and keep it honest
|
|
20
|
+
|
|
21
|
+
Architecture documentation loses value when it becomes another system teams
|
|
22
|
+
must remember to maintain. Portolan starts with the evidence your repositories
|
|
23
|
+
already contain and turns it into one coherent, searchable view of the system.
|
|
24
|
+
|
|
25
|
+
See how bounded contexts, services, APIs, events, data stores, flows and
|
|
26
|
+
decisions fit together. Follow any relationship back to its source. Surface
|
|
27
|
+
missing contracts, ownership conflicts and architectural drift before they
|
|
28
|
+
become production surprises.
|
|
29
|
+
|
|
30
|
+
- **Understand the whole estate.** Move from the landscape to a single flow,
|
|
31
|
+
service, schema or decision without losing context.
|
|
32
|
+
- **Trust what you see.** Portolan merges and validates facts from code,
|
|
33
|
+
specifications and observed traces instead of relying on a second hand-built
|
|
34
|
+
inventory.
|
|
35
|
+
- **Publish anywhere.** The catalog and site are static end to end, require no
|
|
36
|
+
hosted backend and fit naturally into pull requests and CI.
|
|
37
|
+
|
|
38
|
+
DDD enriches the model when a repository uses it; it is never a prerequisite.
|
|
39
|
+
Optional branch comparison and source previews read immutable files from GitHub
|
|
40
|
+
or GitLab at runtime, while local development uses a localhost-only control
|
|
41
|
+
plane.
|
|
15
42
|
|
|
16
43
|
## What it does
|
|
17
44
|
|
|
@@ -49,45 +76,6 @@ Facts carry a status: `declared` (a fragment says so), `verified` (a recorded
|
|
|
49
76
|
trace showed it happening), `unresolved` (nothing in the catalog answers the
|
|
50
77
|
reference).
|
|
51
78
|
|
|
52
|
-
### Projects without DDD
|
|
53
|
-
|
|
54
|
-
`extract-project` is the neutral baseline. It reads repository metadata and
|
|
55
|
-
deployment/build manifests without executing project code, creates a `system`,
|
|
56
|
-
`product`, `team` or `namespace` containing a component, and records its role
|
|
57
|
-
(`application`, `worker`, `job`, `cli`, `library`, and so on) and technologies.
|
|
58
|
-
It also lists the commands the runner files declare - make targets, npm
|
|
59
|
-
scripts, just recipes, Taskfile and poe/pdm tasks, Maven and Gradle goals,
|
|
60
|
-
cargo aliases and xtask subcommands - which `extract-commands` does on its
|
|
61
|
-
own for a service a domain extractor describes.
|
|
62
|
-
Contract, messaging and data extractors then add OpenAPI, AsyncAPI, GraphQL,
|
|
63
|
-
proto, SQL, Redis, River, Watermill and outbound HTTP/SOAP facts to that same
|
|
64
|
-
component.
|
|
65
|
-
For Go services, the HTTP client extractor also joins common router
|
|
66
|
-
registrations to handlers, interface calls, string-keyed factory branches and
|
|
67
|
-
concrete providers. Constructor maps, fixed factories, capability assertions,
|
|
68
|
-
composite/direct field assignment and setter injection are followed when the
|
|
69
|
-
source proves one concrete target. The resulting flow starts at the inbound
|
|
70
|
-
endpoint and fans out by the provider choices proved by source; when a
|
|
71
|
-
provider's transport lives in another module, the flow stops at that
|
|
72
|
-
implementation and says that the outbound transport could not be resolved.
|
|
73
|
-
Endpoints without provider selection are composed too, including handlers
|
|
74
|
-
passed through closures and local variables. Swagger `@Router` evidence can
|
|
75
|
-
root a handler factory behind a custom registry, while calls reached from a
|
|
76
|
-
`main → Run` assembly path become startup flows. These generated transport and
|
|
77
|
-
async flows carry their trigger kind and static-confidence level; `AddFunc`,
|
|
78
|
-
`AfterFunc`, and `Schedule` registrations become scheduled roots. A transport
|
|
79
|
-
fragment with no proven root is marked `unproven` instead of looking like a
|
|
80
|
-
complete scenario.
|
|
81
|
-
|
|
82
|
-
The older JSON keys `contexts` and `services` remain the wire format, so old
|
|
83
|
-
catalogs need no migration (portolan.0004). Optional `kind` fields say when those nodes should
|
|
84
|
-
be read as a neutral group and component. When `kind` is absent, the historical
|
|
85
|
-
`bounded-context` and `service` meanings apply.
|
|
86
|
-
|
|
87
|
-
The setup wizard always offers the neutral extractor. A language-specific DDD
|
|
88
|
-
extractor is selected only when its expected model structure is present; a
|
|
89
|
-
`go.mod` or a directory merely named `internal/domain` is not sufficient.
|
|
90
|
-
|
|
91
79
|
## What the site shows
|
|
92
80
|
|
|
93
81
|
- **Entity pages** — context, service, aggregate (entities, value objects,
|
|
@@ -160,9 +148,19 @@ npx portolan generate
|
|
|
160
148
|
npx portolan dev
|
|
161
149
|
```
|
|
162
150
|
|
|
163
|
-
`init`
|
|
164
|
-
|
|
165
|
-
|
|
151
|
+
`init` looks at the repository the way the site's Settings page does when a
|
|
152
|
+
project is added: it finds the directories that hold a build file, the domain
|
|
153
|
+
layouts, API specifications, schemas, migrations, ADRs and glossaries it can
|
|
154
|
+
read, and proposes a `portolan.json` with an extractor for each. In a terminal
|
|
155
|
+
it asks which directories are projects, what to read in each, and whether to
|
|
156
|
+
run `portolan generate` straight away; every question has the detected answer
|
|
157
|
+
as its default. With `--yes`, or without a terminal, it takes those defaults
|
|
158
|
+
and asks nothing. A plugin whose toolchain is not on `PATH` is pointed out
|
|
159
|
+
before anything is written.
|
|
160
|
+
|
|
161
|
+
`init` never overwrites an existing `portolan.json`. It adds `.portolan/` to
|
|
162
|
+
`.gitignore` and, when the repository has a `package.json`, adds these scripts
|
|
163
|
+
without replacing scripts that are already there:
|
|
166
164
|
|
|
167
165
|
```json
|
|
168
166
|
{
|
|
@@ -181,89 +179,30 @@ deployable static site.
|
|
|
181
179
|
|
|
182
180
|
| command | purpose |
|
|
183
181
|
| --- | --- |
|
|
184
|
-
| `portolan init` |
|
|
182
|
+
| `portolan init` | inspect the repository and write the first manifest; `--yes` takes every detected default |
|
|
185
183
|
| `portolan dev` | run the local site and setup UI |
|
|
186
184
|
| `portolan generate` | update fragments, documentation, and exports |
|
|
187
185
|
| `portolan check` | fail when committed generated files are stale, without writing them |
|
|
188
186
|
| `portolan build` | build the static site into `dist/` |
|
|
189
187
|
| `portolan diff BASE` | describe the architecture change from a branch, tag, or commit |
|
|
190
|
-
| `portolan doctor` | show which
|
|
191
|
-
|
|
192
|
-
Node.js 24 is required. A process extractor also needs the toolchain of the
|
|
193
|
-
language it reads: Go for the Go extractors, Python 3 for Python, Java 21 for
|
|
194
|
-
Java, and Cargo for Rust. `portolan doctor` reports the local set. The Docker
|
|
195
|
-
image contains all of them.
|
|
196
|
-
|
|
197
|
-
### Check pull requests with GitHub Actions
|
|
198
|
-
|
|
199
|
-
Generated files are checked rather than silently rewritten in CI:
|
|
200
|
-
|
|
201
|
-
```yaml
|
|
202
|
-
name: Architecture
|
|
203
|
-
on: [pull_request]
|
|
204
|
-
|
|
205
|
-
permissions:
|
|
206
|
-
contents: read
|
|
207
|
-
|
|
208
|
-
jobs:
|
|
209
|
-
check:
|
|
210
|
-
runs-on: ubuntu-latest
|
|
211
|
-
steps:
|
|
212
|
-
- uses: actions/checkout@v7
|
|
213
|
-
with:
|
|
214
|
-
fetch-depth: 0
|
|
215
|
-
- uses: shortlink-org/portolan@0.1.0
|
|
216
|
-
with:
|
|
217
|
-
command: check
|
|
218
|
-
version: 0.1.0
|
|
219
|
-
```
|
|
188
|
+
| `portolan doctor` | show which toolchains the manifest's plugins need and which are on `PATH` |
|
|
220
189
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
permissions:
|
|
234
|
-
contents: read
|
|
235
|
-
pages: write
|
|
236
|
-
id-token: write
|
|
237
|
-
|
|
238
|
-
jobs:
|
|
239
|
-
build:
|
|
240
|
-
runs-on: ubuntu-latest
|
|
241
|
-
steps:
|
|
242
|
-
- uses: actions/checkout@v7
|
|
243
|
-
- uses: shortlink-org/portolan@0.1.0
|
|
244
|
-
with:
|
|
245
|
-
command: build
|
|
246
|
-
version: 0.1.0
|
|
247
|
-
output: dist
|
|
248
|
-
base: /${{ github.event.repository.name }}/
|
|
249
|
-
- uses: actions/configure-pages@v6
|
|
250
|
-
- uses: actions/upload-pages-artifact@v5
|
|
251
|
-
with:
|
|
252
|
-
path: dist
|
|
253
|
-
|
|
254
|
-
deploy:
|
|
255
|
-
needs: build
|
|
256
|
-
runs-on: ubuntu-latest
|
|
257
|
-
environment:
|
|
258
|
-
name: github-pages
|
|
259
|
-
url: ${{ steps.deployment.outputs.page_url }}
|
|
260
|
-
steps:
|
|
261
|
-
- id: deployment
|
|
262
|
-
uses: actions/deploy-pages@v5
|
|
263
|
-
```
|
|
190
|
+
Node.js 24 is required, and for a repository the built-in extractors can
|
|
191
|
+
read on their own it is the only requirement: every Go plugin runs as one
|
|
192
|
+
wasm module over the workspace (`adr/0006`), the fetchers run inside the
|
|
193
|
+
host (`adr/0008`), and the package ships no Go at all. A Go, TypeScript,
|
|
194
|
+
OpenAPI, proto, SQL or GraphQL tree is read, and another repository or a
|
|
195
|
+
schema registry is vendored, with Node and git alone. An extractor that runs
|
|
196
|
+
in its own runtime still needs it: Python 3 for Django and Celery, Java 21
|
|
197
|
+
for Java, Cargo for Rust. `portolan doctor` reports what the manifest asks
|
|
198
|
+
for against what is on `PATH`. The Docker image contains all of them.
|
|
199
|
+
|
|
200
|
+
### Add delivery automation
|
|
264
201
|
|
|
265
|
-
|
|
266
|
-
|
|
202
|
+
While `portolan dev` is running, open **Settings → Delivery presets**. Portolan
|
|
203
|
+
detects GitHub or GitLab from the repository's `origin`, previews the exact CI
|
|
204
|
+
changes, and installs architecture checks and static catalog publishing in one
|
|
205
|
+
step. Existing unmanaged workflow files are never overwritten.
|
|
267
206
|
|
|
268
207
|
### Run without installing Node or language toolchains
|
|
269
208
|
|
|
@@ -276,7 +215,7 @@ docker run --rm \
|
|
|
276
215
|
-e HOME=/tmp \
|
|
277
216
|
-v "$PWD:/workspace" \
|
|
278
217
|
-w /workspace \
|
|
279
|
-
ghcr.io/shortlink-org/portolan:0.
|
|
218
|
+
ghcr.io/shortlink-org/portolan:0.2.0 generate
|
|
280
219
|
```
|
|
281
220
|
|
|
282
221
|
Use immutable versions in CI. `latest` is intended for trying the CLI, not for
|
|
@@ -310,78 +249,3 @@ Generated output is committed, so a change to it shows up in a diff. CI builds
|
|
|
310
249
|
the site (`npm run build`); the `--check` variants and the test suites are run
|
|
311
250
|
locally before a change lands, since they need the Go, Java, Rust and Python
|
|
312
251
|
toolchains the plugins are written in.
|
|
313
|
-
|
|
314
|
-
### Release Portolan
|
|
315
|
-
|
|
316
|
-
Give the repository an `NPM_TOKEN` Actions secret that can publish the
|
|
317
|
-
`@shortlink-org/portolan` package. Push a tag matching the version in
|
|
318
|
-
`package.json`, for example `0.1.0`. The release workflow verifies the package,
|
|
319
|
-
publishes it to npm with provenance, builds multi-platform container images at
|
|
320
|
-
`ghcr.io/shortlink-org/portolan`, attests the image, and creates or updates the
|
|
321
|
-
GitHub release notes. The tag itself is also the immutable version of the
|
|
322
|
-
composite action used by consumer repositories.
|
|
323
|
-
|
|
324
|
-
### In a pull request
|
|
325
|
-
|
|
326
|
-
`gen:check` says the documentation follows from the catalog; it does not say
|
|
327
|
-
what the change does. `.github/workflows/architecture-diff.yml` puts that on
|
|
328
|
-
the pull request itself, without ever failing it:
|
|
329
|
-
|
|
330
|
-
- one sticky comment with the events, endpoints, transitions and owners that
|
|
331
|
-
are not what they were, breaking first, ending in a link into the published
|
|
332
|
-
site's Changes page for the same pair of branches;
|
|
333
|
-
- the same list as SARIF in the checks tab, breaking as errors, each result
|
|
334
|
-
anchored at the fragment that declares the id, so code scanning shows it;
|
|
335
|
-
- the report again in the job summary, for whoever reads the run.
|
|
336
|
-
|
|
337
|
-
The comment is `scripts/forge-comment.mjs` over the markdown `npm run diff`
|
|
338
|
-
writes. It finds its own earlier comment by a marker and updates it in place.
|
|
339
|
-
`GITHUB_TOKEN` with `pull-requests: write` is enough on GitHub; a pull request
|
|
340
|
-
from a fork has a read-only token, and the step says so and stays green.
|
|
341
|
-
|
|
342
|
-
The Changes page itself compares the built catalog with any branch or tag the
|
|
343
|
-
forge lists, so the link in the comment works for a release just as well.
|
|
344
|
-
|
|
345
|
-
On GitLab the job token cannot write notes, so the job wants a project access
|
|
346
|
-
token with the `api` scope and the Reporter role as `PORTOLAN_TOKEN`:
|
|
347
|
-
|
|
348
|
-
```yaml
|
|
349
|
-
architecture-diff:
|
|
350
|
-
image: node:24
|
|
351
|
-
rules: [{ if: $CI_PIPELINE_SOURCE == "merge_request_event" }]
|
|
352
|
-
script:
|
|
353
|
-
- git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
|
|
354
|
-
- npm ci
|
|
355
|
-
- node scripts/diff.mjs "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" --site "$CI_PAGES_URL" --head "$CI_COMMIT_REF_NAME" --output .portolan/diff.md
|
|
356
|
-
- node scripts/forge-comment.mjs .portolan/diff.md
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
### In a release
|
|
360
|
-
|
|
361
|
-
`.github/workflows/release-notes.yml` runs the same diff when a `v*` tag is
|
|
362
|
-
pushed, against the tag before it, and `scripts/forge-release.mjs` writes it
|
|
363
|
-
into the release as one section between two markers: the rest of the notes,
|
|
364
|
-
hand-written or generated by the forge, stays as it was, and a rerun replaces
|
|
365
|
-
the section rather than repeating it. A tag with no release yet gets one,
|
|
366
|
-
named after the tag. The first tag of a repository has nothing to compare
|
|
367
|
-
against, and the job says so and stops. The same job on GitLab:
|
|
368
|
-
|
|
369
|
-
```yaml
|
|
370
|
-
architecture-release:
|
|
371
|
-
image: node:24
|
|
372
|
-
rules: [{ if: $CI_COMMIT_TAG }]
|
|
373
|
-
script:
|
|
374
|
-
- git fetch --tags origin
|
|
375
|
-
- npm ci
|
|
376
|
-
- previous=$(git describe --tags --abbrev=0 "$CI_COMMIT_TAG^")
|
|
377
|
-
- node scripts/diff.mjs "$previous" --site "$CI_PAGES_URL" --head "$CI_COMMIT_TAG" --output .portolan/release.md
|
|
378
|
-
- node scripts/forge-release.mjs .portolan/release.md
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
Three build-time variables shape the chat:
|
|
382
|
-
|
|
383
|
-
| variable | effect |
|
|
384
|
-
|---|---|
|
|
385
|
-
| `VITE_CHAT=off` | no chat at all: no button, no settings section, and its chunk is not built |
|
|
386
|
-
| `VITE_CHAT_PROXY_URL` | the worker that answers with a key of its own (see `proxy/README.md`); unset, the chat waits for the reader's own model |
|
|
387
|
-
| (a switch in Settings) | the reader turns the chat on or off in their browser; on by default when a proxy answers |
|
package/cli/init.mjs
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// `portolan init`: look at the repository, propose a manifest, write it.
|
|
2
|
+
//
|
|
3
|
+
// Detection is the same code the site's Settings page uses to add a project
|
|
4
|
+
// (scripts/local-api.mjs), so the CLI and the UI agree on what a repository
|
|
5
|
+
// contains. The prompts are the only thing this file adds; with `--yes`, or
|
|
6
|
+
// without a terminal, every question takes its default.
|
|
7
|
+
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { basename, posix, resolve } from "node:path";
|
|
11
|
+
|
|
12
|
+
import * as p from "@clack/prompts";
|
|
13
|
+
|
|
14
|
+
import { INSTALL_ROOT, builtinDefinition } from "../scripts/builtin-plugins.mjs";
|
|
15
|
+
import { discoverProject, manifestWithProject, planProject, writeManifest } from "../scripts/local-api.mjs";
|
|
16
|
+
|
|
17
|
+
const TOOLCHAINS = { go: "Go", cargo: "Cargo", java: "Java", python3: "Python 3" };
|
|
18
|
+
const VERSION_FLAGS = { go: ["version"], cargo: ["--version"], java: ["-version"], python3: ["--version"] };
|
|
19
|
+
|
|
20
|
+
export const SCRIPTS = {
|
|
21
|
+
"architecture": "portolan dev",
|
|
22
|
+
"architecture:gen": "portolan generate",
|
|
23
|
+
"architecture:check": "portolan check",
|
|
24
|
+
"architecture:build": "portolan build",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export class InitError extends Error {}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create portolan.json for `workspace`.
|
|
31
|
+
*
|
|
32
|
+
* `ask` answers the questions init has; `defaultAnswers` takes every default
|
|
33
|
+
* and `promptAnswers` asks a terminal. Returns what was done so the caller can
|
|
34
|
+
* continue with `portolan generate` when the user asked for it.
|
|
35
|
+
*/
|
|
36
|
+
export async function init(workspace, { version, ask = defaultAnswers, log = console.log } = {}) {
|
|
37
|
+
const manifestPath = resolve(workspace, "portolan.json");
|
|
38
|
+
if (existsSync(manifestPath)) throw new InitError("portolan.json already exists; init did not change it");
|
|
39
|
+
|
|
40
|
+
const projectPackage = readJson(resolve(workspace, "package.json"));
|
|
41
|
+
const repository = slug(String(projectPackage?.name ?? "").replace(/^@[^/]+\//, "")) || slug(basename(workspace)) || "project";
|
|
42
|
+
|
|
43
|
+
const scan = ask.scan?.() ?? null;
|
|
44
|
+
const discovery = discoverProject(workspace, ".");
|
|
45
|
+
scan?.done(discovery.filesScanned, discovery.truncated);
|
|
46
|
+
|
|
47
|
+
const roots = await ask.roots(rootChoices(discovery.components));
|
|
48
|
+
const single = roots.length === 1 && roots[0] === ".";
|
|
49
|
+
|
|
50
|
+
const projects = [];
|
|
51
|
+
const usedIds = new Set();
|
|
52
|
+
for (const root of roots) {
|
|
53
|
+
const found = root === "." ? discovery : discoverProject(workspace, root);
|
|
54
|
+
const id = single ? repository : uniqueId(root, usedIds);
|
|
55
|
+
const defaults = { id, name: titleOf(id), group: single ? id : repository, component: id };
|
|
56
|
+
const identity = single && found.detections.length ? await ask.identity(defaults) : defaults;
|
|
57
|
+
usedIds.add(identity.id);
|
|
58
|
+
|
|
59
|
+
const plugins = found.detections.length
|
|
60
|
+
? await ask.plugins(root, found.detections.map((detection) => ({ ...detection, requirement: toolchainFor(detection.plugin) })))
|
|
61
|
+
: [];
|
|
62
|
+
projects.push({ root, ...identity, plugins });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const missing = missingToolchains(projects.flatMap((project) => project.plugins));
|
|
66
|
+
for (const [label, plugins] of missing) ask.warn(`${plugins.join(", ")} need${plugins.length === 1 ? "s" : ""} ${label}, which is not on PATH; portolan doctor lists the toolchains`);
|
|
67
|
+
|
|
68
|
+
const title = single ? projects[0].name : titleOf(repository);
|
|
69
|
+
const manifest = buildManifest(workspace, projects, { version, title });
|
|
70
|
+
|
|
71
|
+
const rows = projects.map((project) => [project.id, project.root, project.plugins.length ? project.plugins.join(", ") : "project (nothing detected)"]);
|
|
72
|
+
const widths = [0, 1].map((column) => Math.max(...rows.map((row) => row[column].length)));
|
|
73
|
+
const summary = rows.map((row) => `${row[0].padEnd(widths[0])} ${row[1].padEnd(widths[1])} ${row[2]}`);
|
|
74
|
+
if (!(await ask.write(summary))) throw new InitError("cancelled; nothing was written");
|
|
75
|
+
writeManifest(manifestPath, manifest);
|
|
76
|
+
log("created portolan.json");
|
|
77
|
+
|
|
78
|
+
const scripts = projectPackage ? await ask.scripts(Object.keys(SCRIPTS).filter((name) => !(name in (projectPackage.scripts ?? {})))) : false;
|
|
79
|
+
if (scripts) {
|
|
80
|
+
const merged = { ...(projectPackage.scripts ?? {}) };
|
|
81
|
+
for (const [name, command] of Object.entries(SCRIPTS)) if (!(name in merged)) merged[name] = command;
|
|
82
|
+
writeFileSync(resolve(workspace, "package.json"), `${JSON.stringify({ ...projectPackage, scripts: merged }, null, 2)}\n`);
|
|
83
|
+
log("added architecture scripts to package.json");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const ignorePath = resolve(workspace, ".gitignore");
|
|
87
|
+
const ignore = existsSync(ignorePath) ? readFileSync(ignorePath, "utf8") : "";
|
|
88
|
+
if (!ignore.split(/\r?\n/).includes(".portolan/")) {
|
|
89
|
+
writeFileSync(ignorePath, `${ignore}${ignore && !ignore.endsWith("\n") ? "\n" : ""}\n# Portolan local build state\n.portolan/\n`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const generate = await ask.generate();
|
|
93
|
+
ask.finish(generate);
|
|
94
|
+
return { manifest, projects, generate };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Every question answered with its default: the `--yes` and CI path. */
|
|
98
|
+
export const defaultAnswers = {
|
|
99
|
+
roots: (choices) => choices.filter((choice) => choice.selected).map((choice) => choice.path),
|
|
100
|
+
identity: (defaults) => defaults,
|
|
101
|
+
plugins: (root, detections) => detections.filter((detection) => detection.selected && detection.confidence === "high").map((detection) => detection.plugin),
|
|
102
|
+
warn: (message) => console.warn(`portolan: ${message}`),
|
|
103
|
+
write: () => true,
|
|
104
|
+
scripts: (names) => names.length > 0,
|
|
105
|
+
generate: () => false,
|
|
106
|
+
finish: (generate) => { if (!generate) console.log("next: portolan generate && portolan dev"); },
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** The same questions, asked in a terminal with @clack/prompts. */
|
|
110
|
+
export function promptAnswers(version) {
|
|
111
|
+
p.intro(`portolan ${version}`);
|
|
112
|
+
let spinner;
|
|
113
|
+
return {
|
|
114
|
+
scan() {
|
|
115
|
+
spinner = p.spinner();
|
|
116
|
+
spinner.start("Scanning the repository");
|
|
117
|
+
return {
|
|
118
|
+
done(files, truncated) {
|
|
119
|
+
spinner.stop(`Scanned ${files} files${truncated ? " (stopped early; a large repository)" : ""}`);
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
async roots(choices) {
|
|
124
|
+
if (choices.length <= 1) return defaultAnswers.roots(choices);
|
|
125
|
+
return answer(await p.multiselect({
|
|
126
|
+
message: "Which directories are projects?",
|
|
127
|
+
options: choices.map((choice) => ({ value: choice.path, label: choice.path, hint: [choice.name, choice.technologies.join(", ")].filter(Boolean).join(" · ") })),
|
|
128
|
+
initialValues: defaultAnswers.roots(choices),
|
|
129
|
+
required: true,
|
|
130
|
+
}));
|
|
131
|
+
},
|
|
132
|
+
async identity(defaults) {
|
|
133
|
+
const id = answer(await p.text({
|
|
134
|
+
message: "Project id",
|
|
135
|
+
initialValue: defaults.id,
|
|
136
|
+
validate: (value) => (slug(value) ? undefined : "Use letters, digits and dashes."),
|
|
137
|
+
}));
|
|
138
|
+
const clean = slug(id);
|
|
139
|
+
return clean === defaults.id ? defaults : { ...defaults, id: clean, name: titleOf(clean), group: clean, component: clean };
|
|
140
|
+
},
|
|
141
|
+
async plugins(root, detections) {
|
|
142
|
+
const chosen = answer(await p.multiselect({
|
|
143
|
+
message: root === "." ? "What should Portolan read?" : `What should Portolan read in ${root}?`,
|
|
144
|
+
options: detections.map((detection) => ({
|
|
145
|
+
value: detection.plugin,
|
|
146
|
+
label: detection.plugin,
|
|
147
|
+
hint: [detection.evidence, detection.requirement?.missing ? `needs ${detection.requirement.label}, not on PATH` : ""].filter(Boolean).join(" · "),
|
|
148
|
+
})),
|
|
149
|
+
initialValues: defaultAnswers.plugins(root, detections),
|
|
150
|
+
required: true,
|
|
151
|
+
}));
|
|
152
|
+
return chosen;
|
|
153
|
+
},
|
|
154
|
+
warn: (message) => p.log.warn(message),
|
|
155
|
+
async write(summary) {
|
|
156
|
+
p.note(summary.join("\n"), "portolan.json");
|
|
157
|
+
return answer(await p.confirm({ message: "Write portolan.json?", initialValue: true }));
|
|
158
|
+
},
|
|
159
|
+
async scripts(names) {
|
|
160
|
+
if (!names.length) return false;
|
|
161
|
+
return answer(await p.confirm({ message: `Add ${names.join(", ")} to package.json scripts?`, initialValue: true }));
|
|
162
|
+
},
|
|
163
|
+
async generate() {
|
|
164
|
+
return answer(await p.confirm({ message: "Run portolan generate now?", initialValue: true }));
|
|
165
|
+
},
|
|
166
|
+
finish(generate) {
|
|
167
|
+
p.outro(generate ? "Generating; then: portolan dev" : "Next: portolan generate && portolan dev");
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function isInteractive(env = process.env) {
|
|
173
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY) && !env.CI;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function commandWorks(command, args) {
|
|
177
|
+
const result = spawnSync(command, args, { stdio: "ignore" });
|
|
178
|
+
return !result.error && result.status === 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function answer(value) {
|
|
182
|
+
if (p.isCancel(value)) {
|
|
183
|
+
p.cancel("init cancelled; nothing was written");
|
|
184
|
+
process.exit(0);
|
|
185
|
+
}
|
|
186
|
+
return value;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Directories that could be projects, with the default choice made: the
|
|
191
|
+
* repository root when it has a build file of its own, otherwise every nested
|
|
192
|
+
* directory that has one.
|
|
193
|
+
*/
|
|
194
|
+
function rootChoices(components) {
|
|
195
|
+
const rootHasMarkers = components.some((candidate) => candidate.path === ".");
|
|
196
|
+
const nested = components.filter((candidate) => candidate.path !== ".");
|
|
197
|
+
const choices = [
|
|
198
|
+
{ path: ".", name: "repository root", markers: [], technologies: [], ...components.find((candidate) => candidate.path === ".") },
|
|
199
|
+
...nested,
|
|
200
|
+
];
|
|
201
|
+
return choices.map((choice) => ({ ...choice, selected: choice.path === "." ? rootHasMarkers || nested.length === 0 : !rootHasMarkers }));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function buildManifest(workspace, projects, { version, title }) {
|
|
205
|
+
let manifest = {
|
|
206
|
+
$schema: `https://raw.githubusercontent.com/shortlink-org/portolan/${version}/schema/portolan.schema.json`,
|
|
207
|
+
sources: [],
|
|
208
|
+
projects: [],
|
|
209
|
+
extract: [],
|
|
210
|
+
};
|
|
211
|
+
for (const project of projects) {
|
|
212
|
+
if (project.plugins.length) {
|
|
213
|
+
const plan = planProject(workspace, manifest, {
|
|
214
|
+
root: project.root,
|
|
215
|
+
id: project.id,
|
|
216
|
+
name: project.name,
|
|
217
|
+
group: project.group,
|
|
218
|
+
component: project.component,
|
|
219
|
+
plugins: project.plugins,
|
|
220
|
+
});
|
|
221
|
+
manifest = manifestWithProject(manifest, plan);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
// Nothing to read yet; the project extractor still gives the site one node.
|
|
225
|
+
const out = posix.join(project.root, "portolan");
|
|
226
|
+
manifest.projects.push({ id: project.id, name: project.name, root: project.root, group: project.group, component: project.component });
|
|
227
|
+
manifest.sources.push(`${out}/*.json`);
|
|
228
|
+
manifest.extract.push({
|
|
229
|
+
plugin: "project",
|
|
230
|
+
in: project.root,
|
|
231
|
+
out,
|
|
232
|
+
options: { group: project.group, groupName: titleOf(project.group), component: project.component, componentName: project.name, out: "project.json" },
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
manifest.sources = [...new Set(manifest.sources)];
|
|
236
|
+
manifest.generate = [
|
|
237
|
+
{ plugin: "markdown", out: "docs", options: { title } },
|
|
238
|
+
{ plugin: "mermaid", out: "exports/mermaid", options: { title: `${title} flows` } },
|
|
239
|
+
];
|
|
240
|
+
return manifest;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const toolchainChecks = new Map();
|
|
244
|
+
|
|
245
|
+
export function toolchainFor(plugin, declared = null) {
|
|
246
|
+
const command = declared?.process?.command ?? builtinDefinition(plugin)?.process?.command;
|
|
247
|
+
const label = TOOLCHAINS[command];
|
|
248
|
+
if (!label) return null;
|
|
249
|
+
if (!toolchainChecks.has(command)) {
|
|
250
|
+
// The Rust extractor ships as a binary in the Docker image, and
|
|
251
|
+
// run-builtin uses it before it would ever ask for Cargo; doctor and init
|
|
252
|
+
// agree, or the image would report a toolchain it does not need.
|
|
253
|
+
const prebuilt = command === "cargo" && !declared && existsSync(resolve(INSTALL_ROOT, "plugins/extract-rust/target/release/portolan-extract-rust"));
|
|
254
|
+
toolchainChecks.set(command, prebuilt || commandWorks(command, VERSION_FLAGS[command]));
|
|
255
|
+
}
|
|
256
|
+
return { command, label, missing: !toolchainChecks.get(command) };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function missingToolchains(plugins) {
|
|
260
|
+
const missing = new Map();
|
|
261
|
+
for (const plugin of new Set(plugins)) {
|
|
262
|
+
const need = toolchainFor(plugin);
|
|
263
|
+
if (!need?.missing) continue;
|
|
264
|
+
missing.set(need.label, [...(missing.get(need.label) ?? []), plugin]);
|
|
265
|
+
}
|
|
266
|
+
return missing;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function uniqueId(root, used) {
|
|
270
|
+
const short = slug(posix.basename(root));
|
|
271
|
+
if (short && !used.has(short)) return short;
|
|
272
|
+
return slug(root) || `project-${used.size + 1}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function readJson(path) {
|
|
276
|
+
try { return JSON.parse(readFileSync(path, "utf8")); } catch { return null; }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function slug(value) {
|
|
280
|
+
return String(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function titleOf(value) {
|
|
284
|
+
return value.split("-").filter(Boolean).map((part) => part[0].toUpperCase() + part.slice(1)).join(" ");
|
|
285
|
+
}
|