@vladpazych/dexter 0.1.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/bin/dexter +6 -0
- package/package.json +43 -0
- package/src/claude/index.ts +6 -0
- package/src/cli.ts +39 -0
- package/src/env/define.ts +190 -0
- package/src/env/index.ts +10 -0
- package/src/env/loader.ts +61 -0
- package/src/env/print.ts +98 -0
- package/src/env/validate.ts +46 -0
- package/src/index.ts +16 -0
- package/src/meta/adapters/fs.ts +22 -0
- package/src/meta/adapters/git.ts +29 -0
- package/src/meta/adapters/glob.ts +14 -0
- package/src/meta/adapters/index.ts +24 -0
- package/src/meta/adapters/process.ts +40 -0
- package/src/meta/cli.ts +340 -0
- package/src/meta/domain/bisect.ts +126 -0
- package/src/meta/domain/blame.ts +136 -0
- package/src/meta/domain/commit.ts +135 -0
- package/src/meta/domain/commits.ts +23 -0
- package/src/meta/domain/constraints/registry.ts +49 -0
- package/src/meta/domain/constraints/types.ts +30 -0
- package/src/meta/domain/diff.ts +34 -0
- package/src/meta/domain/eval.ts +57 -0
- package/src/meta/domain/format.ts +34 -0
- package/src/meta/domain/lint.ts +88 -0
- package/src/meta/domain/pickaxe.ts +99 -0
- package/src/meta/domain/quality.ts +145 -0
- package/src/meta/domain/rules.ts +21 -0
- package/src/meta/domain/scope-context.ts +63 -0
- package/src/meta/domain/service.ts +68 -0
- package/src/meta/domain/setup.ts +34 -0
- package/src/meta/domain/test.ts +72 -0
- package/src/meta/domain/transcripts.ts +88 -0
- package/src/meta/domain/typecheck.ts +41 -0
- package/src/meta/domain/workspace.ts +78 -0
- package/src/meta/errors.ts +19 -0
- package/src/meta/hooks/on-post-read.ts +61 -0
- package/src/meta/hooks/on-post-write.ts +65 -0
- package/src/meta/hooks/on-pre-bash.ts +69 -0
- package/src/meta/hooks/stubs.ts +51 -0
- package/src/meta/index.ts +36 -0
- package/src/meta/lib/actor.ts +53 -0
- package/src/meta/lib/eslint.ts +58 -0
- package/src/meta/lib/format.ts +55 -0
- package/src/meta/lib/paths.ts +36 -0
- package/src/meta/lib/present.ts +231 -0
- package/src/meta/lib/spec-links.ts +83 -0
- package/src/meta/lib/stdin.ts +56 -0
- package/src/meta/ports.ts +50 -0
- package/src/meta/types.ts +113 -0
- package/src/output/build.ts +56 -0
- package/src/output/index.ts +24 -0
- package/src/output/output.test.ts +374 -0
- package/src/output/render-cli.ts +55 -0
- package/src/output/render-json.ts +80 -0
- package/src/output/render-md.ts +43 -0
- package/src/output/render-xml.ts +55 -0
- package/src/output/render.ts +23 -0
- package/src/output/types.ts +44 -0
- package/src/pipe/format.ts +167 -0
- package/src/pipe/index.ts +4 -0
- package/src/pipe/parse.ts +131 -0
- package/src/pipe/spawn.ts +205 -0
- package/src/pipe/types.ts +27 -0
- package/src/terminal/colors.ts +95 -0
- package/src/terminal/index.ts +16 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presenter layer — domain results → Node trees.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions. Each converts a typed domain result into a document tree
|
|
5
|
+
* that renders polymorphically via dexter's output module.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { block, field, heading, list, text } from "../../output/index.ts"
|
|
9
|
+
import type { Node } from "../../output/index.ts"
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
CommitResult,
|
|
13
|
+
EvalResult,
|
|
14
|
+
GitResult,
|
|
15
|
+
Package,
|
|
16
|
+
QueryResult,
|
|
17
|
+
SetupResult,
|
|
18
|
+
TranscriptsResult,
|
|
19
|
+
} from "../types.ts"
|
|
20
|
+
import type { ControlError } from "../errors.ts"
|
|
21
|
+
|
|
22
|
+
// --- Query ---
|
|
23
|
+
|
|
24
|
+
function presentRules(result: Extract<QueryResult, { what: "rules" }>): Node {
|
|
25
|
+
return block(
|
|
26
|
+
"rules",
|
|
27
|
+
...result.data.map((scope) =>
|
|
28
|
+
block(
|
|
29
|
+
"scope",
|
|
30
|
+
{ path: scope.path },
|
|
31
|
+
heading(scope.path),
|
|
32
|
+
field("cascade", list("ref", ...scope.cascade.map((c) => text(c)))),
|
|
33
|
+
),
|
|
34
|
+
),
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function presentDiff(result: Extract<QueryResult, { what: "diff" }>): Node {
|
|
39
|
+
return block(
|
|
40
|
+
"diff",
|
|
41
|
+
...result.data.map((scope) => {
|
|
42
|
+
const parts: Node[] = [heading(scope.path)]
|
|
43
|
+
if (scope.status) parts.push(field("status", scope.status))
|
|
44
|
+
if (scope.diff) parts.push(field("diff", scope.diff))
|
|
45
|
+
return block("scope", { path: scope.path }, ...parts)
|
|
46
|
+
}),
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function presentCommits(result: Extract<QueryResult, { what: "commits" }>): Node {
|
|
51
|
+
const children: Node[] = result.data.map((scope) =>
|
|
52
|
+
block(
|
|
53
|
+
"scope",
|
|
54
|
+
{ path: scope.path },
|
|
55
|
+
heading(scope.path),
|
|
56
|
+
field("log", list("entry", ...scope.log.map((l) => text(l)))),
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
if (result.recent.length > 0) {
|
|
60
|
+
children.push(
|
|
61
|
+
block(
|
|
62
|
+
"recent-commits",
|
|
63
|
+
heading("Recent Commits"),
|
|
64
|
+
field("entries", list("entry", ...result.recent.map((l) => text(l)))),
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
return block("commits", ...children)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function presentCheck(result: Extract<QueryResult, { what: "lint" | "typecheck" | "test" }>): Node {
|
|
72
|
+
const { what, data } = result
|
|
73
|
+
const children: Node[] = [field("errorCount", data.errorCount)]
|
|
74
|
+
if (data.errors.length > 0) {
|
|
75
|
+
children.push(field("errors", list("error", ...data.errors.map((e) => text(e.summary)))))
|
|
76
|
+
}
|
|
77
|
+
if (data.raw) {
|
|
78
|
+
children.push(field("raw", data.raw))
|
|
79
|
+
}
|
|
80
|
+
return block(what, ...children)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function presentQuery(result: QueryResult): Node {
|
|
84
|
+
switch (result.what) {
|
|
85
|
+
case "rules":
|
|
86
|
+
return presentRules(result)
|
|
87
|
+
case "diff":
|
|
88
|
+
return presentDiff(result)
|
|
89
|
+
case "commits":
|
|
90
|
+
return presentCommits(result)
|
|
91
|
+
case "lint":
|
|
92
|
+
case "typecheck":
|
|
93
|
+
case "test":
|
|
94
|
+
return presentCheck(result)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// --- Git ---
|
|
99
|
+
|
|
100
|
+
function presentBlame(result: Extract<GitResult, { what: "blame" }>): Node {
|
|
101
|
+
return block(
|
|
102
|
+
"blame",
|
|
103
|
+
{ file: result.file },
|
|
104
|
+
heading(result.file),
|
|
105
|
+
...result.ranges.map((r) =>
|
|
106
|
+
block(
|
|
107
|
+
"range",
|
|
108
|
+
{ commit: r.commit, lines: `${r.startLine}-${r.endLine}` },
|
|
109
|
+
field("commit", r.commit),
|
|
110
|
+
field("author", r.author),
|
|
111
|
+
field("date", r.date),
|
|
112
|
+
field("message", r.message),
|
|
113
|
+
field("lines", `${r.startLine}-${r.endLine}`),
|
|
114
|
+
field("content", r.content.join("\n")),
|
|
115
|
+
),
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function presentPickaxe(result: Extract<GitResult, { what: "pickaxe" }>): Node {
|
|
121
|
+
if (result.matches.length === 0) {
|
|
122
|
+
return block("pickaxe", { pattern: result.pattern }, field("matches", "none"))
|
|
123
|
+
}
|
|
124
|
+
return block(
|
|
125
|
+
"pickaxe",
|
|
126
|
+
{ pattern: result.pattern },
|
|
127
|
+
...result.matches.map((m) =>
|
|
128
|
+
block(
|
|
129
|
+
"match",
|
|
130
|
+
{ hash: m.hash },
|
|
131
|
+
field("hash", m.hash),
|
|
132
|
+
field("author", m.author),
|
|
133
|
+
field("date", m.date),
|
|
134
|
+
field("message", m.message),
|
|
135
|
+
field("diff", m.diff),
|
|
136
|
+
),
|
|
137
|
+
),
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function presentBisect(result: Extract<GitResult, { what: "bisect" }>): Node {
|
|
142
|
+
const m = result.match
|
|
143
|
+
return block(
|
|
144
|
+
"bisect",
|
|
145
|
+
{ hash: m.hash },
|
|
146
|
+
field("hash", m.hash),
|
|
147
|
+
field("author", m.author),
|
|
148
|
+
field("date", m.date),
|
|
149
|
+
field("message", m.message),
|
|
150
|
+
field("diff", m.diff),
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function presentGit(result: GitResult): Node {
|
|
155
|
+
switch (result.what) {
|
|
156
|
+
case "blame":
|
|
157
|
+
return presentBlame(result)
|
|
158
|
+
case "pickaxe":
|
|
159
|
+
return presentPickaxe(result)
|
|
160
|
+
case "bisect":
|
|
161
|
+
return presentBisect(result)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// --- Commit ---
|
|
166
|
+
|
|
167
|
+
export function presentCommit(result: CommitResult): Node {
|
|
168
|
+
return block(
|
|
169
|
+
"commit",
|
|
170
|
+
field("hash", result.hash),
|
|
171
|
+
field("message", result.message),
|
|
172
|
+
field("files", list("file", ...result.files.map((f) => text(f)))),
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// --- Eval ---
|
|
177
|
+
|
|
178
|
+
export function presentEval(result: EvalResult): Node {
|
|
179
|
+
const children: Node[] = []
|
|
180
|
+
if (result.stdout) children.push(field("stdout", result.stdout))
|
|
181
|
+
if (result.stderr) children.push(field("stderr", result.stderr))
|
|
182
|
+
return block("eval", { ok: String(result.ok) }, ...children)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// --- Packages ---
|
|
186
|
+
|
|
187
|
+
export function presentPackages(packages: Package[]): Node {
|
|
188
|
+
return list(
|
|
189
|
+
"package",
|
|
190
|
+
...packages.map((pkg) => block("package", field("name", pkg.shortName), field("dir", pkg.relDir))),
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// --- Setup ---
|
|
195
|
+
|
|
196
|
+
export function presentSetup(result: SetupResult): Node {
|
|
197
|
+
return block("setup", field("settings", result.settingsPath), field("bin", result.binPath))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// --- Transcripts ---
|
|
201
|
+
|
|
202
|
+
export function presentTranscripts(result: TranscriptsResult): Node {
|
|
203
|
+
if (result.entries.length === 0) {
|
|
204
|
+
return block("transcripts", { project: result.projectSlug }, field("entries", "none"))
|
|
205
|
+
}
|
|
206
|
+
return block(
|
|
207
|
+
"transcripts",
|
|
208
|
+
{ project: result.projectSlug },
|
|
209
|
+
...result.entries.map((entry) =>
|
|
210
|
+
block(
|
|
211
|
+
"transcript",
|
|
212
|
+
{ agentId: entry.agentId },
|
|
213
|
+
field("path", entry.path),
|
|
214
|
+
field("skill", entry.skill),
|
|
215
|
+
field("session", entry.sessionId),
|
|
216
|
+
field("timestamp", entry.timestamp),
|
|
217
|
+
field("size", entry.size),
|
|
218
|
+
),
|
|
219
|
+
),
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// --- Error ---
|
|
224
|
+
|
|
225
|
+
export function presentError(err: ControlError): Node {
|
|
226
|
+
const children: Node[] = [field("code", err.code), field("message", text(err.message, "red"))]
|
|
227
|
+
if (err.hints.length > 0) {
|
|
228
|
+
children.push(field("hints", list("hint", ...err.hints.map((h) => text(h)))))
|
|
229
|
+
}
|
|
230
|
+
return block("error", ...children)
|
|
231
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec link validation.
|
|
3
|
+
*
|
|
4
|
+
* Validates markdown links [text](path.md) in CLAUDE.md and .md files.
|
|
5
|
+
*
|
|
6
|
+
* Paths resolve as:
|
|
7
|
+
* - /path/file.md Root-absolute (from repo root)
|
|
8
|
+
* - ./path/file.md Relative (from file's directory)
|
|
9
|
+
* - path/file.md Relative (from file's directory)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync } from "node:fs"
|
|
13
|
+
import { basename, dirname, join, resolve } from "node:path"
|
|
14
|
+
|
|
15
|
+
export type SpecLink = {
|
|
16
|
+
type: "link"
|
|
17
|
+
path: string
|
|
18
|
+
line: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type BrokenLink = SpecLink & {
|
|
22
|
+
resolved: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Check if a file participates in the spec system */
|
|
26
|
+
export function isSpecFile(filePath: string): boolean {
|
|
27
|
+
if (basename(filePath) === "CLAUDE.md") return true
|
|
28
|
+
if (filePath.endsWith(".md")) return true
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Extract markdown links to .md files */
|
|
33
|
+
export function extractSpecLinks(content: string): SpecLink[] {
|
|
34
|
+
const links: SpecLink[] = []
|
|
35
|
+
const lines = content.split("\n")
|
|
36
|
+
|
|
37
|
+
for (let i = 0; i < lines.length; i++) {
|
|
38
|
+
const line = lines[i]!
|
|
39
|
+
const lineNum = i + 1
|
|
40
|
+
|
|
41
|
+
const linkRegex = /\[[^\]]*\]\(([^)]+\.md)\)/g
|
|
42
|
+
let match
|
|
43
|
+
while ((match = linkRegex.exec(line)) !== null) {
|
|
44
|
+
const path = match[1]!
|
|
45
|
+
if (path.startsWith("http://") || path.startsWith("https://")) continue
|
|
46
|
+
links.push({ type: "link", path, line: lineNum })
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return links
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Resolve a spec link path to absolute filesystem path */
|
|
54
|
+
export function resolveSpecPath(linkPath: string, fromFile: string, repoRoot: string): string {
|
|
55
|
+
if (linkPath.startsWith("/")) {
|
|
56
|
+
return join(repoRoot, linkPath)
|
|
57
|
+
}
|
|
58
|
+
return resolve(dirname(fromFile), linkPath)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Find all broken links in a spec file */
|
|
62
|
+
export function findBrokenLinks(filePath: string, content: string, repoRoot: string): BrokenLink[] {
|
|
63
|
+
const links = extractSpecLinks(content)
|
|
64
|
+
const broken: BrokenLink[] = []
|
|
65
|
+
|
|
66
|
+
for (const link of links) {
|
|
67
|
+
const resolved = resolveSpecPath(link.path, filePath, repoRoot)
|
|
68
|
+
if (!existsSync(resolved)) {
|
|
69
|
+
broken.push({ ...link, resolved })
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return broken
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Format broken links for hook context output */
|
|
77
|
+
export function formatBrokenLinks(broken: BrokenLink[]): string {
|
|
78
|
+
const lines = ["=== Broken Spec Links ==="]
|
|
79
|
+
for (const link of broken) {
|
|
80
|
+
lines.push(`Warning: [...](${link.path}) -> not found (line ${link.line})`)
|
|
81
|
+
}
|
|
82
|
+
return lines.join("\n")
|
|
83
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for reading hook input from stdin
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Read all input from stdin
|
|
7
|
+
*/
|
|
8
|
+
export async function readStdin(): Promise<string> {
|
|
9
|
+
return Bun.stdin.text()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parse JSON from stdin, returning null on parse error
|
|
14
|
+
*/
|
|
15
|
+
export async function readJsonStdin<T>(): Promise<T | null> {
|
|
16
|
+
const input = await readStdin()
|
|
17
|
+
if (!input.trim()) return null
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(input) as T
|
|
21
|
+
} catch {
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Hook input structure from Claude Code
|
|
28
|
+
*/
|
|
29
|
+
export type HookInput = {
|
|
30
|
+
/** Session identifier for correlation */
|
|
31
|
+
session_id?: string
|
|
32
|
+
/** The tool name that triggered the hook */
|
|
33
|
+
tool_name?: string
|
|
34
|
+
/** Tool input parameters */
|
|
35
|
+
tool_input?: Record<string, unknown>
|
|
36
|
+
/** Tool output (for PostToolUse) */
|
|
37
|
+
tool_output?: unknown
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Extract file path from hook input
|
|
42
|
+
*/
|
|
43
|
+
export function getFilePath(input: HookInput | null): string | null {
|
|
44
|
+
if (!input?.tool_input) return null
|
|
45
|
+
const filePath = input.tool_input.file_path
|
|
46
|
+
return typeof filePath === "string" ? filePath : null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extract command from Bash hook input
|
|
51
|
+
*/
|
|
52
|
+
export function getCommand(input: HookInput | null): string | null {
|
|
53
|
+
if (!input?.tool_input) return null
|
|
54
|
+
const command = input.tool_input.command
|
|
55
|
+
return typeof command === "string" ? command : null
|
|
56
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port interfaces — what control domain needs from the outside world.
|
|
3
|
+
*
|
|
4
|
+
* Adapters implement these. Domain functions accept ControlPorts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type SpawnResult = { success: boolean; stdout: string; stderr: string }
|
|
8
|
+
|
|
9
|
+
export type GitPort = {
|
|
10
|
+
run(args: string[], env?: Record<string, string>): SpawnResult
|
|
11
|
+
checkIgnore(file: string): boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type FsPort = {
|
|
15
|
+
exists(path: string): boolean
|
|
16
|
+
readFile(path: string): string
|
|
17
|
+
writeFile(path: string, content: string): void
|
|
18
|
+
readDir(path: string): Array<{ name: string; isDirectory: boolean }>
|
|
19
|
+
unlink(path: string): void
|
|
20
|
+
mkdir(path: string): void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type ProcessHandle = {
|
|
24
|
+
onLine(stream: "stdout" | "stderr", cb: (line: string) => void): void
|
|
25
|
+
wait(): Promise<number | null>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type ProcessPort = {
|
|
29
|
+
spawn(params: {
|
|
30
|
+
cmd: string
|
|
31
|
+
args: string[]
|
|
32
|
+
cwd: string
|
|
33
|
+
env?: Record<string, string>
|
|
34
|
+
timeout?: number
|
|
35
|
+
}): ProcessHandle
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type GlobPort = {
|
|
39
|
+
match(pattern: string, candidates: string[]): string[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type ControlPorts = {
|
|
43
|
+
git: GitPort
|
|
44
|
+
fs: FsPort
|
|
45
|
+
process: ProcessPort
|
|
46
|
+
glob: GlobPort
|
|
47
|
+
tmpdir: () => string
|
|
48
|
+
homedir: () => string
|
|
49
|
+
root: string
|
|
50
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain types — command parameters, results, and shared contracts.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type CommitParams = {
|
|
6
|
+
message: string
|
|
7
|
+
files: string[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type CommitResult = {
|
|
11
|
+
hash: string
|
|
12
|
+
message: string
|
|
13
|
+
files: string[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type QualityCheck = {
|
|
17
|
+
name: string
|
|
18
|
+
status: "pass" | "fail"
|
|
19
|
+
output: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type QualityResult = {
|
|
23
|
+
passed: boolean
|
|
24
|
+
checks: QualityCheck[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type SetupResult = {
|
|
28
|
+
settingsPath: string
|
|
29
|
+
binPath: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// --- eval ---
|
|
33
|
+
|
|
34
|
+
export type EvalParams = { code: string; timeout?: number }
|
|
35
|
+
export type EvalResult = { ok: boolean; stdout: string; stderr: string }
|
|
36
|
+
|
|
37
|
+
// --- transcripts ---
|
|
38
|
+
|
|
39
|
+
export type TranscriptsParams = { skill?: string; minutes?: number }
|
|
40
|
+
|
|
41
|
+
export type TranscriptEntry = {
|
|
42
|
+
path: string
|
|
43
|
+
agentId: string
|
|
44
|
+
sessionId: string
|
|
45
|
+
skill: string
|
|
46
|
+
timestamp: string
|
|
47
|
+
size: number
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type TranscriptsResult = {
|
|
51
|
+
projectSlug: string
|
|
52
|
+
entries: TranscriptEntry[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --- packages ---
|
|
56
|
+
|
|
57
|
+
export type Package = {
|
|
58
|
+
name: string
|
|
59
|
+
shortName: string
|
|
60
|
+
dir: string
|
|
61
|
+
relDir: string
|
|
62
|
+
scripts: Record<string, string>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// --- blame / pickaxe / bisect ---
|
|
66
|
+
|
|
67
|
+
export type BlameRange = {
|
|
68
|
+
commit: string
|
|
69
|
+
author: string
|
|
70
|
+
date: string
|
|
71
|
+
message: string
|
|
72
|
+
startLine: number
|
|
73
|
+
endLine: number
|
|
74
|
+
content: string[]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type PickaxeMatch = {
|
|
78
|
+
hash: string
|
|
79
|
+
author: string
|
|
80
|
+
date: string
|
|
81
|
+
message: string
|
|
82
|
+
diff: string
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type BisectMatch = {
|
|
86
|
+
hash: string
|
|
87
|
+
author: string
|
|
88
|
+
date: string
|
|
89
|
+
message: string
|
|
90
|
+
diff: string
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type GitResult =
|
|
94
|
+
| { what: "blame"; file: string; ranges: BlameRange[] }
|
|
95
|
+
| { what: "pickaxe"; pattern: string; matches: PickaxeMatch[] }
|
|
96
|
+
| { what: "bisect"; match: BisectMatch }
|
|
97
|
+
|
|
98
|
+
// --- rules / diff / commits / lint / typecheck / test ---
|
|
99
|
+
|
|
100
|
+
export type RulesScope = { path: string; cascade: string[] }
|
|
101
|
+
export type DiffScope = { path: string; status: string; diff: string }
|
|
102
|
+
export type CommitsScope = { path: string; log: string[] }
|
|
103
|
+
|
|
104
|
+
export type CheckError = { line: number; summary: string }
|
|
105
|
+
export type CheckData = { errorCount: number; errors: CheckError[]; raw: string }
|
|
106
|
+
|
|
107
|
+
export type QueryResult =
|
|
108
|
+
| { what: "rules"; scopes: string[]; data: RulesScope[] }
|
|
109
|
+
| { what: "diff"; scopes: string[]; data: DiffScope[] }
|
|
110
|
+
| { what: "commits"; scopes: string[]; data: CommitsScope[]; recent: string[] }
|
|
111
|
+
| { what: "lint"; scopes: string[]; data: CheckData }
|
|
112
|
+
| { what: "typecheck"; scopes: string[]; data: CheckData }
|
|
113
|
+
| { what: "test"; scopes: string[]; data: CheckData }
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builder functions — construct document trees from domain data.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions returning immutable nodes.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { BlockNode, FieldNode, HeadingNode, ListNode, Node, Primitive, Style, TextNode } from "./types.ts"
|
|
8
|
+
|
|
9
|
+
/** Styled text content. */
|
|
10
|
+
export function text(value: string, style?: Style): TextNode {
|
|
11
|
+
return style ? { kind: "text", value, style } : { kind: "text", value }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Key-value pair. Primitives preserved for JSON type fidelity. */
|
|
15
|
+
export function field(label: string, value: Primitive | Node): FieldNode {
|
|
16
|
+
return { kind: "field", label, value }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isNode(x: unknown): x is Node {
|
|
20
|
+
return typeof x === "object" && x !== null && "kind" in x
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Semantic container with tag and optional attributes.
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* block("commit", field("hash", "abc"))
|
|
28
|
+
* block("scope", { path: "meta/src" }, field("status", "clean"))
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function block(tag: string, ...args: (Node | Record<string, string>)[]): BlockNode {
|
|
32
|
+
if (args.length > 0 && args[0] !== undefined && !isNode(args[0])) {
|
|
33
|
+
return { kind: "block", tag, attrs: args[0] as Record<string, string>, children: args.slice(1) as Node[] }
|
|
34
|
+
}
|
|
35
|
+
return { kind: "block", tag, children: args as Node[] }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Ordered collection. Optional tag names individual items in XML.
|
|
40
|
+
*
|
|
41
|
+
* ```ts
|
|
42
|
+
* list(text("a.ts"), text("b.ts"))
|
|
43
|
+
* list("file", text("a.ts"), text("b.ts")) // XML: <file>a.ts</file><file>b.ts</file>
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function list(...args: (string | Node)[]): ListNode {
|
|
47
|
+
if (typeof args[0] === "string") {
|
|
48
|
+
return { kind: "list", tag: args[0], items: args.slice(1) as Node[] }
|
|
49
|
+
}
|
|
50
|
+
return { kind: "list", items: args as Node[] }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Display heading — renders in CLI and Markdown, omitted in JSON and XML. */
|
|
54
|
+
export function heading(value: string): HeadingNode {
|
|
55
|
+
return { kind: "heading", text: value }
|
|
56
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polymorphic output — build once, render to any format.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Types
|
|
6
|
+
export type {
|
|
7
|
+
BlockNode,
|
|
8
|
+
FieldNode,
|
|
9
|
+
HeadingNode,
|
|
10
|
+
ListNode,
|
|
11
|
+
Node,
|
|
12
|
+
OutputMode,
|
|
13
|
+
Primitive,
|
|
14
|
+
Style,
|
|
15
|
+
TextNode,
|
|
16
|
+
} from "./types.ts"
|
|
17
|
+
|
|
18
|
+
// Builders
|
|
19
|
+
export { block, field, heading, list, text } from "./build.ts"
|
|
20
|
+
|
|
21
|
+
// Renderers
|
|
22
|
+
export { render } from "./render.ts"
|
|
23
|
+
export { toValue } from "./render-json.ts"
|
|
24
|
+
export { escapeXml } from "./render-xml.ts"
|