@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/cli/perms.ts ADDED
@@ -0,0 +1,182 @@
1
+ import type { Command } from "commander"
2
+ import type {
3
+ SourceAccessBasis,
4
+ SourcePermissionRecord,
5
+ } from "../authority/access.ts"
6
+ import type { Engine } from "../host/engine.ts"
7
+ import { Log } from "../log.ts"
8
+ import type { Source } from "../source/index.ts"
9
+ import { fail, loadApp, rootFromCommand } from "./utils.ts"
10
+
11
+ type PermissionFormat = "text" | "json"
12
+
13
+ type PermissionReport =
14
+ | Readonly<{
15
+ type: "source"
16
+ sourceId: string
17
+ actorId?: string
18
+ permissions: readonly SourcePermissionRecord[]
19
+ }>
20
+ | Readonly<{
21
+ type: "actor"
22
+ actorId: string
23
+ permissions: readonly SourcePermissionRecord[]
24
+ }>
25
+
26
+ type PermissionQuery =
27
+ | Readonly<{ source: Source; actorId?: string }>
28
+ | Readonly<{ source?: never; actorId: string }>
29
+
30
+ function formatOption(value: string): PermissionFormat {
31
+ if (value === "text" || value === "json") return value
32
+ throw new Error(`Unsupported permission format: ${value}`)
33
+ }
34
+
35
+ function capabilityText(record: SourcePermissionRecord) {
36
+ const granted = new Set(record.capabilities)
37
+ return `${granted.has("read") ? "r" : "-"}${granted.has("write") ? "w" : "-"}${granted.has("execute") ? "x" : "-"}`
38
+ }
39
+
40
+ function basisText(basis: SourceAccessBasis) {
41
+ switch (basis.type) {
42
+ case "exact-grant":
43
+ return basis.owner ? "owner · exact grant" : "exact grant"
44
+ case "home-owner":
45
+ return "Home owner"
46
+ case "staff-review":
47
+ return "staff review"
48
+ case "public-source":
49
+ return "public Source"
50
+ case "denied":
51
+ return basis.reason.replaceAll("-", " ")
52
+ }
53
+ }
54
+
55
+ function table(
56
+ headers: readonly string[],
57
+ rows: readonly (readonly string[])[],
58
+ ) {
59
+ const widths = headers.map((header, column) =>
60
+ Math.max(header.length, ...rows.map((row) => row[column]?.length ?? 0)),
61
+ )
62
+ return [headers, ...rows]
63
+ .map((row) =>
64
+ row
65
+ .map((cell, column) => cell.padEnd(widths[column] ?? cell.length))
66
+ .join(" ")
67
+ .trimEnd(),
68
+ )
69
+ .join("\n")
70
+ }
71
+
72
+ function textReport(report: PermissionReport) {
73
+ if (report.type === "source") {
74
+ return [
75
+ report.sourceId,
76
+ table(
77
+ ["Actor", "Caps", "Visibility", "Basis"],
78
+ report.permissions.map((record) => [
79
+ record.actorId ?? "anonymous",
80
+ capabilityText(record),
81
+ record.visibility,
82
+ basisText(record.basis),
83
+ ]),
84
+ ),
85
+ ].join("\n\n")
86
+ }
87
+ return [
88
+ report.actorId,
89
+ table(
90
+ ["Caps", "Source", "Visibility", "Basis"],
91
+ report.permissions.map((record) => [
92
+ capabilityText(record),
93
+ record.sourceId,
94
+ record.visibility,
95
+ basisText(record.basis),
96
+ ]),
97
+ ),
98
+ ].join("\n\n")
99
+ }
100
+
101
+ function sourceFor(app: Engine, target: string) {
102
+ return app.repo.get(target) ?? app.repo.atPath(target)
103
+ }
104
+
105
+ export function permissionReport(
106
+ app: Engine,
107
+ options: PermissionQuery,
108
+ ): PermissionReport {
109
+ if (options.source) {
110
+ let permissions = app.inspectSourcePermissions(options.source)
111
+ if (options.actorId) {
112
+ permissions = permissions.filter(
113
+ (record) => record.actorId === options.actorId,
114
+ )
115
+ if (!permissions.length)
116
+ throw new Error(`Actor not found: ${options.actorId}`)
117
+ }
118
+ return {
119
+ type: "source",
120
+ sourceId: options.source.id,
121
+ ...(options.actorId ? { actorId: options.actorId } : {}),
122
+ permissions,
123
+ }
124
+ }
125
+
126
+ const permissions = app.inspectActorPermissions(options.actorId)
127
+ if (!permissions) throw new Error(`Actor not found: ${options.actorId}`)
128
+ return {
129
+ type: "actor",
130
+ actorId: options.actorId,
131
+ permissions: permissions.filter((record) =>
132
+ record.capabilities.includes("read"),
133
+ ),
134
+ }
135
+ }
136
+
137
+ export function registerPerms(program: Command) {
138
+ program
139
+ .command("perms")
140
+ .description("Inspect effective Source permissions")
141
+ .argument("[source]", "Source id or path")
142
+ .option("--as <actor>", "inspect permissions for an Actor id")
143
+ .option(
144
+ "--format <format>",
145
+ "output format: text or json",
146
+ formatOption,
147
+ "text",
148
+ )
149
+ .action(
150
+ async (
151
+ target: string | undefined,
152
+ opts: { as?: string; format: PermissionFormat },
153
+ command: Command,
154
+ ) => {
155
+ try {
156
+ const resolved = rootFromCommand(command)
157
+ const app = await loadApp(resolved.root, {
158
+ actionsRoot: resolved.actionsRoot,
159
+ buildCache: false,
160
+ log: new Log({ level: "error" }),
161
+ })
162
+ const source = target ? sourceFor(app, target) : undefined
163
+ if (target && !source) throw new Error(`Source not found: ${target}`)
164
+ let report: PermissionReport
165
+ if (source) {
166
+ report = permissionReport(app, { source, actorId: opts.as })
167
+ } else {
168
+ if (!opts.as)
169
+ throw new Error("Provide a Source or use --as <actor>.")
170
+ report = permissionReport(app, { actorId: opts.as })
171
+ }
172
+ console.log(
173
+ opts.format === "json"
174
+ ? JSON.stringify(report, null, 2)
175
+ : textReport(report),
176
+ )
177
+ } catch (error) {
178
+ fail(error)
179
+ }
180
+ },
181
+ )
182
+ }
@@ -0,0 +1,159 @@
1
+ import { dirname, join } from "node:path"
2
+ import type { Command } from "commander"
3
+ import { release } from "./release.ts"
4
+ import { rootFromCommand } from "./utils.ts"
5
+
6
+ type ProjectManifest = Readonly<{
7
+ dependencies?: Readonly<Record<string, string>>
8
+ devDependencies?: Readonly<Record<string, string>>
9
+ optionalDependencies?: Readonly<Record<string, string>>
10
+ }>
11
+
12
+ type InstalledPlugin = Readonly<{
13
+ name: string
14
+ spec: string
15
+ development: boolean
16
+ }>
17
+
18
+ export function pluginPackageName(input: string) {
19
+ const name = input.startsWith("@pathmx/") ? input.slice(8) : input
20
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(name)) {
21
+ throw new Error(
22
+ `Invalid PathMX plugin package: ${input}. Use a name such as react or @pathmx/react.`,
23
+ )
24
+ }
25
+ return `@pathmx/${name}`
26
+ }
27
+
28
+ function projectRoot(command: Command) {
29
+ return dirname(rootFromCommand(command).root)
30
+ }
31
+
32
+ async function projectManifest(root: string) {
33
+ const file = Bun.file(join(root, "package.json"))
34
+ if (!(await file.exists())) {
35
+ throw new Error(`PathMX project package.json not found: ${file.name}`)
36
+ }
37
+ return (await file.json()) as ProjectManifest
38
+ }
39
+
40
+ function installedPlugins(manifest: ProjectManifest) {
41
+ const plugins = new Map<string, InstalledPlugin>()
42
+ for (const [name, spec] of Object.entries(manifest.dependencies ?? {})) {
43
+ if (name.startsWith("@pathmx/")) {
44
+ plugins.set(name, { name, spec, development: false })
45
+ }
46
+ }
47
+ for (const [name, spec] of Object.entries(
48
+ manifest.optionalDependencies ?? {},
49
+ )) {
50
+ if (name.startsWith("@pathmx/") && !plugins.has(name)) {
51
+ plugins.set(name, { name, spec, development: false })
52
+ }
53
+ }
54
+ for (const [name, spec] of Object.entries(manifest.devDependencies ?? {})) {
55
+ if (name.startsWith("@pathmx/") && !plugins.has(name)) {
56
+ plugins.set(name, { name, spec, development: true })
57
+ }
58
+ }
59
+ return [...plugins.values()].sort((left, right) =>
60
+ left.name.localeCompare(right.name),
61
+ )
62
+ }
63
+
64
+ async function resolvedVersion(root: string, name: string) {
65
+ const manifest = Bun.file(join(root, "node_modules", name, "package.json"))
66
+ if (!(await manifest.exists())) return undefined
67
+ const value = (await manifest.json()) as { version?: string }
68
+ return value.version
69
+ }
70
+
71
+ async function bunAdd(
72
+ root: string,
73
+ names: readonly string[],
74
+ development = false,
75
+ ) {
76
+ if (!names.length) return
77
+ const specs = names.map((name) => `${name}@${release.channel}`)
78
+ const args = ["add", "--exact", ...(development ? ["--dev"] : []), ...specs]
79
+ const child = Bun.spawn([process.execPath, ...args], {
80
+ cwd: root,
81
+ stdin: "inherit",
82
+ stdout: "inherit",
83
+ stderr: "inherit",
84
+ })
85
+ const exitCode = await child.exited
86
+ if (exitCode !== 0) {
87
+ throw new Error(
88
+ `Bun could not install ${specs.join(", ")} (exit ${exitCode}).`,
89
+ )
90
+ }
91
+ }
92
+
93
+ export function registerPluginPackages(program: Command) {
94
+ const plugins = program
95
+ .command("plugins")
96
+ .description("manage official @pathmx/* packages")
97
+
98
+ plugins
99
+ .command("list")
100
+ .description("list installed @pathmx/* packages")
101
+ .action(async (_options, command: Command) => {
102
+ const root = projectRoot(command)
103
+ const installed = installedPlugins(await projectManifest(root))
104
+ if (!installed.length) {
105
+ console.log("No @pathmx/* packages are installed.")
106
+ return
107
+ }
108
+ for (const plugin of installed) {
109
+ const version = await resolvedVersion(root, plugin.name)
110
+ console.log(
111
+ `${plugin.name} ${plugin.spec}${version ? ` (${version})` : " (not installed)"}`,
112
+ )
113
+ }
114
+ })
115
+
116
+ plugins
117
+ .command("add")
118
+ .description(`add packages from the PathMX ${release.channel} channel`)
119
+ .argument("<plugins...>", "package names, such as react or mermaid")
120
+ .action(async (inputs: string[], _options, command: Command) => {
121
+ await projectManifest(projectRoot(command))
122
+ await bunAdd(projectRoot(command), inputs.map(pluginPackageName))
123
+ })
124
+
125
+ plugins
126
+ .command("update")
127
+ .description(
128
+ `update installed packages from the ${release.channel} channel`,
129
+ )
130
+ .argument("[plugins...]", "installed package names; defaults to all")
131
+ .action(async (inputs: string[], _options, command: Command) => {
132
+ const root = projectRoot(command)
133
+ const installed = installedPlugins(await projectManifest(root))
134
+ const selected = inputs.length
135
+ ? inputs.map(pluginPackageName)
136
+ : installed.map(({ name }) => name)
137
+ const missing = selected.filter(
138
+ (name) => !installed.some((plugin) => plugin.name === name),
139
+ )
140
+ if (missing.length) {
141
+ throw new Error(
142
+ `${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} not installed; use \`pathmx plugins add\` first.`,
143
+ )
144
+ }
145
+ if (!selected.length) {
146
+ console.log("No @pathmx/* packages are installed.")
147
+ return
148
+ }
149
+ const regular = selected.filter(
150
+ (name) =>
151
+ !installed.find((plugin) => plugin.name === name)?.development,
152
+ )
153
+ const development = selected.filter(
154
+ (name) => installed.find((plugin) => plugin.name === name)?.development,
155
+ )
156
+ await bunAdd(root, regular)
157
+ await bunAdd(root, development, true)
158
+ })
159
+ }
package/cli/plugins.ts ADDED
@@ -0,0 +1,95 @@
1
+ import { dirname } from "node:path"
2
+ import type { Command } from "commander"
3
+ import { PluginDatabases } from "../database.ts"
4
+ import { bunEnvironment } from "../environments/bun.ts"
5
+ import { discoverRepositoryPlugins } from "../environments/bun-plugins.ts"
6
+ import { Log } from "../log.ts"
7
+ import { canonicalPluginId } from "../plugins/types.ts"
8
+ import {
9
+ expandPluginContributions,
10
+ instantiatePlugin,
11
+ } from "../plugins/preset.ts"
12
+ import { createPluginLog } from "../plugins/context.ts"
13
+ import { createPatternInstaller } from "../pattern.ts"
14
+ import { projectDirectory } from "../project.ts"
15
+ import { resolveRoot } from "./utils.ts"
16
+
17
+ function optionFromArgv(argv: readonly string[], long: string, short: string) {
18
+ for (let index = 2; index < argv.length; index++) {
19
+ const value = argv[index]
20
+ if (value === long || value === short) return argv[index + 1]
21
+ if (value?.startsWith(`${long}=`)) return value.slice(long.length + 1)
22
+ }
23
+ }
24
+
25
+ function rootsFromArgv(argv: readonly string[]) {
26
+ const actions = optionFromArgv(argv, "--actions", "-a")
27
+ return {
28
+ pathsRoot: resolveRoot(optionFromArgv(argv, "--root", "-r")),
29
+ actionsRoot: actions ? resolveRoot(actions) : undefined,
30
+ }
31
+ }
32
+
33
+ export async function registerPluginCommands(
34
+ program: Command,
35
+ argv: readonly string[],
36
+ ) {
37
+ const { pathsRoot, actionsRoot } = rootsFromArgv(argv)
38
+ const projectRoot = dirname(pathsRoot)
39
+ const stateRoot = projectDirectory(pathsRoot, ".pathmx/state")
40
+ const log = new Log({ level: "error" })
41
+ const contributions = await discoverRepositoryPlugins(pathsRoot, log)
42
+ const plugins =
43
+ expandPluginContributions(contributions).map(instantiatePlugin)
44
+ const databases = new PluginDatabases(bunEnvironment.database, stateRoot)
45
+ const patterns = createPatternInstaller(bunEnvironment, {
46
+ paths: pathsRoot,
47
+ project: projectRoot,
48
+ })
49
+ const close = () => databases.close()
50
+
51
+ try {
52
+ for (const plugin of plugins) {
53
+ if (!plugin.commands) continue
54
+ canonicalPluginId(plugin.id)
55
+ if (program.commands.some((command) => command.name() === plugin.id)) {
56
+ throw new Error(
57
+ `Plugin command conflicts with core command: ${plugin.id}`,
58
+ )
59
+ }
60
+ if (plugin.database) databases.register(plugin.id, plugin.database)
61
+ const group = program
62
+ .command(plugin.id)
63
+ .description(
64
+ plugin.name
65
+ ? `${plugin.name} administration`
66
+ : `${plugin.id} administration`,
67
+ )
68
+ plugin.commands(
69
+ group,
70
+ Object.freeze({
71
+ plugin: plugin.id,
72
+ pathsRoot,
73
+ actionsRoot,
74
+ projectRoot,
75
+ stateRoot,
76
+ log: createPluginLog(log, plugin.id),
77
+ database: databases.get(plugin.id),
78
+ patterns,
79
+ }),
80
+ )
81
+ }
82
+ } catch (error) {
83
+ try {
84
+ await close()
85
+ } catch (cleanupError) {
86
+ throw new AggregateError(
87
+ [error, cleanupError],
88
+ "Plugin command registration failed.",
89
+ )
90
+ }
91
+ throw error
92
+ }
93
+
94
+ return close
95
+ }
package/cli/query.ts ADDED
@@ -0,0 +1,225 @@
1
+ import type { Command } from "commander"
2
+ import type { Engine } from "../host/engine.ts"
3
+ import type { QueryRequest, QueryScalar, QuerySort } from "../query/model.ts"
4
+ import { Log } from "../log.ts"
5
+ import { queryRecordValue, type QueryField } from "../query/fields.ts"
6
+ import { createInspectionView } from "../view/session.ts"
7
+ import { fail, loadApp, rootFromCommand } from "./utils.ts"
8
+
9
+ type QueryFormat = "json" | "jsonl" | "table"
10
+
11
+ type QueryOptions = Readonly<{
12
+ as?: string
13
+ format: QueryFormat
14
+ from?: string
15
+ input?: string
16
+ where: readonly string[]
17
+ sort: readonly string[]
18
+ limit?: number
19
+ type?: string
20
+ has: readonly string[]
21
+ recursive?: boolean
22
+ request?: string
23
+ }>
24
+
25
+ function collect(value: string, values: string[]) {
26
+ return [...values, value]
27
+ }
28
+
29
+ function positiveInteger(value: string) {
30
+ const parsed = Number(value)
31
+ if (!Number.isSafeInteger(parsed) || parsed < 0) {
32
+ throw new Error(`Invalid query limit: ${value}`)
33
+ }
34
+ return parsed
35
+ }
36
+
37
+ function formatOption(value: string): QueryFormat {
38
+ if (value === "json" || value === "jsonl" || value === "table") return value
39
+ throw new Error(`Unsupported query format: ${value}`)
40
+ }
41
+
42
+ function scalar(value: string): QueryScalar {
43
+ const trimmed = value.trim()
44
+ if (trimmed === "null") return null
45
+ if (trimmed === "true") return true
46
+ if (trimmed === "false") return false
47
+ if (/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(trimmed)) {
48
+ const number = Number(trimmed)
49
+ if (Number.isFinite(number)) return number
50
+ }
51
+ if (trimmed.startsWith('"')) {
52
+ const parsed = JSON.parse(trimmed)
53
+ if (typeof parsed !== "string") {
54
+ throw new Error(`Query value is not a scalar string: ${value}`)
55
+ }
56
+ return parsed
57
+ }
58
+ return value
59
+ }
60
+
61
+ function whereEntries(entries: readonly string[]) {
62
+ const where: Record<string, QueryScalar> = {}
63
+ for (const entry of entries) {
64
+ const separator = entry.indexOf("=")
65
+ if (separator <= 0) {
66
+ throw new Error(`Invalid query filter: ${entry}; expected field=value.`)
67
+ }
68
+ const field = entry.slice(0, separator).trim()
69
+ if (!field) throw new Error(`Invalid query filter: ${entry}`)
70
+ where[field] = scalar(entry.slice(separator + 1))
71
+ }
72
+ return where
73
+ }
74
+
75
+ function sortEntries(entries: readonly string[]) {
76
+ return entries.map((entry): QuerySort => {
77
+ const [field, direction, extra] = entry.split(":")
78
+ if (!field?.trim() || extra !== undefined) {
79
+ throw new Error(`Invalid query sort: ${entry}`)
80
+ }
81
+ if (direction && direction !== "asc" && direction !== "desc") {
82
+ throw new Error(`Invalid query sort direction: ${direction}`)
83
+ }
84
+ const normalizedDirection =
85
+ direction === "asc" || direction === "desc" ? direction : undefined
86
+ return {
87
+ by: field.trim(),
88
+ ...(normalizedDirection ? { direction: normalizedDirection } : {}),
89
+ }
90
+ })
91
+ }
92
+
93
+ async function requestFile(app: Engine, path: string) {
94
+ const text =
95
+ path === "-"
96
+ ? await new Response(Bun.stdin.stream()).text()
97
+ : await Bun.file(path).text()
98
+ let parsed: unknown
99
+ if (path.endsWith(".yaml") || path.endsWith(".yml")) {
100
+ parsed = app.environment.yaml.parse(text)
101
+ } else {
102
+ try {
103
+ parsed = JSON.parse(text)
104
+ } catch (error) {
105
+ if (path.endsWith(".json")) throw error
106
+ parsed = app.environment.yaml.parse(text)
107
+ }
108
+ }
109
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
110
+ throw new Error("Query request must be a JSON or YAML object.")
111
+ }
112
+ return parsed as Record<string, unknown>
113
+ }
114
+
115
+ async function queryRequest(app: Engine, options: QueryOptions) {
116
+ const request = options.request ? await requestFile(app, options.request) : {}
117
+ const where = {
118
+ ...((request.where as Record<string, QueryScalar> | undefined) ?? {}),
119
+ ...whereEntries(options.where),
120
+ }
121
+ const sort = [
122
+ ...((request.sort as readonly QuerySort[] | undefined) ?? []),
123
+ ...sortEntries(options.sort),
124
+ ]
125
+ return {
126
+ ...request,
127
+ ...(options.from !== undefined ? { from: options.from } : {}),
128
+ ...(options.input !== undefined
129
+ ? { input: JSON.parse(options.input) }
130
+ : {}),
131
+ ...(Object.keys(where).length ? { where } : {}),
132
+ ...(sort.length ? { sort } : {}),
133
+ ...(options.limit !== undefined ? { limit: options.limit } : {}),
134
+ ...(options.type !== undefined ? { type: options.type } : {}),
135
+ ...(options.has.length ? { has: options.has } : {}),
136
+ ...(options.recursive === false ? { recursive: false } : {}),
137
+ } as QueryRequest<unknown>
138
+ }
139
+
140
+ function cell(value: unknown) {
141
+ if (value == null) return ""
142
+ return typeof value === "string" ? value : String(value)
143
+ }
144
+
145
+ function table(columns: readonly QueryField[], results: readonly unknown[]) {
146
+ const rows = results.map((result) =>
147
+ columns.map(({ path }) => cell(queryRecordValue(result, path))),
148
+ )
149
+ const headers = columns.map(({ path, label }) => label ?? path)
150
+ const widths = headers.map((header, index) =>
151
+ Math.max(header.length, ...rows.map((row) => row[index]?.length ?? 0)),
152
+ )
153
+ return [headers, ...rows]
154
+ .map((row) =>
155
+ row
156
+ .map((value, index) => value.padEnd(widths[index] ?? value.length))
157
+ .join(" ")
158
+ .trimEnd(),
159
+ )
160
+ .join("\n")
161
+ }
162
+
163
+ export async function executeQuery(
164
+ app: Engine,
165
+ query: string,
166
+ options: QueryOptions,
167
+ ) {
168
+ const request = await queryRequest(app, options)
169
+ const actor = options.as ? app.actor(options.as) : undefined
170
+ if (options.as && !actor) throw new Error(`Actor not found: ${options.as}`)
171
+ const view = createInspectionView(app, actor)
172
+ const results = (await app.queries.run(
173
+ view,
174
+ query,
175
+ request,
176
+ )) as readonly unknown[]
177
+ if (options.format === "json") return JSON.stringify(results, null, 2)
178
+ if (options.format === "jsonl") {
179
+ return results.map((result) => JSON.stringify(result)).join("\n")
180
+ }
181
+ const fields = await app.queries.fields(query, request)
182
+ const columns = fields.filter(({ path }) =>
183
+ results.some((result) => queryRecordValue(result, path) !== undefined),
184
+ )
185
+ return table(columns, results)
186
+ }
187
+
188
+ export function registerQuery(program: Command) {
189
+ program
190
+ .command("query")
191
+ .description("Query Sources, Blocks, or an installed named query")
192
+ .argument("<query>", "sources, blocks, or plugin:name")
193
+ .option("--from <scope>", "canonical or Actor-relative scope")
194
+ .option("--input <json>", "named-query input as JSON")
195
+ .option("--where <field=value>", "scalar equality filter", collect, [])
196
+ .option("--sort <field[:direction]>", "result ordering", collect, [])
197
+ .option("--limit <number>", "maximum result count", positiveInteger)
198
+ .option("--type <type>", "built-in Source or Block type")
199
+ .option("--has <prop>", "required registered data prop", collect, [])
200
+ .option("--no-recursive", "exclude nested scopes")
201
+ .option("--as <actor>", "inspect through an Actor's effective access")
202
+ .option("--request <file>", "JSON/YAML request file, or - for stdin")
203
+ .option(
204
+ "--format <format>",
205
+ "output format: json, jsonl, or table",
206
+ formatOption,
207
+ "json",
208
+ )
209
+ .action(async (query: string, options: QueryOptions, command: Command) => {
210
+ let app: Engine | undefined
211
+ try {
212
+ const resolved = rootFromCommand(command)
213
+ app = await loadApp(resolved.root, {
214
+ actionsRoot: resolved.actionsRoot,
215
+ buildCache: false,
216
+ log: new Log({ sink() {} }),
217
+ })
218
+ console.log(await executeQuery(app, query, options))
219
+ } catch (error) {
220
+ fail(error)
221
+ } finally {
222
+ await app?.close()
223
+ }
224
+ })
225
+ }