@pathmx/core 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/LICENSE.md +172 -0
  2. package/README.md +111 -0
  3. package/actions/actions.ts +307 -0
  4. package/actions/draft.ts +105 -0
  5. package/actions/transaction.ts +164 -0
  6. package/assets.ts +149 -0
  7. package/authority/access.ts +820 -0
  8. package/authority/actors.ts +271 -0
  9. package/authority/agents.ts +95 -0
  10. package/authority/credentials.ts +144 -0
  11. package/authority/model.ts +71 -0
  12. package/authority/sharing.ts +89 -0
  13. package/browser-bundle.ts +61 -0
  14. package/canonical.ts +41 -0
  15. package/cli/dev.ts +134 -0
  16. package/cli/export.ts +74 -0
  17. package/cli/index.ts +52 -0
  18. package/cli/lint.ts +164 -0
  19. package/cli/mv.ts +30 -0
  20. package/cli/perms.ts +182 -0
  21. package/cli/plugin-packages.ts +159 -0
  22. package/cli/plugins.ts +95 -0
  23. package/cli/query.ts +225 -0
  24. package/cli/readme.md +95 -0
  25. package/cli/release.ts +75 -0
  26. package/cli/rm.ts +22 -0
  27. package/cli/serve.ts +100 -0
  28. package/cli/tree.ts +131 -0
  29. package/cli/update.ts +78 -0
  30. package/cli/utils.ts +53 -0
  31. package/core.ts +96 -0
  32. package/database.ts +189 -0
  33. package/dependencies.ts +60 -0
  34. package/dist/pathmx.js +1224 -0
  35. package/document.ts +194 -0
  36. package/environment.ts +110 -0
  37. package/environments/bun-plugins.ts +68 -0
  38. package/environments/bun-publish.ts +194 -0
  39. package/environments/bun.ts +223 -0
  40. package/globals.d.ts +14 -0
  41. package/graph.ts +153 -0
  42. package/host/compiler.ts +314 -0
  43. package/host/create.ts +66 -0
  44. package/host/engine.ts +630 -0
  45. package/host/plugin-host.ts +487 -0
  46. package/host/render.ts +102 -0
  47. package/host/response.ts +20 -0
  48. package/host/shell.html +15 -0
  49. package/host/source-sync.ts +254 -0
  50. package/html-attributes.ts +15 -0
  51. package/html.ts +48 -0
  52. package/icons/LICENSE +43 -0
  53. package/icons/lucide.ts +54 -0
  54. package/index.ts +75 -0
  55. package/invalidation.ts +85 -0
  56. package/log.ts +84 -0
  57. package/ops/href.ts +87 -0
  58. package/ops/index.ts +9 -0
  59. package/ops/move-source.ts +114 -0
  60. package/ops/plan.ts +47 -0
  61. package/ops/remove-source.ts +70 -0
  62. package/package.json +46 -0
  63. package/path.ts +75 -0
  64. package/pattern.ts +314 -0
  65. package/perf.ts +88 -0
  66. package/plugins/actor-path.ts +43 -0
  67. package/plugins/actor-profile.ts +55 -0
  68. package/plugins/agent-browsing.ts +183 -0
  69. package/plugins/app-component.ts +23 -0
  70. package/plugins/block-layout/index.ts +78 -0
  71. package/plugins/block-layout/theme.css +90 -0
  72. package/plugins/callout/index.ts +236 -0
  73. package/plugins/callout/theme.css +138 -0
  74. package/plugins/canonical-links.ts +144 -0
  75. package/plugins/code-highlight/client.ts +89 -0
  76. package/plugins/code-highlight/index.ts +147 -0
  77. package/plugins/code-highlight/theme.css +89 -0
  78. package/plugins/context.ts +131 -0
  79. package/plugins/datatype/Datatype[wdth,wght].woff2 +0 -0
  80. package/plugins/datatype/OFL.txt +96 -0
  81. package/plugins/datatype/index.ts +287 -0
  82. package/plugins/definition-list.ts +236 -0
  83. package/plugins/directive-definitions.ts +13 -0
  84. package/plugins/embedding/client.ts +88 -0
  85. package/plugins/embedding/index.ts +43 -0
  86. package/plugins/image/index.ts +68 -0
  87. package/plugins/image/theme.css +43 -0
  88. package/plugins/include.ts +119 -0
  89. package/plugins/index.ts +91 -0
  90. package/plugins/input/authoring.ts +236 -0
  91. package/plugins/input/config.ts +85 -0
  92. package/plugins/input/index.ts +359 -0
  93. package/plugins/input/model.ts +173 -0
  94. package/plugins/input/state.ts +272 -0
  95. package/plugins/layout.ts +313 -0
  96. package/plugins/literate/client.ts +208 -0
  97. package/plugins/literate/index.ts +742 -0
  98. package/plugins/math/index.ts +140 -0
  99. package/plugins/meta.ts +42 -0
  100. package/plugins/navigation/index.ts +158 -0
  101. package/plugins/navigation/model.ts +364 -0
  102. package/plugins/preset.ts +65 -0
  103. package/plugins/query/index.ts +322 -0
  104. package/plugins/rss/index.ts +46 -0
  105. package/plugins/runtime/actions.ts +62 -0
  106. package/plugins/runtime/client.ts +8 -0
  107. package/plugins/runtime/document.ts +506 -0
  108. package/plugins/runtime/error.ts +154 -0
  109. package/plugins/runtime/heading-links.ts +31 -0
  110. package/plugins/runtime/index.ts +104 -0
  111. package/plugins/runtime/live.ts +51 -0
  112. package/plugins/runtime/navigation.ts +113 -0
  113. package/plugins/runtime/pending.ts +53 -0
  114. package/plugins/runtime/publication.ts +19 -0
  115. package/plugins/runtime/start.ts +177 -0
  116. package/plugins/runtime/static-client.ts +6 -0
  117. package/plugins/runtime/static-live.ts +188 -0
  118. package/plugins/runtime/view-transition.ts +72 -0
  119. package/plugins/runtime/viewport.ts +110 -0
  120. package/plugins/schema.ts +24 -0
  121. package/plugins/scripts.ts +140 -0
  122. package/plugins/source-preview/client.ts +256 -0
  123. package/plugins/source-preview/index.ts +132 -0
  124. package/plugins/source-preview/model.ts +11 -0
  125. package/plugins/source-preview/preview.css +107 -0
  126. package/plugins/styles.ts +145 -0
  127. package/plugins/theme/components.ts +10 -0
  128. package/plugins/theme/foundation.css +358 -0
  129. package/plugins/theme/index.ts +333 -0
  130. package/plugins/theme/prose.css +284 -0
  131. package/plugins/theme/scheme.js +63 -0
  132. package/plugins/theme/toggle.css +44 -0
  133. package/plugins/theme/view-transition.css +68 -0
  134. package/plugins/toc/client.ts +146 -0
  135. package/plugins/toc/index.ts +232 -0
  136. package/plugins/toc/theme.css +172 -0
  137. package/plugins/types.ts +538 -0
  138. package/problem.ts +41 -0
  139. package/project.ts +6 -0
  140. package/publish/build.ts +402 -0
  141. package/publish/concurrent.ts +32 -0
  142. package/publish/identity.ts +63 -0
  143. package/publish/index.ts +11 -0
  144. package/publish/location.ts +167 -0
  145. package/publish/model.ts +58 -0
  146. package/publish/plan.ts +300 -0
  147. package/publish/validate.ts +150 -0
  148. package/query/candidate-index.ts +101 -0
  149. package/query/dependency-index.ts +99 -0
  150. package/query/fields.ts +216 -0
  151. package/query/invalidation.ts +48 -0
  152. package/query/kernel.ts +776 -0
  153. package/query/model.ts +79 -0
  154. package/repository.ts +374 -0
  155. package/runtime-profile.ts +34 -0
  156. package/schema/model.ts +92 -0
  157. package/schema/registry.ts +642 -0
  158. package/schema/standard.ts +226 -0
  159. package/server/context.ts +94 -0
  160. package/server/css-imports.ts +66 -0
  161. package/server/http.ts +333 -0
  162. package/server/page.ts +21 -0
  163. package/server/protocols.ts +12 -0
  164. package/server/representations.ts +66 -0
  165. package/server/router.ts +131 -0
  166. package/server/routes.ts +202 -0
  167. package/server/serve.ts +5 -0
  168. package/server/watch.ts +120 -0
  169. package/source/blocks.ts +71 -0
  170. package/source/format.ts +164 -0
  171. package/source/index.ts +67 -0
  172. package/source/interpolate.ts +63 -0
  173. package/source/markdown/blocks.ts +46 -0
  174. package/source/markdown/comments.ts +32 -0
  175. package/source/markdown/fences.ts +67 -0
  176. package/source/markdown/headings.ts +88 -0
  177. package/source/markdown/index.ts +34 -0
  178. package/source/markdown/inline-code.ts +6 -0
  179. package/source/markdown/links.ts +326 -0
  180. package/source/markdown/lists.ts +223 -0
  181. package/source/markdown/prose.ts +74 -0
  182. package/source/markdown/scan.ts +79 -0
  183. package/source/markdown/text.ts +82 -0
  184. package/source/meta.ts +87 -0
  185. package/source/source.ts +121 -0
  186. package/source/topmatter.ts +57 -0
  187. package/source/utils.ts +21 -0
  188. package/source-diagnostic.ts +63 -0
  189. package/text-file.ts +26 -0
  190. package/util.ts +35 -0
  191. package/view/components.ts +499 -0
  192. package/view/model.ts +91 -0
  193. package/view/projection.ts +162 -0
  194. package/view/session.ts +146 -0
  195. package/view/syntax.ts +372 -0
package/LICENSE.md ADDED
@@ -0,0 +1,172 @@
1
+ # PathMX Proprietary Beta License
2
+
3
+ Version 0.1 — July 21, 2026
4
+
5
+ Copyright © 2026 DUO Interactive, LLC.
6
+ All rights reserved.
7
+
8
+ This license governs the first-party PathMX software distributed in the npm
9
+ package `@pathmx/core`, including its command-line interface, bundled
10
+ library surfaces, runtime and Player components, and accompanying
11
+ documentation (the “Software”). DUO Interactive, LLC is the “Licensor.” This
12
+ license does not cover the private PathMX source-code repository or separately
13
+ identified third-party materials.
14
+
15
+ “Project” means Paths, Sources, responses, data, and other content authored or
16
+ processed with the Software.
17
+
18
+ “Application” means software or a service created by you in which the Software
19
+ supports product-specific functionality but is not itself offered as a
20
+ general-purpose PathMX product.
21
+
22
+ “Redistributable Components” means runtime, Player, or library portions of the
23
+ Software incorporated into a Project, Application, or generated output through
24
+ ordinary use of the Software or ordinary compilation and bundling tools.
25
+
26
+ “Authorized Users” means your employees and individual contractors who use the
27
+ Software on your behalf, and your hosting, continuous-integration, and other
28
+ service providers that possess or run necessary copies solely to provide
29
+ services to you.
30
+
31
+ ## 1. Agreement
32
+
33
+ By installing, accessing, or using the Software, you agree to this license. If
34
+ you use the Software on behalf of an organization, you represent that you have
35
+ authority to accept this license for that organization. If you do not agree,
36
+ do not install or use the Software.
37
+
38
+ ## 2. License grant
39
+
40
+ Subject to this license, the Licensor grants you a non-exclusive, worldwide,
41
+ royalty-free, non-transferable, and non-sublicensable license, perpetual for
42
+ the version of the Software to which it applies unless terminated under
43
+ Section 10, to:
44
+
45
+ - install, execute, and use the Software for personal, educational,
46
+ internal-business, or commercial purposes;
47
+ - use the Software to author, integrate, build, test, serve, play, host, and
48
+ operate Projects and Applications;
49
+ - import, compile, bundle, configure, and integrate the Software through its
50
+ documented library and command-line interfaces;
51
+ - create and distribute independent Projects, Applications, plugins, and
52
+ integrations that use those interfaces without modifying the Software
53
+ itself; and
54
+ - make copies reasonably necessary for local development, testing, continuous
55
+ integration, backup, and hosted production environments.
56
+
57
+ Your Authorized Users may exercise those rights solely on your behalf. You are
58
+ responsible for their compliance with this license.
59
+
60
+ You may reproduce and distribute Redistributable Components only as
61
+ incorporated into your Project, Application, or generated output, and only when
62
+ you preserve applicable license and proprietary notices. You may host and
63
+ commercially provide access to that Project, Application, or output. You may
64
+ not distribute Redistributable Components as a standalone package or as a
65
+ general-purpose substitute for the Software.
66
+
67
+ Each lawful recipient of a Redistributable Component may execute and use that
68
+ component, display its output, and make technically necessary transitory copies
69
+ solely as incorporated into the Project, Application, or output in which they
70
+ received it. This limited recipient permission does not include a right to
71
+ extract, modify, or redistribute the component separately.
72
+
73
+ ## 3. Restrictions
74
+
75
+ Except where applicable law does not allow the restriction, you may not:
76
+
77
+ - modify, adapt, translate, or create derivative works of the Software, except
78
+ for the compilation, bundling, configuration, and integration expressly
79
+ permitted in Section 2;
80
+ - redistribute, sublicense, sell, lease, or otherwise provide the Software to
81
+ another person as a standalone package, product, or service except to an
82
+ Authorized User or as a Redistributable Component under Section 2;
83
+ - resell, rebrand, or provide the Software as a hosted or managed service that
84
+ gives third parties general-purpose access to substantial PathMX authoring,
85
+ building, serving, Player-administration, or library functionality for their
86
+ own unrelated projects;
87
+ - reverse engineer, decompile, disassemble, or otherwise attempt to derive the
88
+ source code of the Software; or
89
+ - remove or alter copyright, license, trademark, or other proprietary notices.
90
+
91
+ For clarity, the managed-service restriction does not prohibit a hosted
92
+ Project or Application in which PathMX supports the licensee's own
93
+ product-specific experience and is not itself the general-purpose product
94
+ offered to customers.
95
+
96
+ ## 4. Your content
97
+
98
+ As between you and the Licensor, you retain ownership of the Paths, Sources,
99
+ responses, data, independent application code, and other content that you
100
+ author or provide while using the Software. You are responsible for that
101
+ content and for having the rights needed to use it.
102
+
103
+ Generated output may contain Redistributable Components or third-party
104
+ materials. Your ownership of your content does not change the ownership or
105
+ license terms of those components and materials.
106
+
107
+ ## 5. Ownership and trademarks
108
+
109
+ The Software is licensed, not sold. The Licensor and its suppliers retain all
110
+ rights in the Software not expressly granted by this license. This license
111
+ does not grant permission to use the PathMX name, logos, or other marks except
112
+ as reasonably necessary to identify the Software accurately.
113
+
114
+ ## 6. Beta status and changes
115
+
116
+ The Software is a proprietary beta release. Its APIs, behavior, file formats,
117
+ and features may change or be removed without notice. The Licensor has no
118
+ obligation to provide support, maintenance, updates, continued availability,
119
+ or backward compatibility. A later version may be offered under different
120
+ license terms, including an open-source license. Different terms for a later
121
+ version do not change the license previously granted for an earlier version.
122
+
123
+ ## 7. Third-party materials
124
+
125
+ The Software and Redistributable Components may include or depend on
126
+ third-party materials governed by their own license terms. Those terms control
127
+ for the applicable third-party materials. You must preserve third-party notices
128
+ when the applicable third-party terms require it.
129
+
130
+ ## 8. Disclaimer of warranties
131
+
132
+ TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE SOFTWARE IS PROVIDED “AS IS” AND
133
+ “AS AVAILABLE,” WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EXPRESS,
134
+ IMPLIED, OR STATUTORY, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
135
+ PARTICULAR PURPOSE, TITLE, NON-INFRINGEMENT, OR THAT THE SOFTWARE WILL BE
136
+ SECURE, ERROR-FREE, OR UNINTERRUPTED.
137
+
138
+ ## 9. Limitation of liability
139
+
140
+ TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE LICENSOR AND ITS CONTRIBUTORS,
141
+ SUPPLIERS, AND AFFILIATES WILL NOT BE LIABLE FOR ANY INDIRECT, INCIDENTAL,
142
+ SPECIAL, CONSEQUENTIAL, EXEMPLARY, OR PUNITIVE DAMAGES, OR FOR LOSS OF DATA,
143
+ PROFITS, REVENUE, GOODWILL, OR BUSINESS OPPORTUNITY, ARISING FROM OR RELATED TO
144
+ THE SOFTWARE OR THIS LICENSE, EVEN IF ADVISED OF THE POSSIBILITY OF THOSE
145
+ DAMAGES.
146
+
147
+ TO THE MAXIMUM EXTENT PERMITTED BY LAW, THEIR TOTAL LIABILITY FOR ALL CLAIMS
148
+ ARISING FROM OR RELATED TO THE SOFTWARE OR THIS LICENSE WILL NOT EXCEED THE
149
+ GREATER OF THE AMOUNT YOU PAID FOR THE SOFTWARE DURING THE TWELVE MONTHS BEFORE
150
+ THE EVENT GIVING RISE TO THE CLAIM OR US$100.
151
+
152
+ ## 10. Termination
153
+
154
+ This license terminates if you materially breach it and do not cure the breach
155
+ within thirty days after receiving written notice from the Licensor. On
156
+ termination, you and your Authorized Users must stop using and delete your
157
+ copies of the Software. Sections 4, 5, 8, 9, 10, and 11 survive termination.
158
+ Termination does not withdraw the limited permission already granted by
159
+ Section 2 to lawful recipients of Redistributable Components distributed
160
+ before termination.
161
+
162
+ ## 11. General
163
+
164
+ If a provision of this license is unenforceable, it will be modified only to
165
+ the minimum extent necessary to make it enforceable, and the remaining
166
+ provisions will continue in effect. Failure to enforce a provision is not a
167
+ waiver. You may not assign or transfer this license without the Licensor's
168
+ written consent. The Licensor may assign this license together with ownership
169
+ of the Software to a successor owner. This license binds and benefits the
170
+ permitted successors and assigns of you and the Licensor. This license is the
171
+ entire agreement concerning the Software unless you and the Licensor have
172
+ signed a separate agreement that expressly replaces it.
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # PathMX Beta
2
+
3
+ PathMX is a minimal, plugin-first framework for building Paths and learning
4
+ applications. This package includes the Bun-first CLI, portable core contracts,
5
+ default Plugins, server adapter, and static publication API.
6
+
7
+ The package root is runtime-neutral. Bun applications use the `/bun` entrypoint
8
+ for the `PathMX()` convenience composition and Bun environment.
9
+
10
+ This is an early beta. APIs and behavior may change between `0.x` releases;
11
+ pin an exact version and review the release notes before upgrading.
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ bun add --exact @pathmx/core@beta
17
+ ```
18
+
19
+ PathMX currently requires Bun 1.4.0 or newer and recommends the Bun version used
20
+ to test each release. Install the CLI globally when you want the `pathmx` and
21
+ `pmx` commands everywhere:
22
+
23
+ ```sh
24
+ bun add --global @pathmx/core@beta
25
+ pathmx --help
26
+ pathmx update
27
+ ```
28
+
29
+ `pmx` is an alias for `pathmx`.
30
+
31
+ ## Create an application
32
+
33
+ ```ts
34
+ import { PathMX, handleRequest } from "@pathmx/core/bun"
35
+
36
+ const app = await PathMX({ root: "./paths" })
37
+
38
+ Bun.serve({
39
+ fetch: (request) => handleRequest(app, request),
40
+ })
41
+ ```
42
+
43
+ `PathMX()` supplies the Bun environment and default Plugin set. Portable hosts
44
+ can compose the smaller core API explicitly:
45
+
46
+ ```ts
47
+ import { createApp } from "@pathmx/core"
48
+ import { bunEnvironment } from "@pathmx/core/bun"
49
+ import { defaultPlugins } from "@pathmx/core/plugins"
50
+
51
+ const app = await createApp({
52
+ root: "./paths",
53
+ environment: bunEnvironment,
54
+ plugins: defaultPlugins,
55
+ })
56
+ ```
57
+
58
+ Official packages follow the CLI's release channel:
59
+
60
+ ```sh
61
+ pathmx plugins add mermaid react
62
+ pathmx plugins list
63
+ pathmx plugins update
64
+ ```
65
+
66
+ ## Image directives
67
+
68
+ The default Image Plugin compiles portable image roles from normal Markdown:
69
+
70
+ ```md
71
+ ![@image.fit Diagram](./diagram.png)
72
+ ![@image.fill Photo](./photo.jpg)
73
+ ![@image.background Hero](./hero.jpg)
74
+ ![@image.background.fit Diagram backdrop](./diagram.png)
75
+ ```
76
+
77
+ The directive is removed from the rendered alt text. Background images render
78
+ behind their containing Block in ordinary documents, while experiences such as
79
+ Player can reuse the same role and fit metadata for their own layouts.
80
+
81
+ ## Block layouts
82
+
83
+ The default Block Layout Plugin compiles reusable composition from Block
84
+ topmatter. The same server-rendered metadata works in ordinary pages, static
85
+ exports, Focus mode, and Slides mode:
86
+
87
+ ```md
88
+ <!--
89
+ layout: media-end
90
+ -->
91
+
92
+ # A diagram and explanation
93
+
94
+ ![System diagram](./diagram.png)
95
+ ```
96
+
97
+ The built-in layouts are `center`, `media-start`, and `media-end`. Media
98
+ layouts preserve Markdown reading order, use logical start/end placement, and
99
+ collapse to one column on narrow screens. Lowercase custom slugs remain open
100
+ for repository CSS, such as `layout: course-timeline`.
101
+
102
+ ## Package channels
103
+
104
+ - `@fellowhumans/pathmx@latest` remains the previous PathMX implementation
105
+ during the transition and is not part of this release train.
106
+ - `@pathmx/core@beta` is this minimal implementation.
107
+ - `@pathmx/core@latest` will become the stable channel after the beta.
108
+ - Published versions are immutable. The `beta` tag advances only after the
109
+ release preflight passes.
110
+
111
+ PathMX is distributed under the [PathMX Proprietary Beta License](./LICENSE.md).
@@ -0,0 +1,307 @@
1
+ import type { Engine } from "../host/engine.ts"
2
+ import type { SourceCapability } from "../authority/access.ts"
3
+ import type { PathMXYaml } from "../environment.ts"
4
+ import type { Source } from "../source/index.ts"
5
+ import {
6
+ formatSchemaIssues,
7
+ isStandardSchema,
8
+ standardJsonSchema,
9
+ type StandardSchemaV1,
10
+ } from "../schema/standard.ts"
11
+ import type { Principal, UserActor } from "../authority/model.ts"
12
+ import { projectDirectory } from "../project.ts"
13
+ import { type SourceTransaction, createTransaction } from "./transaction.ts"
14
+
15
+ export function defaultActionsRoot(pathsRoot: string) {
16
+ return projectDirectory(pathsRoot, "actions")
17
+ }
18
+
19
+ export type ActionInput = Record<string, string | string[]>
20
+
21
+ type ActionResult = unknown
22
+
23
+ export type ActionViewContext = Readonly<{
24
+ principal: Principal
25
+ target: Source
26
+ yaml: PathMXYaml
27
+ can(capability: SourceCapability): boolean
28
+ }>
29
+
30
+ export type ActionAvailability =
31
+ | boolean
32
+ | string
33
+ | Readonly<{
34
+ rel?: string
35
+ title?: string
36
+ description?: string
37
+ status?: "available" | "unavailable"
38
+ reason?: string
39
+ boundInput?: Readonly<Record<string, unknown>>
40
+ }>
41
+
42
+ export type ActionOptions<
43
+ S extends StandardSchemaV1 = StandardSchemaV1<unknown, ActionInput>,
44
+ O extends StandardSchemaV1 | undefined = undefined,
45
+ > = Readonly<{
46
+ title: string
47
+ description?: string
48
+ input?: S
49
+ output?: O
50
+ affordance?: (
51
+ context: ActionViewContext,
52
+ ) => ActionAvailability | Promise<ActionAvailability>
53
+ authorize?: (
54
+ context: ActionContext,
55
+ ) => boolean | string | Promise<boolean | string>
56
+ execute(
57
+ input: StandardSchemaV1.InferOutput<S>,
58
+ context: ActionContext,
59
+ ): ActionResult | Promise<ActionResult>
60
+ }>
61
+
62
+ export type Action<
63
+ S extends StandardSchemaV1 = StandardSchemaV1<unknown, ActionInput>,
64
+ O extends StandardSchemaV1 | undefined = undefined,
65
+ > = ((
66
+ input: StandardSchemaV1.InferOutput<S>,
67
+ context: ActionContext,
68
+ ) => ActionResult | Promise<ActionResult>) &
69
+ Readonly<{
70
+ title?: string
71
+ description?: string
72
+ input?: S
73
+ output?: O
74
+ affordance?: ActionOptions<S, O>["affordance"]
75
+ authorize?: ActionOptions<S, O>["authorize"]
76
+ }>
77
+
78
+ /** An Action whose schema-specific input type is intentionally erased for registration. */
79
+ export type ActionContribution = ((
80
+ input: never,
81
+ context: ActionContext,
82
+ ) => ActionResult | Promise<ActionResult>) &
83
+ Readonly<{
84
+ title?: string
85
+ description?: string
86
+ input?: StandardSchemaV1
87
+ output?: StandardSchemaV1
88
+ affordance?: ActionOptions["affordance"]
89
+ authorize?: ActionOptions["authorize"]
90
+ }>
91
+
92
+ export type ActionContext = {
93
+ actor: UserActor
94
+ tx: SourceTransaction
95
+ target?: Source
96
+ yaml: PathMXYaml
97
+ }
98
+
99
+ export type ActionAffordance = Readonly<{
100
+ type: "action"
101
+ id: string
102
+ rel: string
103
+ title: string
104
+ description?: string
105
+ method: "post"
106
+ href: string
107
+ target: Readonly<{ type: "source"; id: string }>
108
+ input?: Readonly<Record<string, unknown>>
109
+ output?: Readonly<Record<string, unknown>>
110
+ boundInput?: Readonly<Record<string, unknown>>
111
+ status: "available" | "unavailable"
112
+ reason?: string
113
+ }>
114
+
115
+ export class ActionNotFoundError extends Error {
116
+ constructor(name: string) {
117
+ super(`Action not found: ${name}`)
118
+ this.name = "ActionNotFoundError"
119
+ }
120
+ }
121
+
122
+ export class ActionValidationError extends Error {
123
+ readonly issues: ReadonlyArray<StandardSchemaV1.Issue>
124
+
125
+ constructor(issues: ReadonlyArray<StandardSchemaV1.Issue>) {
126
+ super(formatSchemaIssues(issues))
127
+ this.name = "ActionValidationError"
128
+ this.issues = issues
129
+ }
130
+ }
131
+
132
+ export class ActionUnavailableError extends Error {
133
+ constructor(name: string, reason?: string) {
134
+ super(reason ?? `Action unavailable: ${name}`)
135
+ this.name = "ActionUnavailableError"
136
+ }
137
+ }
138
+
139
+ export function defineAction<S extends StandardSchemaV1>(
140
+ input: S,
141
+ execute: Action<S>,
142
+ ): Action<S> & { input: S }
143
+ export function defineAction<
144
+ S extends StandardSchemaV1 = StandardSchemaV1<unknown, ActionInput>,
145
+ O extends StandardSchemaV1 | undefined = undefined,
146
+ >(options: ActionOptions<S, O>): Action<S, O>
147
+ export function defineAction<
148
+ S extends StandardSchemaV1 = StandardSchemaV1<unknown, ActionInput>,
149
+ O extends StandardSchemaV1 | undefined = undefined,
150
+ >(
151
+ inputOrOptions: S | ActionOptions<S, O>,
152
+ legacyExecute?: Action<S>,
153
+ ): Action<S, O> {
154
+ if (legacyExecute)
155
+ return Object.assign(legacyExecute, { input: inputOrOptions as S })
156
+ const options = inputOrOptions as ActionOptions<S, O>
157
+ return Object.assign(options.execute, {
158
+ title: options.title,
159
+ ...(options.description ? { description: options.description } : {}),
160
+ ...(options.input ? { input: options.input } : {}),
161
+ ...(options.output ? { output: options.output } : {}),
162
+ ...(options.affordance ? { affordance: options.affordance } : {}),
163
+ ...(options.authorize ? { authorize: options.authorize } : {}),
164
+ })
165
+ }
166
+
167
+ function availability(
168
+ value: ActionAvailability,
169
+ ): Exclude<ActionAvailability, boolean | string> | undefined {
170
+ if (value === false) return
171
+ if (value === true) return {}
172
+ if (typeof value === "string") {
173
+ return { status: "unavailable", reason: value }
174
+ }
175
+ return value
176
+ }
177
+
178
+ async function resolveAvailability(
179
+ action: ActionContribution,
180
+ context: ActionViewContext,
181
+ ) {
182
+ if (!action.affordance) {
183
+ return Object.freeze({ declared: false, description: undefined })
184
+ }
185
+ return Object.freeze({
186
+ declared: true,
187
+ description: availability(await action.affordance(context)),
188
+ })
189
+ }
190
+
191
+ export async function describeAction(
192
+ name: string,
193
+ action: ActionContribution,
194
+ context: ActionViewContext,
195
+ ): Promise<ActionAffordance | undefined> {
196
+ if (!action.title || !action.affordance) return
197
+ const { description: described } = await resolveAvailability(action, context)
198
+ if (!described) return
199
+ const input = standardJsonSchema(action.input, "input")
200
+ const output = standardJsonSchema(action.output, "output")
201
+ return Object.freeze({
202
+ type: "action",
203
+ id: name,
204
+ rel: described.rel ?? name,
205
+ title: described.title ?? action.title,
206
+ ...((described.description ?? action.description)
207
+ ? { description: described.description ?? action.description }
208
+ : {}),
209
+ method: "post",
210
+ href: `/actions/${name}`,
211
+ target: Object.freeze({ type: "source", id: context.target.id }),
212
+ ...(input ? { input } : {}),
213
+ ...(output ? { output } : {}),
214
+ ...(described.boundInput
215
+ ? { boundInput: Object.freeze({ ...described.boundInput }) }
216
+ : {}),
217
+ status: described.status ?? "available",
218
+ ...(described.reason ? { reason: described.reason } : {}),
219
+ })
220
+ }
221
+
222
+ export async function parseActionInput<S extends StandardSchemaV1>(
223
+ schema: S,
224
+ data: unknown,
225
+ ): Promise<StandardSchemaV1.InferOutput<S>> {
226
+ let result = schema["~standard"].validate(data)
227
+ if (result instanceof Promise) result = await result
228
+ if (result.issues) throw new ActionValidationError(result.issues)
229
+ return result.value
230
+ }
231
+
232
+ export async function dispatchAction(
233
+ engine: Engine,
234
+ name: string,
235
+ input: ActionInput,
236
+ actor: UserActor,
237
+ target?: Source,
238
+ ) {
239
+ const action =
240
+ engine.pluginAction(name) ?? (await loadAuthoredAction(engine, name))
241
+
242
+ engine.log.info("action", {
243
+ name,
244
+ actor: actor.id,
245
+ target: target?.id,
246
+ })
247
+ let value: ActionResult = undefined
248
+ const commit = await engine.commitPrepared(async () => {
249
+ const currentTarget = target ? engine.repo.atPath(target.path) : undefined
250
+ if (target && !currentTarget) throw new ActionUnavailableError(name)
251
+ let boundInput: Readonly<Record<string, unknown>> | undefined
252
+ if (currentTarget) {
253
+ const principal = { type: "actor" as const, actor }
254
+ if (!engine.hasSourceCapability(principal, currentTarget, "execute")) {
255
+ throw new ActionUnavailableError(name)
256
+ }
257
+ const resolved = await resolveAvailability(action, {
258
+ principal,
259
+ target: currentTarget,
260
+ yaml: engine.environment.yaml,
261
+ can: (capability) =>
262
+ engine.hasSourceCapability(principal, currentTarget, capability),
263
+ })
264
+ if (resolved.declared && !resolved.description) {
265
+ throw new ActionUnavailableError(name)
266
+ }
267
+ if (resolved.description?.status === "unavailable") {
268
+ throw new ActionUnavailableError(name, resolved.description.reason)
269
+ }
270
+ boundInput = resolved.description?.boundInput
271
+ }
272
+ const submitted = { ...input, ...(boundInput ?? {}) }
273
+ const parsed = isStandardSchema(action.input)
274
+ ? await parseActionInput(action.input, submitted)
275
+ : submitted
276
+ const tx = createTransaction(engine, actor)
277
+ const context = {
278
+ actor,
279
+ tx,
280
+ target: currentTarget,
281
+ yaml: engine.environment.yaml,
282
+ }
283
+ const authorized = await action.authorize?.(context)
284
+ if (authorized === false || typeof authorized === "string") {
285
+ throw new ActionUnavailableError(
286
+ name,
287
+ typeof authorized === "string" ? authorized : undefined,
288
+ )
289
+ }
290
+ value = await action(parsed as never, context)
291
+ if (isStandardSchema(action.output)) {
292
+ value = await parseActionInput(action.output, value)
293
+ }
294
+ return tx.changeSet()
295
+ })
296
+ return Object.freeze({ value, commit })
297
+ }
298
+
299
+ async function loadAuthoredAction(engine: Engine, name: string) {
300
+ if (!/^[a-z0-9-]+$/.test(name)) throw new ActionNotFoundError(name)
301
+ const root = engine.actionsRoot.replace(/\/+$/, "")
302
+ const file = `${root}/${name}.action.ts`
303
+ const mod = (await engine.environment.loadModule(file)) as
304
+ { default?: ActionContribution } | undefined
305
+ if (typeof mod?.default !== "function") throw new ActionNotFoundError(name)
306
+ return mod.default
307
+ }
@@ -0,0 +1,105 @@
1
+ import {
2
+ appendBlockSection,
3
+ formatNewSource,
4
+ patchFrontmatter,
5
+ prependBlockSection,
6
+ removeBlockSection,
7
+ updateBlockSection,
8
+ type BlockDraft,
9
+ type BlockUpdate,
10
+ } from "../source/format.ts"
11
+ import type { Topmatter } from "../source/topmatter.ts"
12
+ import type { PathMXYaml } from "../environment.ts"
13
+
14
+ type DraftIO = {
15
+ create(path: string, content: string): Promise<void>
16
+ update(path: string, content: string): Promise<void>
17
+ read(path: string): Promise<string>
18
+ }
19
+
20
+ type DraftOperation =
21
+ | { type: "data"; patch: Topmatter }
22
+ | { type: "append"; block: BlockDraft }
23
+ | { type: "prepend"; block: BlockDraft }
24
+ | { type: "update"; id: string; update: BlockUpdate }
25
+ | { type: "remove"; id: string }
26
+
27
+ export type SourceDraft = {
28
+ readonly path: string
29
+ create(): SourceDraft
30
+ data(patch: Topmatter): SourceDraft
31
+ appendBlock(block: BlockDraft): SourceDraft
32
+ prependBlock(block: BlockDraft): SourceDraft
33
+ updateBlock(id: string, update: BlockUpdate): SourceDraft
34
+ removeBlock(id: string): SourceDraft
35
+ }
36
+
37
+ export function createSourceDraft(
38
+ path: string,
39
+ io: DraftIO,
40
+ yaml: PathMXYaml,
41
+ ): SourceDraft & {
42
+ flush(): Promise<void>
43
+ } {
44
+ let creating = false
45
+ const operations: DraftOperation[] = []
46
+
47
+ const apply = (initial: string) => {
48
+ let content = initial
49
+ for (const operation of operations) {
50
+ if (operation.type === "data") {
51
+ content = patchFrontmatter(content, operation.patch, yaml)
52
+ } else if (operation.type === "append") {
53
+ content = appendBlockSection(content, operation.block, yaml)
54
+ } else if (operation.type === "prepend") {
55
+ content = prependBlockSection(content, operation.block, yaml)
56
+ } else if (operation.type === "update") {
57
+ content = updateBlockSection(
58
+ content,
59
+ operation.id,
60
+ operation.update,
61
+ yaml,
62
+ )
63
+ } else {
64
+ content = removeBlockSection(content, operation.id, yaml)
65
+ }
66
+ }
67
+ return content
68
+ }
69
+
70
+ const draft: SourceDraft & { flush(): Promise<void> } = {
71
+ path,
72
+ create() {
73
+ creating = true
74
+ return draft
75
+ },
76
+ data(patch) {
77
+ operations.push({ type: "data", patch })
78
+ return draft
79
+ },
80
+ appendBlock(block) {
81
+ operations.push({ type: "append", block })
82
+ return draft
83
+ },
84
+ prependBlock(block) {
85
+ operations.push({ type: "prepend", block })
86
+ return draft
87
+ },
88
+ updateBlock(id, update) {
89
+ operations.push({ type: "update", id, update })
90
+ return draft
91
+ },
92
+ removeBlock(id) {
93
+ operations.push({ type: "remove", id })
94
+ return draft
95
+ },
96
+ async flush() {
97
+ if (creating) {
98
+ await io.create(path, apply(formatNewSource({}, yaml)))
99
+ return
100
+ }
101
+ await io.update(path, apply(await io.read(path)))
102
+ },
103
+ }
104
+ return draft
105
+ }