@shortlink-org/portolan 0.2.1 → 0.2.3

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 (135) hide show
  1. package/README.md +7 -2
  2. package/cli/init.mjs +14 -6
  3. package/cli/init.test.mjs +8 -1
  4. package/cli/portolan.mjs +92 -13
  5. package/cli/portolan.test.mjs +47 -0
  6. package/package.json +7 -6
  7. package/plugins/extract-django/README.md +1 -1
  8. package/plugins/extract-django/extract_test.py +1 -1
  9. package/plugins/extract-django/openapi.py +4 -14
  10. package/plugins/extract-django/openapi_test.py +4 -4
  11. package/plugins/extract-go/options.schema.json +5 -0
  12. package/plugins/extract-project/options.schema.json +21 -0
  13. package/plugins/extract-ts/README.md +1 -1
  14. package/plugins/extract-ts/clients.ts +1 -1
  15. package/plugins/extract-ts/extract.test.ts +2 -2
  16. package/plugins/extract-ts/graphql.test.ts +2 -2
  17. package/plugins/extract-ts/openapi.test.ts +6 -7
  18. package/plugins/extract-ts/openapi.ts +3 -10
  19. package/plugins/portolan-go.wasm +0 -0
  20. package/public/404-cat-v1.webp +0 -0
  21. package/public/cat-about-v1.webp +0 -0
  22. package/public/cat-clear-skies-v1.webp +0 -0
  23. package/public/cat-no-changes-v1.webp +0 -0
  24. package/public/cat-onboarding-v1.webp +0 -0
  25. package/public/cat-trial-success-v1.webp +0 -0
  26. package/schema/portolan.schema.json +61 -0
  27. package/scripts/builtin-plugins.mjs +3 -2
  28. package/scripts/catalog-sources.mjs +14 -2
  29. package/scripts/catalog-sources.test.mjs +45 -0
  30. package/scripts/delivery-presets.mjs +207 -24
  31. package/scripts/diff.mjs +5 -1
  32. package/scripts/gen-likec4.mjs +242 -85
  33. package/scripts/gen-likec4.test.mjs +168 -14
  34. package/scripts/gen.mjs +5 -3
  35. package/scripts/host-plugins/fetch-git.mjs +22 -12
  36. package/scripts/host-plugins/fetch-git.test.mjs +25 -5
  37. package/scripts/local-api.mjs +330 -346
  38. package/scripts/local-api.test.mjs +245 -1
  39. package/scripts/local-discovery.mjs +378 -0
  40. package/scripts/manifest.mjs +42 -1
  41. package/scripts/manifest.test.mjs +24 -1
  42. package/scripts/plugin-host.mjs +14 -3
  43. package/scripts/plugin-host.test.mjs +8 -0
  44. package/scripts/run-builtin.mjs +4 -2
  45. package/scripts/schema.mjs +11 -0
  46. package/scripts/site-docs.mjs +2 -2
  47. package/src/app/CatalogApp.tsx +23 -8
  48. package/src/app/CommandPalette.tsx +7 -2
  49. package/src/app/Sidebar.tsx +13 -730
  50. package/src/app/SidebarFlowSections.tsx +251 -0
  51. package/src/app/SidebarFooter.tsx +160 -0
  52. package/src/app/SidebarTree.tsx +322 -0
  53. package/src/app/toast.tsx +11 -7
  54. package/src/catalog-index.ts +485 -0
  55. package/src/catalog-model.ts +1339 -0
  56. package/src/catalog-validation.ts +1570 -0
  57. package/src/catalog.test.ts +49 -0
  58. package/src/catalog.ts +6 -3124
  59. package/src/components/CatIllustration.tsx +30 -0
  60. package/src/components/CommitLink.tsx +154 -0
  61. package/src/components/EditorLink.tsx +15 -8
  62. package/src/components/GrpcMethodReference.tsx +93 -0
  63. package/src/components/Markdown.tsx +17 -0
  64. package/src/components/MethodRows.tsx +8 -1
  65. package/src/components/PageHeader.test.tsx +23 -0
  66. package/src/components/PageHeader.tsx +34 -0
  67. package/src/components/ProblemRow.tsx +3 -0
  68. package/src/components/ShapeRows.tsx +12 -0
  69. package/src/components/SourceDoc.tsx +27 -12
  70. package/src/components/SourcePreview.tsx +125 -37
  71. package/src/enrich.test.ts +209 -2
  72. package/src/enrich.ts +284 -15
  73. package/src/er/RedisSchema.tsx +86 -5
  74. package/src/flow/FlowTable.tsx +19 -4
  75. package/src/flow/StepDetail.tsx +461 -101
  76. package/src/flow/StepRail.tsx +36 -13
  77. package/src/flow/answers-response.test.ts +100 -0
  78. package/src/flow/answers.ts +11 -8
  79. package/src/flow/chapters.ts +2 -1
  80. package/src/flow/labels.ts +10 -0
  81. package/src/flow/mermaid.test.ts +87 -3
  82. package/src/flow/mermaid.ts +26 -14
  83. package/src/index.css +132 -17
  84. package/src/landing/LandingPage.tsx +16 -12
  85. package/src/lib/all-problems.ts +1 -1
  86. package/src/lib/api.test.ts +4 -6
  87. package/src/lib/build-info.test.ts +29 -0
  88. package/src/lib/build-info.ts +21 -0
  89. package/src/lib/data-model.ts +14 -0
  90. package/src/lib/derive.ts +1 -0
  91. package/src/lib/flow-tree.ts +1 -1
  92. package/src/lib/github-catalog.test.ts +51 -0
  93. package/src/lib/github-catalog.ts +69 -0
  94. package/src/lib/grpc-reference.test.ts +72 -0
  95. package/src/lib/grpc-reference.ts +117 -0
  96. package/src/lib/local-api.ts +36 -11
  97. package/src/lib/motion.test.ts +4 -2
  98. package/src/lib/motion.tsx +5 -4
  99. package/src/lib/product.ts +7 -0
  100. package/src/lib/proto-problems.test.ts +170 -3
  101. package/src/lib/proto-problems.ts +176 -4
  102. package/src/lib/queries.ts +16 -1
  103. package/src/lib/readme-assets.test.ts +19 -0
  104. package/src/lib/readme-assets.ts +34 -0
  105. package/src/lib/setup-info.test.ts +7 -0
  106. package/src/lib/setup-info.ts +5 -0
  107. package/src/lib/source-link.test.ts +106 -18
  108. package/src/lib/source-link.ts +22 -6
  109. package/src/merge.test.ts +101 -0
  110. package/src/merge.ts +65 -1
  111. package/src/pages/AdrDetail.tsx +3 -2
  112. package/src/pages/AdrIndex.tsx +10 -7
  113. package/src/pages/AggregatePage.tsx +34 -4
  114. package/src/pages/BlockPage.tsx +39 -32
  115. package/src/pages/Changes.tsx +7 -9
  116. package/src/pages/ContextMap.tsx +11 -2
  117. package/src/pages/ContextPage.tsx +11 -4
  118. package/src/pages/FlowIndex.tsx +16 -7
  119. package/src/pages/GraphPage.tsx +21 -7
  120. package/src/pages/Language.tsx +9 -9
  121. package/src/pages/NotFound.tsx +58 -15
  122. package/src/pages/Overview.tsx +6 -0
  123. package/src/pages/Problems.tsx +10 -8
  124. package/src/pages/RegistryIndex.tsx +10 -8
  125. package/src/pages/ServicePage.tsx +1 -1
  126. package/src/pages/Settings.tsx +257 -44
  127. package/src/pages/settings/AboutSettings.tsx +129 -0
  128. package/src/pages/settings/DeliverySettings.tsx +71 -15
  129. package/src/routes.test.ts +2 -0
  130. package/src/routes.ts +2 -1
  131. package/src/selection/DetailPanel.tsx +213 -113
  132. package/src/selection/pages.test.ts +14 -1
  133. package/src/selection/pages.ts +9 -3
  134. package/src/testing/setup.ts +14 -0
  135. package/vite.config.ts +3 -1
@@ -0,0 +1,129 @@
1
+ import { ArrowRight, BookOpen, CircleDot, GitBranch, ShieldCheck } from "lucide-react";
2
+ import type { Variants } from "motion/react";
3
+ import { Link } from "react-router";
4
+
5
+ import { CatIllustration } from "../../components/CatIllustration";
6
+ import { m, transitions } from "../../lib/motion";
7
+ import {
8
+ PRODUCT_ISSUES,
9
+ PRODUCT_LICENSE,
10
+ PRODUCT_README,
11
+ PRODUCT_REPOSITORY,
12
+ } from "../../lib/product";
13
+ import { paths } from "../../routes";
14
+
15
+ const ABOUT_CARD_MOTION: Variants = {
16
+ hidden: { opacity: 0, y: 8 },
17
+ shown: {
18
+ opacity: 1,
19
+ y: 0,
20
+ transition: { ...transitions.page, staggerChildren: 0.05 },
21
+ },
22
+ hover: {},
23
+ };
24
+
25
+ const ABOUT_COPY_MOTION: Variants = {
26
+ hidden: { opacity: 0, y: 6 },
27
+ shown: { opacity: 1, y: 0, transition: transitions.panel },
28
+ };
29
+
30
+ const ABOUT_CAT_MOTION: Variants = {
31
+ hidden: { opacity: 0, x: 18, rotate: 3 },
32
+ shown: {
33
+ opacity: 1,
34
+ x: 0,
35
+ rotate: 0,
36
+ transition: transitions.narrative,
37
+ },
38
+ hover: {
39
+ y: -7,
40
+ rotate: -2,
41
+ scale: 1.035,
42
+ transition: transitions.settle,
43
+ },
44
+ };
45
+
46
+ export function AboutSettings() {
47
+ return (
48
+ <section>
49
+ <div className="grid gap-grid lg:grid-cols-[minmax(0,1.25fr)_minmax(17rem,0.75fr)]">
50
+ <m.div
51
+ data-about-card
52
+ className="card grid items-center gap-4 sm:grid-cols-[minmax(0,1fr)_180px]"
53
+ variants={ABOUT_CARD_MOTION}
54
+ initial="hidden"
55
+ animate="shown"
56
+ whileHover="hover"
57
+ >
58
+ <m.div
59
+ className="flex flex-col items-start"
60
+ variants={ABOUT_COPY_MOTION}
61
+ >
62
+ <p className="max-w-prose text-muted">
63
+ Architecture from code and evidence. Product source,
64
+ documentation, releases and issue tracking live in the public
65
+ GitHub repository.
66
+ </p>
67
+ <div className="mt-5 flex flex-wrap gap-2">
68
+ <Link to={paths.landing()} className="product-primary">
69
+ Product page <ArrowRight size={15} aria-hidden />
70
+ </Link>
71
+ <a
72
+ href={PRODUCT_REPOSITORY}
73
+ target="_blank"
74
+ rel="noreferrer"
75
+ className="tbtn text-ink"
76
+ >
77
+ <GitBranch size={15} aria-hidden /> View product on GitHub
78
+ </a>
79
+ </div>
80
+ </m.div>
81
+ <m.div
82
+ data-about-cat
83
+ className="h-44 w-full justify-self-center sm:h-52"
84
+ variants={ABOUT_CAT_MOTION}
85
+ >
86
+ <CatIllustration scene="about" className="h-full w-full" />
87
+ </m.div>
88
+ </m.div>
89
+ <m.div
90
+ className="card"
91
+ initial={{ opacity: 0, y: 8 }}
92
+ animate={{ opacity: 1, y: 0 }}
93
+ transition={{ ...transitions.page, delay: 0.06 }}
94
+ >
95
+ <div className="label mb-2">resources</div>
96
+ <div className="divide-y divide-line">
97
+ <a
98
+ href={PRODUCT_README}
99
+ target="_blank"
100
+ rel="noreferrer"
101
+ className="flex items-center gap-2 py-2.5 text-muted transition-colors hover:text-ink"
102
+ >
103
+ <BookOpen size={15} aria-hidden className="shrink-0" />
104
+ Documentation
105
+ </a>
106
+ <a
107
+ href={PRODUCT_ISSUES}
108
+ target="_blank"
109
+ rel="noreferrer"
110
+ className="flex items-center gap-2 py-2.5 text-muted transition-colors hover:text-ink"
111
+ >
112
+ <CircleDot size={15} aria-hidden className="shrink-0" />
113
+ Issues and feedback
114
+ </a>
115
+ <a
116
+ href={PRODUCT_LICENSE}
117
+ target="_blank"
118
+ rel="noreferrer"
119
+ className="flex items-center gap-2 py-2.5 text-muted transition-colors hover:text-ink"
120
+ >
121
+ <ShieldCheck size={15} aria-hidden className="shrink-0" />
122
+ MIT License
123
+ </a>
124
+ </div>
125
+ </m.div>
126
+ </div>
127
+ </section>
128
+ );
129
+ }
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useEffect, useState } from "react";
2
+ import { Switch } from "@headlessui/react";
2
3
  import {
3
- Check,
4
4
  ChevronDown,
5
5
  CircleAlert,
6
6
  FolderGit2,
@@ -14,6 +14,7 @@ import {
14
14
  previewDeliveryPreset,
15
15
  } from "../../lib/local-api";
16
16
  import type {
17
+ DeliveryFeatureId,
17
18
  DeliveryPreset,
18
19
  DeliveryProvider,
19
20
  } from "../../lib/local-api";
@@ -24,6 +25,7 @@ const STATUS_CLASS: Record<
24
25
  > = {
25
26
  added: "status-verified",
26
27
  changed: "status-declared",
28
+ removed: "status-unresolved",
27
29
  unchanged: "status-verified",
28
30
  conflict: "status-unresolved",
29
31
  };
@@ -35,12 +37,12 @@ export function DeliverySettings({ local }: { local: boolean }) {
35
37
  const [error, setError] = useState("");
36
38
  const say = useToastStore((state) => state.say);
37
39
 
38
- const load = useCallback(async (provider?: DeliveryProvider) => {
40
+ const load = useCallback(async (provider?: DeliveryProvider, features?: DeliveryFeatureId[]) => {
39
41
  if (!local) return;
40
42
  setLoading(true);
41
43
  setError("");
42
44
  try {
43
- setPlan(await previewDeliveryPreset(provider));
45
+ setPlan(await previewDeliveryPreset(provider, features));
44
46
  } catch (cause) {
45
47
  setError(cause instanceof Error ? cause.message : String(cause));
46
48
  } finally {
@@ -60,6 +62,7 @@ export function DeliverySettings({ local }: { local: boolean }) {
60
62
  const installed = await installDeliveryPreset(
61
63
  plan.provider,
62
64
  plan.revision,
65
+ plan.features.filter((feature) => feature.selected).map((feature) => feature.id),
63
66
  );
64
67
  setPlan(installed);
65
68
  say(
@@ -76,6 +79,23 @@ export function DeliverySettings({ local }: { local: boolean }) {
76
79
  }
77
80
  }
78
81
 
82
+ function selectFeature(id: DeliveryFeatureId) {
83
+ if (!plan) return;
84
+ const feature = plan.features.find((item) => item.id === id);
85
+ if (!feature?.available) return;
86
+ const selected = new Set(
87
+ plan.features.filter((item) => item.selected).map((item) => item.id),
88
+ );
89
+ if (selected.has(id)) {
90
+ selected.delete(id);
91
+ if (id === "diff") selected.delete("sarif");
92
+ } else {
93
+ selected.add(id);
94
+ for (const required of feature.requires) selected.add(required);
95
+ }
96
+ void load(plan.provider, [...selected]);
97
+ }
98
+
79
99
  if (!local) {
80
100
  return (
81
101
  <div className="empty">
@@ -96,8 +116,8 @@ export function DeliverySettings({ local }: { local: boolean }) {
96
116
  <FolderGit2 size={16} className="text-accent" /> Delivery automation
97
117
  </div>
98
118
  <p className="mt-1 max-w-prose text-muted">
99
- Install review checks and static catalog publishing directly into
100
- this repository.
119
+ Choose the automation this repository needs, preview the generated
120
+ jobs, then install them together.
101
121
  </p>
102
122
  </div>
103
123
  <div
@@ -111,7 +131,12 @@ export function DeliverySettings({ local }: { local: boolean }) {
111
131
  type="button"
112
132
  disabled={loading || installing}
113
133
  aria-pressed={plan?.provider === provider}
114
- onClick={() => void load(provider)}
134
+ onClick={() => void load(
135
+ provider,
136
+ plan?.features
137
+ .filter((feature) => feature.selected && (feature.id !== "sarif" || provider === "github"))
138
+ .map((feature) => feature.id),
139
+ )}
115
140
  className={`capitalize ${plan?.provider === provider ? "is-on" : ""}`}
116
141
  >
117
142
  {provider === "github" ? "GitHub" : "GitLab"}
@@ -157,15 +182,46 @@ export function DeliverySettings({ local }: { local: boolean }) {
157
182
  )}
158
183
  </div>
159
184
 
160
- <ul className="mt-4 grid gap-2 text-muted sm:grid-cols-2">
185
+ <div className="mt-4 grid gap-2 sm:grid-cols-2">
161
186
  {plan.features.map((feature) => (
162
- <li key={feature} className="flex items-center gap-2">
163
- <Check size={14} className="shrink-0 text-verified" /> {feature}
164
- </li>
187
+ <div
188
+ key={feature.id}
189
+ className={`flex items-start gap-3 rounded-control border px-3 py-3 transition-colors ${
190
+ feature.available
191
+ ? feature.selected
192
+ ? "border-accent bg-surface"
193
+ : "border-line hover:bg-surface"
194
+ : "border-line opacity-60"
195
+ }`}
196
+ >
197
+ <span className="min-w-0 flex-1">
198
+ <span className="block font-medium text-ink">{feature.label}</span>
199
+ <span className="mt-0.5 block text-muted">{feature.description}</span>
200
+ {!feature.available ? (
201
+ <span className="mt-1 block text-faint">Available for GitHub only.</span>
202
+ ) : feature.requires.length > 0 ? (
203
+ <span className="mt-1 block text-faint">
204
+ Enables {feature.requires.map((required) => plan.features.find((item) => item.id === required)?.label).join(", ")}.
205
+ </span>
206
+ ) : null}
207
+ </span>
208
+ <Switch
209
+ checked={feature.selected}
210
+ disabled={!feature.available || loading || installing}
211
+ onChange={() => selectFeature(feature.id)}
212
+ aria-label={feature.label}
213
+ className="group mt-0.5 inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-line-strong bg-surface transition-colors outline-none data-checked:border-accent data-checked:bg-accent focus-visible:ring-2 focus-visible:ring-accent/40 disabled:cursor-not-allowed"
214
+ >
215
+ <span
216
+ aria-hidden
217
+ className="size-3.5 translate-x-0.5 rounded-full bg-muted shadow-xs transition-transform group-data-checked:translate-x-[18px] group-data-checked:bg-canvas"
218
+ />
219
+ </Switch>
220
+ </div>
165
221
  ))}
166
- </ul>
222
+ </div>
167
223
 
168
- <div className="mt-4 divide-y divide-line overflow-hidden rounded-control border border-line">
224
+ {plan.files.length > 0 ? <div className="mt-4 divide-y divide-line overflow-hidden rounded-control border border-line">
169
225
  {plan.files.map((file) => (
170
226
  <details
171
227
  key={file.path}
@@ -194,7 +250,7 @@ export function DeliverySettings({ local }: { local: boolean }) {
194
250
  </div>
195
251
  </details>
196
252
  ))}
197
- </div>
253
+ </div> : <div className="empty mt-4">No delivery jobs selected.</div>}
198
254
 
199
255
  {error ? <p className="mt-3 text-unresolved">{error}</p> : null}
200
256
  <div className="mt-4 flex flex-wrap items-center justify-between gap-3">
@@ -204,12 +260,12 @@ export function DeliverySettings({ local }: { local: boolean }) {
204
260
  </p>
205
261
  {plan.status === "installed" ? (
206
262
  <span className="flex items-center gap-1.5 text-verified">
207
- <ShieldCheck size={15} /> Preset installed
263
+ <ShieldCheck size={15} /> Selection installed
208
264
  </span>
209
265
  ) : (
210
266
  <button
211
267
  type="button"
212
- className="btn-accent"
268
+ className="product-primary"
213
269
  disabled={installing || conflicts.length > 0}
214
270
  onClick={() => void install()}
215
271
  >
@@ -170,11 +170,13 @@ describe("routes", () => {
170
170
  expect(paths.settingsPipeline()).toBe("/settings/pipeline");
171
171
  expect(paths.settingsDelivery()).toBe("/settings/delivery");
172
172
  expect(paths.settingsPreferences()).toBe("/settings/preferences");
173
+ expect(paths.settingsAbout()).toBe("/settings/about");
173
174
  for (const path of [
174
175
  paths.settingsProjects(),
175
176
  paths.settingsPipeline(),
176
177
  paths.settingsDelivery(),
177
178
  paths.settingsPreferences(),
179
+ paths.settingsAbout(),
178
180
  ]) expect(isRoutable(path)).toBe(true);
179
181
  expect(isRoutable("/settings/nope")).toBe(false);
180
182
  expect(allCatalogPaths(catalog)).toContain(paths.settings());
package/src/routes.ts CHANGED
@@ -46,6 +46,7 @@ export const paths = {
46
46
  settingsPipeline: () => "/settings/pipeline",
47
47
  settingsDelivery: () => "/settings/delivery",
48
48
  settingsPreferences: () => "/settings/preferences",
49
+ settingsAbout: () => "/settings/about",
49
50
  external: (slug: string) => `/externals/${slug}`,
50
51
  registry: () => "/registry",
51
52
  /**
@@ -374,7 +375,7 @@ const ROUTES: RegExp[] = [
374
375
  /^\/language$/,
375
376
  /^\/problems$/,
376
377
  /^\/changes$/,
377
- /^\/settings(?:\/(?:projects|pipeline|delivery|preferences))?$/,
378
+ /^\/settings(?:\/(?:projects|pipeline|delivery|preferences|about))?$/,
378
379
  /^\/externals\/[^/]+$/,
379
380
  /^\/map$/,
380
381
  /^\/adrs\/[^/]+$/,