@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.
Files changed (68) hide show
  1. package/bin/dexter +6 -0
  2. package/package.json +43 -0
  3. package/src/claude/index.ts +6 -0
  4. package/src/cli.ts +39 -0
  5. package/src/env/define.ts +190 -0
  6. package/src/env/index.ts +10 -0
  7. package/src/env/loader.ts +61 -0
  8. package/src/env/print.ts +98 -0
  9. package/src/env/validate.ts +46 -0
  10. package/src/index.ts +16 -0
  11. package/src/meta/adapters/fs.ts +22 -0
  12. package/src/meta/adapters/git.ts +29 -0
  13. package/src/meta/adapters/glob.ts +14 -0
  14. package/src/meta/adapters/index.ts +24 -0
  15. package/src/meta/adapters/process.ts +40 -0
  16. package/src/meta/cli.ts +340 -0
  17. package/src/meta/domain/bisect.ts +126 -0
  18. package/src/meta/domain/blame.ts +136 -0
  19. package/src/meta/domain/commit.ts +135 -0
  20. package/src/meta/domain/commits.ts +23 -0
  21. package/src/meta/domain/constraints/registry.ts +49 -0
  22. package/src/meta/domain/constraints/types.ts +30 -0
  23. package/src/meta/domain/diff.ts +34 -0
  24. package/src/meta/domain/eval.ts +57 -0
  25. package/src/meta/domain/format.ts +34 -0
  26. package/src/meta/domain/lint.ts +88 -0
  27. package/src/meta/domain/pickaxe.ts +99 -0
  28. package/src/meta/domain/quality.ts +145 -0
  29. package/src/meta/domain/rules.ts +21 -0
  30. package/src/meta/domain/scope-context.ts +63 -0
  31. package/src/meta/domain/service.ts +68 -0
  32. package/src/meta/domain/setup.ts +34 -0
  33. package/src/meta/domain/test.ts +72 -0
  34. package/src/meta/domain/transcripts.ts +88 -0
  35. package/src/meta/domain/typecheck.ts +41 -0
  36. package/src/meta/domain/workspace.ts +78 -0
  37. package/src/meta/errors.ts +19 -0
  38. package/src/meta/hooks/on-post-read.ts +61 -0
  39. package/src/meta/hooks/on-post-write.ts +65 -0
  40. package/src/meta/hooks/on-pre-bash.ts +69 -0
  41. package/src/meta/hooks/stubs.ts +51 -0
  42. package/src/meta/index.ts +36 -0
  43. package/src/meta/lib/actor.ts +53 -0
  44. package/src/meta/lib/eslint.ts +58 -0
  45. package/src/meta/lib/format.ts +55 -0
  46. package/src/meta/lib/paths.ts +36 -0
  47. package/src/meta/lib/present.ts +231 -0
  48. package/src/meta/lib/spec-links.ts +83 -0
  49. package/src/meta/lib/stdin.ts +56 -0
  50. package/src/meta/ports.ts +50 -0
  51. package/src/meta/types.ts +113 -0
  52. package/src/output/build.ts +56 -0
  53. package/src/output/index.ts +24 -0
  54. package/src/output/output.test.ts +374 -0
  55. package/src/output/render-cli.ts +55 -0
  56. package/src/output/render-json.ts +80 -0
  57. package/src/output/render-md.ts +43 -0
  58. package/src/output/render-xml.ts +55 -0
  59. package/src/output/render.ts +23 -0
  60. package/src/output/types.ts +44 -0
  61. package/src/pipe/format.ts +167 -0
  62. package/src/pipe/index.ts +4 -0
  63. package/src/pipe/parse.ts +131 -0
  64. package/src/pipe/spawn.ts +205 -0
  65. package/src/pipe/types.ts +27 -0
  66. package/src/terminal/colors.ts +95 -0
  67. package/src/terminal/index.ts +16 -0
  68. package/src/version.ts +1 -0
package/bin/dexter ADDED
@@ -0,0 +1,6 @@
1
+ #!/bin/sh
2
+ # Dexter CLI entry point.
3
+ # Delegates to the TypeScript source via Bun — no build step.
4
+
5
+ DIR="$(cd "${0%/*}" 2>/dev/null && pwd)"
6
+ exec bun run "$DIR/../src/cli.ts" "$@"
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@vladpazych/dexter",
3
+ "version": "0.1.0",
4
+ "description": "Agentic development toolkit — output rendering, env config, CLI framework for Claude Code hooks",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/vladpazych/dexter.git",
9
+ "directory": "packages/dexter"
10
+ },
11
+ "type": "module",
12
+ "files": [
13
+ "src",
14
+ "bin"
15
+ ],
16
+ "exports": {
17
+ ".": "./src/index.ts",
18
+ "./meta": "./src/meta/index.ts",
19
+ "./output": "./src/output/index.ts",
20
+ "./env": "./src/env/index.ts",
21
+ "./pipe": "./src/pipe/index.ts",
22
+ "./terminal": "./src/terminal/index.ts",
23
+ "./claude": "./src/claude/index.ts",
24
+ "./config/eslint": "./src/config/eslint.js",
25
+ "./config/tsconfig/base.json": "./src/config/tsconfig/base.json",
26
+ "./config/tsconfig/node.json": "./src/config/tsconfig/node.json",
27
+ "./config/tsconfig/react.json": "./src/config/tsconfig/react.json"
28
+ },
29
+ "bin": {
30
+ "dexter": "./bin/dexter"
31
+ },
32
+ "scripts": {
33
+ "typecheck": "tsc --noEmit",
34
+ "lint": "eslint .",
35
+ "test": "bun test"
36
+ },
37
+ "dependencies": {
38
+ "zod": "^4.0.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/bun": "^1"
42
+ }
43
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Claude management — .claude/ folder scaffolding, settings, hook wiring.
3
+ */
4
+
5
+ // TODO: Implement
6
+ export {}
package/src/cli.ts ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Dexter standalone CLI — scaffolding and utilities.
3
+ *
4
+ * Usage:
5
+ * dexter init Scaffold meta/ + .claude/ in current repo
6
+ * dexter version Print version
7
+ */
8
+
9
+ import { version } from "./version.js"
10
+
11
+ const [, , cmd] = process.argv
12
+
13
+ switch (cmd) {
14
+ case "init":
15
+ console.log("dexter init — not yet implemented")
16
+ break
17
+
18
+ case "version":
19
+ case "--version":
20
+ case "-v":
21
+ console.log(version)
22
+ break
23
+
24
+ case "--help":
25
+ case "-h":
26
+ case undefined:
27
+ console.log(`dexter v${version} — agentic development toolkit
28
+
29
+ Usage: dexter <command>
30
+
31
+ Commands:
32
+ init Scaffold meta/ + .claude/ in current repo
33
+ version Print version`)
34
+ break
35
+
36
+ default:
37
+ console.error(`Unknown command: ${cmd}`)
38
+ process.exit(1)
39
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * App-owned configuration from environment variables.
3
+ *
4
+ * Each app declares its schema via defineConfig(), which reads process.env,
5
+ * validates types, enforces required fields, and returns a typed object.
6
+ * Metadata is attached via Symbol for printConfig() to read.
7
+ */
8
+
9
+ import { validateBoolean, validateEnum, validateNumber, validatePort, validateString, validateUrl } from "./validate.ts"
10
+
11
+ // ============================================================================
12
+ // TYPES — schema definition
13
+ // ============================================================================
14
+
15
+ type EnvType = "string" | "port" | "url" | "number" | "boolean" | "enum"
16
+
17
+ type FieldDef = {
18
+ readonly env: string
19
+ readonly type?: EnvType
20
+ readonly values?: readonly string[]
21
+ readonly default?: unknown
22
+ readonly required?: boolean
23
+ readonly sensitive?: boolean
24
+ }
25
+
26
+ export type Schema = { readonly [key: string]: FieldDef | Schema }
27
+
28
+ // ============================================================================
29
+ // TYPES — output inference
30
+ // ============================================================================
31
+
32
+ /** Map field's `type` property to the TypeScript value type. */
33
+ type FieldValueType<F> = F extends { type: "port" | "number" }
34
+ ? number
35
+ : F extends { type: "boolean" }
36
+ ? boolean
37
+ : F extends { type: "enum"; values: readonly (infer V)[] }
38
+ ? V
39
+ : string
40
+
41
+ /** True when the field is guaranteed to have a value (required or defaulted). */
42
+ type IsPresent<F> = F extends { required: true }
43
+ ? true
44
+ : F extends { default: undefined }
45
+ ? false
46
+ : F extends { default: unknown }
47
+ ? true
48
+ : false
49
+
50
+ /** Output type for a single field: value type, optionally undefined. */
51
+ type FieldOutput<F> = IsPresent<F> extends true ? FieldValueType<F> : FieldValueType<F> | undefined
52
+
53
+ /** Recursive output type for the entire schema. */
54
+ export type ConfigOutput<S> = {
55
+ -readonly [K in keyof S]: S[K] extends { env: string } ? FieldOutput<S[K]> : ConfigOutput<S[K]>
56
+ }
57
+
58
+ // ============================================================================
59
+ // METADATA — hidden on config object for printConfig
60
+ // ============================================================================
61
+
62
+ export const CONFIG_META = Symbol.for("dexter.config.meta")
63
+
64
+ export type FieldMeta = {
65
+ path: string
66
+ env: string
67
+ sensitive: boolean
68
+ type: EnvType
69
+ values?: readonly string[]
70
+ required: boolean
71
+ hasDefault: boolean
72
+ }
73
+
74
+ export type ConfigMeta = {
75
+ name?: string
76
+ fields: FieldMeta[]
77
+ }
78
+
79
+ // ============================================================================
80
+ // ERROR
81
+ // ============================================================================
82
+
83
+ export class ConfigError extends Error {
84
+ readonly issues: string[]
85
+
86
+ constructor(issues: string[]) {
87
+ super(`Invalid environment configuration\n${issues.map((i) => ` ${i}`).join("\n")}`)
88
+ this.name = "ConfigError"
89
+ this.issues = issues
90
+ }
91
+ }
92
+
93
+ // ============================================================================
94
+ // RUNTIME
95
+ // ============================================================================
96
+
97
+ function isField(value: unknown): value is FieldDef {
98
+ return (
99
+ typeof value === "object" &&
100
+ value !== null &&
101
+ "env" in value &&
102
+ typeof (value as Record<string, unknown>).env === "string"
103
+ )
104
+ }
105
+
106
+ function coerce(raw: string, type: EnvType, values?: readonly string[]): unknown {
107
+ switch (type) {
108
+ case "string":
109
+ return validateString(raw)
110
+ case "port":
111
+ return validatePort(raw)
112
+ case "number":
113
+ return validateNumber(raw)
114
+ case "boolean":
115
+ return validateBoolean(raw)
116
+ case "url":
117
+ return validateUrl(raw)
118
+ case "enum":
119
+ return validateEnum(raw, values ?? [])
120
+ }
121
+ }
122
+
123
+ export function defineConfig<const S extends Schema>(schema: S): ConfigOutput<S>
124
+ export function defineConfig<const S extends Schema>(name: string, schema: S): ConfigOutput<S>
125
+ export function defineConfig<const S extends Schema>(nameOrSchema: string | S, maybeSchema?: S): ConfigOutput<S> {
126
+ const name = typeof nameOrSchema === "string" ? nameOrSchema : undefined
127
+ const schema = typeof nameOrSchema === "string" ? maybeSchema! : nameOrSchema
128
+
129
+ const errors: string[] = []
130
+ const fields: FieldMeta[] = []
131
+
132
+ function walk(obj: Record<string, unknown>, path: string[]): Record<string, unknown> {
133
+ const result: Record<string, unknown> = {}
134
+
135
+ for (const [key, def] of Object.entries(obj)) {
136
+ if (isField(def)) {
137
+ const fieldPath = [...path, key].join(".")
138
+ const type: EnvType = (def.type as EnvType) ?? "string"
139
+
140
+ fields.push({
141
+ path: fieldPath,
142
+ env: def.env,
143
+ sensitive: def.sensitive ?? false,
144
+ type,
145
+ values: def.values,
146
+ required: def.required ?? false,
147
+ hasDefault: "default" in def,
148
+ })
149
+
150
+ const raw = process.env[def.env]
151
+
152
+ if (raw === undefined || raw === "") {
153
+ if (def.required) {
154
+ errors.push(`${def.env}: required but not set`)
155
+ } else if ("default" in def) {
156
+ result[key] = def.default
157
+ } else {
158
+ result[key] = undefined
159
+ }
160
+ continue
161
+ }
162
+
163
+ try {
164
+ result[key] = coerce(raw, type, def.values)
165
+ } catch (msg) {
166
+ errors.push(`${def.env}: ${msg}`)
167
+ }
168
+ } else {
169
+ result[key] = walk(def as Record<string, unknown>, [...path, key])
170
+ }
171
+ }
172
+
173
+ return result
174
+ }
175
+
176
+ const config = walk(schema as unknown as Record<string, unknown>, [])
177
+
178
+ if (errors.length > 0) {
179
+ throw new ConfigError(errors)
180
+ }
181
+
182
+ Object.defineProperty(config, CONFIG_META, {
183
+ value: { name, fields } satisfies ConfigMeta,
184
+ enumerable: false,
185
+ configurable: false,
186
+ writable: false,
187
+ })
188
+
189
+ return config as ConfigOutput<S>
190
+ }
@@ -0,0 +1,10 @@
1
+ // Loading: dotenv plumbing
2
+ export type { LoadResult } from "./loader.ts"
3
+ export { applyEnv, loadEnv, parseEnvFile } from "./loader.ts"
4
+
5
+ // Config: app-owned schema, validation, metadata
6
+ export type { ConfigMeta, ConfigOutput, FieldMeta, Schema } from "./define.ts"
7
+ export { CONFIG_META, ConfigError, defineConfig } from "./define.ts"
8
+
9
+ // Print: formatted output with sensitive masking
10
+ export { formatConfig, printConfig } from "./print.ts"
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Load environment variables from .env files.
3
+ * Order: .env (defaults) → .env.local (overrides)
4
+ */
5
+
6
+ import { existsSync, readFileSync } from "node:fs"
7
+ import { join } from "node:path"
8
+
9
+ export type LoadResult = {
10
+ env: Record<string, string>
11
+ sources: Record<string, string>
12
+ }
13
+
14
+ /** Parse a plain key=value .env file. Ignores comments and empty lines. */
15
+ export function parseEnvFile(filePath: string): Record<string, string> {
16
+ if (!existsSync(filePath)) return {}
17
+ const content = readFileSync(filePath, "utf-8")
18
+ const result: Record<string, string> = {}
19
+
20
+ for (const line of content.split("\n")) {
21
+ const trimmed = line.trim()
22
+ if (trimmed.startsWith("#") || trimmed === "") continue
23
+
24
+ const match = trimmed.match(/^([A-Z][A-Z0-9_]*)=(.*)$/)
25
+ if (match) {
26
+ let value = match[2]
27
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
28
+ value = value.slice(1, -1)
29
+ }
30
+ result[match[1]] = value
31
+ }
32
+ }
33
+
34
+ return result
35
+ }
36
+
37
+ export function loadEnv(rootDir: string): LoadResult {
38
+ const env: Record<string, string> = {}
39
+ const sources: Record<string, string> = {}
40
+
41
+ const merge = (filename: string) => {
42
+ const values = parseEnvFile(join(rootDir, filename))
43
+ for (const [key, value] of Object.entries(values)) {
44
+ if (value !== "") {
45
+ env[key] = value
46
+ sources[key] = filename
47
+ }
48
+ }
49
+ }
50
+
51
+ merge(".env")
52
+ merge(".env.local")
53
+
54
+ return { env, sources }
55
+ }
56
+
57
+ export function applyEnv(env: Record<string, string>): void {
58
+ for (const [key, value] of Object.entries(env)) {
59
+ process.env[key] = value
60
+ }
61
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Config printing with sensitive value masking.
3
+ *
4
+ * Reads metadata attached by defineConfig() via CONFIG_META symbol.
5
+ * Nested schema keys become section headers in the output.
6
+ */
7
+
8
+ import { c } from "../terminal/colors.ts"
9
+ import { CONFIG_META, type ConfigMeta, type FieldMeta } from "./define.ts"
10
+
11
+ const MASK = "••••"
12
+ const UNSET = "—"
13
+ const BOX_TOP = "┌"
14
+ const BOX_MID = "│"
15
+ const BOX_BOT = "└"
16
+
17
+ function getMeta(config: unknown): ConfigMeta | undefined {
18
+ if (typeof config === "object" && config !== null && CONFIG_META in config) {
19
+ return (config as Record<symbol, unknown>)[CONFIG_META] as ConfigMeta
20
+ }
21
+ return undefined
22
+ }
23
+
24
+ function formatValue(raw: unknown, field: FieldMeta): string {
25
+ if (raw === undefined || raw === null) return c.gray(UNSET)
26
+ if (field.sensitive) return c.gray(MASK)
27
+ return String(raw)
28
+ }
29
+
30
+ function getNestedValue(obj: unknown, path: string): unknown {
31
+ let current = obj
32
+ for (const key of path.split(".")) {
33
+ if (typeof current !== "object" || current === null) return undefined
34
+ current = (current as Record<string, unknown>)[key]
35
+ }
36
+ return current
37
+ }
38
+
39
+ /**
40
+ * Format config as a printable string with sections and masking.
41
+ */
42
+ export function formatConfig(config: unknown, name?: string): string {
43
+ const meta = getMeta(config)
44
+ if (!meta) return String(config)
45
+
46
+ const label = name ?? meta.name ?? "config"
47
+ const lines: string[] = []
48
+
49
+ lines.push(`${c.gray(BOX_TOP)} ${c.bolded(label)}`)
50
+
51
+ // Group fields by their first path segment (section)
52
+ const sections = new Map<string, FieldMeta[]>()
53
+ for (const field of meta.fields) {
54
+ const parts = field.path.split(".")
55
+ const section = parts.length > 1 ? parts[0] : ""
56
+ const existing = sections.get(section) ?? []
57
+ existing.push(field)
58
+ sections.set(section, existing)
59
+ }
60
+
61
+ // Find longest label for alignment
62
+ const allLabels = meta.fields.map((f) => {
63
+ const parts = f.path.split(".")
64
+ return parts[parts.length - 1]
65
+ })
66
+ const maxLen = Math.max(...allLabels.map((l) => l.length))
67
+
68
+ let first = true
69
+ for (const [section, fields] of sections) {
70
+ if (!first) lines.push(c.gray(BOX_MID))
71
+ first = false
72
+
73
+ if (section) {
74
+ lines.push(`${c.gray(BOX_MID)} ${c.cyan(section)}`)
75
+ }
76
+
77
+ for (const field of fields) {
78
+ const parts = field.path.split(".")
79
+ const key = parts[parts.length - 1]
80
+ const indent = section ? " " : ""
81
+ const value = getNestedValue(config, field.path)
82
+ const formatted = formatValue(value, field)
83
+ const padding = " ".repeat(maxLen - key.length + 2)
84
+
85
+ lines.push(`${c.gray(BOX_MID)} ${indent}${c.gray(key)}${padding}${formatted}`)
86
+ }
87
+ }
88
+
89
+ lines.push(c.gray(BOX_BOT))
90
+ return lines.join("\n")
91
+ }
92
+
93
+ /**
94
+ * Print config to stdout with sections and sensitive value masking.
95
+ */
96
+ export function printConfig(config: unknown, name?: string): void {
97
+ console.log(formatConfig(config, name))
98
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Pure validators for environment variable types.
3
+ * Each returns the coerced value or throws a descriptive message.
4
+ */
5
+
6
+ export function validateString(value: string): string {
7
+ return value
8
+ }
9
+
10
+ export function validatePort(value: string): number {
11
+ const n = parseInt(value, 10)
12
+ if (isNaN(n) || n < 1 || n > 65535) {
13
+ throw `expected port number (1–65535), got "${value}"`
14
+ }
15
+ return n
16
+ }
17
+
18
+ export function validateNumber(value: string): number {
19
+ const n = parseFloat(value)
20
+ if (isNaN(n)) {
21
+ throw `expected number, got "${value}"`
22
+ }
23
+ return n
24
+ }
25
+
26
+ export function validateBoolean(value: string): boolean {
27
+ if (value === "true" || value === "1") return true
28
+ if (value === "false" || value === "0") return false
29
+ throw `expected true/false/1/0, got "${value}"`
30
+ }
31
+
32
+ export function validateUrl(value: string): string {
33
+ try {
34
+ new URL(value)
35
+ return value
36
+ } catch {
37
+ throw `expected valid URL, got "${value}"`
38
+ }
39
+ }
40
+
41
+ export function validateEnum(value: string, allowed: readonly string[]): string {
42
+ if (!allowed.includes(value)) {
43
+ throw `expected ${allowed.join(" | ")}, got "${value}"`
44
+ }
45
+ return value
46
+ }
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @vladpazych/dexter — agentic development toolkit.
3
+ *
4
+ * Subpath exports:
5
+ * dexter/meta — CLI factory, hook framework, domain commands
6
+ * dexter/output — polymorphic structured output (block, field, render)
7
+ * dexter/env — env file loading
8
+ * dexter/pipe — pipe utilities
9
+ * dexter/terminal — terminal helpers
10
+ * dexter/claude — .claude folder management
11
+ */
12
+
13
+ export { version } from "./version.ts"
14
+
15
+ // Re-export top-level env utilities for convenience
16
+ export { applyEnv, loadEnv } from "./env/index.ts"
@@ -0,0 +1,22 @@
1
+ /**
2
+ * FsPort adapter — wraps Node fs for file system operations.
3
+ */
4
+
5
+ import { existsSync, readFileSync, writeFileSync, readdirSync, unlinkSync, mkdirSync } from "node:fs"
6
+
7
+ import type { FsPort } from "../ports.ts"
8
+
9
+ export function createNodeFs(): FsPort {
10
+ return {
11
+ exists: (path) => existsSync(path),
12
+ readFile: (path) => readFileSync(path, "utf-8"),
13
+ writeFile: (path, content) => writeFileSync(path, content),
14
+ readDir: (path) =>
15
+ readdirSync(path, { withFileTypes: true }).map((d) => ({
16
+ name: d.name,
17
+ isDirectory: d.isDirectory(),
18
+ })),
19
+ unlink: (path) => unlinkSync(path),
20
+ mkdir: (path) => mkdirSync(path, { recursive: true }),
21
+ }
22
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * GitPort adapter — wraps Bun.spawnSync for git operations.
3
+ */
4
+
5
+ import type { GitPort } from "../ports.ts"
6
+
7
+ export function createBunGit(): GitPort {
8
+ return {
9
+ run(args, env) {
10
+ const result = Bun.spawnSync(["git", ...args], {
11
+ stdout: "pipe",
12
+ stderr: "pipe",
13
+ env: env ? { ...process.env, ...env } : undefined,
14
+ })
15
+ return {
16
+ success: result.success,
17
+ stdout: result.stdout.toString(),
18
+ stderr: result.stderr.toString(),
19
+ }
20
+ },
21
+ checkIgnore(file) {
22
+ const result = Bun.spawnSync(["git", "check-ignore", "-q", "--", file], {
23
+ stdout: "pipe",
24
+ stderr: "pipe",
25
+ })
26
+ return result.success
27
+ },
28
+ }
29
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * GlobPort adapter — wraps Bun.Glob for pattern matching.
3
+ */
4
+
5
+ import type { GlobPort } from "../ports.ts"
6
+
7
+ export function createBunGlob(): GlobPort {
8
+ return {
9
+ match(pattern, candidates) {
10
+ const glob = new Bun.Glob(pattern)
11
+ return candidates.filter((f) => glob.match(f))
12
+ },
13
+ }
14
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Adapter wiring — creates ControlPorts from real implementations.
3
+ */
4
+
5
+ import { homedir, tmpdir } from "node:os"
6
+
7
+ import type { ControlPorts } from "../ports.ts"
8
+
9
+ import { createBunGit } from "./git.ts"
10
+ import { createNodeFs } from "./fs.ts"
11
+ import { createNodeProcess } from "./process.ts"
12
+ import { createBunGlob } from "./glob.ts"
13
+
14
+ export function createControlPorts(root: string): ControlPorts {
15
+ return {
16
+ git: createBunGit(),
17
+ fs: createNodeFs(),
18
+ process: createNodeProcess(),
19
+ glob: createBunGlob(),
20
+ tmpdir: () => tmpdir(),
21
+ homedir: () => homedir(),
22
+ root,
23
+ }
24
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * ProcessPort adapter — wraps child_process.spawn + readline for streaming output.
3
+ */
4
+
5
+ import { spawn } from "node:child_process"
6
+ import { createInterface } from "node:readline"
7
+
8
+ import type { ProcessPort } from "../ports.ts"
9
+
10
+ export function createNodeProcess(): ProcessPort {
11
+ return {
12
+ spawn({ cmd, args, cwd, env, timeout }) {
13
+ const child = spawn(cmd, args, {
14
+ cwd,
15
+ stdio: ["ignore", "pipe", "pipe"],
16
+ env: env ? { ...process.env, ...env } : undefined,
17
+ })
18
+
19
+ let timer: ReturnType<typeof setTimeout> | undefined
20
+ if (timeout) {
21
+ timer = setTimeout(() => child.kill(), timeout)
22
+ }
23
+
24
+ return {
25
+ onLine(stream, cb) {
26
+ const source = stream === "stdout" ? child.stdout : child.stderr
27
+ createInterface({ input: source }).on("line", cb)
28
+ },
29
+ wait() {
30
+ return new Promise((resolve) => {
31
+ child.on("close", (code) => {
32
+ if (timer) clearTimeout(timer)
33
+ resolve(code)
34
+ })
35
+ })
36
+ },
37
+ }
38
+ },
39
+ }
40
+ }