@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.
- package/LICENSE.md +172 -0
- package/README.md +111 -0
- package/actions/actions.ts +307 -0
- package/actions/draft.ts +105 -0
- package/actions/transaction.ts +164 -0
- package/assets.ts +149 -0
- package/authority/access.ts +820 -0
- package/authority/actors.ts +271 -0
- package/authority/agents.ts +95 -0
- package/authority/credentials.ts +144 -0
- package/authority/model.ts +71 -0
- package/authority/sharing.ts +89 -0
- package/browser-bundle.ts +61 -0
- package/canonical.ts +41 -0
- package/cli/dev.ts +134 -0
- package/cli/export.ts +74 -0
- package/cli/index.ts +52 -0
- package/cli/lint.ts +164 -0
- package/cli/mv.ts +30 -0
- package/cli/perms.ts +182 -0
- package/cli/plugin-packages.ts +159 -0
- package/cli/plugins.ts +95 -0
- package/cli/query.ts +225 -0
- package/cli/readme.md +95 -0
- package/cli/release.ts +75 -0
- package/cli/rm.ts +22 -0
- package/cli/serve.ts +100 -0
- package/cli/tree.ts +131 -0
- package/cli/update.ts +78 -0
- package/cli/utils.ts +53 -0
- package/core.ts +96 -0
- package/database.ts +189 -0
- package/dependencies.ts +60 -0
- package/dist/pathmx.js +1224 -0
- package/document.ts +194 -0
- package/environment.ts +110 -0
- package/environments/bun-plugins.ts +68 -0
- package/environments/bun-publish.ts +194 -0
- package/environments/bun.ts +223 -0
- package/globals.d.ts +14 -0
- package/graph.ts +153 -0
- package/host/compiler.ts +314 -0
- package/host/create.ts +66 -0
- package/host/engine.ts +630 -0
- package/host/plugin-host.ts +487 -0
- package/host/render.ts +102 -0
- package/host/response.ts +20 -0
- package/host/shell.html +15 -0
- package/host/source-sync.ts +254 -0
- package/html-attributes.ts +15 -0
- package/html.ts +48 -0
- package/icons/LICENSE +43 -0
- package/icons/lucide.ts +54 -0
- package/index.ts +75 -0
- package/invalidation.ts +85 -0
- package/log.ts +84 -0
- package/ops/href.ts +87 -0
- package/ops/index.ts +9 -0
- package/ops/move-source.ts +114 -0
- package/ops/plan.ts +47 -0
- package/ops/remove-source.ts +70 -0
- package/package.json +46 -0
- package/path.ts +75 -0
- package/pattern.ts +314 -0
- package/perf.ts +88 -0
- package/plugins/actor-path.ts +43 -0
- package/plugins/actor-profile.ts +55 -0
- package/plugins/agent-browsing.ts +183 -0
- package/plugins/app-component.ts +23 -0
- package/plugins/block-layout/index.ts +78 -0
- package/plugins/block-layout/theme.css +90 -0
- package/plugins/callout/index.ts +236 -0
- package/plugins/callout/theme.css +138 -0
- package/plugins/canonical-links.ts +144 -0
- package/plugins/code-highlight/client.ts +89 -0
- package/plugins/code-highlight/index.ts +147 -0
- package/plugins/code-highlight/theme.css +89 -0
- package/plugins/context.ts +131 -0
- package/plugins/datatype/Datatype[wdth,wght].woff2 +0 -0
- package/plugins/datatype/OFL.txt +96 -0
- package/plugins/datatype/index.ts +287 -0
- package/plugins/definition-list.ts +236 -0
- package/plugins/directive-definitions.ts +13 -0
- package/plugins/embedding/client.ts +88 -0
- package/plugins/embedding/index.ts +43 -0
- package/plugins/image/index.ts +68 -0
- package/plugins/image/theme.css +43 -0
- package/plugins/include.ts +119 -0
- package/plugins/index.ts +91 -0
- package/plugins/input/authoring.ts +236 -0
- package/plugins/input/config.ts +85 -0
- package/plugins/input/index.ts +359 -0
- package/plugins/input/model.ts +173 -0
- package/plugins/input/state.ts +272 -0
- package/plugins/layout.ts +313 -0
- package/plugins/literate/client.ts +208 -0
- package/plugins/literate/index.ts +742 -0
- package/plugins/math/index.ts +140 -0
- package/plugins/meta.ts +42 -0
- package/plugins/navigation/index.ts +158 -0
- package/plugins/navigation/model.ts +364 -0
- package/plugins/preset.ts +65 -0
- package/plugins/query/index.ts +322 -0
- package/plugins/rss/index.ts +46 -0
- package/plugins/runtime/actions.ts +62 -0
- package/plugins/runtime/client.ts +8 -0
- package/plugins/runtime/document.ts +506 -0
- package/plugins/runtime/error.ts +154 -0
- package/plugins/runtime/heading-links.ts +31 -0
- package/plugins/runtime/index.ts +104 -0
- package/plugins/runtime/live.ts +51 -0
- package/plugins/runtime/navigation.ts +113 -0
- package/plugins/runtime/pending.ts +53 -0
- package/plugins/runtime/publication.ts +19 -0
- package/plugins/runtime/start.ts +177 -0
- package/plugins/runtime/static-client.ts +6 -0
- package/plugins/runtime/static-live.ts +188 -0
- package/plugins/runtime/view-transition.ts +72 -0
- package/plugins/runtime/viewport.ts +110 -0
- package/plugins/schema.ts +24 -0
- package/plugins/scripts.ts +140 -0
- package/plugins/source-preview/client.ts +256 -0
- package/plugins/source-preview/index.ts +132 -0
- package/plugins/source-preview/model.ts +11 -0
- package/plugins/source-preview/preview.css +107 -0
- package/plugins/styles.ts +145 -0
- package/plugins/theme/components.ts +10 -0
- package/plugins/theme/foundation.css +358 -0
- package/plugins/theme/index.ts +333 -0
- package/plugins/theme/prose.css +284 -0
- package/plugins/theme/scheme.js +63 -0
- package/plugins/theme/toggle.css +44 -0
- package/plugins/theme/view-transition.css +68 -0
- package/plugins/toc/client.ts +146 -0
- package/plugins/toc/index.ts +232 -0
- package/plugins/toc/theme.css +172 -0
- package/plugins/types.ts +538 -0
- package/problem.ts +41 -0
- package/project.ts +6 -0
- package/publish/build.ts +402 -0
- package/publish/concurrent.ts +32 -0
- package/publish/identity.ts +63 -0
- package/publish/index.ts +11 -0
- package/publish/location.ts +167 -0
- package/publish/model.ts +58 -0
- package/publish/plan.ts +300 -0
- package/publish/validate.ts +150 -0
- package/query/candidate-index.ts +101 -0
- package/query/dependency-index.ts +99 -0
- package/query/fields.ts +216 -0
- package/query/invalidation.ts +48 -0
- package/query/kernel.ts +776 -0
- package/query/model.ts +79 -0
- package/repository.ts +374 -0
- package/runtime-profile.ts +34 -0
- package/schema/model.ts +92 -0
- package/schema/registry.ts +642 -0
- package/schema/standard.ts +226 -0
- package/server/context.ts +94 -0
- package/server/css-imports.ts +66 -0
- package/server/http.ts +333 -0
- package/server/page.ts +21 -0
- package/server/protocols.ts +12 -0
- package/server/representations.ts +66 -0
- package/server/router.ts +131 -0
- package/server/routes.ts +202 -0
- package/server/serve.ts +5 -0
- package/server/watch.ts +120 -0
- package/source/blocks.ts +71 -0
- package/source/format.ts +164 -0
- package/source/index.ts +67 -0
- package/source/interpolate.ts +63 -0
- package/source/markdown/blocks.ts +46 -0
- package/source/markdown/comments.ts +32 -0
- package/source/markdown/fences.ts +67 -0
- package/source/markdown/headings.ts +88 -0
- package/source/markdown/index.ts +34 -0
- package/source/markdown/inline-code.ts +6 -0
- package/source/markdown/links.ts +326 -0
- package/source/markdown/lists.ts +223 -0
- package/source/markdown/prose.ts +74 -0
- package/source/markdown/scan.ts +79 -0
- package/source/markdown/text.ts +82 -0
- package/source/meta.ts +87 -0
- package/source/source.ts +121 -0
- package/source/topmatter.ts +57 -0
- package/source/utils.ts +21 -0
- package/source-diagnostic.ts +63 -0
- package/text-file.ts +26 -0
- package/util.ts +35 -0
- package/view/components.ts +499 -0
- package/view/model.ts +91 -0
- package/view/projection.ts +162 -0
- package/view/session.ts +146 -0
- 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
|
+
}
|